forked from Shinonome/dots-hyprland
ai: handle thinking
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user