import QtQuick import QtQuick.Layouts import ZShell.Llm import ZShell.Config import qs.Modules.Notifications.Sidebar.Chat import qs.Components import qs.Services Item { id: root required property bool isUser required property LlmSegment segment required property bool hovered required property Repeater repeater required property int index required property ChatMessage message required property ChatGeneration current readonly property real contentMaxWidth: width - Tokens.spacing.extraSmall - Tokens.spacing.extraLarge signal edit(text: string) implicitHeight: bubble.implicitHeight + actionsRow.implicitHeight + actionsRow.anchors.topMargin CustomClippingRect { id: bubble property real liveTargetWidth: root.isUser ? Math.min(msgText.implicitWidth + Tokens.padding.medium * 2, root.contentMaxWidth) : root.contentMaxWidth property real frozenWidth: 0.0 property bool layoutFrozen: false property real layoutWidth: root.isUser ? Math.min(msgText.implicitWidth + Tokens.padding.medium * 2, root.contentMaxWidth) : root.contentMaxWidth radius: Tokens.rounding.medium color: root.isUser ? Colors.palette.m3primary : Colors.palette.m3surfaceContainer implicitWidth: layoutFrozen ? frozenWidth : layoutWidth implicitHeight: root.isUser ? msgText.contentHeight + Tokens.padding.medium * 2 : blocks.implicitHeight + blocks.anchors.topMargin * 2 anchors.right: root.isUser ? parent.right : undefined layer.enabled: false layer.smooth: true transform: Scale { id: stretchScale origin.x: root.isUser ? bubble.width : 0 xScale: bubble.layoutFrozen && bubble.frozenWidth > 0 ? bubble.liveTargetWidth / bubble.frozenWidth : 1.0 } states: [ State { name: "toSlim" when: !ChatState.isWide && !ChatState.settledSlim PropertyChanges { bubble.layer.enabled: true bubble.layoutFrozen: true } }, State { name: "toWide" when: ChatState.isWide && !ChatState.settledWide PropertyChanges { bubble.layer.enabled: true bubble.layoutFrozen: true } } ] onLayoutFrozenChanged: { if (layoutFrozen) frozenWidth = width; } TextEditBase { id: msgText property string cachedText: root.segment.text property bool cancelled: false visible: root.isUser anchors.left: parent.left anchors.margins: Tokens.padding.medium anchors.right: parent.right anchors.top: parent.top animateCursor: false color: root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface cursor.color: root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3primary font.pointSize: Tokens.font.size.smaller readOnly: true selectionColor: Qt.alpha((root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3primary), 0.4) text: root.isUser ? root.segment.text : "" textFormat: CustomText.MarkdownText wrapMode: Text.WordWrap Keys.onPressed: event => { if (event.key == Qt.Key_Return) { if (!(event.modifiers & Qt.ShiftModifier)) { readOnly = true; event.accepted = true; } } else if (event.key == Qt.Key_Escape) { cancelled = true; readOnly = true; event.accepted = true; } } onEditingFinished: { readOnly = true; } onReadOnlyChanged: { if (readOnly) { animateCursor = false; textFormat = CustomText.MarkdownText; if (cancelled) { text = cachedText; cancelled = false; return; } root.edit(text); } else { textFormat = CustomText.PlainText; text = cachedText; forceActiveFocus(); cursorPosition = text.length; animateCursor = true; } } } // Assistant content: parsed markdown blocks. MarkdownBlocks { id: blocks visible: !root.isUser anchors.topMargin: Tokens.padding.medium anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right blocks: root.segment.markdown } } Actions { id: actionsRow readonly property bool shouldBeActive: (root.segment.type === LlmSegment.Type.Content) && !root.segment.running && root.repeater.count === (root.index + 1) anchors.left: parent.left anchors.right: (implicitWidth > bubble.implicitWidth) ? undefined : bubble.right anchors.top: bubble.bottom opacity: shouldBeActive ? 1 : 0 visible: opacity > 0 anchors.topMargin: Tokens.spacing.medium edit: msgText hovered: root.hovered isUser: root.isUser segment: root.segment current: root.current message: root.message Behavior on opacity { Anim {} } } }