import QtQuick import ZShell.Components import ZShell.Config import ZShell.Llm import qs.Effects import qs.Components import qs.Services CustomClippingRect { id: root property bool expanded: false property bool highlighted: false required property ChatSession modelData signal clicked(content: ChatSession) signal remove(content: ChatSession) color: highlighted ? Qt.alpha(Colors.palette.m3primaryContainer, 0.3) : Qt.alpha(Colors.tPalette.m3surfaceContainer, 0.2) 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 clip: true CustomRect { id: borderHighlight anchors.fill: parent border.width: root.highlighted ? 2 : 0 border.color: Colors.palette.m3secondary radius: root.radius - border.width + 1 Behavior on border.width { Anim {} } } 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" color: Colors.palette.m3onSurface } 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 {} } } Item { id: titleWrapper anchors.left: parent.left anchors.top: parent.top anchors.right: parent.right implicitHeight: titleText.implicitHeight + Tokens.padding.small * 2 layer.enabled: true layer.effect: OpacityMask { maskSource: fadeMask } TextFieldBase { 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 || qsTr("New chat") readOnly: true color: Colors.palette.m3onSurface 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(); } } } } CustomRect { id: fadeMask // anchors.top: titleText.top // anchors.bottom: titleText.bottom // anchors.left: titleText.left // anchors.right: parent.right anchors.fill: titleWrapper layer.enabled: true visible: false gradient: Gradient { orientation: Gradient.Horizontal GradientStop { color: Qt.rgba(1, 1, 1, 1.0) position: 0.85 } GradientStop { color: Qt.rgba(1, 1, 1, 0) position: 1.0 } } } 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: title.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() ?? qsTr("No messages yet") 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 ?? qsTr("No messages yet") 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.m3surfaceContainerHighest, 3) StateLayer { onClicked: root.expanded = !root.expanded } MaterialIcon { id: expandIcon anchors.centerIn: parent anchors.verticalCenterOffset: root.expanded ? -1 : 1 rotation: root.expanded ? 180 : 0 color: Colors.palette.m3onSurface text: "expand_more" Behavior on anchors.verticalCenterOffset { Anim {} } Behavior on rotation { Anim {} } } } }