Compare commits

...

10 Commits

Author SHA1 Message Date
7380391424 Make character parameter required when AI character selection is enabled
- Add character to required parameters when setting is enabled
- Update documentation to clarify character is required
- Ensure handler logic provides default character if missing
- Maintain backward compatibility

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-16 16:55:41 +01:00
c8af3ab3cb Add toggle for AI character selection parameter
- Add setting to control whether AI can choose character
- Modify function schema based on user settings
- Only expose character parameter to AI when setting is enabled
- Update test function to reflect current settings
- Provide separate documentation for each setting mode
- Default is character selection disabled, set by extension user

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-16 16:21:53 +01:00
7d6dc1c4cd Center text horizontally and vertically in bubbles
- Add horizontal centering for each line of text
- Add vertical centering for the entire text block
- Ensure text doesn't overflow the top of the bubble
- Maintain compatibility with both built-in and TTF fonts

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-16 16:17:34 +01:00
48610c7ba6 Update SillyBubble extension to support new image features
- Add support for character-based bubble styling
- Add support for speech and thought bubble types
- Update function schema with new parameters
- Add UI settings for character and bubble type selection
- Update examples and documentation
- Keep backward compatibility with style parameter

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-16 16:13:48 +01:00
e0d54bd1b6 Add character-based chat bubble system with new directory structure
- Implement 5:1 ratio comic-style chat bubbles with character images
- Create modular design with character-specific assets
- Organize assets in /characters/{name}/ directories
- Add fallback system using Example character
- Support both speech and thought bubble types
- Maintain backward compatibility with style parameter

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-16 16:09:01 +01:00
34bd7e44e6 Fix duplicate image rendering in chat bubble tool calls 2025-03-16 13:37:02 +01:00
17796630af Fix bubble image visibility in collapsed tool calls 2025-03-16 13:34:40 +01:00
c0a9345c17 Fix bubble rendering in tool calls 2025-03-16 13:25:29 +01:00
1e09bd6638 Fix HTTPS to HTTP protocol in image URLs 2025-03-16 13:20:13 +01:00
a907c4d073 Add HTML fallback and improve bubble rendering in collapsed tool calls
- Create image_html.php as fallback when GD not available
- Add test.html page to demonstrate different bubble styles
- Update index.js to support rendering bubbles in collapsed tool calls
- Improve example.html settings page with new options
- Add reference/ to .gitignore

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-16 12:49:14 +01:00
11 changed files with 991 additions and 168 deletions

3
.gitignore vendored
View File

@ -4,6 +4,9 @@ mounts/
# Ignore fonts directory
fonts/
# Ignore reference repos
reference/
# Node modules
node_modules/

115
Generator.md Normal file
View File

