From 4ff645abd0a117607af2c4d93887fd27852f8e33 Mon Sep 17 00:00:00 2001 From: Sven Olderaan Date: Sun, 16 Mar 2025 11:48:08 +0100 Subject: [PATCH] Add feature to render chat bubble images in collapsed tool calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Intercept tool call messages in chat and render markdown images - Add setting to toggle feature on/off - Add proper styling for rendered images - Handle mutation observer for new messages - Hide rendered images when details element is opened 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- example.html | 5 ++ index.js | 173 ++++++++++++++++++++++++++++++++++++++++++++++++-- manifest.json | 2 +- style.css | 40 ++++++++++++ 4 files changed, 215 insertions(+), 5 deletions(-) diff --git a/example.html b/example.html index 9f2e13e..cc731dc 100644 --- a/example.html +++ b/example.html @@ -10,6 +10,11 @@ +
+ + +
+
diff --git a/index.js b/index.js index 8a02899..aed6db1 100644 --- a/index.js +++ b/index.js @@ -10,7 +10,8 @@ const extensionFolderPath = `scripts/extensions/third-party/${extensionName}`; const defaultSettings = { image_service_url: "image.php", default_style: "default", - enabled: true + enabled: true, + render_in_collapse: true // New setting to enable/disable rendering in collapsed tool calls }; // Make sure settings exist @@ -53,6 +54,7 @@ function loadSettings() { $("#sillybubble_enabled").prop("checked", extension_settings[extensionName].enabled).trigger("input"); $("#sillybubble_image_url").val(extension_settings[extensionName].image_service_url).trigger("input"); $("#sillybubble_default_style").val(extension_settings[extensionName].default_style).trigger("input"); + $("#sillybubble_render_in_collapse").prop("checked", extension_settings[extensionName].render_in_collapse).trigger("input"); } // Handle enable/disable toggle @@ -76,6 +78,16 @@ function onDefaultStyleInput(event) { saveSettingsDebounced(); } +// Handle render in collapse toggle +function onRenderInCollapseInput(event) { + const value = Boolean($(event.target).prop("checked")); + extension_settings[extensionName].render_in_collapse = value; + saveSettingsDebounced(); + + // Re-process messages when the setting is toggled + processToolCallMessages(); +} + // Test function to visualize a bubble function onTestButtonClick() { const testText = "This is a test chat bubble generated by SillyBubble!"; @@ -145,6 +157,120 @@ function registerFunctionTool() { } } +// Process tool call messages to render chat bubbles directly +function processToolCallMessages() { + if (!extension_settings[extensionName].render_in_collapse) { + // Remove any previously rendered images if the feature is disabled + $('.sillybubble-rendered-image').remove(); + $('.smallSysMes.toolCall').removeAttr('data-sb-processed'); + return; + } + + // Look for tool call messages + $('.mes.smallSysMes.toolCall').each(function() { + // Check if this message has already been processed + if ($(this).attr('data-sb-processed') === 'true') { + return; // Skip processed messages + } + + try { + // Find the details element and its summary + const detailsElement = $(this).find('details'); + const summaryElement = $(this).find('summary'); + + if (detailsElement.length && summaryElement.length) { + // Find the JSON content within the pre/code block + const codeElement = $(this).find('pre code'); + + if (codeElement.length) { + // Try to parse the JSON content + const jsonText = codeElement.text(); + try { + const toolData = JSON.parse(jsonText); + + // Check if it's an array + if (Array.isArray(toolData)) { + // Process each tool call + for (const tool of toolData) { + // Check if this is our tool and it has a result + if (tool.name === 'generateChatBubbleImage' && tool.result) { + // Create a container for the rendered markdown + const renderContainer = $('
'); + + // Add the markdown content (render the image) + renderContainer.html(tool.result); + + // Remove any existing rendered images + $(this).find('.sillybubble-rendered-image').remove(); + + // Add this image after the summary element + summaryElement.after(renderContainer); + + // Mark this message as processed + $(this).attr('data-sb-processed', 'true'); + + console.log(`[${extensionName}] Rendered chat bubble image in tool call`); + } + } + } + } catch (e) { + console.warn(`[${extensionName}] Failed to parse JSON in tool call:`, e); + } + } + } + } catch (error) { + console.error(`[${extensionName}] Error processing tool call message:`, error); + } + }); +} + +// Observer function to watch for new messages +function setupMessageObserver() { + // Create a mutation observer to monitor the chat for new messages + const chatObserver = new MutationObserver((mutations) => { + let shouldProcess = false; + + // Check for relevant mutations (new messages added) + for (const mutation of mutations) { + if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { + // Look for new message elements + for (const node of mutation.addedNodes) { + if (node.nodeType === Node.ELEMENT_NODE) { + // Check if the node is a message or contains messages + if (node.classList?.contains('mes') || + node.querySelector?.('.mes')) { + shouldProcess = true; + break; + } + } + } + } + + if (shouldProcess) break; + } + + // Process tool call messages if needed + if (shouldProcess) { + // Small delay to ensure the DOM is fully updated + setTimeout(() => processToolCallMessages(), 100); + } + }); + + // Start observing the chat container + const chatElement = document.getElementById('chat'); + if (chatElement) { + chatObserver.observe(chatElement, { childList: true, subtree: true }); + console.log(`[${extensionName}] Message observer started`); + + // Process any existing messages + setTimeout(() => processToolCallMessages(), 500); + } else { + console.warn(`[${extensionName}] Chat element not found, observer not started`); + // Try again after a delay + setTimeout(() => setupMessageObserver(), 2000); + } +} + // Initialize extension jQuery(async () => { console.log(`[${extensionName}] Initializing...`); @@ -159,6 +285,7 @@ jQuery(async () => { $("#sillybubble_enabled").on("input", onEnabledInput); $("#sillybubble_image_url").on("input", onImageUrlInput); $("#sillybubble_default_style").on("input", onDefaultStyleInput); + $("#sillybubble_render_in_collapse").on("input", onRenderInCollapseInput); $("#sillybubble_test_button").on("click", onTestButtonClick); // Initial attempt to register the function tool @@ -167,13 +294,51 @@ jQuery(async () => { // Make function globally accessible as fallback window.generateChatBubbleImage = generateChatBubbleImage; - // Wait for SillyTavern to fully initialize, then register again + // Add CSS for rendered images + $('head').append(` + + `); + + // Wait for SillyTavern to fully initialize $(document).ready(() => { - // Try again after a short delay + // Try registering again after a short delay setTimeout(() => registerFunctionTool(), 2000); + // Setup the message observer + setupMessageObserver(); + // Final attempt after SillyTavern is fully loaded - setTimeout(() => registerFunctionTool(), 10000); + setTimeout(() => { + registerFunctionTool(); + // Process any messages that might have been missed + processToolCallMessages(); + }, 5000); }); // Load settings diff --git a/manifest.json b/manifest.json index 0febe22..4ec3266 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Crystal", - "version": "1.0.0", + "version": "1.1.0", "homePage": "https://github.com/crystal/SillyBubble" } diff --git a/style.css b/style.css index 8b5dce3..1ba4d6c 100644 --- a/style.css +++ b/style.css @@ -52,4 +52,44 @@ border-radius: 5px; overflow-x: auto; margin: 10px 0; +} + +/* Rendered Images in Tool Calls */ +.sillybubble-rendered-image { + padding: 10px; + background-color: rgba(0, 0, 0, 0.05); + border-radius: 5px; + margin: 5px 0; + text-align: center; +} + +.sillybubble-rendered-image img { + max-width: 100%; + border-radius: 3px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); +} + +/* Style adjustments for processed tool calls */ +.smallSysMes.toolCall[data-sb-processed="true"] details { + margin-bottom: 0; +} + +.smallSysMes.toolCall[data-sb-processed="true"] summary { + cursor: pointer; + opacity: 0.7; + transition: opacity 0.2s ease; +} + +.smallSysMes.toolCall[data-sb-processed="true"] summary:hover { + opacity: 1; +} + +/* Make sure details are properly styled when open */ +.smallSysMes.toolCall[data-sb-processed="true"] details[open] .sillybubble-rendered-image { + display: none; +} + +/* Remove the margin between details and summary when image is rendered */ +.smallSysMes.toolCall[data-sb-processed="true"] details { + margin-bottom: 0; } \ No newline at end of file