import ZShell.Config import ZShell.Llm import QtQuick import QtQuick.Layouts 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(list.positionViewAtBeginning); } function send(text: string): void { if (text.trim() === "") return; following = true; chatData.sendMessage(text); input.text = ""; } Component.onCompleted: input.focus = true 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) } } VerticalFadeListView { id: list property bool userScrolledUp: false 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 cacheBuffer: height * 20 clip: true fadeAmount: 0.05 fadeThreshold: Tokens.padding.medium model: root.chatData.messagesModel spacing: 0 verticalLayoutDirection: VerticalFadeListView.BottomToTop add: Transition { Anim { from: list.width property: "x" to: 0 } } delegate: MessageDelegate {} displaced: Transition { Anim { property: "y" } } move: Transition { Anim { property: "y" } } Component.onCompleted: positionViewAtBeginning() onAtYEndChanged: { if (atYEnd) userScrolledUp = false; } onContentHeightChanged: { if (!userScrolledUp && atYEnd) root.scrollToBottom(); } onCountChanged: { if (!userScrolledUp) root.scrollToBottom(); } onMovingChanged: { if (moving) userScrolledUp = !atYEnd; } Anim { id: scrollAnim property: "contentY" target: list type: Anim.DefaultEffects } } 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: list.visibleArea.yPosition + list.visibleArea.heightRatio < 1 && list.contentHeight > list.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: { list.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) } }