ai: handle thinking

This commit is contained in:
end-4
2025-05-09 18:23:22 +02:00
parent efcfd375c0
commit de2ead426f
5 changed files with 266 additions and 17 deletions
@@ -17,20 +17,70 @@ function shellSingleQuoteEscape(str) {
}
function splitMarkdownBlocks(markdown) {
const regex = /```(\w+)?\n([\s\S]*?)```/g;
const regex = /```(\w+)?\n([\s\S]*?)```|<think>([\s\S]*?)<\/think>/g;
let result = [];
let lastIndex = 0;
let match;
while ((match = regex.exec(markdown)) !== null) {
if (match.index > lastIndex) {
result.push({ type: "text", content: markdown.slice(lastIndex, match.index) });
const text = markdown.slice(lastIndex, match.index);
if (text.trim()) {
result.push({ type: "text", content: text });
}
}
if (match[0].startsWith('```')) {
if (match[2] && match[2].trim()) {
result.push({ type: "code", lang: match[1] || "", content: match[2], completed: true });
}
} else if (match[0].startsWith('<think>')) {
if (match[3] && match[3].trim()) {
result.push({ type: "think", content: match[3], completed: true });
}
}
result.push({ type: "code", lang: match[1] || "", content: match[2] });
lastIndex = regex.lastIndex;
}
// Handle any remaining text after the last match
if (lastIndex < markdown.length) {
result.push({ type: "text", content: markdown.slice(lastIndex) });
const text = markdown.slice(lastIndex);
// Check for unfinished <think> block
const thinkStart = text.indexOf('<think>');
const codeStart = text.indexOf('```');
if (
thinkStart !== -1 &&
(codeStart === -1 || thinkStart < codeStart)
) {
const beforeThink = text.slice(0, thinkStart);
if (beforeThink.trim()) {
result.push({ type: "text", content: beforeThink });
}
const thinkContent = text.slice(thinkStart + 7);
if (thinkContent.trim()) {
result.push({ type: "think", content: thinkContent, completed: false });
}
} else if (codeStart !== -1) {
const beforeCode = text.slice(0, codeStart);
if (beforeCode.trim()) {
result.push({ type: "text", content: beforeCode });
}
// Try to detect language after ```
const codeLangMatch = text.slice(codeStart + 3).match(/^(\w+)?\n/);
let lang = "";
let codeContentStart = codeStart + 3;
if (codeLangMatch) {
lang = codeLangMatch[1] || "";
codeContentStart += codeLangMatch[0].length;
} else if (text[codeStart + 3] === '\n') {
codeContentStart += 1;
}
const codeContent = text.slice(codeContentStart);
if (codeContent.trim()) {
result.push({ type: "code", lang, content: codeContent, completed: false });
}
} else if (text.trim()) {
result.push({ type: "text", content: text });
}
}
// console.log(JSON.stringify(result, null, 2));
return result;
}
@@ -80,6 +80,11 @@ Item {
description: qsTr("Markdown test"),
execute: () => {
Ai.addMessage(`
<think>
A longer think block to test revealing animation
OwO wem ipsum dowo sit amet, consekituwet awipiscing ewit, sed do eiuwsmod tempow inwididunt ut wabowe et dowo mawa. Ut enim ad minim weniam, quis nostwud exeucitation uwuwamcow bowowis nisi ut awiquip ex ea commowo consequat. Duuis aute iwuwe dowo in wepwependewit in wowuptate velit esse ciwwum dowo eu fugiat nuwa pawiatuw. Excepteuw sint occaecat cupidatat non pwowoident, sunt in cuwpa qui officia desewunt mowit anim id est wabowum. Meouw! >w<
Mowe uwu wem ipsum!
</think>
## Markdown test
### Formatting
@@ -118,7 +123,6 @@ int main(int argc, char* argv[]) {
- Simple inline: $\\frac{1}{2} = \\frac{2}{4}$
- Complex inline: $$\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$
- Another complex inline: \\\\[\\int_0^\\infty \\frac{1}{x^2} dx = \\infty\\\\]
`,
Ai.interfaceRole);
}
@@ -23,9 +23,6 @@ Rectangle {
property real messagePadding: 7
property real contentSpacing: 3
property real codeBlockBackgroundRounding: Appearance.rounding.small
property real codeBlockHeaderPadding: 3
property real codeBlockComponentSpacing: 2
property bool enableMouseSelection: false
property bool renderMarkdown: true
@@ -44,7 +41,6 @@ Rectangle {
const segments = messageContentColumnLayout.children
.map(child => child.segment)
.filter(segment => (segment));
// console.log("Segments: " + JSON.stringify(segments))
// Reconstruct markdown
const newContent = segments.map(segment => {
@@ -238,11 +234,7 @@ Rectangle {
spacing: 0
Repeater {
model: ScriptModel {
values: {
const result = StringUtils.splitMarkdownBlocks(root.messageData.content)
// console.log(JSON.stringify(result))
return result
}
values: StringUtils.splitMarkdownBlocks(root.messageData.content)
}
delegate: Loader {
Layout.fillWidth: true
@@ -255,8 +247,12 @@ Rectangle {
property var enableMouseSelection: root.enableMouseSelection
property bool thinking: root.messageData.thinking
property bool done: root.messageData.done
property bool completed: modelData.completed ?? false
source: modelData.type === "code" ? "MessageCodeBlock.qml" : "MessageTextBlock.qml"
source: modelData.type === "code" ? "MessageCodeBlock.qml" :
modelData.type === "think" ? "MessageThinkBlock.qml" :
"MessageTextBlock.qml"
}
}
}
@@ -12,7 +12,6 @@ import QtQuick.Layouts
import Quickshell.Io
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Hyprland
import Qt5Compat.GraphicalEffects
import org.kde.syntaxhighlighting
@@ -26,6 +25,10 @@ ColumnLayout {
property var segmentLang: parent?.segmentLang ?? "plaintext"
property var messageData: parent?.messageData ?? {}
property real codeBlockBackgroundRounding: Appearance.rounding.small
property real codeBlockHeaderPadding: 3
property real codeBlockComponentSpacing: 2
spacing: codeBlockComponentSpacing
anchors.left: parent.left
anchors.right: parent.right
@@ -161,7 +164,7 @@ ColumnLayout {
id: codeTextArea
Layout.fillWidth: true
readOnly: !editing
// selectByMouse: enableMouseSelection || editing
selectByMouse: enableMouseSelection || editing
renderType: Text.NativeRendering
font.family: Appearance.font.family.monospace
font.hintingPreference: Font.PreferNoHinting // Prevent weird bold text
@@ -0,0 +1,196 @@
pragma ComponentBehavior: Bound
import "root:/"
import "root:/services"
import "root:/modules/common/"
import "root:/modules/common/widgets"
import "../"
import "root:/modules/common/functions/string_utils.js" as StringUtils
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Io
import Quickshell
import Quickshell.Widgets
import Quickshell.Hyprland
import Qt5Compat.GraphicalEffects
Item {
id: root
// These are needed on the parent loader
property bool editing: parent?.editing ?? false
property bool renderMarkdown: parent?.renderMarkdown ?? true
property bool enableMouseSelection: parent?.enableMouseSelection ?? false
property string segmentContent: parent?.segmentContent ?? ({})
property var messageData: parent?.messageData ?? {}
property bool done: parent?.done ?? true
property bool completed: parent?.completed ?? false
property real thinkBlockBackgroundRounding: Appearance.rounding.small
property real thinkBlockHeaderPaddingVertical: 3
property real thinkBlockHeaderPaddingHorizontal: 10
property real thinkBlockComponentSpacing: 2
property var collapseAnimation: messageTextBlock.implicitHeight > 40 ? Appearance.animation.elementMoveEnter : Appearance.animation.elementMoveFast
property bool collapsed: root.completed || !root.done
Layout.fillWidth: true
implicitHeight: collapsed ? header.implicitHeight : columnLayout.implicitHeight
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: root.width
height: root.height
radius: thinkBlockBackgroundRounding
}
}
Behavior on implicitHeight {
enabled: root.done ?? false
NumberAnimation {
duration: collapseAnimation.duration
easing.type: collapseAnimation.type
easing.bezierCurve: collapseAnimation.bezierCurve
}
}
ColumnLayout {
id: columnLayout
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
spacing: 0
Rectangle { // Header background
id: header
color: Appearance.m3colors.m3surfaceContainerHighest
Layout.fillWidth: true
implicitHeight: thinkBlockTitleBarRowLayout.implicitHeight + thinkBlockHeaderPaddingVertical * 2
MouseArea { // Click to reveal
id: headerMouseArea
enabled: root.done
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: {
root.collapsed = !root.collapsed
}
}
RowLayout { // Header content
id: thinkBlockTitleBarRowLayout
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: thinkBlockHeaderPaddingHorizontal
anchors.rightMargin: thinkBlockHeaderPaddingHorizontal
spacing: 10
MaterialSymbol {
Layout.fillWidth: false
Layout.topMargin: 7
Layout.bottomMargin: 7
Layout.leftMargin: 3
text: "linked_services"
}
StyledText {
id: thinkBlockLanguage
Layout.fillWidth: false
Layout.alignment: Qt.AlignLeft
text: root.done ? "Chain of Thought" : "Thinking..."
}
Item { Layout.fillWidth: true }
Button { // Expand button
id: expandButton
visible: root.done
implicitWidth: 22
implicitHeight: 22
PointingHandInteraction{}
onClicked: {
root.collapsed = !root.collapsed
}
background: Rectangle {
anchors.fill: parent
radius: Appearance.rounding.full
color: (headerMouseArea.pressed) ? Appearance.colors.colLayer2Active
: (headerMouseArea.containsMouse ? Appearance.colors.colLayer2Hover
: Appearance.transparentize(Appearance.colors.colLayer2, 1))
Behavior on color {
ColorAnimation {
duration: collapseAnimation.duration
easing.type: collapseAnimation.type
easing.bezierCurve: collapseAnimation.bezierCurve
}
}
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
text: "keyboard_arrow_down"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
iconSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer2
rotation: root.collapsed ? 0 : 180
Behavior on rotation {
NumberAnimation {
duration: Appearance.animation.elementMoveFast.duration
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
}
}
}
}
Item {
id: content
Layout.fillWidth: true
implicitHeight: collapsed ? 0 : contentBackground.implicitHeight + thinkBlockComponentSpacing
clip: true
Behavior on implicitHeight {
enabled: root.done ?? false
NumberAnimation {
duration: collapseAnimation.duration
easing.type: collapseAnimation.easing
easing.bezierCurve: collapseAnimation.bezierCurve
}
}
Rectangle {
id: contentBackground
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
implicitHeight: messageTextBlock.implicitHeight
color: Appearance.colors.colLayer2
// Load data for the message at the correct scope
property bool editing: root.editing
property bool renderMarkdown: root.renderMarkdown
property bool enableMouseSelection: root.enableMouseSelection
property string segmentContent: root.segmentContent
property var messageData: root.messageData
property bool done: root.done
MessageTextBlock {
id: messageTextBlock
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
}
}
}
}
}