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