import QtQuick import QtQuick.Layouts import ZShell.Config import ZShell.Llm import qs.Components import qs.Services Item { id: root required property var segments required property bool isActive property bool expanded: false readonly property LlmSegment lastSegment: root.segments[root.segments.length - 1] readonly property real totalElapsedMs: root.segments.reduce((sum, s) => sum + (s.elapsedMs ?? 0), 0) implicitHeight: expandedRect.implicitHeight + collapsedText.implicitHeight + expandedRect.anchors.topMargin LoadingIndicator { id: spinnerReasoning anchors.centerIn: expandBtn implicitSize: collapsedText.implicitHeight opacity: root.isActive ? 1 : 0 visible: opacity > 0 Behavior on opacity { Anim {} } } IconButton { id: expandBtn anchors.left: parent.left anchors.verticalCenter: collapsedText.verticalCenter font.pointSize: Tokens.font.size.large anchors.topMargin: 0 icon: "keyboard_arrow_down" inactiveOnColor: hovered ? Colors.palette.m3onSurface : Colors.palette.m3outline opacity: root.isActive ? 0 : 1 rotation: root.expanded ? 180 : 0 type: IconButton.Text Behavior on rotation { Anim {} } onClicked: root.expanded = !root.expanded } CustomText { id: collapsedText anchors.left: 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: { if (root.isActive) { if (root.lastSegment.type === LlmSegment.Type.ToolCall) return qsTr("Using %1...").arg(root.lastSegment.name); return qsTr("Thinking..."); } return qsTr("Worked for %1s").arg((root.totalElapsedMs / 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 ? expandedContent.contentHeight + expandedContent.anchors.margins * 2 : 0 opacity: root.expanded ? 1 : 0 radius: Tokens.rounding.medium visible: opacity > 0 Behavior on implicitHeight { Anim { type: Anim.DefaultEffects } } Behavior on opacity { Anim {} } ListView { id: expandedContent anchors.fill: parent anchors.margins: Tokens.padding.medium model: root.segments spacing: Tokens.spacing.medium delegate: Item { id: segment required property LlmSegment modelData required property int index implicitWidth: ListView.view.width implicitHeight: header.implicitHeight + content.implicitHeight + content.anchors.topMargin RowLayout { id: header anchors.left: parent.left anchors.right: parent.right spacing: Tokens.spacing.small MaterialIcon { text: segment.modelData.type === LlmSegment.Type.Reasoning ? "cognition" : "build" fill: 1 } CustomText { Layout.fillWidth: true text: segment.modelData.type === LlmSegment.Type.Reasoning ? qsTr("Thought for %1s").arg((segment.modelData.elapsedMs / 1000).toFixed(1)) : qsTr("Used %1 for %2s").arg(segment.modelData.name).arg((segment.modelData.elapsedMs / 1000).toFixed(1)) } } CustomText { id: content anchors.left: parent.left anchors.leftMargin: Tokens.padding.small anchors.top: header.bottom anchors.topMargin: Tokens.spacing.small anchors.right: parent.right color: Colors.palette.m3outline font.pointSize: Tokens.font.size.small wrapMode: CustomText.WrapAtWordBoundaryOrAnywhere text: segment.modelData.type === LlmSegment.Type.Reasoning ? segment.modelData.text : qsTr("Fetched %1").arg(JSON.parse(segment.modelData.arguments)?.url ?? "website") } } } } }