Files
harbour-hermes/qml/pages/ChatPage.qml
T
kaneda 98139be96a fix(qml): correzioni Silica post-primo-test su device (v0.1.1)
Pagine non caricabili ("Impossibile caricare la pagina") per API
inesistenti nella Silica del kit Sailfish 5.1 / Qt 5.6 — verificate
sui docs ufficiali (pagine -members) e incrociate coi pattern
provati su device (tide/calibreweb/quill):

- TextField: rimosso onAccepted (segnale "accepted" inesistente)
  -> EnterKey.enabled/.onClicked (LoginPage, ChatPage)
- Button: rimosso "highlighted" (inesistente su Button) dal mic
- ComboBox: menu con MenuItem espliciti (via Repeater non provato)
- Theme.highlightDimmerColor -> Theme.rgba(highlightBackgroundColor)
- Azioni nei menu instradate via funzioni della Page (pattern tide)
- SessionsPage: titolo delegato piu' robusto (no "undefined")
2026-09-12 08:09:30 +02:00

259 lines
7.8 KiB
QML

import QtQuick 2.0
import Sailfish.Silica 1.0
// Conversazione con Hermes: bolle, streaming live, composer con dettatura.
Page {
id: page
property string sessionId
property string noticeText: ""
// Altezza tastiera: alza il composer quando la tastiera e' aperta.
property real kbHeight: Qt.inputMethod.visible
? Math.max(0, page.height - Qt.inputMethod.keyboardRectangle.y)
: 0
allowedOrientations: Orientation.Portrait
SilicaListView {
id: list
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: composer.top
model: chat
spacing: Theme.paddingSmall
clip: true
header: Item {
width: list.width
height: headerCol.height
Column {
id: headerCol
width: parent.width
PageHeader {
title: chat.title.length > 0 ? chat.title : qsTr("Chat")
}
Label {
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
font.pixelSize: Theme.fontSizeExtraSmall
color: Theme.secondaryHighlightColor
visible: chat.busy || player.playing
text: chat.busy ? qsTr("Hermes is writing…")
: qsTr("Speaking…")
}
}
}
PullDownMenu {
MenuItem {
text: qsTr("Read last reply aloud")
onClicked: page.readLast()
}
MenuItem {
text: appSettings.ttsAutoRead ? qsTr("Auto-read: on") : qsTr("Auto-read: off")
onClicked: page.toggleAutoRead()
}
MenuItem {
text: qsTr("Stop playback")
visible: player.playing
onClicked: page.stopPlayback()
}
MenuItem {
text: qsTr("Cancel run")
visible: chat.busy
onClicked: page.cancelRun()
}
MenuItem {
text: qsTr("Resync")
onClicked: page.resync()
}
}
delegate: Item {
id: row
width: list.width
height: bubble.height + Theme.paddingMedium
Rectangle {
id: bubble
property real maxW: list.width - 2 * Theme.horizontalPageMargin
width: Math.min(lbl.implicitWidth + 2 * Theme.paddingMedium, maxW)
height: lbl.height + 2 * Theme.paddingSmall
radius: Theme.paddingSmall
x: model.role === "user"
? list.width - width - Theme.horizontalPageMargin
: Theme.horizontalPageMargin
color: model.role === "user"
? Theme.rgba(Theme.highlightBackgroundColor, 0.35)
: Theme.rgba(Theme.secondaryHighlightColor, 0.18)
Label {
id: lbl
x: Theme.paddingMedium
y: Theme.paddingSmall
width: Math.min(implicitWidth, bubble.maxW - 2 * Theme.paddingMedium)
wrapMode: Text.Wrap
color: Theme.primaryColor
text: model.content + (model.streaming ? " ▍" : "")
}
}
}
}
Rectangle {
id: composer
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: page.kbHeight
height: compCol.height + Theme.paddingMedium
color: Theme.rgba(Theme.highlightBackgroundColor, 0.15)
Column {
id: compCol
width: parent.width
spacing: Theme.paddingSmall / 2
Label {
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
font.pixelSize: Theme.fontSizeExtraSmall
color: recorder.recording ? Theme.highlightColor
: Theme.secondaryHighlightColor
visible: recorder.recording || api.transcribing || page.noticeText.length > 0
truncationMode: TruncationMode.Fade
text: recorder.recording
? qsTr("Recording… %1 — tap Stop to transcribe").arg(page.formatMs(recorder.durationMs))
: api.transcribing
? qsTr("Transcribing…")
: page.noticeText
}
Row {
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
spacing: Theme.paddingSmall
Button {
id: micBtn
text: recorder.recording ? qsTr("Stop") : qsTr("Mic")
// Button non ha "highlighted": lo stato è nel testo (Mic/Stop)
enabled: !api.transcribing && !chat.busy || recorder.recording
onClicked: {
if (recorder.recording)
recorder.stop()
else {
player.stop()
recorder.start()
}
}
}
TextField {
id: input
width: parent.width - micBtn.width - sendBtn.width - 2 * parent.spacing
placeholderText: qsTr("Message…")
// Invio con EnterKey (vedi LoginPage)
EnterKey.enabled: text.length > 0
EnterKey.onClicked: page.doSend()
}
Button {
id: sendBtn
text: qsTr("Send")
enabled: input.text.length > 0 && !chat.busy
onClicked: page.doSend()
}
}
}
}
Timer {
id: scrollDebounce
interval: 150
onTriggered: list.positionViewAtEnd()
}
Timer {
id: noticeClear
interval: 5000
onTriggered: page.noticeText = ""
}
Connections {
target: chat
onRowCountChanged: scrollDebounce.restart()
onStreamTick: scrollDebounce.restart()
onAssistantFinished: {
if (appSettings.ttsAutoRead)
page.readLast()
}
onNotice: page.showNotice(text)
}
Connections {
target: api
onTranscriptionReady: {
input.text = text
input.focus = true
}
onTranscriptionFailed: page.showNotice(error)
onTtsFailed: page.showNotice(error)
}
Component.onCompleted: {
if (sessionId.length > 0)
chat.openSession(sessionId)
}
function doSend() {
var t = input.text
if (t.trim().length === 0)
return
input.text = ""
chat.send(t)
scrollDebounce.restart()
}
function readLast() {
var t = chat.lastAssistantText()
if (t.length === 0) {
page.showNotice(qsTr("No reply to read yet"))
return
}
api.speak(t)
}
function toggleAutoRead() {
appSettings.ttsAutoRead = !appSettings.ttsAutoRead
}
function stopPlayback() {
player.stop()
}
function cancelRun() {
chat.cancel()
}
function resync() {
chat.resync()
}
function showNotice(text) {
page.noticeText = text
noticeClear.restart()
}
function formatMs(ms) {
var s = Math.floor(ms / 1000)
var m = Math.floor(s / 60)
s = s % 60
return m + ":" + (s < 10 ? "0" + s : s)
}
}