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 / 4 : -root.width / 4 from: 0 type: Anim.FastEffects } Anim { target: root property: "opacity" from: 1 to: 0 type: Anim.FastEffects } } PropertyAction {} ParallelAnimation { Anim { target: root property: "x" from: changeAnim.next ? -root.width / 4 : root.width / 4 type: Anim.FastEffects to: 0 } Anim { target: root property: "opacity" from: 0 type: Anim.FastEffects to: 1 } } } } Binding { property: "contentY" restoreMode: Binding.RestoreNone target: root.ListView.view value: root.y + root.height + Tokens.padding.large * 2 - root.ListView.view.height when: root.reasoningExpanded && root.ListView.view && (root.y + root.height + Tokens.padding.large * 2 > root.ListView.view.contentY + root.ListView.view.height) } 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 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(); } } } } } } }