Add feature to render chat bubble images in collapsed tool calls
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
ffe901e1ff
commit
4ff645abd0
@ -10,6 +10,11 @@
|
|||||||
<label for="sillybubble_enabled">Enable SillyBubble</label>
|
<label for="sillybubble_enabled">Enable SillyBubble</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="sillybubble_block flex-container">
|
||||||
|
<input id="sillybubble_render_in_collapse" type="checkbox" />
|
||||||
|
<label for="sillybubble_render_in_collapse">Render bubbles in collapsed tool calls</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="sillybubble_block flex-container">
|
<div class="sillybubble_block flex-container">
|
||||||
<label for="sillybubble_image_url">Image Service URL:</label>
|
<label for="sillybubble_image_url">Image Service URL:</label>
|
||||||
<input id="sillybubble_image_url" type="text" class="text_pole" />
|
<input id="sillybubble_image_url" type="text" class="text_pole" />
|
||||||
|
173
index.js
173
index.js
@ -10,7 +10,8 @@ const extensionFolderPath = `scripts/extensions/third-party/${extensionName}`;
|
|||||||
const defaultSettings = {
|
const defaultSettings = {
|
||||||
image_service_url: "image.php",
|
image_service_url: "image.php",
|
||||||
default_style: "default",
|
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
|
// Make sure settings exist
|
||||||
@ -53,6 +54,7 @@ function loadSettings() {
|
|||||||
$("#sillybubble_enabled").prop("checked", extension_settings[extensionName].enabled).trigger("input");
|
$("#sillybubble_enabled").prop("checked", extension_settings[extensionName].enabled).trigger("input");
|
||||||
$("#sillybubble_image_url").val(extension_settings[extensionName].image_service_url).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_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
|
// Handle enable/disable toggle
|
||||||
@ -76,6 +78,16 @@ function onDefaultStyleInput(event) {
|
|||||||
saveSettingsDebounced();
|
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
|
// Test function to visualize a bubble
|
||||||
function onTestButtonClick() {
|
function onTestButtonClick() {
|
||||||
const testText = "This is a test chat bubble generated by SillyBubble!";
|
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 = $('<div class="sillybubble-rendered-image"></div>');
|
||||||
|
|
||||||
|
// 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
|
// Initialize extension
|
||||||
jQuery(async () => {
|
jQuery(async () => {
|
||||||
console.log(`[${extensionName}] Initializing...`);
|
console.log(`[${extensionName}] Initializing...`);
|
||||||
@ -159,6 +285,7 @@ jQuery(async () => {
|
|||||||
$("#sillybubble_enabled").on("input", onEnabledInput);
|
$("#sillybubble_enabled").on("input", onEnabledInput);
|
||||||
$("#sillybubble_image_url").on("input", onImageUrlInput);
|
$("#sillybubble_image_url").on("input", onImageUrlInput);
|
||||||
$("#sillybubble_default_style").on("input", onDefaultStyleInput);
|
$("#sillybubble_default_style").on("input", onDefaultStyleInput);
|
||||||
|
$("#sillybubble_render_in_collapse").on("input", onRenderInCollapseInput);
|
||||||
$("#sillybubble_test_button").on("click", onTestButtonClick);
|
$("#sillybubble_test_button").on("click", onTestButtonClick);
|
||||||
|
|
||||||
// Initial attempt to register the function tool
|
// Initial attempt to register the function tool
|
||||||
@ -167,13 +294,51 @@ jQuery(async () => {
|
|||||||
// Make function globally accessible as fallback
|
// Make function globally accessible as fallback
|
||||||
window.generateChatBubbleImage = generateChatBubbleImage;
|
window.generateChatBubbleImage = generateChatBubbleImage;
|
||||||
|
|
||||||
// Wait for SillyTavern to fully initialize, then register again
|
// Add CSS for rendered images
|
||||||
|
$('head').append(`
|
||||||
|
<style>
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smallSysMes.toolCall[data-sb-processed="true"] summary {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make the text in the summary a bit less prominent when an image is shown */
|
||||||
|
.smallSysMes.toolCall[data-sb-processed="true"] summary {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smallSysMes.toolCall[data-sb-processed="true"] summary:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Wait for SillyTavern to fully initialize
|
||||||
$(document).ready(() => {
|
$(document).ready(() => {
|
||||||
// Try again after a short delay
|
// Try registering again after a short delay
|
||||||
setTimeout(() => registerFunctionTool(), 2000);
|
setTimeout(() => registerFunctionTool(), 2000);
|
||||||
|
|
||||||
|
// Setup the message observer
|
||||||
|
setupMessageObserver();
|
||||||
|
|
||||||
// Final attempt after SillyTavern is fully loaded
|
// Final attempt after SillyTavern is fully loaded
|
||||||
setTimeout(() => registerFunctionTool(), 10000);
|
setTimeout(() => {
|
||||||
|
registerFunctionTool();
|
||||||
|
// Process any messages that might have been missed
|
||||||
|
processToolCallMessages();
|
||||||
|
}, 5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load settings
|
// Load settings
|
||||||
|
@ -6,6 +6,6 @@
|
|||||||
"js": "index.js",
|
"js": "index.js",
|
||||||
"css": "style.css",
|
"css": "style.css",
|
||||||
"author": "Crystal",
|
"author": "Crystal",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"homePage": "https://github.com/crystal/SillyBubble"
|
"homePage": "https://github.com/crystal/SillyBubble"
|
||||||
}
|
}
|
||||||
|
40
style.css
40
style.css
@ -52,4 +52,44 @@
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
margin: 10px 0;
|
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;
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user