import QtQuick import ZShell.Components import ZShell.Config import ZShell.Llm import qs.Components import qs.Services CustomClippingRect { id: root property bool expanded: false required property ChatSession modelData signal clicked(content: ChatSession) signal remove(content: ChatSession) color: Colors.tPalette.m3surfaceContainer 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.top: parent.top anchors.topMargin: Tokens.padding.large font.pointSize: Tokens.font.size.extraLarge text: root.modelData.icon || "check" } Item { id: infoContainer anchors.left: chatIcon.right anchors.leftMargin: Tokens.spacing.medium anchors.top: parent.top 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: title 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 Behavior on opacity { Anim {} } } TextEditBase { id: titleText 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 {} } onEditingFinished: root.modelData.title = text onReadOnlyChanged: { if (!readOnly) { this.forceActiveFocus(); cursorPosition = text.length; } else { root.forceActiveFocus(); } } } CustomText { 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 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 {} } } } }