From 9d33e8a404e963e742d388676b962a66d8d403bb Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Tue, 8 Apr 2025 11:28:31 +0200 Subject: [PATCH] ai: actually make chat messages update incrementally (instead of destroying and recreating every update) MUCH better performance and no more hundreds of latex files for one integration by parts work --- .config/ags/modules/.miscutils/md2pango.js | 2 +- .../modules/sideleft/apis/ai_chatmessage.js | 116 +++++++++++------- .config/ags/services/gemini.js | 2 +- 3 files changed, 73 insertions(+), 47 deletions(-) diff --git a/.config/ags/modules/.miscutils/md2pango.js b/.config/ags/modules/.miscutils/md2pango.js index c49354659..e619cf972 100644 --- a/.config/ags/modules/.miscutils/md2pango.js +++ b/.config/ags/modules/.miscutils/md2pango.js @@ -49,7 +49,7 @@ const replaceCategory = (text, replaces) => { // Main function export function replaceInlineLatexWithCodeBlocks(text) { - return text.replace(/\\\[(.*?)\\\]|\\\((.*?)\\\)|\$\$(.*?)\$\$|(? { + return text.replace(/\\\[(.*?)\\\]|\\\((.*?)\\\)|\$\$(.*?)\$\$|(?:^|[^\w])\$(.*?[^\\])\$(?!\w)/gs, (match, square, round, double, single) => { const latex = square || round || double || single; return `\n\`\`\`latex\n${latex}\n\`\`\`\n`; }); diff --git a/.config/ags/modules/sideleft/apis/ai_chatmessage.js b/.config/ags/modules/sideleft/apis/ai_chatmessage.js index b6cc9fc61..4e7346e43 100644 --- a/.config/ags/modules/sideleft/apis/ai_chatmessage.js +++ b/.config/ags/modules/sideleft/apis/ai_chatmessage.js @@ -49,12 +49,15 @@ const HighlightedCode = (content, lang) => { const TextBlock = (content = '') => { const widget = Label({ attribute: { - 'updateTextPlain': (text) => { - widget.label = text; - }, + 'text': content, 'updateText': (text) => { - widget.attribute.updateTextPlain(md2pango(text)); - } + widget.attribute.text = text; + widget.label = md2pango(widget.attribute.text) + }, + 'appendText': (text) => { + widget.attribute.text += text; + widget.label = md2pango(widget.attribute.text) + }, }, hpack: 'fill', className: 'txt sidebar-chat-txtblock sidebar-chat-txt', @@ -93,11 +96,14 @@ const ThinkBlock = (content = '', revealChild = true) => { }); const widget = Box({ attribute: { - 'updateTextPlain': (text) => { - mainText.label = text; - }, + 'text': content, 'updateText': (text) => { - widget.attribute.updateTextPlain(md2pango(text)); + widget.attribute.text = text; + mainText.label = md2pango(widget.attribute.text); + }, + 'appendText': (text) => { + widget.attribute.text += text; + mainText.label = md2pango(widget.attribute.text); }, 'done': () => { revealThought.value = false; @@ -150,7 +156,7 @@ const LatexBlock = (content = '') => { // hscroll: 'automatic', // homogeneous: true, attribute: { - render: async (self, text) => { + 'render': async (self, text) => { if (text.length == 0) return; const styleContext = self.get_style_context(); const fontSize = styleContext.get_property('font-size', Gtk.StateFlags.NORMAL); @@ -193,9 +199,15 @@ sed -i 's/stroke="rgb(0%, 0%, 0%)"/stroke="${darkMode.value ? '#ffffff' : '#0000 className: 'sidebar-chat-latex', homogeneous: true, attribute: { + 'text': content, 'updateText': (text) => { - latexViewArea.attribute.render(latexViewArea, text).catch(print); - } + wholeThing.attribute.text = text; + latexViewArea.attribute.render(latexViewArea, wholeThing.attribute.text).catch(print); + }, + 'appendText': (text) => { + wholeThing.attribute.text += text; + latexViewArea.attribute.render(latexViewArea, wholeThing.attribute.text).catch(print); + }, }, children: [Scrollable({ vscroll: 'never', @@ -253,7 +265,10 @@ const CodeBlock = (content = '', lang = 'txt') => { sourceView.showLineMarks = true; } sourceView.get_buffer().set_text(text, -1); - } + }, + 'appendText': (text) => { + codeBlock.attribute.updateText(sourceView.get_buffer().text + text); + }, }, className: 'sidebar-chat-codeblock', vertical: true, @@ -294,76 +309,83 @@ const MessageContent = (content) => { const contentBox = Box({ vertical: true, attribute: { + 'lastUpdateTextLength': 0, + 'inCode': false, 'fullUpdate': (self, content, useCursor = false) => { - // Clear and add first text widget - const children = contentBox.get_children(); - for (let i = 0; i < children.length; i++) { - const child = children[i]; - child.destroy(); + // First text widget + if (contentBox.attribute.lastUpdateTextLength === 0 + && contentBox.get_children().length === 0 + ) { + contentBox.add(TextBlock()) } - contentBox.add(TextBlock()) - let lines = replaceInlineLatexWithCodeBlocks(content).split('\n'); + const codeBlockRegex = /^\s*```([a-zA-Z0-9]+)?\n?/; + const thinkBlockStartRegex = /^\s*/; // Start: + const thinkBlockEndRegex = /<\/think>\s*$/; // End: + const dividerRegex = /^\s*---/; + const newContent = content.slice(contentBox.attribute.lastUpdateTextLength); + // print("CONTENT:'" + content + "'") + // print("LAST UPDATE LENGTH:" + contentBox.attribute.lastUpdateTextLength) + // print("NEW CONTENT:" + newContent) + if (newContent.length == 0) return; + let lines = replaceInlineLatexWithCodeBlocks(newContent).split('\n'); + // let lines = newContent.split('\n'); + + // Process each line except the last line (potentially incomplete) let lastProcessed = 0; - let inCode = false; for (let [index, line] of lines.entries()) { + if (index == lines.length - 1) break; // Code blocks - const codeBlockRegex = /^\s*```([a-zA-Z0-9]+)?\n?/; if (codeBlockRegex.test(line)) { const kids = self.get_children(); const lastLabel = kids[kids.length - 1]; const blockContent = lines.slice(lastProcessed, index).join('\n'); - if (!inCode) { - lastLabel.attribute.updateText(blockContent); - if (lastLabel.label == '') lastLabel.destroy(); + + if (!contentBox.attribute.inCode) { + lastLabel.attribute.appendText(blockContent); + if (lastLabel.label === '') lastLabel.destroy(); contentBox.add(CodeBlock('', codeBlockRegex.exec(line)[1])); } else { - lastLabel.attribute.updateText(blockContent); + lastLabel.attribute.appendText(blockContent); contentBox.add(TextBlock()); } lastProcessed = index + 1; - inCode = !inCode; + contentBox.attribute.inCode = !contentBox.attribute.inCode; } // Think block - const thinkBlockStartRegex = /^\s*/; // Start: - const thinkBlockEndRegex = /<\/think>\s*$/; // End: - if (!inCode && (thinkBlockStartRegex.test(line) || thinkBlockEndRegex.test(line))) { + if (!contentBox.attribute.inCode && (thinkBlockStartRegex.test(line) || thinkBlockEndRegex.test(line))) { const kids = self.get_children(); const lastLabel = kids[kids.length - 1]; const blockContent = lines.slice(lastProcessed, index).join('\n'); - lastLabel.attribute.updateTextPlain(blockContent); - if (lastLabel.label == '') lastLabel.destroy(); + lastLabel.attribute.appendText(blockContent); + if (lastLabel.label === '') lastLabel.destroy(); if (thinkBlockStartRegex.test(line)) contentBox.add(ThinkBlock()); else { - // lastLabel.attribute.done(); + lastLabel.attribute.done(); contentBox.add(TextBlock()); } lastProcessed = index + 1; } // Breaks - const dividerRegex = /^\s*---/; - if (!inCode && dividerRegex.test(line)) { + if (!contentBox.attribute.inCode && dividerRegex.test(line)) { const kids = self.get_children(); const lastLabel = kids[kids.length - 1]; const blockContent = lines.slice(lastProcessed, index).join('\n'); - lastLabel.attribute.updateTextPlain(blockContent); + lastLabel.attribute.appendText(blockContent); contentBox.add(Divider()); contentBox.add(TextBlock()); lastProcessed = index + 1; } } - if (lastProcessed < lines.length) { + if (lastProcessed < lines.length - 1) { const kids = self.get_children(); const lastLabel = kids[kids.length - 1]; - let blockContent = lines.slice(lastProcessed, lines.length).join('\n'); - if (!inCode) - lastLabel.attribute.updateTextPlain(`${md2pango(blockContent)}${useCursor ? userOptions.ai.writingCursor : ''}`); - else - lastLabel.attribute.updateText(blockContent); + let blockContent = lines.slice(lastProcessed, lines.length - 1).join('\n') + '\n'; + lastLabel.attribute.appendText(blockContent); } // Debug: plain text // contentBox.add(Label({ @@ -376,6 +398,7 @@ const MessageContent = (content) => { // label: '------------------------------\n' + md2pango(content), // })) contentBox.show_all(); + contentBox.attribute.lastUpdateTextLength = content.length - lines[lines.length - 1].length; } } }); @@ -416,7 +439,7 @@ export const ChatMessage = (message, modelName = 'Model') => { className: `txt txt-bold sidebar-chat-name sidebar-chat-name-${message.role == 'user' ? 'user' : 'bot'}`, wrap: true, useMarkup: true, - label: (message.role == 'user' ? USERNAME : modelName), + label: (message.role === 'user' ? USERNAME : modelName), }), Box({ homogeneous: true, @@ -432,7 +455,10 @@ export const ChatMessage = (message, modelName = 'Model') => { messageContentBox.attribute.fullUpdate(messageContentBox, message.content, message.role != 'user'); }, 'notify::content') .hook(message, (label, isDone) => { // Remove the cursor - messageContentBox.attribute.fullUpdate(messageContentBox, message.content, false); + if (!isDone && message.role !== 'user') return; + messageContentBox.attribute.fullUpdate(messageContentBox, message.content + '\n', false); + // print('----------------') + // print(message.content) }, 'notify::done') , }) @@ -442,7 +468,7 @@ export const ChatMessage = (message, modelName = 'Model') => { } export const SystemMessage = (content, commandName, scrolledWindow) => { - const messageContentBox = MessageContent(content); + const messageContentBox = MessageContent(content + '\n'); // Add newline so everything is added const thisMessage = Box({ className: 'sidebar-chat-message', children: [ diff --git a/.config/ags/services/gemini.js b/.config/ags/services/gemini.js index 5109b3650..6775173c7 100644 --- a/.config/ags/services/gemini.js +++ b/.config/ags/services/gemini.js @@ -284,7 +284,7 @@ class GeminiService extends Service { send(msg) { this._messages.push(new GeminiMessage('user', msg, false)); this.emit('newMsg', this._messages.length - 1); - const aiResponse = new GeminiMessage('model', 'thinking...', true, false) + const aiResponse = new GeminiMessage('model', '', true, false) const body = {