better chat list view + anchor to bottom. sqlite db for chats + LIFO-ordered
This commit is contained in:
@@ -1,10 +1,8 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ZShell.Config
|
||||
import ZShell.Llm
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Modules.Notifications.Sidebar.Chat.Content
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
@@ -13,18 +11,23 @@ Item {
|
||||
|
||||
property ChatSession chatData
|
||||
property bool following: true
|
||||
readonly property int messageCount: chatData.messages.length
|
||||
|
||||
signal requestClose
|
||||
|
||||
function send(): void {
|
||||
if (Chat.busy || input.text.trim() === "")
|
||||
function scrollToBottom(): void {
|
||||
Qt.callLater(list.positionViewAtBeginning);
|
||||
}
|
||||
|
||||
function send(text: string): void {
|
||||
if (text.trim() === "")
|
||||
return;
|
||||
following = true;
|
||||
Chat.send(chatData.id, input.text);
|
||||
chatData.sendMessage(text);
|
||||
input.text = "";
|
||||
}
|
||||
|
||||
Component.onCompleted: input.focus = true
|
||||
|
||||
RowLayout {
|
||||
id: header
|
||||
|
||||
@@ -54,194 +57,58 @@ Item {
|
||||
VerticalFadeListView {
|
||||
id: list
|
||||
|
||||
readonly property real bottomThreshold: Tokens.spacing.extraSmall
|
||||
property bool stickToBottom: true
|
||||
property bool userScrolledUp: false
|
||||
|
||||
function scrollToEnd(animated: bool): void {
|
||||
const target = Math.max(0, list.contentHeight - list.height);
|
||||
if (!animated) {
|
||||
scrollAnim.stop();
|
||||
list.contentY = target;
|
||||
return;
|
||||
}
|
||||
scrollAnim.to = target;
|
||||
scrollAnim.restart();
|
||||
}
|
||||
|
||||
anchors.bottom: inputRow.top
|
||||
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
|
||||
currentIndex: messages.values.length - 1
|
||||
fadeAmount: 0.05
|
||||
fadeThreshold: Tokens.padding.medium
|
||||
model: root.chatData.messagesModel
|
||||
spacing: Tokens.spacing.medium
|
||||
verticalLayoutDirection: VerticalFadeListView.BottomToTop
|
||||
|
||||
CustomScrollBar.vertical: CustomScrollBar {
|
||||
flickable: list
|
||||
}
|
||||
delegate: ColumnLayout {
|
||||
id: messageRow
|
||||
|
||||
readonly property bool isUser: modelData.role === ChatMessage.Role.User
|
||||
required property var modelData
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: messageRow.isUser ? 0 : Tokens.spacing.extraSmall
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: messageRow.isUser ? Tokens.spacing.extraSmall : 0
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: Tokens.spacing.extraLarge
|
||||
active: !messageRow.isUser && messageRow.modelData.reasoning !== ""
|
||||
|
||||
sourceComponent: CustomRect {
|
||||
id: reasoning
|
||||
|
||||
property bool expanded: false
|
||||
|
||||
color: Colors.palette.m3surfaceContainer
|
||||
implicitHeight: expanded ? expandedText.implicitHeight : collapsedText.implicitHeight + Tokens.padding.medium * 2
|
||||
radius: Tokens.rounding.medium
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: spinnerReasoning
|
||||
|
||||
active: opacity > 0
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
opacity: messageRow.modelData.reasoningActive ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
sourceComponent: LoadingIndicator {
|
||||
implicitSize: collapsedText.implicitHeight
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: reasoningDone
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
font.pointSize: Tokens.font.size.large
|
||||
opacity: messageRow.modelData.reasoningActive || reasoning.expanded ? 0 : 1
|
||||
text: "check"
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: collapsedText
|
||||
|
||||
anchors.left: messageRow.modelData.reasoningActive ? spinnerReasoning.right : reasoningDone.right
|
||||
anchors.margins: Tokens.padding.medium
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
opacity: reasoning.expanded ? 0 : 1
|
||||
text: messageRow.modelData.reasoningActive ? qsTr("Thinking...") : qsTr("Thought for %1s").arg((messageRow.modelData.reasoningElapsedMs / 1000).toFixed(1))
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: expandedText
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.medium
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
color: Colors.palette.m3onSurface
|
||||
opacity: reasoning.expanded ? 1 : 0
|
||||
text: messageRow.modelData.reasoning
|
||||
visible: opacity > 0
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
onClicked: reasoning.expanded = !reasoning.expanded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
Layout.alignment: messageRow.isUser ? Qt.AlignRight : Qt.AlignLeft
|
||||
color: messageRow.isUser ? Colors.palette.m3primary : Colors.palette.m3surfaceContainer
|
||||
implicitHeight: Math.max(msgText.implicitHeight + Tokens.padding.medium * 2, spinnerLoader.implicitHeight)
|
||||
implicitWidth: Math.min(msgText.implicitWidth + Tokens.padding.medium * 2, messageRow.ListView.view.width - Tokens.spacing.extraSmall - Tokens.spacing.extraLarge)
|
||||
radius: Tokens.rounding.medium
|
||||
visible: !messageRow.modelData.reasoningActive
|
||||
|
||||
CustomText {
|
||||
id: msgText
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.medium
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
color: messageRow.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
text: messageRow.modelData.content
|
||||
textFormat: CustomText.MarkdownText
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: spinnerLoader
|
||||
|
||||
active: messageRow.modelData.streaming && msgText.text.length === 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
sourceComponent: Item {
|
||||
implicitHeight: 18
|
||||
implicitWidth: 18
|
||||
|
||||
LoadingIndicator {
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
}
|
||||
add: Transition {
|
||||
Anim {
|
||||
from: list.width
|
||||
property: "x"
|
||||
to: 0
|
||||
}
|
||||
}
|
||||
model: ScriptModel {
|
||||
id: messages
|
||||
|
||||
values: root.chatData.messages
|
||||
delegate: MessageDelegate {}
|
||||
displaced: Transition {
|
||||
Anim {
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
move: Transition {
|
||||
Anim {
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: scrollToEnd(false)
|
||||
Component.onCompleted: positionViewAtBeginning()
|
||||
onAtYEndChanged: {
|
||||
if (atYEnd)
|
||||
userScrolledUp = false;
|
||||
}
|
||||
onContentHeightChanged: {
|
||||
if (list.stickToBottom)
|
||||
list.scrollToEnd(false);
|
||||
if (!userScrolledUp && atYEnd)
|
||||
root.scrollToBottom();
|
||||
}
|
||||
onMovementEnded: {
|
||||
list.stickToBottom = (list.contentY >= list.contentHeight - list.height - list.bottomThreshold);
|
||||
onCountChanged: {
|
||||
if (!userScrolledUp)
|
||||
root.scrollToBottom();
|
||||
}
|
||||
onMovingChanged: {
|
||||
if (moving)
|
||||
userScrolledUp = !atYEnd;
|
||||
}
|
||||
onMovementStarted: list.stickToBottom = false
|
||||
|
||||
Anim {
|
||||
id: scrollAnim
|
||||
@@ -252,88 +119,53 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
EmptyBackground {
|
||||
id: emptyState
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: Tokens.spacing.small
|
||||
visible: (root.messageCount === 0 && !Chat.busy) || !root.chatData
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Colors.tPalette.m3outlineVariant
|
||||
font.pointSize: 36
|
||||
text: "chat"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Colors.tPalette.m3onSurfaceVariant
|
||||
text: !root.chatData ? qsTr("Open a previous chat or start a new one") : qsTr("No messages yet")
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
visible: !root.chatData
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: scrollToBottom
|
||||
|
||||
anchors.bottom: inputRow.top
|
||||
anchors.bottom: input.top
|
||||
anchors.bottomMargin: Tokens.spacing.extraLarge
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
font.pointSize: Tokens.font.size.large
|
||||
icon: "arrow_downward"
|
||||
isRound: true
|
||||
padding: Tokens.padding.extraSmall
|
||||
scale: list.visibleArea.yPosition + list.visibleArea.heightRatio < 1 ? 1 : 0
|
||||
scale: list.visibleArea.yPosition + list.visibleArea.heightRatio < 1 && list.contentHeight > list.height ? 1 : 0
|
||||
type: IconButton.Filled
|
||||
visible: scale > 0
|
||||
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
list.stickToBottom = true;
|
||||
list.scrollToEnd(true);
|
||||
list.positionViewAtBeginning();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: inputRow
|
||||
ChatInput {
|
||||
id: input
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Tokens.spacing.extraSmall
|
||||
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
|
||||
|
||||
CustomTextField {
|
||||
id: input
|
||||
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr("Send a message...")
|
||||
type: CustomTextField.Filled
|
||||
|
||||
onAccepted: root.send()
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: actionBtn
|
||||
|
||||
enabled: Chat.busy || input.text.trim() !== ""
|
||||
font.pointSize: Tokens.font.size.normal * 2
|
||||
icon: Chat.busy ? "stop" : "send"
|
||||
isRound: true
|
||||
type: IconButton.Filled
|
||||
|
||||
onClicked: Chat.busy ? Chat.stop() : root.send()
|
||||
}
|
||||
onAccepted: root.send(text)
|
||||
onSendPressed: root.send(text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,91 +1,311 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import ZShell.Llm
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
CustomRect {
|
||||
CustomClippingRect {
|
||||
id: root
|
||||
|
||||
required property Item chatItem
|
||||
property alias layout: layout
|
||||
property bool expanded: false
|
||||
required property ChatSession modelData
|
||||
|
||||
signal clicked(content: ChatSession)
|
||||
signal remove(content: ChatSession)
|
||||
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: layout.implicitHeight + layout.anchors.topMargin + layout.anchors.bottomMargin
|
||||
implicitHeight: {
|
||||
let h = 0;
|
||||
|
||||
h += infoContainer.implicitHeight;
|
||||
|
||||
if (expanded)
|
||||
h += body.implicitHeight + body.topMargin + actionRow.implicitHeight + actionRow.anchors.topMargin;
|
||||
else
|
||||
h += preview.implicitHeight + preview.topMargin;
|
||||
|
||||
h += Tokens.padding.small;
|
||||
|
||||
const icon = chatIcon.implicitHeight + chatIcon.anchors.topMargin * 2;
|
||||
if (h < icon)
|
||||
return icon;
|
||||
return h;
|
||||
}
|
||||
radius: Tokens.rounding.large
|
||||
|
||||
Behavior on implicitHeight {
|
||||
enabled: titleText.readOnly
|
||||
|
||||
Anim {}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: chatIcon
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Tokens.padding.large
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: root.chatItem.ListView.isCurrentItem ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Tokens.padding.large
|
||||
font.pointSize: Tokens.font.size.extraLarge
|
||||
text: root.modelData.icon || "check"
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
Item {
|
||||
id: infoContainer
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Tokens.padding.small
|
||||
anchors.left: chatIcon.right
|
||||
anchors.leftMargin: Tokens.padding.medium
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Tokens.padding.medium
|
||||
anchors.leftMargin: Tokens.spacing.medium
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Tokens.padding.small
|
||||
spacing: Tokens.spacing.extraSmall
|
||||
anchors.right: expandBtn.left
|
||||
anchors.rightMargin: Tokens.spacing.medium
|
||||
implicitHeight: {
|
||||
let h = 0;
|
||||
h += title.implicitHeight + title.anchors.topMargin;
|
||||
h += timestamp.implicitHeight + timestamp.anchors.topMargin;
|
||||
if (root.expanded)
|
||||
h += created.implicitHeight + created.anchors.topMargin;
|
||||
return h;
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: titleContainer
|
||||
id: title
|
||||
|
||||
readonly property bool enoughSpace: width > title.implicitWidth + timeSep.implicitWidth + timeSep.anchors.leftMargin + timestamp.implicitWidth + timestamp.anchors.leftMargin
|
||||
anchors.left: parent.left
|
||||
anchors.topMargin: Tokens.padding.extraSmall
|
||||
anchors.top: parent.top
|
||||
color: Colors.layer(Colors.palette.m3surfaceContainerHighest, 1)
|
||||
implicitHeight: titleText.implicitHeight + Tokens.padding.extraSmall * 2
|
||||
radius: Tokens.rounding.full
|
||||
implicitWidth: titleText.implicitWidth + Tokens.padding.large * 2
|
||||
opacity: titleText.readOnly ? 0 : 1
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: enoughSpace ? title.implicitHeight : title.implicitHeight + timestamp.implicitHeight + timestamp.anchors.topMargin
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: title
|
||||
TextEditBase {
|
||||
id: titleText
|
||||
|
||||
color: root.chatItem.ListView.isCurrentItem ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
text: root.modelData.title
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: readOnly ? 0 : Tokens.padding.large
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: readOnly ? Tokens.padding.small : Tokens.padding.extraSmall * 2
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
text: root.modelData.title
|
||||
readOnly: true
|
||||
|
||||
Behavior on anchors.leftMargin {
|
||||
Anim {}
|
||||
}
|
||||
Behavior on anchors.topMargin {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: timeSep
|
||||
|
||||
anchors.left: title.right
|
||||
anchors.leftMargin: Tokens.spacing.small
|
||||
anchors.verticalCenter: title.verticalCenter
|
||||
color: root.chatItem.ListView.isCurrentItem ? Qt.alpha(Colors.palette.m3onPrimary, 0.9) : Colors.palette.m3onSurfaceVariant
|
||||
text: "•"
|
||||
visible: titleContainer.enoughSpace
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: timestamp
|
||||
|
||||
anchors.left: titleContainer.enoughSpace ? timeSep.right : parent.left
|
||||
anchors.leftMargin: titleContainer.enoughSpace ? Tokens.spacing.small : 0
|
||||
anchors.top: titleContainer.enoughSpace ? undefined : title.bottom
|
||||
anchors.topMargin: Tokens.spacing.extraSmall
|
||||
anchors.verticalCenter: titleContainer.enoughSpace ? title.verticalCenter : undefined
|
||||
color: root.chatItem.ListView.isCurrentItem ? Qt.alpha(Colors.palette.m3onPrimary, 0.9) : Colors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Tokens.font.size.small
|
||||
text: root.modelData.updatedAt.toLocaleString(Qt.locale("en_US"), "MMM d, yyyy - h:mm AP")
|
||||
onEditingFinished: root.modelData.title = text
|
||||
onReadOnlyChanged: {
|
||||
if (!readOnly) {
|
||||
this.forceActiveFocus();
|
||||
cursorPosition = text.length;
|
||||
} else {
|
||||
root.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: root.chatItem.ListView.isCurrentItem ? Qt.alpha(Colors.palette.m3onPrimary, 0.9) : Colors.palette.m3onSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
id: created
|
||||
|
||||
anchors.top: title.bottom
|
||||
anchors.topMargin: Tokens.spacing.extraSmall
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
font.pointSize: Tokens.font.size.small
|
||||
maximumLineCount: 1
|
||||
text: root.modelData.messages[root.modelData.messages.length - 1]?.content.replace(/\s+/g, " ").trim() ?? ""
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
text: qsTr("Created on: %1").arg(root.modelData.createdAt.toLocaleString(Qt.locale("en_US"), "MMM d, yyyy - h:mm AP"))
|
||||
opacity: root.expanded ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: updated
|
||||
|
||||
anchors.top: created.bottom
|
||||
anchors.topMargin: Tokens.spacing.small
|
||||
anchors.left: parent.left
|
||||
font.pointSize: Tokens.font.size.small
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
text: qsTr("Updated on: ")
|
||||
opacity: root.expanded ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: timestamp
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.top: titleText.bottom
|
||||
anchors.topMargin: Tokens.spacing.extraSmall
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Tokens.font.size.small
|
||||
text: root.modelData.updatedAt.toLocaleString(Qt.locale("en_US"), "MMM d, yyyy - h:mm AP")
|
||||
|
||||
states: State {
|
||||
name: "expanded"
|
||||
when: root.expanded
|
||||
|
||||
AnchorChanges {
|
||||
target: timestamp
|
||||
anchors.left: updated.right
|
||||
anchors.top: undefined
|
||||
anchors.verticalCenter: updated.verticalCenter
|
||||
}
|
||||
}
|
||||
transitions: Transition {
|
||||
AnchorAnim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: preview
|
||||
|
||||
readonly property int topMargin: Tokens.spacing.extraSmall
|
||||
|
||||
anchors.left: chatIcon.right
|
||||
anchors.margins: Tokens.spacing.medium
|
||||
anchors.right: parent.right
|
||||
y: infoContainer.implicitHeight + topMargin
|
||||
elide: Text.ElideRight
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Tokens.font.size.small
|
||||
maximumLineCount: 1
|
||||
opacity: !root.expanded ? 1 : 0
|
||||
text: root.modelData.messagesModel.lastMessage?.activeGeneration.content.replace(/\s+/g, " ").trim() ?? ""
|
||||
|
||||
Behavior on y {
|
||||
Anim {}
|
||||
}
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: body
|
||||
|
||||
readonly property int topMargin: root.expanded ? Tokens.spacing.medium : Tokens.spacing.extraSmall
|
||||
|
||||
anchors.left: chatIcon.right
|
||||
anchors.margins: Tokens.spacing.medium
|
||||
anchors.right: parent.right
|
||||
y: infoContainer.implicitHeight + topMargin
|
||||
animate: true
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
maximumLineCount: 5
|
||||
opacity: root.expanded ? 1 : 0
|
||||
text: root.modelData.messagesModel.lastMessage?.activeGeneration.content ?? ""
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
|
||||
Behavior on y {
|
||||
Anim {}
|
||||
}
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
id: sLayer
|
||||
|
||||
property int startY
|
||||
|
||||
onClicked: {
|
||||
root.clicked(root.modelData);
|
||||
}
|
||||
}
|
||||
|
||||
ButtonRow {
|
||||
id: actionRow
|
||||
|
||||
anchors.top: body.bottom
|
||||
anchors.left: chatIcon.right
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: Tokens.spacing.small
|
||||
anchors.leftMargin: Tokens.spacing.medium
|
||||
anchors.rightMargin: Tokens.padding.medium
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
IconButton {
|
||||
icon: "close"
|
||||
fillWidth: true
|
||||
shapeMorph: true
|
||||
isRound: true
|
||||
font.pointSize: Tokens.font.size.large
|
||||
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
|
||||
inactiveOnColor: Colors.palette.m3onSurfaceVariant
|
||||
|
||||
onClicked: root.remove(root.modelData)
|
||||
}
|
||||
|
||||
IconButton {
|
||||
icon: titleText.readOnly ? "edit" : "check"
|
||||
label.animate: true
|
||||
isToggle: true
|
||||
fillWidth: true
|
||||
shapeMorph: true
|
||||
font.pointSize: Tokens.font.size.large
|
||||
isRound: true
|
||||
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
|
||||
inactiveOnColor: Colors.palette.m3onSurfaceVariant
|
||||
activeColor: Colors.palette.m3secondary
|
||||
activeOnColor: Colors.palette.m3onSecondary
|
||||
checked: !titleText.readOnly
|
||||
|
||||
onClicked: titleText.readOnly = !titleText.readOnly
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: expandBtn
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Tokens.padding.small
|
||||
implicitHeight: expandIcon.implicitHeight
|
||||
implicitWidth: expandIcon.implicitHeight
|
||||
radius: Tokens.rounding.full
|
||||
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 3)
|
||||
|
||||
StateLayer {
|
||||
onClicked: root.expanded = !root.expanded
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: expandIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: root.expanded ? -1 : 1
|
||||
rotation: root.expanded ? 180 : 0
|
||||
text: "expand_more"
|
||||
|
||||
Behavior on anchors.verticalCenterOffset {
|
||||
Anim {}
|
||||
}
|
||||
Behavior on rotation {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
TextFieldBase {
|
||||
id: root
|
||||
|
||||
readonly property alias bg: bg
|
||||
readonly property alias sendIcon: sendIcon
|
||||
|
||||
signal sendPressed
|
||||
|
||||
bottomPadding: Tokens.padding.large
|
||||
leftPadding: Tokens.padding.extraLarge
|
||||
rightPadding: sendIcon.width + sendIcon.anchors.rightMargin + Tokens.spacing.small
|
||||
topPadding: Tokens.padding.large
|
||||
wrapMode: TextFieldBase.Wrap
|
||||
|
||||
background: CustomRect {
|
||||
id: bg
|
||||
|
||||
anchors.fill: parent
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
radius: Tokens.rounding.extraLarge
|
||||
|
||||
StateLayer {
|
||||
id: stateLayer
|
||||
|
||||
cursorShape: Qt.IBeamCursor
|
||||
enabled: !root.activeFocus
|
||||
manualPressOverride: tapHandler.pressed
|
||||
|
||||
onClicked: root.focus = true
|
||||
}
|
||||
}
|
||||
|
||||
onPressed: {
|
||||
if (stateLayer.enabled)
|
||||
stateLayer.press(stateLayer.mouseX, stateLayer.mouseY);
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: placeholder
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.leftPadding
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: root.placeholderTextColor
|
||||
font: root.font
|
||||
opacity: root.text ? 0 : 1
|
||||
text: root.placeholderText
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: sendIcon
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Tokens.padding.small
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
enabled: root.text
|
||||
icon: "upward_arrow"
|
||||
opacity: root.text ? 1 : 0
|
||||
radius: Tokens.rounding.full
|
||||
radiusMorph: false
|
||||
stateLayer.hoverEnabled: enabled
|
||||
type: IconButton.Text
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: root.sendPressed()
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
id: tapHandler
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ Item {
|
||||
property alias model: list.model
|
||||
|
||||
signal deleteChatRequest(content: ChatSession)
|
||||
signal loadChatRequest(content: ChatSession, index: int)
|
||||
signal loadChatRequest(content: ChatSession)
|
||||
signal newChatRequest
|
||||
|
||||
CustomText {
|
||||
@@ -44,140 +44,13 @@ Item {
|
||||
clip: true
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
delegate: Component {
|
||||
Item {
|
||||
id: chatItem
|
||||
delegate: ChatDelegate {
|
||||
id: chat
|
||||
|
||||
property alias chat: chat
|
||||
property bool closing: false
|
||||
property bool editVisible: false
|
||||
required property int index
|
||||
required property var modelData
|
||||
property real startY: 0.0
|
||||
implicitWidth: ListView.view.width
|
||||
|
||||
anchors.fill: undefined
|
||||
implicitHeight: chat.layout.implicitHeight + chat.layout.anchors.topMargin + chat.layout.anchors.bottomMargin
|
||||
implicitWidth: ListView.view.width
|
||||
|
||||
onEditVisibleChanged: if (!editVisible)
|
||||
chat.x = 0
|
||||
|
||||
ParallelAnimation {
|
||||
id: removeAnim
|
||||
|
||||
onFinished: root.deleteChatRequest(chatItem.modelData)
|
||||
onStarted: chatItem.closing = true
|
||||
|
||||
Anim {
|
||||
property: "implicitHeight"
|
||||
target: chatItem
|
||||
to: 0
|
||||
}
|
||||
|
||||
Anim {
|
||||
property: "x"
|
||||
target: chat
|
||||
to: -chat.implicitWidth
|
||||
}
|
||||
|
||||
Anim {
|
||||
property: "opacity"
|
||||
target: chatItem
|
||||
to: 0
|
||||
}
|
||||
}
|
||||
|
||||
ChatDelegate {
|
||||
id: chat
|
||||
|
||||
chatItem: chatItem
|
||||
implicitWidth: chatItem.implicitWidth
|
||||
modelData: chatItem.modelData
|
||||
radius: Tokens.rounding.medium
|
||||
|
||||
Behavior on x {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
cursorShape: drag.active ? Qt.ClosedHandCursor : Qt.PointingHandCursor
|
||||
drag.axis: Drag.XAxis
|
||||
drag.target: chat
|
||||
hoverEnabled: true
|
||||
preventStealing: true
|
||||
|
||||
onClicked: {
|
||||
if (chatItem.editVisible)
|
||||
chatItem.editVisible = false;
|
||||
else
|
||||
root.loadChatRequest(chatItem.modelData, chatItem.index);
|
||||
}
|
||||
onPressed: event => {
|
||||
chatItem.startY = event.y;
|
||||
}
|
||||
onReleased: event => {
|
||||
if (chat.x > -(editTools.implicitWidth + Tokens.padding.extraSmall)) {
|
||||
chat.x = 0;
|
||||
} else {
|
||||
chatItem.editVisible = true;
|
||||
chat.x = -(editTools.implicitWidth + Tokens.spacing.medium + Tokens.padding.extraSmall);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
anchors.left: chat.right
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Tokens.padding.extraSmall
|
||||
anchors.verticalCenter: chat.verticalCenter
|
||||
implicitHeight: editTools.implicitHeight
|
||||
|
||||
RowLayout {
|
||||
id: editTools
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
IconButton {
|
||||
id: deleteChatBtn
|
||||
|
||||
font.pointSize: Tokens.font.size.large
|
||||
icon: "close"
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
transform: [
|
||||
Scale {
|
||||
property bool shouldBeActive: (chat.x < -deleteChatBtn.implicitWidth) && !chatItem.closing
|
||||
|
||||
origin.x: deleteChatBtn.implicitWidth / 2
|
||||
origin.y: deleteChatBtn.implicitWidth / 2
|
||||
xScale: shouldBeActive ? 1 : 0
|
||||
yScale: shouldBeActive ? 1 : 0
|
||||
|
||||
Behavior on xScale {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on yScale {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
onClicked: {
|
||||
removeAnim.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onClicked: content => root.loadChatRequest(content)
|
||||
onRemove: content => root.deleteChatRequest(content)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,20 +43,19 @@ Item {
|
||||
active: ChatState.isWindow && root.width > (ChatState.screen.width / 4)
|
||||
anchors.fill: parent
|
||||
|
||||
sourceComponent: SidebarView {
|
||||
}
|
||||
sourceComponent: SidebarView {}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
anchors.right: parent.right
|
||||
icon: "close"
|
||||
visible: !ChatState.isWindow
|
||||
|
||||
onClicked: {
|
||||
Detach.create();
|
||||
Visibilities.getForActive().sidebar = false;
|
||||
}
|
||||
}
|
||||
// IconButton {
|
||||
// anchors.right: parent.right
|
||||
// icon: "close"
|
||||
// visible: !ChatState.isWindow
|
||||
//
|
||||
// onClicked: {
|
||||
// Detach.create();
|
||||
// Visibilities.getForActive().sidebar = false;
|
||||
// }
|
||||
// }
|
||||
|
||||
Component {
|
||||
id: chatList
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
import ZShell.Components
|
||||
import ZShell.Llm
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ChatGeneration current
|
||||
required property TextEdit edit
|
||||
required property bool hovered
|
||||
readonly property bool isUser: message.role === ChatMessage.Role.User
|
||||
required property ChatMessage message
|
||||
|
||||
implicitHeight: root.current.content !== "" ? editButton.implicitHeight : 0
|
||||
implicitWidth: generationTools.implicitWidth + editTools.implicitWidth + Tokens.spacing.small
|
||||
|
||||
ButtonRow {
|
||||
id: generationTools
|
||||
|
||||
enabled: visible
|
||||
visible: !root.isUser
|
||||
|
||||
IconButton {
|
||||
enabled: root.message.generationCount > 1 && root.message.activeGenerationIndex !== 0
|
||||
icon: "chevron_left"
|
||||
type: IconButton.Tonal
|
||||
|
||||
onClicked: {
|
||||
if (!root.edit.readOnly)
|
||||
root.edit.readOnly = true;
|
||||
root.message.setActiveGeneration(root.message.activeGenerationIndex - 1);
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
enabled: false
|
||||
icon: `${root.message.activeGenerationIndex + 1}`
|
||||
label.font.family: Config.appearance.font.family.sans
|
||||
label.font.pointSize: Tokens.font.size.small
|
||||
type: IconButton.Text
|
||||
}
|
||||
|
||||
IconButton {
|
||||
enabled: root.message.generationCount > 1 && root.message.activeGenerationIndex !== root.message.generationCount - 1
|
||||
icon: "chevron_right"
|
||||
type: IconButton.Tonal
|
||||
|
||||
onClicked: {
|
||||
if (!root.edit.readOnly)
|
||||
root.edit.readOnly = true;
|
||||
root.message.setActiveGeneration(root.message.activeGenerationIndex + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ButtonRow {
|
||||
id: editTools
|
||||
|
||||
anchors.right: parent.right
|
||||
opacity: !root.current.streaming && root.hovered ? 1 : 0
|
||||
spacing: Tokens.spacing.small
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
icon: "refresh"
|
||||
inactiveColor: Colors.palette.m3secondary
|
||||
inactiveOnColor: Colors.palette.m3onSecondary
|
||||
scale: root.edit.readOnly ? 1 : 0
|
||||
visible: !root.isUser
|
||||
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
root.message.retry();
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
icon: "content_copy"
|
||||
inactiveColor: Colors.palette.m3secondary
|
||||
inactiveOnColor: Colors.palette.m3onSecondary
|
||||
scale: root.edit.readOnly ? 1 : 0
|
||||
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: Quickshell.clipboardText = root.current.content
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: editButton
|
||||
|
||||
icon: root.edit.readOnly ? "edit" : "check"
|
||||
inactiveColor: Colors.palette.m3tertiary
|
||||
inactiveOnColor: Colors.palette.m3onTertiary
|
||||
|
||||
onClicked: root.edit.readOnly = !root.edit.readOnly
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Colors.tPalette.m3outlineVariant
|
||||
font.pointSize: 36
|
||||
text: "chat"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Colors.tPalette.m3onSurfaceVariant
|
||||
text: qsTr("No messages yet")
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
import ZShell.Llm
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
MouseArea {
|
||||
id: root
|
||||
|
||||
readonly property ChatGeneration current: modelData.activeGeneration
|
||||
required property int index
|
||||
readonly property bool isUser: modelData.role === ChatMessage.Role.User
|
||||
required property ChatMessage modelData
|
||||
property bool reasoningExpanded: false
|
||||
property real savedContentY: -1
|
||||
|
||||
function handleReasoningToggle(expanded: bool): void {
|
||||
const view = root.ListView.view;
|
||||
if (!view)
|
||||
return;
|
||||
|
||||
if (expanded) {
|
||||
root.savedContentY = view.contentY;
|
||||
root.reasoningExpanded = true;
|
||||
} else {
|
||||
root.reasoningExpanded = false;
|
||||
if (root.savedContentY !== -1) {
|
||||
restoreAnim.to = root.savedContentY;
|
||||
restoreAnim.restart();
|
||||
root.savedContentY = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.isUser ? 0 : Tokens.spacing.extraSmall
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: root.isUser ? Tokens.spacing.extraSmall : 0
|
||||
hoverEnabled: true
|
||||
// Explicit, synchronous height — sum of children, no Layout polish involved.
|
||||
implicitHeight: reasoningLoader.implicitHeight + (reasoningLoader.active ? Tokens.spacing.medium : 0) + bubble.implicitHeight + actionsRow.implicitHeight + actionsRow.anchors.topMargin
|
||||
preventStealing: true
|
||||
|
||||
Binding {
|
||||
property: "contentY"
|
||||
restoreMode: Binding.RestoreNone
|
||||
target: root.ListView.view
|
||||
value: root.y - Tokens.padding.large * 2
|
||||
when: root.reasoningExpanded && root.ListView.view && ((root.y - Tokens.padding.large * 2) < root.ListView.view.contentY)
|
||||
}
|
||||
|
||||
Anim {
|
||||
id: restoreAnim
|
||||
|
||||
property: "contentY"
|
||||
target: root.ListView.view
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: reasoningLoader
|
||||
|
||||
readonly property bool shouldBeActive: root.current.reasoningActive || root.current.content === ""
|
||||
|
||||
active: !root.isUser
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Tokens.spacing.extraLarge
|
||||
anchors.top: parent.top
|
||||
|
||||
sourceComponent: Reasoning {
|
||||
current: root.current
|
||||
|
||||
onExpandedChanged: {
|
||||
root.handleReasoningToggle(expanded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: bubble
|
||||
|
||||
anchors.left: root.isUser ? undefined : parent.left
|
||||
anchors.right: root.isUser ? parent.right : undefined
|
||||
anchors.top: reasoningLoader.active ? reasoningLoader.bottom : parent.top
|
||||
anchors.topMargin: reasoningLoader.active ? Tokens.spacing.medium : 0
|
||||
color: root.isUser ? Colors.palette.m3primary : Colors.palette.m3surfaceContainer
|
||||
implicitHeight: root.current.content !== "" ? msgText.contentHeight + Tokens.padding.medium * 2 : 0
|
||||
implicitWidth: Math.min(msgText.implicitWidth + Tokens.padding.medium * 2, root.width - Tokens.spacing.extraSmall - Tokens.spacing.extraLarge)
|
||||
radius: Tokens.rounding.medium
|
||||
visible: !root.current.reasoningActive
|
||||
|
||||
TextEditBase {
|
||||
id: msgText
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.medium
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
animateCursor: false
|
||||
color: root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
cursor.color: root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3primary
|
||||
font.pointSize: Tokens.font.size.smaller
|
||||
readOnly: true
|
||||
selectionColor: Qt.alpha((root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3primary), 0.4)
|
||||
text: root.current.content
|
||||
textFormat: CustomText.MarkdownText
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key == Qt.Key_Return) {
|
||||
if (!(event.modifiers & Qt.ShiftModifier)) {
|
||||
readOnly = true;
|
||||
event.accepted = true;
|
||||
}
|
||||
} else if (event.key == Qt.Key_Escape) {
|
||||
text = root.current.content;
|
||||
readOnly = true;
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
onEditingFinished: {
|
||||
const old = root.current.content;
|
||||
if (old !== text) {
|
||||
root.modelData.edit(text);
|
||||
|
||||
if (root.isUser)
|
||||
root.modelData.generate();
|
||||
}
|
||||
|
||||
readOnly = true;
|
||||
}
|
||||
onReadOnlyChanged: {
|
||||
if (readOnly) {
|
||||
animateCursor = false;
|
||||
root.forceActiveFocus();
|
||||
textFormat = CustomText.MarkdownText;
|
||||
} else {
|
||||
var raw = root.current.content;
|
||||
textFormat = CustomText.PlainText;
|
||||
text = raw;
|
||||
forceActiveFocus();
|
||||
cursorPosition = text.length;
|
||||
animateCursor = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Actions {
|
||||
id: actionsRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: (implicitWidth > bubble.implicitWidth) ? undefined : bubble.right
|
||||
anchors.top: bubble.bottom
|
||||
anchors.topMargin: Tokens.spacing.medium
|
||||
current: root.current
|
||||
edit: msgText
|
||||
hovered: root.containsMouse
|
||||
message: root.modelData
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
import ZShell.Llm
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ChatGeneration current
|
||||
property bool expanded: false
|
||||
readonly property bool isReasoning: root.current.reasoningActive || root.current.content === ""
|
||||
|
||||
implicitHeight: expandedRect.implicitHeight + collapsedText.implicitHeight + expandedRect.anchors.topMargin
|
||||
|
||||
LoadingIndicator {
|
||||
id: spinnerReasoning
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
implicitSize: collapsedText.implicitHeight
|
||||
opacity: root.isReasoning ? 1 : 0
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: expandBtn
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.medium
|
||||
anchors.verticalCenter: collapsedText.verticalCenter
|
||||
font.pointSize: Tokens.font.size.large
|
||||
icon: "keyboard_arrow_down"
|
||||
inactiveOnColor: hovered ? Colors.palette.m3onSurface : Colors.palette.m3outline
|
||||
opacity: root.isReasoning ? 0 : 1
|
||||
rotation: root.expanded ? 180 : 0
|
||||
type: IconButton.Text
|
||||
|
||||
Behavior on rotation {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: root.expanded = !root.expanded
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: collapsedText
|
||||
|
||||
anchors.left: root.current.reasoningActive ? spinnerReasoning.right : expandBtn.right
|
||||
anchors.margins: Tokens.padding.medium
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 0
|
||||
color: Colors.palette.m3outline
|
||||
font.pointSize: Tokens.font.size.small
|
||||
text: root.current.reasoningActive ? qsTr("Thinking...") : qsTr("Thought for %1s").arg((root.current.reasoningElapsedMs / 1000).toFixed(1))
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
id: expandedRect
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: collapsedText.bottom
|
||||
anchors.topMargin: Tokens.spacing.medium
|
||||
color: Colors.palette.m3surfaceContainerLow
|
||||
implicitHeight: root.expanded ? expandedText.implicitHeight + expandedText.anchors.margins * 2 : -anchors.topMargin
|
||||
implicitWidth: expandedText.implicitWidth + expandedText.anchors.margins * 2
|
||||
opacity: root.expanded ? 1 : 0
|
||||
radius: Tokens.rounding.medium
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: expandedText
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.medium
|
||||
color: Colors.palette.m3onSurface
|
||||
font.pointSize: Tokens.font.size.small
|
||||
text: root.current.reasoning
|
||||
textFormat: CustomText.MarkdownText
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,26 +8,49 @@ import ZShell.Llm
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
RowLayout {
|
||||
Item {
|
||||
id: root
|
||||
|
||||
ChatList {
|
||||
id: chatList
|
||||
property int breakpoint: 700
|
||||
property alias conversationModel: sidebar.model
|
||||
property var currentConversation: null // whatever model item is "open"
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Config.sidebar.sizes.width
|
||||
Layout.minimumWidth: Config.sidebar.sizes.width / 2
|
||||
readonly property bool isWide: root.width >= root.breakpoint
|
||||
property bool narrowShowsSidebar: true
|
||||
|
||||
function closeConversation() {
|
||||
narrowShowsSidebar = true;
|
||||
}
|
||||
|
||||
function openConversation(conv) {
|
||||
currentConversation = conv;
|
||||
narrowShowsSidebar = false; // in narrow mode, jump to the chat
|
||||
}
|
||||
|
||||
ChatList {
|
||||
id: sidebar
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: root.isWide ? undefined : parent.right
|
||||
anchors.top: parent.top
|
||||
implicitWidth: 20
|
||||
list.currentIndex: -1
|
||||
list.highlightFollowsCurrentItem: false
|
||||
|
||||
Behavior on anchors.right {
|
||||
AnchorAnim {}
|
||||
}
|
||||
Behavior on implicitWidth {
|
||||
Anim {}
|
||||
}
|
||||
list.highlight: CustomRect {
|
||||
color: Colors.palette.m3primary
|
||||
implicitHeight: chatList.list.currentItem?.implicitHeight ?? 0
|
||||
implicitWidth: chatList.list.width
|
||||
implicitHeight: sidebar.list.currentItem?.implicitHeight ?? 0
|
||||
implicitWidth: sidebar.list.width
|
||||
radius: Tokens.rounding.medium
|
||||
x: chatList.list.currentItem?.chat.x ?? 0
|
||||
y: chatList.list.currentItem?.y ?? 0
|
||||
x: sidebar.list.currentItem?.chat.x ?? 0
|
||||
y: sidebar.list.currentItem?.y ?? 0
|
||||
|
||||
Behavior on y {
|
||||
Anim {
|
||||
@@ -40,18 +63,29 @@ RowLayout {
|
||||
values: Chat.chats.values
|
||||
}
|
||||
|
||||
anchors.onRightChanged: {
|
||||
if (anchors.right === undefined)
|
||||
implicitWidth = 20;
|
||||
}
|
||||
onLoadChatRequest: (chat, index) => {
|
||||
chatList.list.currentIndex = index;
|
||||
chatContent.chatData = chat;
|
||||
sidebar.list.currentIndex = index;
|
||||
root.openConversation(chat);
|
||||
}
|
||||
}
|
||||
|
||||
ChatContent {
|
||||
id: chatContent
|
||||
CustomClippingWrapperRect {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: sidebar.right
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
child: conversationView
|
||||
}
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: Config.sidebar.sizes.width
|
||||
Layout.preferredWidth: Math.round(Config.sidebar.sizes.width * 1.5)
|
||||
ChatContent {
|
||||
id: conversationView
|
||||
|
||||
anchors.fill: root
|
||||
anchors.leftMargin: root.isWide ? Config.sidebar.sizes.width / 2 : 0
|
||||
chatData: root.currentConversation
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,14 +45,21 @@ Item {
|
||||
Item {
|
||||
id: pages
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.top: parent.top
|
||||
implicitWidth: parent.width
|
||||
opacity: root.props.currentTab === 0 ? 1 : 0
|
||||
visible: opacity > 0.01
|
||||
visible: opacity > 0
|
||||
x: root.props.currentTab === 0 ? 0 : -root.width
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on x {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
anchors.fill: parent
|
||||
@@ -69,15 +76,22 @@ Item {
|
||||
Item {
|
||||
id: chatPage
|
||||
|
||||
anchors.fill: parent
|
||||
opacity: visible ? 1 : 0
|
||||
visible: root.props.currentTab === 1
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.top: parent.top
|
||||
implicitWidth: parent.width
|
||||
opacity: root.props.currentTab === 1 ? 1 : 0
|
||||
visible: opacity > 0
|
||||
x: root.props.currentTab === 0 ? root.width : 0
|
||||
z: 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on x {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
anchors.fill: parent
|
||||
|
||||
@@ -12,13 +12,11 @@ import qs.Services
|
||||
import qs.Helpers
|
||||
import qs.Daemons
|
||||
import qs.Modules.Settings
|
||||
import qs.Modules.Bar.Popouts as BarPopouts
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
readonly property bool needExtraRow: quickToggles.length > 6
|
||||
required property BarPopouts.Wrapper popouts
|
||||
readonly property var quickToggles: {
|
||||
const seenIds = new Set();
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Modules.Bar.Popouts as BarPopouts
|
||||
import qs.Modules.Notifications.Sidebar.Utils.Cards
|
||||
import ZShell.Config
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property BarPopouts.Wrapper popouts
|
||||
required property PersistentProperties props
|
||||
required property var visibilities
|
||||
|
||||
@@ -21,8 +19,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
spacing: 8
|
||||
|
||||
IdleInhibit {
|
||||
}
|
||||
IdleInhibit {}
|
||||
|
||||
Record {
|
||||
props: root.props
|
||||
@@ -32,7 +29,6 @@ Item {
|
||||
|
||||
Toggles {
|
||||
Layout.fillWidth: true
|
||||
popouts: root.popouts
|
||||
visibilities: root.visibilities
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,11 @@ import QtQuick
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Modules.Bar.Popouts as BarPopouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real offsetScale: shouldBeActive ? 0 : 1
|
||||
required property BarPopouts.Wrapper popouts
|
||||
readonly property PersistentProperties props: PersistentProperties {
|
||||
property string recordingConfirmDelete
|
||||
property bool recordingListExpanded: false
|
||||
@@ -18,17 +16,14 @@ Item {
|
||||
|
||||
reloadableId: "utilities"
|
||||
}
|
||||
readonly property bool shouldBeActive: visibilities.sidebar
|
||||
readonly property bool shouldBeActive: visibilities.sidebar && !sidebar.chatActive
|
||||
required property Item sidebar
|
||||
required property var visibilities
|
||||
|
||||
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
|
||||
clip: chatActive
|
||||
implicitHeight: chatActive ? 21 : content.implicitHeight + 8 * 2
|
||||
implicitHeight: content.implicitHeight + Tokens.padding.small * 2
|
||||
implicitWidth: sidebar.width * (1 - sidebar.offsetScale)
|
||||
opacity: (1 - offsetScale) * chatFade
|
||||
readonly property bool chatActive: sidebar.chatActive
|
||||
property real chatFade: chatActive ? 0 : 1
|
||||
opacity: 1 - offsetScale
|
||||
visible: offsetScale < 1
|
||||
|
||||
Behavior on offsetScale {
|
||||
@@ -38,20 +33,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on chatFade {
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||
easing: Tokens.anim.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||
easing: Tokens.anim.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: content
|
||||
|
||||
@@ -62,7 +43,6 @@ Item {
|
||||
|
||||
sourceComponent: Content {
|
||||
implicitWidth: root.implicitWidth - 8 * 2
|
||||
popouts: root.popouts
|
||||
props: root.props
|
||||
visibilities: root.visibilities
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user