@ -0,0 +1,115 @@
# SillyBubble Image Generator - Design Document
## Component Architecture
The SillyBubble image generator creates comic-style chat visualizations with a modular component system. The final image has a 5:1 width-to-height ratio, composed of layered elements.
### Core Components
1. **Background Layer**
- Full 5:1 ratio base image for setting style/mood
- Can be themed (e.g., fantasy, sci-fi, cozy, etc.)
- Provides consistent canvas for all other elements
2. **Character Layer (Chibi)**
- Positioned on the left side of the image
- Various character images with different expressions/poses
- Takes approximately 20% of the total width
- Named by character (e.g., "bianca.png", "ruby.png")
3. **Bubble Layer**
- Semi-transparent speech bubble
- Positioned to the right of the character
- Includes a pointer/tail connecting to the character
- Takes approximately 70% of the total width
- Various styles (rounded, square, cloud, thought, etc.)
4. **Text Layer**
- Dynamically rendered text content
- Positioned within the bubble boundaries
- Supports word-wrapping and styling
- Font options compatible with the overall theme
## Implementation Approach
### File Structure and Naming Convention
```
/characters/ - Main directory for all characters
/Example/ - Example character (fallback)
background.png - Background image
character.png - Character image
speech.png - Speech bubble
thought.png - Thought bubble
/Bianca/ - Another character
background.png - Background image
character.png - Character image
speech.png - Speech bubble
thought.png - Thought bubble
/fonts/*.ttf - Font files
```
Each character has their own directory containing all assets. If a specific character's asset is missing, the system will fall back to the Example character's corresponding asset.
### Image Dimensions
- Total Image: 2000×400px (5:1 ratio)
- Background: Full 2000×400px canvas
- Character: ~400×400px (20% of width)
- Bubble: ~1400×300px (70% of width)
- Text Area: ~1300×250px (inside bubble)
- Remaining 10% (200px width) for margins and spacing
### Parameter System
The enhanced image.php will accept:
- `q`: Text content (required)
- `character`: Character to use (e.g., "bianca") - if not provided, defaults to "Example"
- `bubble_type`: "speech" or "thought" (defaults to "speech")
- `style`: Legacy parameter, can be used instead of character parameter
**Backward Compatibility**:
- If only `style` is provided (no `character`), the script will use the style value as the character name
- This ensures the SillyTavern extension doesn't need modification
### Image Composition Process
1. Load or create background layer (full canvas)
2. Check if character exists and overlay on left side
3. Position and overlay appropriate bubble template
4. Calculate text boundaries within bubble
5. Render text with proper wrapping and styling
6. Output final composed image
### Dynamic Bubble Generation
- If no bubble template exists, dynamically draw a bubble
- Support both template-based and on-the-fly bubble generation
- Ensure proper connection between character and bubble
## Visual Representation
```
FINAL COMPOSITION (5:1 ratio):
+--------------------------------------------------------------------------------------+
| |
| +--------+ +--------------------------------------------------------------+ |
| | | | | |
| | CHIBI |<---+ TEXT CONTENT | |
| | | | | |
| +--------+ +--------------------------------------------------------------+ |
| |
+--------------------------------------------------------------------------------------+
```
## Feature Roadmap
1. **Basic Implementation**
- Support for character-based styling
- Simple bubble positioning
- Proper text wrapping
2. **Enhanced Features**
- Multiple character positions (left/right)
- Various bubble styles
- Expression selection for characters
3. **Advanced Features**
- Multiple characters in one image
- Animated GIF output option
- Theme-based text styling

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -16,12 +16,17 @@
</div>
<div class="sillybubble_block flex-container">
<label for="sillybubble_image_url">Image Service URL:</label>
<input id="sillybubble_image_url" type="text" class="text_pole" />
<input id="sillybubble_use_character_param" type="checkbox" />
<label for="sillybubble_use_character_param">Let AI choose character (expose character parameter to AI)</label>
</div>
<div class="sillybubble_block flex-container">
<label for="sillybubble_default_style">Default Bubble Style:</label>
<label for="sillybubble_image_url">Image Service URL (full URL including https://):</label>
<input id="sillybubble_image_url" type="text" class="text_pole" placeholder="https://calista.the.sexiest.cat/image.php" />
</div>
<div class="sillybubble_block flex-container">
<label for="sillybubble_default_style">Default Bubble Style (Legacy):</label>
<select id="sillybubble_default_style" class="text_pole">
<option value="default">Default</option>
<option value="modern">Modern</option>
@ -30,6 +35,22 @@
</select>
</div>
<div class="sillybubble_block flex-container">
<label for="sillybubble_default_character">Default Character:</label>
<select id="sillybubble_default_character" class="text_pole">
<option value="Example">Example</option>
<option value="Bianca">Bianca</option>
</select>
</div>
<div class="sillybubble_block flex-container">
<label for="sillybubble_default_bubble_type">Default Bubble Type:</label>
<select id="sillybubble_default_bubble_type" class="text_pole">
<option value="speech">Speech</option>
<option value="thought">Thought</option>
</select>
</div>
<div class="sillybubble_block flex-container">
<input id="sillybubble_test_button" class="menu_button" type="submit" value="Test Chat Bubble" />
</div>
@ -40,7 +61,9 @@
<p>Parameters:</p>
<ul>
<li><code>text</code>: The text to display in the bubble</li>
<li><code>style</code>: (Optional) Bubble style</li>
<li><code>character</code>: (Optional) Character to use (Example, Bianca, etc.)</li>
<li><code>bubble_type</code>: (Optional) Bubble type (speech, thought)</li>
<li><code>style</code>: (Optional, Legacy) Bubble style</li>
</ul>
<div class="sillybubble_important">
<p><strong>Important:</strong> Make sure function calling is enabled in SillyTavern's AI settings and the model you're using supports function calling.</p>
@ -50,17 +73,33 @@
<li>"Include functions in context" is checked</li>
<li>The function appears in the function list</li>
</ol>
<p><strong>🚨 REQUIRED SETUP:</strong> You MUST add the following to your system prompt or character card:</p>
<pre>You have access to a function called generateChatBubbleImage(text, style) that creates chat bubbles with the given text.
<p><strong>🚨 REQUIRED SETUP:</strong> You MUST add one of the following to your system prompt or character card (depending on your settings):</p>
<p><strong>If "Let AI choose character" is disabled (default):</strong></p>
<pre>You have access to a function called generateChatBubbleImage(text, bubble_type) that creates styled chat bubbles.
The function parameters are:
- text: The text to display in the chat bubble (required string)
- style: Visual style of the bubble (optional string: "default", "modern", "retro", or "minimal")
- bubble_type: The type of bubble to use (optional string: "speech" or "thought")
When you want to create a chat bubble, call this function with the text you want to display.
Example usage:
generateChatBubbleImage("Hello world!")
generateChatBubbleImage("Hello in retro style", "retro")</pre>
generateChatBubbleImage("I'm thinking...", "thought")</pre>
<p><strong>If "Let AI choose character" is enabled:</strong></p>
<pre>You have access to a function called generateChatBubbleImage(text, character, bubble_type) that creates character-styled chat bubbles.
The function parameters are:
- text: The text to display in the chat bubble (required string)
- character: The character to use for the bubble (required string: "Example", "Bianca", etc.)
- bubble_type: The type of bubble to use (optional string: "speech" or "thought")
When you want to create a chat bubble, call this function with the text and character.
Example usage:
generateChatBubbleImage("Hello world!", "Example")
generateChatBubbleImage("Hello from Bianca", "Bianca")
generateChatBubbleImage("I'm thinking...", "Example", "thought")</pre>
<p><strong>ALTERNATIVE:</strong> If the AI can't call the function directly, you can instruct it to respond with Markdown formatted like this:</p>
<pre>![](image.php?q=Hello%20world)</pre>
</div>

403
image.php
View File

@ -2,12 +2,14 @@
/**
* SillyBubble Image Generator
*
* This script generates chat bubble images from text.
* It takes a 'q' parameter for the text content and an optional 'style' parameter.
* This script generates comic-style chat bubble images with characters.
* It supports character-based styling with speech or thought bubbles.
*
* Usage:
* image.php?q=Hello+World
* image.php?q=Hello+World&style=modern
* image.php?q=Hello+World&character=Example
* image.php?q=Hello+World&character=Example&bubble_type=thought
* image.php?q=Hello+World&style=Example (legacy style parameter maps to character)
*/
// Set content type to PNG image
@ -15,97 +17,301 @@ header('Content-Type: image/png');
// Get parameters
$text = isset($_GET['q']) ? urldecode($_GET['q']) : 'No text provided';
$character = isset($_GET['character']) ? $_GET['character'] : 'Example';
$bubble_type = isset($_GET['bubble_type']) ? $_GET['bubble_type'] : 'speech';
$style = isset($_GET['style']) ? $_GET['style'] : 'default';
// Define styles
// If style parameter is used but no character is specified, use style as character name
if (!isset($_GET['character']) && isset($_GET['style']) && $style != 'default') {
$character = $style;
}
// Validate bubble_type - only 'speech' or 'thought' allowed
if ($bubble_type != 'speech' && $bubble_type != 'thought') {
$bubble_type = 'speech';
}
// Define canvas dimensions (5:1 ratio)
$canvasWidth = 2000;
$canvasHeight = 400;
$charWidth = 400; // 20% of canvas width
$bubbleWidth = 1400; // 70% of canvas width
$bubbleMargin = 100; // 5% margin on each side
$bubblePadding = 50; // Padding inside the bubble
// Define legacy styles for backward compatibility
$styles = [
'default' => [
'bg_color' => [245, 245, 245],
'text_color' => [50, 50, 50],
'border_color' => [200, 200, 200],
'padding' => 20,
'rounded' => 15,
'font_size' => 14,
'max_width' => 600,
'line_height' => 20,
'font' => __DIR__ . '/fonts/arial.ttf', // Adjust path as needed
'font_size' => 24, // Increased for higher resolution
'line_height' => 30,
'font' => __DIR__ . '/fonts/NotoSans-Regular.ttf',
],
'modern' => [
'bg_color' => [66, 133, 244],
'text_color' => [255, 255, 255],
'border_color' => [59, 120, 220],
'padding' => 20,
'rounded' => 20,
'font_size' => 14,
'max_width' => 600,
'line_height' => 20,
'font' => __DIR__ . '/fonts/arial.ttf', // Adjust path as needed
'font_size' => 24,
'line_height' => 30,
'font' => __DIR__ . '/fonts/NotoSans-Regular.ttf',
],
'retro' => [
'bg_color' => [255, 204, 102],
'text_color' => [51, 51, 51],
'border_color' => [204, 153, 0],
'padding' => 20,
'rounded' => 5,
'font_size' => 14,
'max_width' => 600,
'line_height' => 20,
'font' => __DIR__ . '/fonts/arial.ttf', // Adjust path as needed
'font_size' => 24,
'line_height' => 30,
'font' => __DIR__ . '/fonts/NotoSans-Regular.ttf',
],
'minimal' => [
'bg_color' => [255, 255, 255],
'text_color' => [0, 0, 0],
'border_color' => [220, 220, 220],
'padding' => 15,
'rounded' => 0,
'font_size' => 14,
'max_width' => 600,
'line_height' => 20,
'font' => __DIR__ . '/fonts/arial.ttf', // Adjust path as needed
'font_size' => 24,
'line_height' => 30,
'font' => __DIR__ . '/fonts/NotoSans-Regular.ttf',
],
];
// Use default style if specified style doesn't exist
if (!isset($styles[$style])) {
$style = 'default';
}
// Set default styling
$config = [
'bg_color' => [245, 245, 245],
'text_color' => [50, 50, 50],
'border_color' => [200, 200, 200],
'font_size' => 24,
'line_height' => 30,
'font' => __DIR__ . '/fonts/NotoSans-Regular.ttf',
];
// Get style settings
$config = $styles[$style];
// Ensure required directories exist
$dirs = [
__DIR__ . '/characters/Example',
__DIR__ . '/fonts'
];
// Check for fonts directory and create if it doesn't exist
$fontsDir = __DIR__ . '/fonts';
if (!is_dir($fontsDir)) {
mkdir($fontsDir, 0755, true);
foreach ($dirs as $dir) {
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
}
// Fallback to a built-in font if the specified font file doesn't exist
if (!file_exists($config['font'])) {
// Try to find any TTF font in the fonts directory
$foundFont = false;
if (is_dir($fontsDir)) {
$fontFiles = glob($fontsDir . '/*.ttf');
if (!empty($fontFiles)) {
$config['font'] = $fontFiles[0];
$foundFont = true;
}
}
// If no TTF font found, use built-in font
if (!$foundFont) {
$fontFiles = glob(__DIR__ . '/fonts/*.ttf');
if (!empty($fontFiles)) {
$config['font'] = $fontFiles[0];
} else {
$config['use_builtin_font'] = true;
}
}
// Create a temporary image to calculate text dimensions
$tempImage = imagecreatetruecolor(100, 100);
$textColor = imagecolorallocate($tempImage, $config['text_color'][0], $config['text_color'][1], $config['text_color'][2]);
// Create the canvas
$canvas = imagecreatetruecolor($canvasWidth, $canvasHeight);
// Set default white background
$whiteColor = imagecolorallocate($canvas, 255, 255, 255);
imagefill($canvas, 0, 0, $whiteColor);
// Set text color
$textColor = imagecolorallocate($canvas, $config['text_color'][0], $config['text_color'][1], $config['text_color'][2]);
// Function to get image path with fallback to Example
function getAssetPath($assetType, $character) {
// Primary path in character's directory
$primaryPath = __DIR__ . '/characters/' . $character . '/' . $assetType . '.png';
// Fallback to Example character
$fallbackPath = __DIR__ . '/characters/Example/' . $assetType . '.png';
return file_exists($primaryPath) ? $primaryPath : $fallbackPath;
}
// Check for and load the background image
$backgroundPath = getAssetPath('background', $character);
$backgroundLoaded = false;
if (file_exists($backgroundPath)) {
$backgroundImage = imagecreatefrompng($backgroundPath);
if ($backgroundImage) {
// Resize if needed
$bgWidth = imagesx($backgroundImage);
$bgHeight = imagesy($backgroundImage);
// Copy background to canvas, resizing if necessary
imagecopyresampled($canvas, $backgroundImage, 0, 0, 0, 0, $canvasWidth, $canvasHeight, $bgWidth, $bgHeight);
imagedestroy($backgroundImage);
$backgroundLoaded = true;
}
}
// If no background was loaded, use a solid color background
if (!$backgroundLoaded) {
// Use legacy style background color if it exists
if (isset($styles[$style])) {
$bgColor = imagecolorallocate($canvas,
$styles[$style]['bg_color'][0],
$styles[$style]['bg_color'][1],
$styles[$style]['bg_color'][2]
);
} else {
$bgColor = imagecolorallocate($canvas, 245, 245, 245); // Default light gray
}
imagefilledrectangle($canvas, 0, 0, $canvasWidth-1, $canvasHeight-1, $bgColor);
}
// Check for and load the character image
$characterPath = getAssetPath('character', $character);
$characterLoaded = false;
if (file_exists($characterPath)) {
$characterImage = imagecreatefrompng($characterPath);
if ($characterImage) {
// Enable alpha blending
imagesavealpha($characterImage, true);
// Calculate position (center vertically, align left)
$charWidth = imagesx($characterImage);
$charHeight = imagesy($characterImage);
$charX = $bubbleMargin;
$charY = ($canvasHeight - $charHeight) / 2;
// Copy character to canvas
imagecopy($canvas, $characterImage, $charX, $charY, 0, 0, $charWidth, $charHeight);
imagedestroy($characterImage);
$characterLoaded = true;
}
}
// Check for and load the bubble image
$bubblePath = getAssetPath($bubble_type, $character);
$bubbleLoaded = false;
if (file_exists($bubblePath)) {
$bubbleImage = imagecreatefrompng($bubblePath);
if ($bubbleImage) {
// Enable alpha blending
imagesavealpha($bubbleImage, true);
// Calculate position (center vertically, align right of character)
$bubbleWidth = imagesx($bubbleImage);
$bubbleHeight = imagesy($bubbleImage);
$bubbleX = $charWidth + $bubbleMargin * 2;
$bubbleY = ($canvasHeight - $bubbleHeight) / 2;
// Copy bubble to canvas
imagecopy($canvas, $bubbleImage, $bubbleX, $bubbleY, 0, 0, $bubbleWidth, $bubbleHeight);
imagedestroy($bubbleImage);
$bubbleLoaded = true;
}
}
// If no bubble was loaded, draw a simple bubble
if (!$bubbleLoaded) {
// Calculate bubble dimensions and position
$bubbleX = $charWidth + $bubbleMargin * 2;
$bubbleY = $canvasHeight * 0.1;
$bubbleWidth = $canvasWidth - $bubbleX - $bubbleMargin;
$bubbleHeight = $canvasHeight * 0.8;
// Determine bubble style based on legacy styles if available
if (isset($styles[$style])) {
$bubbleBgColor = imagecolorallocate($canvas,
$styles[$style]['bg_color'][0],
$styles[$style]['bg_color'][1],
$styles[$style]['bg_color'][2]
);
$bubbleBorderColor = imagecolorallocate($canvas,
$styles[$style]['border_color'][0],
$styles[$style]['border_color'][1],
$styles[$style]['border_color'][2]
);
} else {
$bubbleBgColor = imagecolorallocate($canvas, 245, 245, 245); // Default light gray
$bubbleBorderColor = imagecolorallocate($canvas, 200, 200, 200); // Default gray border
}
// Draw bubble based on type
if ($bubble_type == 'thought') {
// Draw a thought bubble (rounded rectangle with smaller circles)
$radius = 40;
// Main bubble
imagefilledrectangle($canvas, $bubbleX + $radius, $bubbleY, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight, $bubbleBgColor);
imagefilledrectangle($canvas, $bubbleX, $bubbleY + $radius, $bubbleX + $bubbleWidth, $bubbleY + $bubbleHeight - $radius, $bubbleBgColor);
// Corners
imagefilledarc($canvas, $bubbleX + $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 180, 270, $bubbleBgColor, IMG_ARC_PIE);
imagefilledarc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 270, 360, $bubbleBgColor, IMG_ARC_PIE);
imagefilledarc($canvas, $bubbleX + $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 90, 180, $bubbleBgColor, IMG_ARC_PIE);
imagefilledarc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 0, 90, $bubbleBgColor, IMG_ARC_PIE);
// Draw small circles leading to character
$circleCount = 3;
$startX = $bubbleX - 20;
$startY = $bubbleY + $bubbleHeight/2 + 20;
for ($i = 0; $i < $circleCount; $i++) {
$circleRadius = 15 - ($i * 4);
$circleX = $startX - ($i * 30);
$circleY = $startY + ($i * 20);
imagefilledellipse($canvas, $circleX, $circleY, $circleRadius*2, $circleRadius*2, $bubbleBgColor);
imageellipse($canvas, $circleX, $circleY, $circleRadius*2, $circleRadius*2, $bubbleBorderColor);
}
// Border
imagerectangle($canvas, $bubbleX + $radius, $bubbleY, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight, $bubbleBorderColor);
imagerectangle($canvas, $bubbleX, $bubbleY + $radius, $bubbleX + $bubbleWidth, $bubbleY + $bubbleHeight - $radius, $bubbleBorderColor);
imagearc($canvas, $bubbleX + $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 180, 270, $bubbleBorderColor);
imagearc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 270, 360, $bubbleBorderColor);
imagearc($canvas, $bubbleX + $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 90, 180, $bubbleBorderColor);
imagearc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 0, 90, $bubbleBorderColor);
} else {
// Draw a speech bubble (rounded rectangle with a pointer)
$radius = 40;
// Main bubble
imagefilledrectangle($canvas, $bubbleX + $radius, $bubbleY, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight, $bubbleBgColor);
imagefilledrectangle($canvas, $bubbleX, $bubbleY + $radius, $bubbleX + $bubbleWidth, $bubbleY + $bubbleHeight - $radius, $bubbleBgColor);
// Corners
imagefilledarc($canvas, $bubbleX + $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 180, 270, $bubbleBgColor, IMG_ARC_PIE);
imagefilledarc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 270, 360, $bubbleBgColor, IMG_ARC_PIE);
imagefilledarc($canvas, $bubbleX + $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 90, 180, $bubbleBgColor, IMG_ARC_PIE);
imagefilledarc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 0, 90, $bubbleBgColor, IMG_ARC_PIE);
// Draw pointer
$pointerX = [$bubbleX, $bubbleX - 40, $bubbleX];
$pointerY = [$bubbleY + $bubbleHeight/2 - 40, $bubbleY + $bubbleHeight/2, $bubbleY + $bubbleHeight/2 + 40];
imagefilledpolygon($canvas, $pointerX, $pointerY, 3, $bubbleBgColor);
// Border
imagerectangle($canvas, $bubbleX + $radius, $bubbleY, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight, $bubbleBorderColor);
imagerectangle($canvas, $bubbleX, $bubbleY + $radius, $bubbleX + $bubbleWidth, $bubbleY + $bubbleHeight - $radius, $bubbleBorderColor);
imagearc($canvas, $bubbleX + $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 180, 270, $bubbleBorderColor);
imagearc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 270, 360, $bubbleBorderColor);
imagearc($canvas, $bubbleX + $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 90, 180, $bubbleBorderColor);
imagearc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 0, 90, $bubbleBorderColor);
// Pointer border
imagepolygon($canvas, $pointerX, $pointerY, 3, $bubbleBorderColor);
}
}
// Set text area dimensions
$textAreaX = $bubbleX + $bubblePadding;
$textAreaY = $bubbleY + $bubblePadding;
$textAreaWidth = $bubbleWidth - ($bubblePadding * 2);
$textAreaHeight = $bubbleHeight - ($bubblePadding * 2);
// Word wrap the text
$words = explode(' ', $text);
$lines = [];
$currentLine = '';
$maxWidth = $config['max_width'] - (2 * $config['padding']);
$maxWidth = $textAreaWidth;
foreach ($words as $word) {
$testLine = $currentLine . ' ' . $word;
@ -130,92 +336,43 @@ if ($currentLine !== '') {
$lines[] = $currentLine;
}
// Calculate image dimensions
// Center text vertically in the bubble
$lineCount = count($lines);
if (isset($config['use_builtin_font'])) {
$lineHeight = imagefontheight(5);
$textHeight = $lineHeight * $lineCount;
// Find the longest line to determine width
$textWidth = 0;
foreach ($lines as $line) {
$lineWidth = imagefontwidth(5) * strlen($line);
$textWidth = max($textWidth, $lineWidth);
}
$totalTextHeight = $lineCount * imagefontheight(5);
} else {
$lineHeight = $config['line_height'];
$textHeight = $lineHeight * $lineCount;
// Find the longest line to determine width
$textWidth = 0;
foreach ($lines as $line) {
$bbox = imagettfbbox($config['font_size'], 0, $config['font'], $line);
$lineWidth = $bbox[2] - $bbox[0];
$textWidth = max($textWidth, $lineWidth);
}
$totalTextHeight = $lineCount * $config['line_height'];
}
// Add padding to dimensions
$imgWidth = min($config['max_width'], $textWidth + (2 * $config['padding']));
$imgHeight = $textHeight + (2 * $config['padding']);
// Create the bubble image
$image = imagecreatetruecolor($imgWidth, $imgHeight);
// Allocate colors
$bgColor = imagecolorallocate($image, $config['bg_color'][0], $config['bg_color'][1], $config['bg_color'][2]);
$borderColor = imagecolorallocate($image, $config['border_color'][0], $config['border_color'][1], $config['border_color'][2]);
$textColor = imagecolorallocate($image, $config['text_color'][0], $config['text_color'][1], $config['text_color'][2]);
// Fill background
imagefill($image, 0, 0, $bgColor);
// Draw rounded rectangle (if rounded corners are requested)
if ($config['rounded'] > 0) {
// Draw filled rectangle
imagefilledrectangle($image, 0, 0, $imgWidth - 1, $imgHeight - 1, $bgColor);
// Draw rounded corners - basic simulation for rounded corners
// This could be improved with proper arc drawing
$r = $config['rounded'];
// Top left corner
imagefilledarc($image, $r, $r, $r * 2, $r * 2, 180, 270, $bgColor, IMG_ARC_PIE);
// Top right corner
imagefilledarc($image, $imgWidth - $r - 1, $r, $r * 2, $r * 2, 270, 360, $bgColor, IMG_ARC_PIE);
// Bottom left corner
imagefilledarc($image, $r, $imgHeight - $r - 1, $r * 2, $r * 2, 90, 180, $bgColor, IMG_ARC_PIE);
// Bottom right corner
imagefilledarc($image, $imgWidth - $r - 1, $imgHeight - $r - 1, $r * 2, $r * 2, 0, 90, $bgColor, IMG_ARC_PIE);
// Draw border
imagerectangle($image, 0, 0, $imgWidth - 1, $imgHeight - 1, $borderColor);
} else {
// Draw simple rectangle
imagefilledrectangle($image, 0, 0, $imgWidth - 1, $imgHeight - 1, $bgColor);
imagerectangle($image, 0, 0, $imgWidth - 1, $imgHeight - 1, $borderColor);
}
// Calculate vertical starting position for centered text
$startY = $textAreaY + ($textAreaHeight - $totalTextHeight) / 2;
if ($startY < $textAreaY) $startY = $textAreaY; // Ensure text doesn't overflow top
// Draw text
$y = $config['padding'];
$y = $startY + $config['font_size']; // Start position for centered text
foreach ($lines as $line) {
if (isset($config['use_builtin_font'])) {
// Calculate width for centering horizontally
$lineWidth = imagefontwidth(5) * strlen($line);
$x = $textAreaX + ($textAreaWidth - $lineWidth) / 2;
// Use built-in font (less nice but always available)
imagestring($image, 5, $config['padding'], $y, $line, $textColor);
imagestring($canvas, 5, $x, $y, $line, $textColor);
$y += imagefontheight(5);
} else {
// Calculate width for centering horizontally
$bbox = imagettfbbox($config['font_size'], 0, $config['font'], $line);
$lineWidth = $bbox[2] - $bbox[0];
$x = $textAreaX + ($textAreaWidth - $lineWidth) / 2;
// Use TrueType font (nicer but requires font file)
imagettftext($image, $config['font_size'], 0, $config['padding'], $y + $config['font_size'], $textColor, $config['font'], $line);
$y += $lineHeight;
imagettftext($canvas, $config['font_size'], 0, $x, $y, $textColor, $config['font'], $line);
$y += $config['line_height'];
}
}
// Output the image
imagepng($image);
imagepng($canvas);
// Clean up
imagedestroy($image);
imagedestroy($tempImage);
imagedestroy($canvas);

114
image_html.php Normal file
View File

@ -0,0 +1,114 @@
<?php
/**
* SillyBubble HTML Fallback
*
* Creates a chat bubble using HTML/CSS when the GD library is not available
* Accepts the same parameters as image.php
*/
// Get the text from the URL parameter
$text = isset($_GET['q']) ? $_GET['q'] : 'Hello world!';
$style = isset($_GET['style']) ? strtolower($_GET['style']) : 'default';
// Decode URL-encoded text
$text = htmlspecialchars(urldecode($text));
// Define styles
$styles = [
'default' => [
'background' => '#c8dcff',
'border' => '#96aade',
'text' => '#000000',
],
'modern' => [
'background' => '#646464',
'border' => '#464646',
'text' => '#ffffff',
],
'retro' => [
'background' => '#fff0c8',
'border' => '#dcb48c',
'text' => '#64321e',
],
'minimal' => [
'background' => '#f5f5f5',
'border' => '#c8c8c8',
'text' => '#323232',
]
];
// Use default style if specified style doesn't exist
if (!isset($styles[$style])) {
$style = 'default';
}
// CSS for different shapes
$shapeCSS = [
'default' => 'border-radius: 10px;',
'modern' => 'border-radius: 10px;',
'retro' => 'border-radius: 0;',
'minimal' => 'border-radius: 0;',
];
// Output HTML/CSS
header('Content-Type: text/html');
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat Bubble</title>
<style>
body, html {
margin: 0;
padding: 0;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: transparent;
}
.bubble-container {
max-width: 600px;
position: relative;
padding: 20px;
}
.bubble {
position: relative;
background-color: <?php echo $styles[$style]['background']; ?>;
border: 1px solid <?php echo $styles[$style]['border']; ?>;
color: <?php echo $styles[$style]['text']; ?>;
padding: 20px;
<?php echo $shapeCSS[$style]; ?>
font-family: Arial, sans-serif;
line-height: 1.5;
}
.bubble:after {
content: '';
position: absolute;
bottom: -15px;
left: 50px;
border-width: 15px 15px 0;
border-style: solid;
border-color: <?php echo $styles[$style]['background']; ?> transparent;
}
.bubble:before {
content: '';
position: absolute;
bottom: -16px;
left: 49px;
border-width: 16px 16px 0;
border-style: solid;
border-color: <?php echo $styles[$style]['border']; ?> transparent;
}
</style>
</head>
<body>
<div class="bubble-container">
<div class="bubble">
<?php echo nl2br($text); ?>
</div>
</div>
</body>
</html>

312
index.js
View File

@ -8,10 +8,13 @@ const extensionFolderPath = `scripts/extensions/third-party/${extensionName}`;
// Default settings
const defaultSettings = {
image_service_url: "image.php",
image_service_url: "http://calista.the.sexiest.cat/image.php", // Use fully qualified URL by default
default_style: "default",
default_character: "Example",
default_bubble_type: "speech",
enabled: true,
render_in_collapse: true // New setting to enable/disable rendering in collapsed tool calls
render_in_collapse: true, // Setting to enable/disable rendering in collapsed tool calls
use_character_param: false // Whether to expose the character parameter to the AI
};
// Make sure settings exist
@ -24,28 +27,56 @@ if (Object.keys(extension_settings[extensionName]).length === 0) {
Object.assign(extension_settings[extensionName], defaultSettings);
}
// Validate settings - ensure image URL is absolute
if (extension_settings[extensionName].image_service_url &&
!extension_settings[extensionName].image_service_url.startsWith('http://') &&
!extension_settings[extensionName].image_service_url.startsWith('https://')) {
console.warn(`[${extensionName}] Saved image URL is not absolute, updating to use http://`);
extension_settings[extensionName].image_service_url = `http://${extension_settings[extensionName].image_service_url}`;
}
// Function for AI to call - generates markdown image with URL-encoded text
function generateChatBubbleImage(text, style) {
function generateChatBubbleImage(text, style, character, bubble_type) {
if (!extension_settings[extensionName].enabled) {
return text;
}
// Use default style if not specified
// Use default values if parameters not specified
const bubbleStyle = style || extension_settings[extensionName].default_style;
const characterName = character || extension_settings[extensionName].default_character;
const bubbleType = bubble_type || extension_settings[extensionName].default_bubble_type;
// URL encode the text parameter
const encodedText = encodeURIComponent(text);
// Construct the URL with the encoded text
const imageUrl = `${extension_settings[extensionName].image_service_url}?q=${encodedText}`;
// Ensure the URL is absolute (starts with http:// or https://)
let serviceUrl = extension_settings[extensionName].image_service_url;
if (!serviceUrl.startsWith('http://') && !serviceUrl.startsWith('https://')) {
console.warn(`[${extensionName}] Image service URL is not absolute: ${serviceUrl}`);
serviceUrl = `http://${serviceUrl}`;
}
// Add style parameter if specified
const fullUrl = bubbleStyle !== "default"
? `${imageUrl}&style=${bubbleStyle}`
: imageUrl;
// Construct the URL with the encoded text
let imageUrl = `${serviceUrl}?q=${encodedText}`;
// Add parameters if they differ from defaults
if (characterName && characterName !== "Example") {
imageUrl += `&character=${characterName}`;
}
if (bubbleType && bubbleType !== "speech") {
imageUrl += `&bubble_type=${bubbleType}`;
}
// Add style parameter if specified (for backward compatibility)
if (bubbleStyle && bubbleStyle !== "default" && !characterName) {
imageUrl += `&style=${bubbleStyle}`;
}
console.log(`[${extensionName}] Generated image URL: ${imageUrl}`);
// Return markdown image format
return `![](${fullUrl})`;
return `![](${imageUrl})`;
}
// Load extension settings into UI
@ -54,7 +85,10 @@ 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_default_character").val(extension_settings[extensionName].default_character).trigger("input");
$("#sillybubble_default_bubble_type").val(extension_settings[extensionName].default_bubble_type).trigger("input");
$("#sillybubble_render_in_collapse").prop("checked", extension_settings[extensionName].render_in_collapse).trigger("input");
$("#sillybubble_use_character_param").prop("checked", extension_settings[extensionName].use_character_param).trigger("input");
}
// Handle enable/disable toggle
@ -66,7 +100,15 @@ function onEnabledInput(event) {
// Handle image service URL changes
function onImageUrlInput(event) {
const value = $(event.target).val();
let value = $(event.target).val().trim();
// Ensure URL is absolute
if (value && !value.startsWith('http://') && !value.startsWith('https://')) {
console.warn(`[${extensionName}] Entered URL is not absolute, updating: ${value}`);
value = `http://${value}`;
$(event.target).val(value);
}
extension_settings[extensionName].image_service_url = value;
saveSettingsDebounced();
}
@ -78,6 +120,20 @@ function onDefaultStyleInput(event) {
saveSettingsDebounced();
}
// Handle default character changes
function onDefaultCharacterInput(event) {
const value = $(event.target).val();
extension_settings[extensionName].default_character = value;
saveSettingsDebounced();
}
// Handle default bubble type changes
function onDefaultBubbleTypeInput(event) {
const value = $(event.target).val();
extension_settings[extensionName].default_bubble_type = value;
saveSettingsDebounced();
}
// Handle render in collapse toggle
function onRenderInCollapseInput(event) {
const value = Boolean($(event.target).prop("checked"));
@ -88,14 +144,36 @@ function onRenderInCollapseInput(event) {
processToolCallMessages();
}
// Handle use character parameter toggle
function onUseCharacterParamInput(event) {
const value = Boolean($(event.target).prop("checked"));
extension_settings[extensionName].use_character_param = value;
saveSettingsDebounced();
// Re-register the function tool with updated schema when the setting is toggled
registerFunctionTool();
}
// Test function to visualize a bubble
function onTestButtonClick() {
const testText = "This is a test chat bubble generated by SillyBubble!";
const markdown = generateChatBubbleImage(testText);
const useCharParam = extension_settings[extensionName].use_character_param;
const character = useCharParam ? extension_settings[extensionName].default_character : null;
const bubbleType = extension_settings[extensionName].default_bubble_type;
const markdown = generateChatBubbleImage(testText, null, character, bubbleType);
// Determine what parameters to display in the test popup
let paramInfo = `<p>Using bubble type: <strong>${bubbleType}</strong></p>`;
if (useCharParam) {
paramInfo = `<p>Using character: <strong>${character}</strong>, bubble type: <strong>${bubbleType}</strong></p>`;
}
const testPopup = `
<div class="sillybubble-test">
<p>Generated Markdown:</p>
<pre>${markdown.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</pre>
${paramInfo}
<p>Preview (if connected to image service):</p>
<div>${markdown}</div>
</div>
@ -115,32 +193,69 @@ function registerFunctionTool() {
// Check if registerFunctionTool exists in context
if (typeof context.registerFunctionTool === 'function') {
// Create properties object for schema
const properties = {
text: {
type: 'string',
description: 'The text to display in the chat bubble'
},
bubble_type: {
type: 'string',
description: 'The type of bubble to use (speech, thought). Defaults to speech.'
}
};
// Only add character parameter if enabled in settings
if (extension_settings[extensionName].use_character_param) {
properties.character = {
type: 'string',
description: 'The character to display (Example, Bianca, etc.). Each character has its own bubble style.'
};
}
// Always include style parameter for backward compatibility
properties.style = {
type: 'string',
description: 'Legacy parameter: The visual style of the chat bubble (default, modern, retro, minimal).'
};
// Define required parameters
const requiredParams = ['text'];
// Add character to required parameters if enabled
if (extension_settings[extensionName].use_character_param) {
requiredParams.push('character');
}
// Define parameter schema following JSON schema format
const bubbleSchema = Object.freeze({
$schema: 'http://json-schema.org/draft-04/schema#',
type: 'object',
properties: {
text: {
type: 'string',
description: 'The text to display in the chat bubble'
},
style: {
type: 'string',
description: 'The visual style of the chat bubble (default, modern, retro, minimal)'
},
},
required: ['text']
properties: properties,
required: requiredParams
});
// Register the function tool using SillyTavern's own API
context.registerFunctionTool({
name: 'generateChatBubbleImage',
displayName: 'Chat Bubble Image',
description: 'Creates a markdown image link with the text displayed as a styled chat bubble. Use when you want to visually style messages or display text in a speech bubble.',
description: 'Creates a markdown image link with the text displayed as a styled chat bubble. Use when you want to visually style messages or display text in a speech or thought bubble.',
parameters: bubbleSchema,
action: async (args) => {
if (!args?.text) return '';
return generateChatBubbleImage(args.text, args.style);
let character;
if (extension_settings[extensionName].use_character_param) {
// When the setting is enabled, character is required
// If not provided, use default
character = args.character || extension_settings[extensionName].default_character;
} else {
// When the setting is disabled, always use the default character
character = extension_settings[extensionName].default_character;
}
return generateChatBubbleImage(args.text, args.style, character, args.bubble_type);
},
formatMessage: () => '',
});
@ -201,23 +316,59 @@ function processToolCallMessages() {
const markdownImgRegex = /!\[\]\(([^)]+)\)/;
const match = tool.result.match(markdownImgRegex);
if (match && match[1]) {
renderContainer.html(`<img src="${match[1]}" alt="Chat Bubble">`);
// Ensure the image URL is absolute
let imgUrl = match[1];
if (!imgUrl.startsWith('http://') && !imgUrl.startsWith('https://')) {
console.warn(`[${extensionName}] Image URL is not absolute: ${imgUrl}`);
imgUrl = `http://${imgUrl}`;
}
console.log(`[${extensionName}] Rendering image from URL: ${imgUrl}`);
renderContainer.html(`<img src="${imgUrl}" alt="Chat Bubble">`);
// Store the image URL in a data attribute for easier access
$(this).attr('data-sb-image-url', imgUrl);
} else {
renderContainer.html(tool.result);
}
// Remove any existing rendered images
// Clean up all images for this message
$(this).find('.sillybubble-rendered-image').remove();
$(this).find('.sillybubble-collapsed-image').remove();
// Add this image after the summary element, but only if details is present
if (summaryElement.length) {
summaryElement.after(renderContainer);
// Create a special always-visible container for collapsed state
const collapsedContainer = $('<div class="sillybubble-collapsed-image"></div>');
collapsedContainer.html(renderContainer.html());
// Get the message text element (where tool calls are shown)
const messageTextDiv = $(this).find('.mes_text');
// Check if the details element is part of mes_text (most common case)
const detailsInText = messageTextDiv.find('details').length > 0;
if (detailsInText) {
// For typical tool calls, add collapsed image just before the details element in mes_text
messageTextDiv.find('details').before(collapsedContainer);
} else {
// Fallback - add it before the details element wherever it is
detailsElement.before(collapsedContainer);
}
// ONLY add image in the reasoning div, NOT in the details summary
const reasoningDiv = $(this).find('.mes_reasoning');
if (reasoningDiv.length) {
// Make sure no existing image is there
reasoningDiv.find('.sillybubble-rendered-image').remove();
// Add the image to the reasoning section
reasoningDiv.prepend(renderContainer);
}
// Mark this message as processed
$(this).attr('data-sb-processed', 'true');
console.log(`[${extensionName}] Rendered chat bubble image in tool call`);
// Manage visibility based on details open state
handleDetailsVisibility($(this), detailsElement);
console.log(`[${extensionName}] Rendered chat bubble image in tool call: ${imgUrl}`);
}
}
}
@ -232,6 +383,33 @@ function processToolCallMessages() {
});
}
// Helper function to handle visibility of images based on details open state
function handleDetailsVisibility(messageElement, detailsElement) {
// Add a class to the details element to mark it as containing a bubble image
detailsElement.addClass('has-bubble-image');
// Set up a click handler for the details/summary to ensure proper rendering
detailsElement.on('click', function() {
// Small delay to ensure the open state has changed
setTimeout(() => {
// Use the CSS-based visibility approach defined in our styles
// This is more reliable than show/hide in the DOM
if (detailsElement.prop('open')) {
messageElement.addClass('details-open');
} else {
messageElement.removeClass('details-open');
}
}, 10);
});
// Set initial state
if (detailsElement.prop('open')) {
messageElement.addClass('details-open');
} else {
messageElement.removeClass('details-open');
}
}
// Observer function to watch for new messages
function setupMessageObserver() {
// Create a mutation observer to monitor the chat for new messages
@ -272,6 +450,20 @@ function setupMessageObserver() {
// Process any existing messages
setTimeout(() => processToolCallMessages(), 500);
// Add additional event listener for details element clicks
$(document).on('click', '.mes.smallSysMes.toolCall details, .mes.smallSysMes.toolCall summary', function() {
// Force reprocessing of this specific tool call message after a short delay
// This ensures the image is visible regardless of details open/closed state
setTimeout(() => {
const toolCallMessage = $(this).closest('.mes.smallSysMes.toolCall');
if (toolCallMessage.length) {
// Remove the processed flag to force reprocessing
toolCallMessage.removeAttr('data-sb-processed');
processToolCallMessages();
}
}, 50);
});
} else {
console.warn(`[${extensionName}] Chat element not found, observer not started`);
// Try again after a delay
@ -293,7 +485,10 @@ jQuery(async () => {
$("#sillybubble_enabled").on("input", onEnabledInput);
$("#sillybubble_image_url").on("input", onImageUrlInput);
$("#sillybubble_default_style").on("input", onDefaultStyleInput);
$("#sillybubble_default_character").on("input", onDefaultCharacterInput);
$("#sillybubble_default_bubble_type").on("input", onDefaultBubbleTypeInput);
$("#sillybubble_render_in_collapse").on("input", onRenderInCollapseInput);
$("#sillybubble_use_character_param").on("input", onUseCharacterParamInput);
$("#sillybubble_test_button").on("click", onTestButtonClick);
// Initial attempt to register the function tool
@ -305,30 +500,73 @@ jQuery(async () => {
// Add CSS for rendered images
$('head').append(`
<style>
/* Style for bubble images in expanded details */
.sillybubble-rendered-image {
padding: 10px;
background-color: rgba(0, 0, 0, 0.05);
border-radius: 5px;
margin: 5px 0;
text-align: center;
display: block !important;
visibility: visible !important;
}
.sillybubble-rendered-image img {
max-width: 100%;
border-radius: 3px;
display: inline-block !important;
visibility: visible !important;
}
/* Style for bubble images when details is collapsed */
.sillybubble-collapsed-image {
padding: 10px 5px;
background-color: rgba(0, 0, 0, 0.03);
border-radius: 5px;
margin: 5px 0;
text-align: center;
display: block !important;
visibility: visible !important;
position: relative;
z-index: 1;
}
.sillybubble-collapsed-image img {
max-width: 100%;
border-radius: 3px;
display: inline-block !important;
visibility: visible !important;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
/* Hide the collapsed image when details is open using classes */
.details-open .sillybubble-collapsed-image,
details[open] ~ .sillybubble-collapsed-image,
details[open] + .sillybubble-collapsed-image,
.mes_text details[open] ~ .sillybubble-collapsed-image,
.mes_text details[open] + .sillybubble-collapsed-image {
display: none !important;
}
/* Hide the "Tool calls: Chat Bubble Image" summary text when collapsed */
.smallSysMes.toolCall[data-sb-processed="true"] summary {
font-size: 0.8em;
opacity: 0.6;
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: 0.9;
}
.smallSysMes.toolCall[data-sb-processed="true"] summary:hover {
opacity: 1;
/* Ensure details content is visible */
.smallSysMes.toolCall details .mes_reasoning {
display: block !important;
}
/* Make the collapsed image look nicer */
.smallSysMes.toolCall[data-sb-processed="true"] {
position: relative;
}
</style>
`);

157
test.html Normal file
View File

@ -0,0 +1,157 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SillyBubble Image Generator Test</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
h1 {
color: #333;
border-bottom: 1px solid #ddd;
padding-bottom: 10px;
}
.bubble-container {
display: flex;
flex-direction: column;
gap: 20px;
margin: 20px 0;
}
.bubble {
background-color: #fff;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.bubble h3 {
margin-top: 0;
}
.bubble img {
max-width: 100%;
height: auto;
display: block;
margin: 10px 0;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="text"], select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
button {
background-color: #4CAF50;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
.code {
background-color: #f0f0f0;
padding: 10px;
border-radius: 4px;
font-family: monospace;
overflow-x: auto;
}
</style>
</head>
<body>
<h1>SillyBubble Image Generator Test</h1>
<div class="bubble">
<h3>Generate Your Own Chat Bubble</h3>
<div class="form-group">
<label for="text">Text:</label>
<input type="text" id="text" value="Hello, I'm a chat bubble generated on the fly!" />
</div>
<div class="form-group">
<label for="style">Style:</label>
<select id="style">
<option value="default">Default</option>
<option value="modern">Modern</option>
<option value="retro">Retro</option>
<option value="minimal">Minimal</option>
</select>
</div>
<button onclick="generateImage()">Generate Image</button>
<div id="result" style="margin-top: 20px;"></div>
</div>
<div class="bubble-container">
<div class="bubble">
<h3>Default Style</h3>
<img src="image.php?q=This%20is%20the%20default%20style%20for%20chat%20bubbles.%20It%20has%20a%20light%20blue%20background%20with%20rounded%20corners." alt="Default Style Chat Bubble" />
</div>
<div class="bubble">
<h3>Modern Style</h3>
<img src="image.php?q=This%20is%20the%20modern%20style%20for%20chat%20bubbles.%20It%20has%20a%20dark%20background%20with%20white%20text.&style=modern" alt="Modern Style Chat Bubble" />
</div>
<div class="bubble">
<h3>Retro Style</h3>
<img src="image.php?q=This%20is%20the%20retro%20style%20for%20chat%20bubbles.%20It%20has%20a%20warm%20color%20scheme%20and%20square%20corners.&style=retro" alt="Retro Style Chat Bubble" />
</div>
<div class="bubble">
<h3>Minimal Style</h3>
<img src="image.php?q=This%20is%20the%20minimal%20style%20for%20chat%20bubbles.%20It%20has%20a%20clean,%20simple%20design%20with%20square%20corners.&style=minimal" alt="Minimal Style Chat Bubble" />
</div>
</div>
<div class="bubble">
<h3>How to Use in SillyBubble Extension</h3>
<p>The SillyBubble extension uses this image generator to create chat bubbles on the fly. The extension passes text to this script and gets back an image.</p>
<h4>URL Format:</h4>
<div class="code">image.php?q=[URL-ENCODED-TEXT]&style=[STYLE-NAME]</div>
<h4>Available Styles:</h4>
<ul>
<li><strong>default</strong> - Light blue with rounded corners</li>
<li><strong>modern</strong> - Dark with light text</li>
<li><strong>retro</strong> - Warm colors with square corners</li>
<li><strong>minimal</strong> - Clean gray with square corners</li>
</ul>
</div>
<script>
function generateImage() {
const text = document.getElementById('text').value;
const style = document.getElementById('style').value;
const encodedText = encodeURIComponent(text);
let imageUrl = `image.php?q=${encodedText}`;
if (style !== 'default') {
imageUrl += `&style=${style}`;
}
const resultDiv = document.getElementById('result');
resultDiv.innerHTML = `
<h4>Generated Image:</h4>
<img src="${imageUrl}" alt="Generated Chat Bubble" style="max-width: 100%;" />
<h4>URL:</h4>
<div class="code">${imageUrl}</div>
`;
}
</script>
</body>
</html>