Compare commits
10 Commits
0e532e81f4
...
master
Author | SHA1 | Date | |
---|---|---|---|
7380391424 | |||
c8af3ab3cb | |||
7d6dc1c4cd | |||
48610c7ba6 | |||
e0d54bd1b6 | |||
34bd7e44e6 | |||
17796630af | |||
c0a9345c17 | |||
1e09bd6638 | |||
a907c4d073 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -4,6 +4,9 @@ mounts/
|
||||
# Ignore fonts directory
|
||||
fonts/
|
||||
|
||||
# Ignore reference repos
|
||||
reference/
|
||||
|
||||
# Node modules
|
||||
node_modules/
|
||||
|
||||
|
115
Generator.md
Normal file
115
Generator.md
Normal 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
|
BIN
characters/Example/background.png
Normal file
BIN
characters/Example/background.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
BIN
characters/Example/character.png
Normal file
BIN
characters/Example/character.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
BIN
characters/Example/speech.png
Normal file
BIN
characters/Example/speech.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.0 KiB |
BIN
characters/Example/thought.png
Normal file
BIN
characters/Example/thought.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.2 KiB |
55
example.html
55
example.html
@ -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></pre>
|
||||
</div>
|
||||
|
403
image.php
403
image.php
@ -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
114
image_html.php
Normal 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
312
index.js
@ -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 ``;
|
||||
return ``;
|
||||
}
|
||||
|
||||
// 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, '<').replace(/>/g, '>')}</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
157
test.html
Normal 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>
|
Reference in New Issue
Block a user