// ChatBlockDelegate.qml — Delegate for rendering chat blocks in the ListView import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 ColumnLayout { id: root spacing: 2 // Required properties from model required property string blockType required property string blockName required property var blockSegments // Reference to parent ListView for scroll control property ListView parentListView: null // Expand state based on block type property bool blockExpanded: { switch (blockType) { case "user": case "assistant": case "tool": case "info": case "retry": case "interrupt": return true default: return false } } // Header color based on block type and theme function headerColor() { var bg = theme.baseColor if (theme.isDark) { switch (blockType) { case "user": return Qt.lighter(bg, 1.8) case "assistant": return Qt.lighter(bg, 1.5) case "reasoning": return Qt.lighter(bg, 1.3) case "call": return Qt.lighter(bg, 1.7) case "tool": return Qt.lighter(bg, 1.2) case "error": return Qt.rgba(0.6, 0.15, 0.15, 0.7) case "info": return Qt.lighter(bg, 1.4) case "retry": return Qt.rgba(0.6, 0.5, 0.0, 0.5) case "interrupt": return "#3d2e00" default: return bg } } else { switch (blockType) { case "user": return Qt.darker(bg, 1.12) case "assistant": return Qt.darker(bg, 1.06) case "reasoning": return Qt.darker(bg, 1.03) case "call": return Qt.darker(bg, 1.1) case "tool": return Qt.darker(bg, 1.04) case "error": return Qt.rgba(1.0, 0.8, 0.8, 0.7) case "info": return Qt.darker(bg, 1.02) case "retry": return Qt.rgba(1.0, 0.9, 0.6, 0.5) case "interrupt": return "#fff8e1" default: return bg } } } // Header label text based on block type function headerText() { switch (blockType) { case "user": return "\uD83E\uDDD1 You" case "assistant": return "\uD83E\uDD16 Ollie" case "reasoning": return "Reasoning" case "call": return "\u25B6\uFE0F " + blockName case "tool": return "\uD83D\uDDA5\uFE0F " + blockName case "error": return "\u274C Error" case "info": return "\u2139\uFE0F Info" case "retry": return "Retry" case "interrupt": return "\u26A0\uFE0F User Interrupt" default: return "" } } // Clickable header Rectangle { Layout.fillWidth: true height: 22 radius: 3 color: root.headerColor() MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: root.blockExpanded = !root.blockExpanded } RowLayout { anchors.fill: parent anchors.leftMargin: 6 spacing: 4 Label { text: root.blockExpanded ? "▼" : "▶" font.pixelSize: 8 opacity: 0.6 } Label { Layout.fillWidth: true text: root.headerText() font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.85 : 11 font.bold: true color: theme.textColor opacity: 0.9 } } } // Block content rendered as segments Column { Layout.fillWidth: true visible: root.blockExpanded spacing: 4 Repeater { model: root.blockSegments || [] delegate: Loader { width: parent ? parent.width : 0 active: modelData !== null && modelData !== undefined sourceComponent: (modelData && modelData.type === "diagram") ? diagramComponent : textComponent property var segmentData: modelData || {} } } } // Text segment component Component { id: textComponent TextEdit { id: textEdit width: parent ? parent.width : 100 text: segmentData.html || "" textFormat: Text.RichText readOnly: true selectByMouse: true persistentSelection: true wrapMode: TextEdit.Wrap font.family: theme.fontFamily || Qt.application.font.family font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12 selectionColor: theme.highlightColor selectedTextColor: theme.highlightedTextColor onActiveFocusChanged: { if (!activeFocus && root.parentListView) root.parentListView.interactive = true } Menu { id: contextMenu MenuItem { text: "Copy" enabled: textEdit.selectedText.length > 0 onTriggered: textEdit.copy() } MenuItem { text: "Select All" onTriggered: textEdit.selectAll() } } MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton propagateComposedEvents: true cursorShape: Qt.IBeamCursor onPressed: function(mouse) { if (root.parentListView) root.parentListView.interactive = false mouse.accepted = false } onReleased: function(mouse) { if (root.parentListView) root.parentListView.interactive = true mouse.accepted = false } } MouseArea { anchors.fill: parent acceptedButtons: Qt.RightButton cursorShape: Qt.IBeamCursor onClicked: contextMenu.popup() } } } // Diagram segment component Component { id: diagramComponent Rectangle { id: diagramRect width: parent ? parent.width : 100 height: diagramContent.height + diagramHeader.height + 8 color: "transparent" border.color: theme.midColor border.width: 1 radius: 4 property bool showSource: false RowLayout { id: diagramHeader anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.margins: 4 height: 24 Label { text: segmentData.lang || "diagram" font.pointSize: theme.fontSize > 0 ? theme.fontSize - 1 : 11 color: theme.textColor opacity: 0.7 } Item { Layout.fillWidth: true } Button { text: diagramRect.showSource ? "📊" : "" font.pointSize: theme.fontSize > 0 ? theme.fontSize - 2 : 10 implicitWidth: 32 implicitHeight: 20 ToolTip.visible: hovered ToolTip.text: diagramRect.showSource ? "Show diagram" : "Show source" onClicked: diagramRect.showSource = !diagramRect.showSource } Button { text: "📋" font.pointSize: theme.fontSize > 0 ? theme.fontSize - 2 : 10 implicitWidth: 32 implicitHeight: 20 ToolTip.visible: hovered ToolTip.text: diagramRect.showSource ? "Copy source" : "Copy image" onClicked: { if (diagramRect.showSource) { clipboard.copyText(segmentData.source) } else if (segmentData.svg) { clipboard.copyImage(segmentData.svg) } } } } Item { id: diagramContent anchors.top: diagramHeader.bottom anchors.left: parent.left anchors.right: parent.right anchors.margins: 4 height: diagramRect.showSource ? sourceEdit.height : imageColumn.height TextEdit { id: sourceEdit width: parent.width visible: diagramRect.showSource text: segmentData.html || "" textFormat: Text.RichText readOnly: true selectByMouse: true persistentSelection: true wrapMode: TextEdit.Wrap font.family: theme.fontFamily || Qt.application.font.family font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12 selectionColor: theme.highlightColor selectedTextColor: theme.highlightedTextColor MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton propagateComposedEvents: true cursorShape: Qt.IBeamCursor onPressed: function(mouse) { if (root.parentListView) root.parentListView.interactive = false mouse.accepted = false } onReleased: function(mouse) { if (root.parentListView) root.parentListView.interactive = true mouse.accepted = false } } } Column { id: imageColumn width: parent.width visible: !diagramRect.showSource Rectangle { visible: segmentData.svg && !segmentData.loading color: "white" width: diagramImg.width height: diagramImg.height Image { id: diagramImg source: segmentData.svg || "" fillMode: Image.PreserveAspectFit width: Math.min(implicitWidth, imageColumn.width) } } Label { visible: segmentData.loading text: "Loading " + (segmentData.lang || "diagram") + "..." font.italic: true color: theme.textColor opacity: 0.7 } } } } } }