import QtQuick import QtQuick.Layouts import ZShell.Llm import ZShell.Config 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 // Widest the bubble may grow to; assistant bubbles always use it so // the markdown blocks have a deterministic width to wrap against. 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 radius: Tokens.rounding.medium color: root.isUser ? Colors.palette.m3primary : Colors.palette.m3surfaceContainer implicitWidth: root.isUser ? Math.min(msgText.implicitWidth + Tokens.padding.medium * 2, root.contentMaxWidth) : root.contentMaxWidth implicitHeight: root.isUser ? msgText.contentHeight + Tokens.padding.medium * 2 : blocks.implicitHeight + blocks.anchors.topMargin * 2 anchors.right: root.isUser ? parent.right : undefined // Behavior on implicitHeight { // enabled: !root.segment.running // // Anim { // type: Anim.DefaultEffects // } // } // User messages stay a plain editable text field. 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 {} } } }