import QtQuick import QtQuick.Layouts import ZShell.Config import ZShell.Llm import qs.Components import qs.Services CustomClippingRect { id: root required property int index required property var modelData required property var blocks readonly property var segments: modelData.segments readonly property bool isActive: index === blocks.length - 1 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: expanded ? expandedRect.implicitHeight + layout.implicitHeight + expandedRect.anchors.topMargin * 2 : layout.implicitHeight + Tokens.spacing.small Behavior on implicitHeight { Anim { type: Anim.DefaultEffects } } RowLayout { id: layout anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right Item { implicitHeight: expandBtn.implicitHeight implicitWidth: expandBtn.implicitWidth LoadingIndicator { id: spinnerReasoning implicitSize: collapsedText.implicitHeight anchors.centerIn: parent opacity: root.isActive ? 1 : 0 visible: opacity > 0 Behavior on opacity { Anim {} } } IconButton { id: expandBtn font.pointSize: Tokens.font.size.large icon: "keyboard_arrow_down" inactiveOnColor: hovered ? Colors.palette.m3onSurface : Colors.palette.m3outline anchors.centerIn: parent 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 color: Colors.palette.m3outline font.pointSize: Tokens.font.size.small Layout.fillWidth: true Layout.leftMargin: Tokens.spacing.medium 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 {} } } } CustomRect { id: expandedRect anchors.left: parent.left anchors.right: parent.right anchors.top: layout.bottom anchors.bottom: parent.bottom anchors.topMargin: Tokens.spacing.small anchors.bottomMargin: Tokens.spacing.small color: Colors.palette.m3surfaceContainerLow implicitHeight: expandedContent.contentHeight + expandedContent.anchors.margins * 2 opacity: root.expanded ? 1 : 0 radius: Tokens.rounding.medium visible: opacity > 0 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") } } } } }