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: appSettings.ttsAutoRead = !appSettings.ttsAutoRead } MenuItem { text: qsTr("Stop playback") visible: player.playing onClicked: player.stop() } MenuItem { text: qsTr("Cancel run") visible: chat.busy onClicked: chat.cancel() } MenuItem { text: qsTr("Resync") onClicked: chat.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.highlightDimmerColor 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") highlighted: recorder.recording 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…") onAccepted: 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 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) } }