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
|
# Ignore fonts directory
|
||||||
fonts/
|
fonts/
|
||||||
|
|
||||||
|
# Ignore reference repos
|
||||||
|
reference/
|
||||||
|
|
||||||
# Node modules
|
# Node modules
|
||||||
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>
|
||||||
|
|
||||||
<div class="sillybubble_block flex-container">
|
<div class="sillybubble_block flex-container">
|
||||||
<label for="sillybubble_image_url">Image Service URL:</label>
|
<input id="sillybubble_use_character_param" type="checkbox" />
|
||||||
<input id="sillybubble_image_url" type="text" class="text_pole" />
|
<label for="sillybubble_use_character_param">Let AI choose character (expose character parameter to AI)</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sillybubble_block flex-container">
|
<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">
|
<select id="sillybubble_default_style" class="text_pole">
|
||||||
<option value="default">Default</option>
|
<option value="default">Default</option>
|
||||||
<option value="modern">Modern</option>
|
<option value="modern">Modern</option>
|
||||||
@ -30,6 +35,22 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</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">
|
<div class="sillybubble_block flex-container">
|
||||||
<input id="sillybubble_test_button" class="menu_button" type="submit" value="Test Chat Bubble" />
|
<input id="sillybubble_test_button" class="menu_button" type="submit" value="Test Chat Bubble" />
|
||||||
</div>
|
</div>
|
||||||
@ -40,7 +61,9 @@
|
|||||||
<p>Parameters:</p>
|
<p>Parameters:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><code>text</code>: The text to display in the bubble</li>
|
<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>
|
</ul>
|
||||||
<div class="sillybubble_important">
|
<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>
|
<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>"Include functions in context" is checked</li>
|
||||||
<li>The function appears in the function list</li>
|
<li>The function appears in the function list</li>
|
||||||
</ol>
|
</ol>
|
||||||
<p><strong>🚨 REQUIRED SETUP:</strong> You MUST add the following to your system prompt or character card:</p>
|
<p><strong>🚨 REQUIRED SETUP:</strong> You MUST add one of the following to your system prompt or character card (depending on your settings):</p>
|
||||||
<pre>You have access to a function called generateChatBubbleImage(text, style) that creates chat bubbles with the given text.
|
|
||||||
|
<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:
|
The function parameters are:
|
||||||
- text: The text to display in the chat bubble (required string)
|
- 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.
|
When you want to create a chat bubble, call this function with the text you want to display.
|
||||||
Example usage:
|
Example usage:
|
||||||
generateChatBubbleImage("Hello world!")
|
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>
|
<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>
|
<pre></pre>
|
||||||
</div>
|
</div>
|
||||||
|
399
image.php
399
image.php
@ -2,12 +2,14 @@
|
|||||||
/**
|
/**
|
||||||
* SillyBubble Image Generator
|
* SillyBubble Image Generator
|
||||||
*
|
*
|
||||||
* This script generates chat bubble images from text.
|
* This script generates comic-style chat bubble images with characters.
|
||||||
* It takes a 'q' parameter for the text content and an optional 'style' parameter.
|
* It supports character-based styling with speech or thought bubbles.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* image.php?q=Hello+World
|
* 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
|
// Set content type to PNG image
|
||||||
@ -15,97 +17,301 @@ header('Content-Type: image/png');
|
|||||||
|
|
||||||
// Get parameters
|
// Get parameters
|
||||||
$text = isset($_GET['q']) ? urldecode($_GET['q']) : 'No text provided';
|
$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';
|
$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 = [
|
$styles = [
|
||||||
'default' => [
|
'default' => [
|
||||||
'bg_color' => [245, 245, 245],
|
'bg_color' => [245, 245, 245],
|
||||||
'text_color' => [50, 50, 50],
|
'text_color' => [50, 50, 50],
|
||||||
'border_color' => [200, 200, 200],
|
'border_color' => [200, 200, 200],
|
||||||
'padding' => 20,
|
'font_size' => 24, // Increased for higher resolution
|
||||||
'rounded' => 15,
|
'line_height' => 30,
|
||||||
'font_size' => 14,
|
'font' => __DIR__ . '/fonts/NotoSans-Regular.ttf',
|
||||||
'max_width' => 600,
|
|
||||||
'line_height' => 20,
|
|
||||||
'font' => __DIR__ . '/fonts/arial.ttf', // Adjust path as needed
|
|
||||||
],
|
],
|
||||||
'modern' => [
|
'modern' => [
|
||||||
'bg_color' => [66, 133, 244],
|
'bg_color' => [66, 133, 244],
|
||||||
'text_color' => [255, 255, 255],
|
'text_color' => [255, 255, 255],
|
||||||
'border_color' => [59, 120, 220],
|
'border_color' => [59, 120, 220],
|
||||||
'padding' => 20,
|
'font_size' => 24,
|
||||||
'rounded' => 20,
|
'line_height' => 30,
|
||||||
'font_size' => 14,
|
'font' => __DIR__ . '/fonts/NotoSans-Regular.ttf',
|
||||||
'max_width' => 600,
|
|
||||||
'line_height' => 20,
|
|
||||||
'font' => __DIR__ . '/fonts/arial.ttf', // Adjust path as needed
|
|
||||||
],
|
],
|
||||||
'retro' => [
|
'retro' => [
|
||||||
'bg_color' => [255, 204, 102],
|
'bg_color' => [255, 204, 102],
|
||||||
'text_color' => [51, 51, 51],
|
'text_color' => [51, 51, 51],
|
||||||
'border_color' => [204, 153, 0],
|
'border_color' => [204, 153, 0],
|
||||||
'padding' => 20,
|
'font_size' => 24,
|
||||||
'rounded' => 5,
|
'line_height' => 30,
|
||||||
'font_size' => 14,
|
'font' => __DIR__ . '/fonts/NotoSans-Regular.ttf',
|
||||||
'max_width' => 600,
|
|
||||||
'line_height' => 20,
|
|
||||||
'font' => __DIR__ . '/fonts/arial.ttf', // Adjust path as needed
|
|
||||||
],
|
],
|
||||||
'minimal' => [
|
'minimal' => [
|
||||||
'bg_color' => [255, 255, 255],
|
'bg_color' => [255, 255, 255],
|
||||||
'text_color' => [0, 0, 0],
|
'text_color' => [0, 0, 0],
|
||||||
'border_color' => [220, 220, 220],
|
'border_color' => [220, 220, 220],
|
||||||
'padding' => 15,
|
'font_size' => 24,
|
||||||
'rounded' => 0,
|
'line_height' => 30,
|
||||||
'font_size' => 14,
|
'font' => __DIR__ . '/fonts/NotoSans-Regular.ttf',
|
||||||
'max_width' => 600,
|
|
||||||
'line_height' => 20,
|
|
||||||
'font' => __DIR__ . '/fonts/arial.ttf', // Adjust path as needed
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
// Use default style if specified style doesn't exist
|
// Set default styling
|
||||||
if (!isset($styles[$style])) {
|
$config = [
|
||||||
$style = 'default';
|
'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
|
// Ensure required directories exist
|
||||||
$config = $styles[$style];
|
$dirs = [
|
||||||
|
__DIR__ . '/characters/Example',
|
||||||
|
__DIR__ . '/fonts'
|
||||||
|
];
|
||||||
|
|
||||||
// Check for fonts directory and create if it doesn't exist
|
foreach ($dirs as $dir) {
|
||||||
$fontsDir = __DIR__ . '/fonts';
|
if (!is_dir($dir)) {
|
||||||
if (!is_dir($fontsDir)) {
|
mkdir($dir, 0755, true);
|
||||||
mkdir($fontsDir, 0755, true);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to a built-in font if the specified font file doesn't exist
|
// Fallback to a built-in font if the specified font file doesn't exist
|
||||||
if (!file_exists($config['font'])) {
|
if (!file_exists($config['font'])) {
|
||||||
// Try to find any TTF font in the fonts directory
|
// Try to find any TTF font in the fonts directory
|
||||||
$foundFont = false;
|
$fontFiles = glob(__DIR__ . '/fonts/*.ttf');
|
||||||
if (is_dir($fontsDir)) {
|
|
||||||
$fontFiles = glob($fontsDir . '/*.ttf');
|
|
||||||
if (!empty($fontFiles)) {
|
if (!empty($fontFiles)) {
|
||||||
$config['font'] = $fontFiles[0];
|
$config['font'] = $fontFiles[0];
|
||||||
$foundFont = true;
|
} else {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no TTF font found, use built-in font
|
|
||||||
if (!$foundFont) {
|
|
||||||
$config['use_builtin_font'] = true;
|
$config['use_builtin_font'] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a temporary image to calculate text dimensions
|
// Create the canvas
|
||||||
$tempImage = imagecreatetruecolor(100, 100);
|
$canvas = imagecreatetruecolor($canvasWidth, $canvasHeight);
|
||||||
$textColor = imagecolorallocate($tempImage, $config['text_color'][0], $config['text_color'][1], $config['text_color'][2]);
|
|
||||||
|
// 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
|
// Word wrap the text
|
||||||
$words = explode(' ', $text);
|
$words = explode(' ', $text);
|
||||||
$lines = [];
|
$lines = [];
|
||||||
$currentLine = '';
|
$currentLine = '';
|
||||||
$maxWidth = $config['max_width'] - (2 * $config['padding']);
|
$maxWidth = $textAreaWidth;
|
||||||
|
|
||||||
foreach ($words as $word) {
|
foreach ($words as $word) {
|
||||||
$testLine = $currentLine . ' ' . $word;
|
$testLine = $currentLine . ' ' . $word;
|
||||||
@ -130,92 +336,43 @@ if ($currentLine !== '') {
|
|||||||
$lines[] = $currentLine;
|
$lines[] = $currentLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate image dimensions
|
// Center text vertically in the bubble
|
||||||
$lineCount = count($lines);
|
$lineCount = count($lines);
|
||||||
if (isset($config['use_builtin_font'])) {
|
if (isset($config['use_builtin_font'])) {
|
||||||
$lineHeight = imagefontheight(5);
|
$totalTextHeight = $lineCount * 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);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
$lineHeight = $config['line_height'];
|
$totalTextHeight = $lineCount * $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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add padding to dimensions
|
// Calculate vertical starting position for centered text
|
||||||
$imgWidth = min($config['max_width'], $textWidth + (2 * $config['padding']));
|
$startY = $textAreaY + ($textAreaHeight - $totalTextHeight) / 2;
|
||||||
$imgHeight = $textHeight + (2 * $config['padding']);
|
if ($startY < $textAreaY) $startY = $textAreaY; // Ensure text doesn't overflow top
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw text
|
// Draw text
|
||||||
$y = $config['padding'];
|
$y = $startY + $config['font_size']; // Start position for centered text
|
||||||
foreach ($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
if (isset($config['use_builtin_font'])) {
|
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)
|
// 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);
|
$y += imagefontheight(5);
|
||||||
} else {
|
} 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)
|
// Use TrueType font (nicer but requires font file)
|
||||||
imagettftext($image, $config['font_size'], 0, $config['padding'], $y + $config['font_size'], $textColor, $config['font'], $line);
|
imagettftext($canvas, $config['font_size'], 0, $x, $y, $textColor, $config['font'], $line);
|
||||||
$y += $lineHeight;
|
$y += $config['line_height'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output the image
|
// Output the image
|
||||||
imagepng($image);
|
imagepng($canvas);
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
imagedestroy($image);
|
imagedestroy($canvas);
|
||||||
imagedestroy($tempImage);
|
|
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>
|
310
index.js
310
index.js
@ -8,10 +8,13 @@ const extensionFolderPath = `scripts/extensions/third-party/${extensionName}`;
|
|||||||
|
|
||||||
// Default settings
|
// Default settings
|
||||||
const defaultSettings = {
|
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_style: "default",
|
||||||
|
default_character: "Example",
|
||||||
|
default_bubble_type: "speech",
|
||||||
enabled: true,
|
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
|
// Make sure settings exist
|
||||||
@ -24,28 +27,56 @@ if (Object.keys(extension_settings[extensionName]).length === 0) {
|
|||||||
Object.assign(extension_settings[extensionName], defaultSettings);
|
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 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) {
|
if (!extension_settings[extensionName].enabled) {
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use default style if not specified
|
// Use default values if parameters not specified
|
||||||
const bubbleStyle = style || extension_settings[extensionName].default_style;
|
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
|
// URL encode the text parameter
|
||||||
const encodedText = encodeURIComponent(text);
|
const encodedText = encodeURIComponent(text);
|
||||||
|
|
||||||
// Construct the URL with the encoded text
|
// Ensure the URL is absolute (starts with http:// or https://)
|
||||||
const imageUrl = `${extension_settings[extensionName].image_service_url}?q=${encodedText}`;
|
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
|
// Construct the URL with the encoded text
|
||||||
const fullUrl = bubbleStyle !== "default"
|
let imageUrl = `${serviceUrl}?q=${encodedText}`;
|
||||||
? `${imageUrl}&style=${bubbleStyle}`
|
|
||||||
: imageUrl;
|
// 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 markdown image format
|
||||||
return ``;
|
return ``;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load extension settings into UI
|
// Load extension settings into UI
|
||||||
@ -54,7 +85,10 @@ function loadSettings() {
|
|||||||
$("#sillybubble_enabled").prop("checked", extension_settings[extensionName].enabled).trigger("input");
|
$("#sillybubble_enabled").prop("checked", extension_settings[extensionName].enabled).trigger("input");
|
||||||
$("#sillybubble_image_url").val(extension_settings[extensionName].image_service_url).trigger("input");
|
$("#sillybubble_image_url").val(extension_settings[extensionName].image_service_url).trigger("input");
|
||||||
$("#sillybubble_default_style").val(extension_settings[extensionName].default_style).trigger("input");
|
$("#sillybubble_default_style").val(extension_settings[extensionName].default_style).trigger("input");
|
||||||
|
$("#sillybubble_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_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
|
// Handle enable/disable toggle
|
||||||
@ -66,7 +100,15 @@ function onEnabledInput(event) {
|
|||||||
|
|
||||||
// Handle image service URL changes
|
// Handle image service URL changes
|
||||||
function onImageUrlInput(event) {
|
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;
|
extension_settings[extensionName].image_service_url = value;
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
}
|
}
|
||||||
@ -78,6 +120,20 @@ function onDefaultStyleInput(event) {
|
|||||||
saveSettingsDebounced();
|
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
|
// Handle render in collapse toggle
|
||||||
function onRenderInCollapseInput(event) {
|
function onRenderInCollapseInput(event) {
|
||||||
const value = Boolean($(event.target).prop("checked"));
|
const value = Boolean($(event.target).prop("checked"));
|
||||||
@ -88,14 +144,36 @@ function onRenderInCollapseInput(event) {
|
|||||||
processToolCallMessages();
|
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
|
// Test function to visualize a bubble
|
||||||
function onTestButtonClick() {
|
function onTestButtonClick() {
|
||||||
const testText = "This is a test chat bubble generated by SillyBubble!";
|
const testText = "This is a test chat bubble generated by SillyBubble!";
|
||||||
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 = `
|
const testPopup = `
|
||||||
<div class="sillybubble-test">
|
<div class="sillybubble-test">
|
||||||
<p>Generated Markdown:</p>
|
<p>Generated Markdown:</p>
|
||||||
<pre>${markdown.replace(/</g, '<').replace(/>/g, '>')}</pre>
|
<pre>${markdown.replace(/</g, '<').replace(/>/g, '>')}</pre>
|
||||||
|
${paramInfo}
|
||||||
<p>Preview (if connected to image service):</p>
|
<p>Preview (if connected to image service):</p>
|
||||||
<div>${markdown}</div>
|
<div>${markdown}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -115,32 +193,69 @@ function registerFunctionTool() {
|
|||||||
|
|
||||||
// Check if registerFunctionTool exists in context
|
// Check if registerFunctionTool exists in context
|
||||||
if (typeof context.registerFunctionTool === 'function') {
|
if (typeof context.registerFunctionTool === 'function') {
|
||||||
// Define parameter schema following JSON schema format
|
// Create properties object for schema
|
||||||
const bubbleSchema = Object.freeze({
|
const properties = {
|
||||||
$schema: 'http://json-schema.org/draft-04/schema#',
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
text: {
|
text: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The text to display in the chat bubble'
|
description: 'The text to display in the chat bubble'
|
||||||
},
|
},
|
||||||
style: {
|
bubble_type: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The visual style of the chat bubble (default, modern, retro, minimal)'
|
description: 'The type of bubble to use (speech, thought). Defaults to speech.'
|
||||||
},
|
}
|
||||||
},
|
};
|
||||||
required: ['text']
|
|
||||||
|
// 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: properties,
|
||||||
|
required: requiredParams
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register the function tool using SillyTavern's own API
|
// Register the function tool using SillyTavern's own API
|
||||||
context.registerFunctionTool({
|
context.registerFunctionTool({
|
||||||
name: 'generateChatBubbleImage',
|
name: 'generateChatBubbleImage',
|
||||||
displayName: 'Chat Bubble Image',
|
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,
|
parameters: bubbleSchema,
|
||||||
action: async (args) => {
|
action: async (args) => {
|
||||||
if (!args?.text) return '';
|
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: () => '',
|
formatMessage: () => '',
|
||||||
});
|
});
|
||||||
@ -201,23 +316,59 @@ function processToolCallMessages() {
|
|||||||
const markdownImgRegex = /!\[\]\(([^)]+)\)/;
|
const markdownImgRegex = /!\[\]\(([^)]+)\)/;
|
||||||
const match = tool.result.match(markdownImgRegex);
|
const match = tool.result.match(markdownImgRegex);
|
||||||
if (match && match[1]) {
|
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 {
|
} else {
|
||||||
renderContainer.html(tool.result);
|
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-rendered-image').remove();
|
||||||
|
$(this).find('.sillybubble-collapsed-image').remove();
|
||||||
|
|
||||||
// Add this image after the summary element, but only if details is present
|
// Create a special always-visible container for collapsed state
|
||||||
if (summaryElement.length) {
|
const collapsedContainer = $('<div class="sillybubble-collapsed-image"></div>');
|
||||||
summaryElement.after(renderContainer);
|
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
|
// Mark this message as processed
|
||||||
$(this).attr('data-sb-processed', 'true');
|
$(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
|
// Observer function to watch for new messages
|
||||||
function setupMessageObserver() {
|
function setupMessageObserver() {
|
||||||
// Create a mutation observer to monitor the chat for new messages
|
// Create a mutation observer to monitor the chat for new messages
|
||||||
@ -272,6 +450,20 @@ function setupMessageObserver() {
|
|||||||
|
|
||||||
// Process any existing messages
|
// Process any existing messages
|
||||||
setTimeout(() => processToolCallMessages(), 500);
|
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 {
|
} else {
|
||||||
console.warn(`[${extensionName}] Chat element not found, observer not started`);
|
console.warn(`[${extensionName}] Chat element not found, observer not started`);
|
||||||
// Try again after a delay
|
// Try again after a delay
|
||||||
@ -293,7 +485,10 @@ jQuery(async () => {
|
|||||||
$("#sillybubble_enabled").on("input", onEnabledInput);
|
$("#sillybubble_enabled").on("input", onEnabledInput);
|
||||||
$("#sillybubble_image_url").on("input", onImageUrlInput);
|
$("#sillybubble_image_url").on("input", onImageUrlInput);
|
||||||
$("#sillybubble_default_style").on("input", onDefaultStyleInput);
|
$("#sillybubble_default_style").on("input", onDefaultStyleInput);
|
||||||
|
$("#sillybubble_default_character").on("input", onDefaultCharacterInput);
|
||||||
|
$("#sillybubble_default_bubble_type").on("input", onDefaultBubbleTypeInput);
|
||||||
$("#sillybubble_render_in_collapse").on("input", onRenderInCollapseInput);
|
$("#sillybubble_render_in_collapse").on("input", onRenderInCollapseInput);
|
||||||
|
$("#sillybubble_use_character_param").on("input", onUseCharacterParamInput);
|
||||||
$("#sillybubble_test_button").on("click", onTestButtonClick);
|
$("#sillybubble_test_button").on("click", onTestButtonClick);
|
||||||
|
|
||||||
// Initial attempt to register the function tool
|
// Initial attempt to register the function tool
|
||||||
@ -305,30 +500,73 @@ jQuery(async () => {
|
|||||||
// Add CSS for rendered images
|
// Add CSS for rendered images
|
||||||
$('head').append(`
|
$('head').append(`
|
||||||
<style>
|
<style>
|
||||||
|
/* Style for bubble images in expanded details */
|
||||||
.sillybubble-rendered-image {
|
.sillybubble-rendered-image {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
display: block !important;
|
||||||
|
visibility: visible !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sillybubble-rendered-image img {
|
.sillybubble-rendered-image img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
border-radius: 3px;
|
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 {
|
.smallSysMes.toolCall[data-sb-processed="true"] summary {
|
||||||
|
font-size: 0.8em;
|
||||||
|
opacity: 0.6;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Make the text in the summary a bit less prominent when an image is shown */
|
.smallSysMes.toolCall[data-sb-processed="true"] summary:hover {
|
||||||
.smallSysMes.toolCall[data-sb-processed="true"] summary {
|
opacity: 0.9;
|
||||||
opacity: 0.7;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.smallSysMes.toolCall[data-sb-processed="true"] summary:hover {
|
/* Ensure details content is visible */
|
||||||
opacity: 1;
|
.smallSysMes.toolCall details .mes_reasoning {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make the collapsed image look nicer */
|
||||||
|
.smallSysMes.toolCall[data-sb-processed="true"] {
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
</style>
|
</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