pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell import ZShell.Config import ZShell.Llm import qs.Components import qs.Services MouseArea { id: root 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 readonly property var blocks: blockify(current.segments) 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; } } } function blockify(segs) { const blocks = []; for (let i = 0; i < segs.length; i++) { const seg = segs[i]; if (seg.type === LlmSegment.Type.Content) { blocks.push({ kind: "content", id: i, segments: [seg] }); } else { const last = blocks.length ? blocks[blocks.length - 1] : null; if (last && last.kind === "process") { last.segments.push(seg); } else { blocks.push({ kind: "process", id: i, segments: [seg] }); } } } return blocks; } implicitWidth: ListView.view.width hoverEnabled: true preventStealing: false implicitHeight: layout.implicitHeight + Tokens.padding.medium Behavior on current { id: changeAnim property var object: targetProperty readonly property bool next: { const cond = root.modelData.generations.indexOf(current) < root.modelData.generations.indexOf(changeAnim.targetValue); return cond; } animation: SequentialAnimation { ParallelAnimation { Anim { target: root property: "x" to: changeAnim.next ? -root.width / 2 : root.width / 2 from: 0 } Anim { target: root property: "opacity" from: 1 to: 0 } } PropertyAction {} ParallelAnimation { Anim { target: root property: "x" from: changeAnim.next ? root.width / 2 : -root.width / 2 to: 0 } Anim { target: root property: "opacity" from: 0 to: 1 } } } } // Behavior on implicitHeight { // enabled: !root.isUser && root.current.streaming // // Anim {} // } onCurrentChanged: { console.log(modelData.generations.indexOf(current)); } // 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 } Column { id: layout anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right // onImplicitHeightChanged: console.log("LAYOUT:", layout.implicitHeight) // add: Transition { // Anim { // from: 10 // property: "y" // } // } // move: Transition { // Anim { // property: "y" // } // } Repeater { id: segmentRep model: ScriptModel { values: root.blocks objectProp: "id" } delegate: DelegateChooser { role: "kind" DelegateChoice { roleValue: "process" delegate: ProcessBlock { width: root.width blocks: root.blocks onExpandedChanged: root.handleReasoningToggle(expanded) } } DelegateChoice { roleValue: "content" delegate: ContentBubble { required property var modelData isUser: root.isUser segment: modelData.segments[0] width: root.width repeater: segmentRep message: root.modelData current: root.current hovered: root.containsMouse onEdit: text => { root.modelData.edit(text); if (root.isUser) root.modelData.generate(); } } } } } } }