import QtQuick import QtQuick.Layouts import ZShell.Llm import ZShell.Config import qs.Modules.Sidebar.Chat import qs.Components import qs.Services Item { id: root required property bool isUser required property string text required property var blocks required property bool running required property bool hovered required property Repeater repeater required property int index required property ChatMessage message readonly property real contentMaxWidth: width - Tokens.spacing.extraSmall - Tokens.spacing.extraLarge signal edit(text: string) implicitHeight: bubble.implicitHeight + (actionsRow.shouldBeActive ? actionsRow.implicitHeight + actionsRow.anchors.topMargin : 0) CustomRect { 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 : markdown.implicitHeight + markdown.anchors.topMargin * 2 anchors.right: root.isUser ? parent.right : undefined clip: 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.isWindow && !ChatState.isWide && !ChatState.settledSlim PropertyChanges { bubble.layer.enabled: true bubble.layoutFrozen: true } }, State { name: "toWide" when: ChatState.isWindow && ChatState.isWide && !ChatState.settledWide PropertyChanges { bubble.layer.enabled: true bubble.layoutFrozen: true } } ] onLayoutFrozenChanged: { if (layoutFrozen) frozenWidth = width; } TextEditBase { id: msgText property string cachedText: root.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: Tokens.font.body.small readOnly: true selectionColor: Qt.alpha((root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3primary), 0.4) text: root.isUser ? root.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; } } } MarkdownBlocks { id: markdown visible: !root.isUser anchors.topMargin: Tokens.padding.medium anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right blocks: root.blocks } } Actions { id: actionsRow readonly property bool shouldBeActive: !root.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 running: root.running message: root.message Behavior on opacity { Anim {} } } }