import QtQuick import QtQuick.Layouts import ZShell.Components import ZShell.Config import ZShell.Llm import qs.Modules.Notifications.Sidebar.Chat.Content import qs.Components import qs.Services Item { id: root property ChatSession chatData property bool following: true signal requestClose function scrollToBottom(): void { Qt.callLater(() => listLoader.item?.positionViewAtBeginning()); } function send(text: string): void { if (text.trim() === "") return; following = true; chatData.sendMessage(text); input.text = ""; } RowLayout { id: header anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top spacing: Tokens.spacing.large IconButton { icon: "arrow_back" inactiveColor: Colors.tPalette.m3surfaceContainerHigh inactiveOnColor: Colors.palette.m3onSurfaceVariant isRound: true type: IconButton.Tonal onClicked: root.requestClose() } CustomText { Layout.fillWidth: true elide: Text.ElideRight font.pointSize: Tokens.font.size.larger text: qsTr(root.chatData.title) } } Loader { id: listLoader anchors.bottom: input.top anchors.bottomMargin: Tokens.spacing.medium anchors.left: parent.left anchors.right: parent.right anchors.top: header.bottom anchors.topMargin: Tokens.spacing.medium active: root.chatData && root.chatData.loaded sourceComponent: ListView { id: list property bool userScrolledUp: false function scrollToBottom(): void { Qt.callLater(() => positionViewAtBeginning()); } cacheBuffer: height * 20 clip: true // fadeAmount: 0.05 // fadeThreshold: Tokens.padding.medium model: root.chatData.messagesModel spacing: 0 rotation: 180 // add: Transition { // Anim { // from: 10 // property: "y" // } // } delegate: MessageDelegate { rotation: 180 } // displaced: Transition { // Anim { // property: "y" // } // } // move: Transition { // Anim { // property: "y" // } // } Component.onCompleted: { positionViewAtBeginning(); forceActiveFocus(); } onAtYEndChanged: { if (atYEnd) userScrolledUp = false; } onContentHeightChanged: { if (!userScrolledUp && atYEnd) scrollToBottom(); } onCountChanged: { if (!userScrolledUp) scrollToBottom(); } onMovingChanged: { if (moving) userScrolledUp = !atYEnd; } Anim { id: scrollAnim property: "contentY" target: list type: Anim.DefaultEffects } WheelInverter { target: list } } // Trigger the load even before we're ready to show the list. Component.onCompleted: if (root.chatData) root.chatData.messagesModel } EmptyBackground { id: emptyState anchors.fill: parent spacing: Tokens.spacing.small visible: !root.chatData } Item { anchors.bottom: input.top anchors.bottomMargin: Tokens.spacing.extraLarge anchors.horizontalCenter: parent.horizontalCenter implicitHeight: scrollToBottom.implicitHeight implicitWidth: scrollToBottom.implicitWidth scale: !listLoader.item.atYBeginning && listLoader.item.contentHeight > listLoader.item.height ? 1 : 0 visible: scale > 0 Behavior on scale { Anim {} } Elevation { anchors.fill: parent level: 2 radius: scrollToBottom.radius } IconButton { id: scrollToBottom anchors.centerIn: parent font.pointSize: Tokens.font.size.large icon: "arrow_downward" isRound: true padding: Tokens.padding.extraSmall type: IconButton.Tonal onClicked: { listLoader.item.positionViewAtBeginning(); } } } ChatInput { id: input anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right bg.border.color: Colors.palette.m3outlineVariant bg.color: Colors.tPalette.m3surfaceContainerLowest font.pointSize: Tokens.font.size.normal implicitHeight: Math.min(root.height / 5, contentHeight + topPadding + bottomPadding) placeholderText: qsTr("Send a message") sendIcon.font.pointSize: Tokens.font.size.large sendIcon.icon: "arrow_upward" sendIcon.padding: Tokens.padding.extraSmall Keys.onPressed: e => { if (e.key == Qt.Key_Return) { if (!(e.modifiers & Qt.ShiftModifier)) { root.send(text); e.accepted = true; } } } onSendPressed: root.send(text) } }