add markdown parsing + latex + tree-sitter highlighting for codeblocks in llm responses
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
import ZShell.Llm
|
||||
import qs.Components
|
||||
@@ -14,6 +16,7 @@ MouseArea {
|
||||
readonly property bool isUser: modelData.role === ChatMessage.Role.User
|
||||
required property ChatMessage modelData
|
||||
property bool reasoningExpanded: false
|
||||
readonly property var blocks: blockify(current.segments)
|
||||
property real savedContentY: -1
|
||||
|
||||
function handleReasoningToggle(expanded: bool): void {
|
||||
@@ -34,14 +37,36 @@ MouseArea {
|
||||
}
|
||||
}
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.isUser ? 0 : Tokens.spacing.extraSmall
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: root.isUser ? Tokens.spacing.extraSmall : 0
|
||||
function blockify(segs) {
|
||||
const blocks = [];
|
||||
for (let i = 0; i < segs.length; i++) {
|
||||
const seg = segs[i];
|
||||
if (seg.type === LlmSegment.Type.Content) {
|
||||
blocks.push({
|
||||
kind: "content",
|
||||
id: i,
|
||||
segments: [seg]
|
||||
});
|
||||
} else {
|
||||
const last = blocks.length ? blocks[blocks.length - 1] : null;
|
||||
if (last && last.kind === "process") {
|
||||
last.segments.push(seg);
|
||||
} else {
|
||||
blocks.push({
|
||||
kind: "process",
|
||||
id: i,
|
||||
segments: [seg]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
implicitWidth: ListView.view.width
|
||||
hoverEnabled: true
|
||||
// Explicit, synchronous height — sum of children, no Layout polish involved.
|
||||
implicitHeight: reasoningLoader.implicitHeight + (reasoningLoader.active ? Tokens.spacing.medium : 0) + bubble.implicitHeight + actionsRow.implicitHeight + actionsRow.anchors.topMargin
|
||||
preventStealing: true
|
||||
implicitHeight: layout.implicitHeight + Tokens.padding.medium
|
||||
|
||||
Binding {
|
||||
property: "contentY"
|
||||
@@ -59,106 +84,61 @@ MouseArea {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: reasoningLoader
|
||||
Column {
|
||||
id: layout
|
||||
|
||||
readonly property bool shouldBeActive: root.current.reasoningActive || root.current.content === ""
|
||||
|
||||
active: !root.isUser
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Tokens.spacing.extraLarge
|
||||
anchors.top: parent.top
|
||||
|
||||
sourceComponent: Reasoning {
|
||||
current: root.current
|
||||
Repeater {
|
||||
id: segmentRep
|
||||
|
||||
onExpandedChanged: {
|
||||
root.handleReasoningToggle(expanded);
|
||||
model: ScriptModel {
|
||||
values: root.blocks
|
||||
objectProp: "id"
|
||||
}
|
||||
}
|
||||
}
|
||||
delegate: DelegateChooser {
|
||||
role: "kind"
|
||||
|
||||
CustomRect {
|
||||
id: bubble
|
||||
DelegateChoice {
|
||||
roleValue: "process"
|
||||
|
||||
anchors.left: root.isUser ? undefined : parent.left
|
||||
anchors.right: root.isUser ? parent.right : undefined
|
||||
anchors.top: reasoningLoader.active ? reasoningLoader.bottom : parent.top
|
||||
anchors.topMargin: reasoningLoader.active ? Tokens.spacing.medium : 0
|
||||
color: root.isUser ? Colors.palette.m3primary : Colors.palette.m3surfaceContainer
|
||||
implicitHeight: root.current.content !== "" ? msgText.contentHeight + Tokens.padding.medium * 2 : 0
|
||||
implicitWidth: Math.min(msgText.implicitWidth + Tokens.padding.medium * 2, root.width - Tokens.spacing.extraSmall - Tokens.spacing.extraLarge)
|
||||
radius: Tokens.rounding.medium
|
||||
visible: !root.current.reasoningActive
|
||||
delegate: ProcessBlock {
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
TextEditBase {
|
||||
id: msgText
|
||||
segments: modelData.segments
|
||||
isActive: index === root.blocks.length - 1
|
||||
width: root.width
|
||||
|
||||
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.current.content
|
||||
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;
|
||||
onExpandedChanged: root.handleReasoningToggle(expanded)
|
||||
}
|
||||
} else if (event.key == Qt.Key_Escape) {
|
||||
text = root.current.content;
|
||||
readOnly = true;
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
onEditingFinished: {
|
||||
const old = root.current.content;
|
||||
if (old !== text) {
|
||||
root.modelData.edit(text);
|
||||
|
||||
if (root.isUser)
|
||||
root.modelData.generate();
|
||||
}
|
||||
|
||||
readOnly = true;
|
||||
}
|
||||
onReadOnlyChanged: {
|
||||
if (readOnly) {
|
||||
animateCursor = false;
|
||||
root.forceActiveFocus();
|
||||
textFormat = CustomText.MarkdownText;
|
||||
} else {
|
||||
var raw = root.current.content;
|
||||
textFormat = CustomText.PlainText;
|
||||
text = raw;
|
||||
forceActiveFocus();
|
||||
cursorPosition = text.length;
|
||||
animateCursor = true;
|
||||
DelegateChoice {
|
||||
roleValue: "content"
|
||||
|
||||
delegate: ContentBubble {
|
||||
required property var modelData
|
||||
|
||||
isUser: root.isUser
|
||||
segment: modelData.segments[0]
|
||||
width: root.width
|
||||
repeater: segmentRep
|
||||
message: root.modelData
|
||||
current: root.current
|
||||
hovered: root.containsMouse
|
||||
|
||||
onEdit: text => {
|
||||
root.modelData.edit(text);
|
||||
|
||||
if (root.isUser)
|
||||
root.modelData.generate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Actions {
|
||||
id: actionsRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: (implicitWidth > bubble.implicitWidth) ? undefined : bubble.right
|
||||
anchors.top: bubble.bottom
|
||||
anchors.topMargin: Tokens.spacing.medium
|
||||
current: root.current
|
||||
edit: msgText
|
||||
hovered: root.containsMouse
|
||||
message: root.modelData
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user