eldarski
commited on
Commit
Β·
80f4262
1
Parent(s):
c32653f
π¨ Deploy Gradio Themer Demo - Dual Track Hackathon Submission (Track 1: MCP Server + Track 2: Custom Component)
Browse files- README.md +214 -7
- app.py +113 -0
- css.css +157 -0
- mcp_tools.py +728 -0
- page.py +568 -0
- requirements.txt +8 -0
- user_themes.json +321 -0
README.md
CHANGED
|
@@ -1,12 +1,219 @@
|
|
| 1 |
---
|
| 2 |
-
title: Gradio Themer Demo
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 5.
|
| 8 |
app_file: app.py
|
| 9 |
-
pinned:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: π¨ Gradio Themer - Demo & MCP Server
|
| 3 |
+
emoji: π¨
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 5.31.0
|
| 8 |
app_file: app.py
|
| 9 |
+
pinned: true
|
| 10 |
+
license: mit
|
| 11 |
+
short_description: User-configurable themes for Gradio applications with MCP server integration
|
| 12 |
+
models:
|
| 13 |
+
- MasterControlAIML/DeepSeek-R1-Qwen2.5-1.5b-SFT-R1-JSON-Unstructured-To-Structured
|
| 14 |
+
tags:
|
| 15 |
+
- mcp-server-track
|
| 16 |
+
- custom-component-track
|
| 17 |
+
- Agents-MCP-Hackathon
|
| 18 |
+
- gradio
|
| 19 |
+
- theming
|
| 20 |
+
- agent-ui
|
| 21 |
---
|
| 22 |
|
| 23 |
+
# π¨ Gradio Themer - Demo & MCP Server
|
| 24 |
+
|
| 25 |
+
**π Dual Track Hackathon Submission**: Custom Component + MCP Server
|
| 26 |
+
|
| 27 |
+
## π Features
|
| 28 |
+
|
| 29 |
+
- **Dynamic Theme System**: User-configurable themes via JSON
|
| 30 |
+
- **AI-Powered Conversion**: Convert CSS/descriptions to theme JSON using HuggingFace models
|
| 31 |
+
- **MCP Server Integration**: 4 practical tools for theme development
|
| 32 |
+
- **Production Ready**: Built and tested package
|
| 33 |
+
|
| 34 |
+
## π€ MCP Tools (Production-Focused)
|
| 35 |
+
|
| 36 |
+
### 1. `generate_theme`
|
| 37 |
+
|
| 38 |
+
**Purpose**: Create complete theme JSON with intelligent color harmonies
|
| 39 |
+
**Parameters**:
|
| 40 |
+
|
| 41 |
+
- `theme_name`: Name of the theme
|
| 42 |
+
- `primary_color`: Main color (hex format, default: "#3b82f6")
|
| 43 |
+
- `theme_style`: "light" or "dark" (default: "light")
|
| 44 |
+
- `color_harmony`: "complementary", "triadic", "analogous", "monochromatic" (default: "complementary")
|
| 45 |
+
|
| 46 |
+
**Output**: Complete theme JSON with harmonious colors and accessibility considerations
|
| 47 |
+
|
| 48 |
+
### 2. `convert_css_to_theme`
|
| 49 |
+
|
| 50 |
+
**Purpose**: Convert existing CSS or natural language descriptions to theme JSON
|
| 51 |
+
**Parameters**:
|
| 52 |
+
|
| 53 |
+
- `css_input`: CSS code or style description
|
| 54 |
+
- `theme_name`: Name for the converted theme (default: "converted_theme")
|
| 55 |
+
|
| 56 |
+
**Output**: Theme JSON extracted from CSS using AI and pattern matching
|
| 57 |
+
|
| 58 |
+
### 3. `apply_theme_preview`
|
| 59 |
+
|
| 60 |
+
**Purpose**: Generate CSS code for immediate theme preview in any Gradio app
|
| 61 |
+
**Parameters**:
|
| 62 |
+
|
| 63 |
+
- `theme_json`: Theme JSON configuration
|
| 64 |
+
|
| 65 |
+
**Output**: Ready-to-use CSS code for instant theme application
|
| 66 |
+
|
| 67 |
+
### 4. `export_theme_package`
|
| 68 |
+
|
| 69 |
+
**Purpose**: Export complete theme package with JSON, CSS, and usage instructions
|
| 70 |
+
**Parameters**:
|
| 71 |
+
|
| 72 |
+
- `theme_json`: Theme JSON configuration
|
| 73 |
+
- `package_name`: Name for the package files (default: "my_theme")
|
| 74 |
+
|
| 75 |
+
**Output**: Complete package with files, documentation, and sample code
|
| 76 |
+
|
| 77 |
+
## π― Why These Tools?
|
| 78 |
+
|
| 79 |
+
**β Removed**: Package management tools (not useful for theme development)
|
| 80 |
+
**β
Added**: Practical workflow tools that developers actually need:
|
| 81 |
+
|
| 82 |
+
1. **Smart Theme Generation**: Uses color theory for harmonious palettes
|
| 83 |
+
2. **CSS Migration**: Convert existing designs to Gradio themes
|
| 84 |
+
3. **Instant Preview**: Get CSS for immediate testing
|
| 85 |
+
4. **Complete Export**: Ready-to-use packages with documentation
|
| 86 |
+
|
| 87 |
+
## π§ Usage Examples
|
| 88 |
+
|
| 89 |
+
### Generate Theme with Color Harmony
|
| 90 |
+
|
| 91 |
+
```python
|
| 92 |
+
# Create complementary color scheme from blue primary
|
| 93 |
+
generate_theme("Ocean Theme", "#0ea5e9", "light", "complementary")
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
### Convert Existing CSS
|
| 97 |
+
|
| 98 |
+
```python
|
| 99 |
+
# Convert CSS variables to theme JSON
|
| 100 |
+
convert_css_to_theme("""
|
| 101 |
+
:root {
|
| 102 |
+
--primary: #3b82f6;
|
| 103 |
+
--background: #ffffff;
|
| 104 |
+
--text: #1e293b;
|
| 105 |
+
}
|
| 106 |
+
""", "Converted Blue Theme")
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
### Get Preview CSS
|
| 110 |
+
|
| 111 |
+
```python
|
| 112 |
+
# Generate CSS for immediate application
|
| 113 |
+
apply_theme_preview('{"ocean_theme": {"colors": {...}}}')
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
### Export Complete Package
|
| 117 |
+
|
| 118 |
+
```python
|
| 119 |
+
# Create ready-to-use theme package
|
| 120 |
+
export_theme_package('{"my_theme": {...}}', "awesome_theme")
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
## π Production Benefits
|
| 124 |
+
|
| 125 |
+
1. **Workflow Efficiency**: Tools cover the complete theme development cycle
|
| 126 |
+
2. **Color Theory**: Automatic generation of harmonious color schemes
|
| 127 |
+
3. **Accessibility**: Built-in contrast considerations
|
| 128 |
+
4. **Migration Support**: Easy conversion from existing CSS
|
| 129 |
+
5. **Instant Testing**: Preview CSS for immediate application
|
| 130 |
+
6. **Complete Packages**: Everything needed for distribution
|
| 131 |
+
|
| 132 |
+
## π Setup & Installation
|
| 133 |
+
|
| 134 |
+
### For HuggingFace Spaces:
|
| 135 |
+
|
| 136 |
+
This Space is ready to deploy! Just upload all files to your HF Space.
|
| 137 |
+
|
| 138 |
+
### For Local Development:
|
| 139 |
+
|
| 140 |
+
```bash
|
| 141 |
+
pip install gradio>=5.31.0 gradio-themer>=0.1.0 requests transformers torch
|
| 142 |
+
python app.py
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
### For MCP Client Integration:
|
| 146 |
+
|
| 147 |
+
Configure your MCP client (Claude Desktop, Cursor, etc.) to connect to this Space:
|
| 148 |
+
|
| 149 |
+
```json
|
| 150 |
+
{
|
| 151 |
+
"mcpServers": {
|
| 152 |
+
"gradio-themer": {
|
| 153 |
+
"command": "python",
|
| 154 |
+
"args": ["-m", "gradio_mcp"],
|
| 155 |
+
"env": {
|
| 156 |
+
"GRADIO_MCP_URL": "https://your-space-url.hf.space"
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
## π Hackathon Submission
|
| 164 |
+
|
| 165 |
+
**Track 1 (MCP Server)**:
|
| 166 |
+
|
| 167 |
+
- Tag: `mcp-server-track`
|
| 168 |
+
- 4 production-focused MCP tools
|
| 169 |
+
- Complete theme development workflow
|
| 170 |
+
|
| 171 |
+
**Track 2 (Custom Component)**:
|
| 172 |
+
|
| 173 |
+
- Tag: `custom-component-track`
|
| 174 |
+
- Published package: `gradio_themer`
|
| 175 |
+
- Interactive Gradio component
|
| 176 |
+
|
| 177 |
+
## π¨ Demo Features
|
| 178 |
+
|
| 179 |
+
- **Random Theme Button**: Apply any of 13+ available themes
|
| 180 |
+
- **CSS to JSON Converter**: AI-powered conversion using HuggingFace models
|
| 181 |
+
- **Theme Showcase**: Live preview of different theme styles
|
| 182 |
+
- **MCP Integration**: Embedded MCP server functionality
|
| 183 |
+
|
| 184 |
+
## π¦ Package Information
|
| 185 |
+
|
| 186 |
+
- **Package**: `gradio_themer-0.1.0`
|
| 187 |
+
- **Class**: `GradioThemer`
|
| 188 |
+
- **Import**: `from gradio_themer import GradioThemer`
|
| 189 |
+
- **Status**: Production ready
|
| 190 |
+
|
| 191 |
+
## π Links
|
| 192 |
+
|
| 193 |
+
- **π¨ This Demo Space**: `https://huggingface.co/spaces/eldarski/gradio-themer-demo`
|
| 194 |
+
- **π¦ PyPI Package**: `https://pypi.org/project/gradio-themer/`
|
| 195 |
+
- **π GitHub Repository**: `[Repository URL]`
|
| 196 |
+
- **π¬ MCP Demo Video**: `[Video URL - Coming Soon]`
|
| 197 |
+
- **π¬ Component Demo Video**: `[Video URL - Coming Soon]`
|
| 198 |
+
|
| 199 |
+
---
|
| 200 |
+
|
| 201 |
+
**Built for HuggingFace Gradio Hackathon 2024**
|
| 202 |
+
|
| 203 |
+
## πΊ Demo Videos
|
| 204 |
+
|
| 205 |
+
### π€ Track 1: MCP Server Demo (`mcp-server-track`)
|
| 206 |
+
|
| 207 |
+
**MCP Server in Action with Cursor AI**: [Video Demo Link - Coming Soon]
|
| 208 |
+
|
| 209 |
+
- Shows the MCP server being used with Cursor AI for automated theme generation
|
| 210 |
+
- Demonstrates all 4 MCP tools in a real development workflow
|
| 211 |
+
- Live theme creation and application via AI agent
|
| 212 |
+
|
| 213 |
+
### π¨ Track 2: Custom Component Demo (`custom-component-track`)
|
| 214 |
+
|
| 215 |
+
**Component Usage Demo**: [Video Demo Link - Coming Soon]
|
| 216 |
+
|
| 217 |
+
- Interactive demonstration of the GradioThemer custom component
|
| 218 |
+
- Shows theme switching and configuration in various Gradio applications
|
| 219 |
+
- Demonstrates the published package in action
|
app.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Gradio Themer - MCP Server Entry Point
|
| 3 |
+
Clean entry point that exposes only the 4 intended MCP tools.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import gradio as gr
|
| 8 |
+
|
| 9 |
+
# CRITICAL: Enable MCP server mode (as per GRADIO_MCP_HF_SPACES_GUIDE.md)
|
| 10 |
+
os.environ["GRADIO_MCP_SERVER"] = "True"
|
| 11 |
+
|
| 12 |
+
# Import the 4 MCP tools - these will be exposed by the MCP server
|
| 13 |
+
from mcp_tools import (
|
| 14 |
+
setup_package,
|
| 15 |
+
generate_theme,
|
| 16 |
+
convert_css_to_theme,
|
| 17 |
+
generate_app_code,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
if __name__ == "__main__":
|
| 21 |
+
try:
|
| 22 |
+
import page
|
| 23 |
+
|
| 24 |
+
# Add hidden MCP endpoints to the existing demo
|
| 25 |
+
with page.demo:
|
| 26 |
+
# Add hidden MCP tool endpoints (invisible to users, visible to MCP)
|
| 27 |
+
|
| 28 |
+
# Hidden setup_package endpoint
|
| 29 |
+
setup_btn = gr.Button("Setup Package", visible=False)
|
| 30 |
+
setup_output = gr.JSON(visible=False)
|
| 31 |
+
setup_btn.click(
|
| 32 |
+
fn=setup_package, outputs=setup_output, api_name="setup_package"
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
# Hidden generate_theme endpoint
|
| 36 |
+
theme_name_input = gr.Textbox(visible=False)
|
| 37 |
+
primary_color_input = gr.Textbox(visible=False, value="#3b82f6")
|
| 38 |
+
theme_style_input = gr.Textbox(visible=False, value="light")
|
| 39 |
+
accent_color_input = gr.Textbox(visible=False, value="")
|
| 40 |
+
generate_theme_btn = gr.Button("Generate Theme", visible=False)
|
| 41 |
+
generate_theme_output = gr.JSON(visible=False)
|
| 42 |
+
generate_theme_btn.click(
|
| 43 |
+
fn=generate_theme,
|
| 44 |
+
inputs=[
|
| 45 |
+
theme_name_input,
|
| 46 |
+
primary_color_input,
|
| 47 |
+
theme_style_input,
|
| 48 |
+
accent_color_input,
|
| 49 |
+
],
|
| 50 |
+
outputs=generate_theme_output,
|
| 51 |
+
api_name="generate_theme",
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
# Hidden convert_css_to_theme endpoint
|
| 55 |
+
css_input = gr.Textbox(visible=False)
|
| 56 |
+
convert_theme_name_input = gr.Textbox(
|
| 57 |
+
visible=False, value="converted_theme"
|
| 58 |
+
)
|
| 59 |
+
user_token_input = gr.Textbox(visible=False, value="")
|
| 60 |
+
model_choice_input = gr.Textbox(visible=False, value="qwen")
|
| 61 |
+
convert_css_btn = gr.Button("Convert CSS", visible=False)
|
| 62 |
+
convert_css_output = gr.Textbox(visible=False)
|
| 63 |
+
convert_css_btn.click(
|
| 64 |
+
fn=convert_css_to_theme,
|
| 65 |
+
inputs=[
|
| 66 |
+
css_input,
|
| 67 |
+
convert_theme_name_input,
|
| 68 |
+
user_token_input,
|
| 69 |
+
model_choice_input,
|
| 70 |
+
],
|
| 71 |
+
outputs=convert_css_output,
|
| 72 |
+
api_name="convert_css_to_theme",
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
# Hidden generate_app_code endpoint
|
| 76 |
+
app_theme_names_input = gr.Textbox(
|
| 77 |
+
visible=False, value="ocean_breeze,sunset_orange"
|
| 78 |
+
)
|
| 79 |
+
app_title_input = gr.Textbox(visible=False, value="My Themed App")
|
| 80 |
+
include_components_input = gr.Textbox(
|
| 81 |
+
visible=False, value="button,textbox,slider"
|
| 82 |
+
)
|
| 83 |
+
generate_app_btn = gr.Button("Generate App", visible=False)
|
| 84 |
+
generate_app_output = gr.Textbox(visible=False)
|
| 85 |
+
generate_app_btn.click(
|
| 86 |
+
fn=generate_app_code,
|
| 87 |
+
inputs=[
|
| 88 |
+
app_theme_names_input,
|
| 89 |
+
app_title_input,
|
| 90 |
+
include_components_input,
|
| 91 |
+
],
|
| 92 |
+
outputs=generate_app_output,
|
| 93 |
+
api_name="generate_app_code",
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
# Launch the demo with MCP server enabled
|
| 97 |
+
page.demo.launch(
|
| 98 |
+
mcp_server=True, # CRITICAL: Enable MCP server functionality
|
| 99 |
+
server_name="0.0.0.0",
|
| 100 |
+
server_port=7860,
|
| 101 |
+
share=False,
|
| 102 |
+
debug=True,
|
| 103 |
+
allowed_paths=["./"],
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
except ImportError as e:
|
| 107 |
+
print(f"β Error importing demo interface: {e}")
|
| 108 |
+
print("Make sure page.py exists and contains the 'demo' variable")
|
| 109 |
+
exit(1)
|
| 110 |
+
except AttributeError as e:
|
| 111 |
+
print(f"β Error accessing demo object: {e}")
|
| 112 |
+
print("Make sure page.py contains a 'demo' variable")
|
| 113 |
+
exit(1)
|
css.css
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
html {
|
| 2 |
+
font-family: Inter;
|
| 3 |
+
font-size: 16px;
|
| 4 |
+
font-weight: 400;
|
| 5 |
+
line-height: 1.5;
|
| 6 |
+
-webkit-text-size-adjust: 100%;
|
| 7 |
+
background: #fff;
|
| 8 |
+
color: #323232;
|
| 9 |
+
-webkit-font-smoothing: antialiased;
|
| 10 |
+
-moz-osx-font-smoothing: grayscale;
|
| 11 |
+
text-rendering: optimizeLegibility;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
:root {
|
| 15 |
+
--space: 1;
|
| 16 |
+
--vspace: calc(var(--space) * 1rem);
|
| 17 |
+
--vspace-0: calc(3 * var(--space) * 1rem);
|
| 18 |
+
--vspace-1: calc(2 * var(--space) * 1rem);
|
| 19 |
+
--vspace-2: calc(1.5 * var(--space) * 1rem);
|
| 20 |
+
--vspace-3: calc(0.5 * var(--space) * 1rem);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.app {
|
| 24 |
+
max-width: 748px !important;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.prose p {
|
| 28 |
+
margin: var(--vspace) 0;
|
| 29 |
+
line-height: var(--vspace * 2);
|
| 30 |
+
font-size: 1rem;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
code {
|
| 34 |
+
font-family: "Inconsolata", sans-serif;
|
| 35 |
+
font-size: 16px;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
h1,
|
| 39 |
+
h1 code {
|
| 40 |
+
font-weight: 400;
|
| 41 |
+
line-height: calc(2.5 / var(--space) * var(--vspace));
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
h1 code {
|
| 45 |
+
background: none;
|
| 46 |
+
border: none;
|
| 47 |
+
letter-spacing: 0.05em;
|
| 48 |
+
padding-bottom: 5px;
|
| 49 |
+
position: relative;
|
| 50 |
+
padding: 0;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
h2 {
|
| 54 |
+
margin: var(--vspace-1) 0 var(--vspace-2) 0;
|
| 55 |
+
line-height: 1em;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
h3,
|
| 59 |
+
h3 code {
|
| 60 |
+
margin: var(--vspace-1) 0 var(--vspace-2) 0;
|
| 61 |
+
line-height: 1em;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
h4,
|
| 65 |
+
h5,
|
| 66 |
+
h6 {
|
| 67 |
+
margin: var(--vspace-3) 0 var(--vspace-3) 0;
|
| 68 |
+
line-height: var(--vspace);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.bigtitle,
|
| 72 |
+
h1,
|
| 73 |
+
h1 code {
|
| 74 |
+
font-size: calc(8px * 4.5);
|
| 75 |
+
word-break: break-word;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.title,
|
| 79 |
+
h2,
|
| 80 |
+
h2 code {
|
| 81 |
+
font-size: calc(8px * 3.375);
|
| 82 |
+
font-weight: lighter;
|
| 83 |
+
word-break: break-word;
|
| 84 |
+
border: none;
|
| 85 |
+
background: none;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.subheading1,
|
| 89 |
+
h3,
|
| 90 |
+
h3 code {
|
| 91 |
+
font-size: calc(8px * 1.8);
|
| 92 |
+
font-weight: 600;
|
| 93 |
+
border: none;
|
| 94 |
+
background: none;
|
| 95 |
+
letter-spacing: 0.1em;
|
| 96 |
+
text-transform: uppercase;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
h2 code {
|
| 100 |
+
padding: 0;
|
| 101 |
+
position: relative;
|
| 102 |
+
letter-spacing: 0.05em;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
blockquote {
|
| 106 |
+
font-size: calc(8px * 1.1667);
|
| 107 |
+
font-style: italic;
|
| 108 |
+
line-height: calc(1.1667 * var(--vspace));
|
| 109 |
+
margin: var(--vspace-2) var(--vspace-2);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.subheading2,
|
| 113 |
+
h4 {
|
| 114 |
+
font-size: calc(8px * 1.4292);
|
| 115 |
+
text-transform: uppercase;
|
| 116 |
+
font-weight: 600;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.subheading3,
|
| 120 |
+
h5 {
|
| 121 |
+
font-size: calc(8px * 1.2917);
|
| 122 |
+
line-height: calc(1.2917 * var(--vspace));
|
| 123 |
+
|
| 124 |
+
font-weight: lighter;
|
| 125 |
+
text-transform: uppercase;
|
| 126 |
+
letter-spacing: 0.15em;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
h6 {
|
| 130 |
+
font-size: calc(8px * 1.1667);
|
| 131 |
+
font-size: 1.1667em;
|
| 132 |
+
font-weight: normal;
|
| 133 |
+
font-style: italic;
|
| 134 |
+
font-family: "le-monde-livre-classic-byol", serif !important;
|
| 135 |
+
letter-spacing: 0px !important;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
#start .md > *:first-child {
|
| 139 |
+
margin-top: 0;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
h2 + h3 {
|
| 143 |
+
margin-top: 0;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.md hr {
|
| 147 |
+
border: none;
|
| 148 |
+
border-top: 1px solid var(--block-border-color);
|
| 149 |
+
margin: var(--vspace-2) 0 var(--vspace-2) 0;
|
| 150 |
+
}
|
| 151 |
+
.prose ul {
|
| 152 |
+
margin: var(--vspace-2) 0 var(--vspace-1) 0;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.gap {
|
| 156 |
+
gap: 0;
|
| 157 |
+
}
|
mcp_tools.py
ADDED
|
@@ -0,0 +1,728 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import random
|
| 4 |
+
import colorsys
|
| 5 |
+
import re
|
| 6 |
+
import subprocess
|
| 7 |
+
import sys
|
| 8 |
+
from typing import Dict, Any, Optional, Tuple
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
import requests
|
| 11 |
+
|
| 12 |
+
# CRITICAL: Enable MCP server mode
|
| 13 |
+
os.environ["GRADIO_MCP_SERVER"] = "True"
|
| 14 |
+
|
| 15 |
+
# Model API configuration for CSS to theme conversion
|
| 16 |
+
AVAILABLE_MODELS = {
|
| 17 |
+
"qwen": {
|
| 18 |
+
"hf_model": "Qwen/Qwen2.5-Coder-7B",
|
| 19 |
+
"nebius_model": "Qwen/Qwen2.5-Coder-7B",
|
| 20 |
+
"name": "Qwen2.5-Coder-7B",
|
| 21 |
+
},
|
| 22 |
+
"llama": {
|
| 23 |
+
"hf_model": "meta-llama/Meta-Llama-3.1-8B-Instruct",
|
| 24 |
+
"nebius_model": "meta-llama/Meta-Llama-3.1-8B-Instruct",
|
| 25 |
+
"name": "Meta-Llama-3.1-8B-Instruct",
|
| 26 |
+
},
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
# Nebius Studio API configuration
|
| 30 |
+
NEBIUS_API_URL = "https://api.studio.nebius.ai/v1/chat/completions"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def setup_package() -> Dict[str, Any]:
|
| 34 |
+
"""
|
| 35 |
+
Install and verify the gradio-themer package is available and working.
|
| 36 |
+
|
| 37 |
+
Returns:
|
| 38 |
+
Dict[str, Any]: Status information about package installation
|
| 39 |
+
"""
|
| 40 |
+
try:
|
| 41 |
+
# Test import to verify package is available
|
| 42 |
+
from gradio_themer import GradioThemer
|
| 43 |
+
|
| 44 |
+
return {
|
| 45 |
+
"status": "success",
|
| 46 |
+
"message": "β
gradio-themer package is installed and working",
|
| 47 |
+
"package_info": {
|
| 48 |
+
"name": "gradio_themer",
|
| 49 |
+
"class": "GradioThemer",
|
| 50 |
+
"version": "0.1.0",
|
| 51 |
+
},
|
| 52 |
+
"usage_example": """
|
| 53 |
+
import gradio as gr
|
| 54 |
+
from gradio_themer import GradioThemer
|
| 55 |
+
|
| 56 |
+
with gr.Blocks() as demo:
|
| 57 |
+
themer = GradioThemer(label="Theme Selector")
|
| 58 |
+
themer.change(fn=handle_theme_change, inputs=[themer])
|
| 59 |
+
""",
|
| 60 |
+
}
|
| 61 |
+
except ImportError as e:
|
| 62 |
+
return {
|
| 63 |
+
"status": "error",
|
| 64 |
+
"message": f"β gradio-themer package not found: {str(e)}",
|
| 65 |
+
"solution": "Install with: pip install gradio-themer",
|
| 66 |
+
}
|
| 67 |
+
except Exception as e:
|
| 68 |
+
return {"status": "error", "message": f"β Error testing package: {str(e)}"}
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def generate_theme(
|
| 72 |
+
theme_name: str,
|
| 73 |
+
primary_color: str = "#3b82f6",
|
| 74 |
+
theme_style: str = "light",
|
| 75 |
+
accent_color: Optional[str] = None,
|
| 76 |
+
) -> Dict[str, Any]:
|
| 77 |
+
"""
|
| 78 |
+
Generate a complete theme JSON configuration with intelligent color harmonies.
|
| 79 |
+
|
| 80 |
+
Args:
|
| 81 |
+
theme_name (str): Name for the new theme
|
| 82 |
+
primary_color (str): Primary color in hex format (e.g., "#3b82f6")
|
| 83 |
+
theme_style (str): Theme style - "light", "dark", or "auto"
|
| 84 |
+
accent_color (Optional[str]): Optional accent color, auto-generated if not provided
|
| 85 |
+
|
| 86 |
+
Returns:
|
| 87 |
+
Dict[str, Any]: Complete theme configuration ready for use
|
| 88 |
+
"""
|
| 89 |
+
try:
|
| 90 |
+
# Generate intelligent color palette based on primary color
|
| 91 |
+
colors = _generate_color_palette(primary_color, theme_style, accent_color)
|
| 92 |
+
|
| 93 |
+
# Create theme configuration
|
| 94 |
+
theme_config = {
|
| 95 |
+
theme_name.lower().replace(" ", "_"): {
|
| 96 |
+
"name": theme_name,
|
| 97 |
+
"colors": colors,
|
| 98 |
+
"background": colors["base-200"],
|
| 99 |
+
"style": theme_style,
|
| 100 |
+
"generated": True,
|
| 101 |
+
"font": {
|
| 102 |
+
"family": "Inter",
|
| 103 |
+
"type": "google_font",
|
| 104 |
+
"name": "Inter",
|
| 105 |
+
},
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
return {
|
| 110 |
+
"status": "success",
|
| 111 |
+
"message": f"β
Generated theme '{theme_name}' with {theme_style} style",
|
| 112 |
+
"theme_config": theme_config,
|
| 113 |
+
"usage_instructions": f"""
|
| 114 |
+
1. Save this JSON to your user_themes.json file
|
| 115 |
+
2. Use in your Gradio app:
|
| 116 |
+
themer = GradioThemer(
|
| 117 |
+
value={{"currentTheme": "{theme_name.lower().replace(' ', '_')}"}},
|
| 118 |
+
theme_config_path="user_themes.json"
|
| 119 |
+
)
|
| 120 |
+
""",
|
| 121 |
+
"color_info": {
|
| 122 |
+
"primary": colors["primary"],
|
| 123 |
+
"background": colors["base-200"],
|
| 124 |
+
"text": colors["base-content"],
|
| 125 |
+
"accent": colors["accent"],
|
| 126 |
+
},
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
except Exception as e:
|
| 130 |
+
return {
|
| 131 |
+
"status": "error",
|
| 132 |
+
"message": f"β Error generating theme: {str(e)}",
|
| 133 |
+
"suggestion": "Check that primary_color is in valid hex format (e.g., '#3b82f6')",
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def convert_css_to_theme(
|
| 138 |
+
css_input: str,
|
| 139 |
+
theme_name: str = "converted_theme",
|
| 140 |
+
user_token: str = "",
|
| 141 |
+
model_choice: str = "qwen",
|
| 142 |
+
) -> str:
|
| 143 |
+
"""
|
| 144 |
+
Convert CSS styles or style descriptions into standardized theme JSON format using HF hosted LLM.
|
| 145 |
+
|
| 146 |
+
Args:
|
| 147 |
+
css_input (str): CSS code or natural language style description
|
| 148 |
+
theme_name (str): Name for the converted theme
|
| 149 |
+
user_token (str): Optional Nebius API token for better performance
|
| 150 |
+
model_choice (str): AI model to use ("qwen" or "llama")
|
| 151 |
+
|
| 152 |
+
Returns:
|
| 153 |
+
str: JSON string of converted theme configuration
|
| 154 |
+
"""
|
| 155 |
+
if not css_input.strip():
|
| 156 |
+
return json.dumps(
|
| 157 |
+
{
|
| 158 |
+
"status": "error",
|
| 159 |
+
"message": "Please provide CSS code or describe your desired style.",
|
| 160 |
+
},
|
| 161 |
+
indent=2,
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
try:
|
| 165 |
+
# Create the prompt with schema definition
|
| 166 |
+
SCHEMA = """{
|
| 167 |
+
"themes": {
|
| 168 |
+
"generated_theme": {
|
| 169 |
+
"name": "Generated Theme",
|
| 170 |
+
"colors": {
|
| 171 |
+
"base-100": "#ffffff",
|
| 172 |
+
"base-200": "#f8fafc",
|
| 173 |
+
"base-300": "#e2e8f0",
|
| 174 |
+
"base-content": "#1e293b",
|
| 175 |
+
"primary": "#3b82f6",
|
| 176 |
+
"primary-content": "#ffffff",
|
| 177 |
+
"secondary": "#64748b",
|
| 178 |
+
"secondary-content": "#ffffff",
|
| 179 |
+
"accent": "#f59e0b",
|
| 180 |
+
"accent-content": "#000000",
|
| 181 |
+
"neutral": "#374151",
|
| 182 |
+
"neutral-content": "#ffffff",
|
| 183 |
+
"error": "#ef4444",
|
| 184 |
+
"error-content": "#ffffff"
|
| 185 |
+
},
|
| 186 |
+
"background": "#f1f5f9",
|
| 187 |
+
"font": {
|
| 188 |
+
"family": "Inter",
|
| 189 |
+
"type": "google_font",
|
| 190 |
+
"name": "Inter"
|
| 191 |
+
}
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
}"""
|
| 195 |
+
|
| 196 |
+
ALPACA_PROMPT = """Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.
|
| 197 |
+
|
| 198 |
+
### Instruction:
|
| 199 |
+
Convert the provided CSS code or style description into a JSON theme configuration that follows the exact schema structure. Extract colors from CSS variables, class names, or generate appropriate colors based on the description. Return ONLY valid JSON that matches the schema format.
|
| 200 |
+
|
| 201 |
+
Expected JSON Schema:
|
| 202 |
+
{schema}
|
| 203 |
+
|
| 204 |
+
### Input:
|
| 205 |
+
{input_text}
|
| 206 |
+
|
| 207 |
+
### Response:
|
| 208 |
+
"""
|
| 209 |
+
|
| 210 |
+
prompt = ALPACA_PROMPT.format(schema=SCHEMA, input_text=css_input)
|
| 211 |
+
|
| 212 |
+
# Use the AI API to convert CSS to theme
|
| 213 |
+
result = _query_ai_api(prompt, user_token, model_choice)
|
| 214 |
+
|
| 215 |
+
# Process the AI response
|
| 216 |
+
generated_text = ""
|
| 217 |
+
|
| 218 |
+
if user_token and user_token.strip():
|
| 219 |
+
# Handle Nebius API response (OpenAI format)
|
| 220 |
+
if isinstance(result, dict):
|
| 221 |
+
if "error" in result or "detail" in result:
|
| 222 |
+
error_msg = result.get(
|
| 223 |
+
"error", result.get("detail", "Unknown error")
|
| 224 |
+
)
|
| 225 |
+
if (
|
| 226 |
+
"authentication" in str(error_msg).lower()
|
| 227 |
+
or "unauthorized" in str(error_msg).lower()
|
| 228 |
+
):
|
| 229 |
+
return json.dumps(
|
| 230 |
+
{
|
| 231 |
+
"status": "error",
|
| 232 |
+
"message": "β Invalid Nebius API token provided. Please check your Nebius API key.",
|
| 233 |
+
},
|
| 234 |
+
indent=2,
|
| 235 |
+
)
|
| 236 |
+
else:
|
| 237 |
+
return json.dumps(
|
| 238 |
+
{
|
| 239 |
+
"status": "error",
|
| 240 |
+
"message": f"β Nebius API error: {error_msg}",
|
| 241 |
+
},
|
| 242 |
+
indent=2,
|
| 243 |
+
)
|
| 244 |
+
elif "choices" in result and len(result["choices"]) > 0:
|
| 245 |
+
generated_text = (
|
| 246 |
+
result["choices"][0].get("message", {}).get("content", "")
|
| 247 |
+
)
|
| 248 |
+
else:
|
| 249 |
+
return json.dumps(
|
| 250 |
+
{
|
| 251 |
+
"status": "error",
|
| 252 |
+
"message": f"β Unexpected Nebius API response format",
|
| 253 |
+
},
|
| 254 |
+
indent=2,
|
| 255 |
+
)
|
| 256 |
+
else:
|
| 257 |
+
# Handle HuggingFace Zero API response
|
| 258 |
+
if isinstance(result, list) and len(result) > 0:
|
| 259 |
+
generated_text = result[0].get("generated_text", "")
|
| 260 |
+
elif isinstance(result, dict):
|
| 261 |
+
if "error" in result:
|
| 262 |
+
if "loading" in result["error"].lower():
|
| 263 |
+
return json.dumps(
|
| 264 |
+
{
|
| 265 |
+
"status": "error",
|
| 266 |
+
"message": f"π Model is still loading on HuggingFace servers. Please try again in a few moments.",
|
| 267 |
+
},
|
| 268 |
+
indent=2,
|
| 269 |
+
)
|
| 270 |
+
else:
|
| 271 |
+
return json.dumps(
|
| 272 |
+
{
|
| 273 |
+
"status": "error",
|
| 274 |
+
"message": f"β HuggingFace API error: {result['error']}",
|
| 275 |
+
},
|
| 276 |
+
indent=2,
|
| 277 |
+
)
|
| 278 |
+
generated_text = result.get("generated_text", "")
|
| 279 |
+
|
| 280 |
+
if not generated_text:
|
| 281 |
+
return json.dumps(
|
| 282 |
+
{
|
| 283 |
+
"status": "error",
|
| 284 |
+
"message": "β No response generated. Please try again or rephrase your request.",
|
| 285 |
+
},
|
| 286 |
+
indent=2,
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
# Clean up and extract JSON from response
|
| 290 |
+
json_part = _extract_json_from_response(generated_text)
|
| 291 |
+
|
| 292 |
+
if json_part.startswith("β"):
|
| 293 |
+
return json.dumps({"status": "error", "message": json_part}, indent=2)
|
| 294 |
+
|
| 295 |
+
# Try to parse and validate the JSON
|
| 296 |
+
try:
|
| 297 |
+
parsed_json = json.loads(json_part)
|
| 298 |
+
|
| 299 |
+
# Ensure proper structure
|
| 300 |
+
if "themes" in parsed_json:
|
| 301 |
+
for theme_key, theme_data in parsed_json["themes"].items():
|
| 302 |
+
if "background" not in theme_data:
|
| 303 |
+
theme_data["background"] = theme_data.get("colors", {}).get(
|
| 304 |
+
"base-100", "#ffffff"
|
| 305 |
+
)
|
| 306 |
+
if "font" not in theme_data:
|
| 307 |
+
theme_data["font"] = {
|
| 308 |
+
"family": "Inter",
|
| 309 |
+
"type": "google_font",
|
| 310 |
+
"name": "Inter",
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
return json.dumps(parsed_json, indent=2)
|
| 314 |
+
|
| 315 |
+
except json.JSONDecodeError as e:
|
| 316 |
+
return json.dumps(
|
| 317 |
+
{
|
| 318 |
+
"status": "error",
|
| 319 |
+
"message": f"β AI model generated invalid JSON. Please try rephrasing your request.\n\nError: {str(e)}",
|
| 320 |
+
},
|
| 321 |
+
indent=2,
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
except Exception as e:
|
| 325 |
+
return json.dumps(
|
| 326 |
+
{
|
| 327 |
+
"status": "error",
|
| 328 |
+
"message": f"β Error converting CSS to theme: {str(e)}",
|
| 329 |
+
},
|
| 330 |
+
indent=2,
|
| 331 |
+
)
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def generate_app_code(
|
| 335 |
+
theme_names: str = "ocean_breeze,sunset_orange",
|
| 336 |
+
app_title: str = "My Themed App",
|
| 337 |
+
include_components: str = "button,textbox,slider",
|
| 338 |
+
) -> str:
|
| 339 |
+
"""
|
| 340 |
+
Generate complete Gradio application code with integrated theming system.
|
| 341 |
+
|
| 342 |
+
Args:
|
| 343 |
+
theme_names (str): Comma-separated list of theme names to include
|
| 344 |
+
app_title (str): Title for the generated application
|
| 345 |
+
include_components (str): Comma-separated list of components to include
|
| 346 |
+
|
| 347 |
+
Returns:
|
| 348 |
+
str: Complete Python code for a themed Gradio application
|
| 349 |
+
"""
|
| 350 |
+
try:
|
| 351 |
+
# Parse input parameters
|
| 352 |
+
themes = [t.strip() for t in theme_names.split(",") if t.strip()]
|
| 353 |
+
components = [
|
| 354 |
+
c.strip().lower() for c in include_components.split(",") if c.strip()
|
| 355 |
+
]
|
| 356 |
+
|
| 357 |
+
# Generate the complete app code
|
| 358 |
+
app_code = f'''"""
|
| 359 |
+
{app_title}
|
| 360 |
+
A themed Gradio application generated with gradio-themer
|
| 361 |
+
"""
|
| 362 |
+
|
| 363 |
+
import gradio as gr
|
| 364 |
+
from gradio_themer import GradioThemer
|
| 365 |
+
import json
|
| 366 |
+
|
| 367 |
+
def handle_theme_change(theme_data):
|
| 368 |
+
"""Handle theme changes from the GradioThemer component"""
|
| 369 |
+
print(f"Theme changed to: {{theme_data.get('currentTheme', 'default')}}")
|
| 370 |
+
return theme_data
|
| 371 |
+
|
| 372 |
+
# Custom CSS for enhanced styling
|
| 373 |
+
custom_css = """
|
| 374 |
+
.theme-container {{
|
| 375 |
+
max-width: 1200px;
|
| 376 |
+
margin: 0 auto;
|
| 377 |
+
padding: 2rem;
|
| 378 |
+
}}
|
| 379 |
+
|
| 380 |
+
.section {{
|
| 381 |
+
margin-bottom: 2rem;
|
| 382 |
+
padding: 1.5rem;
|
| 383 |
+
border-radius: 8px;
|
| 384 |
+
border: 1px solid #e2e8f0;
|
| 385 |
+
background: #fafafa;
|
| 386 |
+
}}
|
| 387 |
+
"""
|
| 388 |
+
|
| 389 |
+
# Build the {app_title}
|
| 390 |
+
with gr.Blocks(css=custom_css, title="{app_title}") as demo:
|
| 391 |
+
|
| 392 |
+
# Header
|
| 393 |
+
with gr.Column(elem_classes="theme-container"):
|
| 394 |
+
gr.Markdown("# {app_title}")
|
| 395 |
+
gr.Markdown("**Powered by gradio-themer** - Dynamic theme system")
|
| 396 |
+
|
| 397 |
+
# Theme Controller
|
| 398 |
+
themer = GradioThemer(
|
| 399 |
+
value={{
|
| 400 |
+
"currentTheme": "{themes[0] if themes else 'ocean_breeze'}",
|
| 401 |
+
"type": "custom",
|
| 402 |
+
"removeBorders": True
|
| 403 |
+
}},
|
| 404 |
+
label="π¨ Theme Selector",
|
| 405 |
+
theme_config_path="user_themes.json"
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
# Main content sections
|
| 409 |
+
with gr.Column(elem_classes="theme-container"):
|
| 410 |
+
'''
|
| 411 |
+
|
| 412 |
+
# Add components based on user selection
|
| 413 |
+
if "button" in components:
|
| 414 |
+
app_code += """
|
| 415 |
+
# Button demonstration
|
| 416 |
+
with gr.Column(elem_classes="section"):
|
| 417 |
+
gr.Markdown("### Button Components")
|
| 418 |
+
with gr.Row():
|
| 419 |
+
btn_primary = gr.Button("Primary Action", variant="primary")
|
| 420 |
+
btn_secondary = gr.Button("Secondary Action", variant="secondary")
|
| 421 |
+
btn_stop = gr.Button("Stop Action", variant="stop")
|
| 422 |
+
"""
|
| 423 |
+
|
| 424 |
+
if "textbox" in components:
|
| 425 |
+
app_code += """
|
| 426 |
+
# Text input demonstration
|
| 427 |
+
with gr.Column(elem_classes="section"):
|
| 428 |
+
gr.Markdown("### Text Input Components")
|
| 429 |
+
text_input = gr.Textbox(
|
| 430 |
+
label="Enter your text",
|
| 431 |
+
placeholder="Type something here...",
|
| 432 |
+
lines=3
|
| 433 |
+
)
|
| 434 |
+
text_output = gr.Textbox(label="Output", interactive=False)
|
| 435 |
+
"""
|
| 436 |
+
|
| 437 |
+
if "slider" in components:
|
| 438 |
+
app_code += """
|
| 439 |
+
# Slider demonstration
|
| 440 |
+
with gr.Column(elem_classes="section"):
|
| 441 |
+
gr.Markdown("### Slider Components")
|
| 442 |
+
slider_value = gr.Slider(
|
| 443 |
+
minimum=0,
|
| 444 |
+
maximum=100,
|
| 445 |
+
value=50,
|
| 446 |
+
label="Adjust Value"
|
| 447 |
+
)
|
| 448 |
+
slider_output = gr.Number(label="Current Value", value=50)
|
| 449 |
+
"""
|
| 450 |
+
|
| 451 |
+
if "dropdown" in components:
|
| 452 |
+
app_code += """
|
| 453 |
+
# Dropdown demonstration
|
| 454 |
+
with gr.Column(elem_classes="section"):
|
| 455 |
+
gr.Markdown("### Selection Components")
|
| 456 |
+
dropdown = gr.Dropdown(
|
| 457 |
+
choices=["Option 1", "Option 2", "Option 3"],
|
| 458 |
+
label="Choose an option",
|
| 459 |
+
value="Option 1"
|
| 460 |
+
)
|
| 461 |
+
radio = gr.Radio(
|
| 462 |
+
choices=["Choice A", "Choice B", "Choice C"],
|
| 463 |
+
label="Select one",
|
| 464 |
+
value="Choice A"
|
| 465 |
+
)
|
| 466 |
+
"""
|
| 467 |
+
|
| 468 |
+
# Add event handlers
|
| 469 |
+
app_code += """
|
| 470 |
+
# Event handlers
|
| 471 |
+
themer.change(fn=handle_theme_change, inputs=[themer])
|
| 472 |
+
|
| 473 |
+
"""
|
| 474 |
+
|
| 475 |
+
# Add simple interactions if textbox is included
|
| 476 |
+
if "textbox" in components:
|
| 477 |
+
app_code += """ # Simple text processing
|
| 478 |
+
def process_text(text):
|
| 479 |
+
return f"Processed: {text.upper()}"
|
| 480 |
+
|
| 481 |
+
text_input.change(fn=process_text, inputs=[text_input], outputs=[text_output])
|
| 482 |
+
"""
|
| 483 |
+
|
| 484 |
+
# Add slider interaction if slider is included
|
| 485 |
+
if "slider" in components:
|
| 486 |
+
app_code += """ # Slider value update
|
| 487 |
+
slider_value.change(fn=lambda x: x, inputs=[slider_value], outputs=[slider_output])
|
| 488 |
+
"""
|
| 489 |
+
|
| 490 |
+
# Launch configuration
|
| 491 |
+
app_code += """
|
| 492 |
+
# Launch the application
|
| 493 |
+
if __name__ == "__main__":
|
| 494 |
+
demo.launch(
|
| 495 |
+
server_name="0.0.0.0",
|
| 496 |
+
server_port=7860,
|
| 497 |
+
share=False,
|
| 498 |
+
debug=True
|
| 499 |
+
)
|
| 500 |
+
"""
|
| 501 |
+
|
| 502 |
+
return app_code
|
| 503 |
+
|
| 504 |
+
except Exception as e:
|
| 505 |
+
return f"""# Error generating app code
|
| 506 |
+
# {str(e)}
|
| 507 |
+
|
| 508 |
+
import gradio as gr
|
| 509 |
+
|
| 510 |
+
with gr.Blocks(title="Error") as demo:
|
| 511 |
+
gr.Markdown("β Failed to generate app code. Please check your parameters.")
|
| 512 |
+
|
| 513 |
+
if __name__ == "__main__":
|
| 514 |
+
demo.launch()
|
| 515 |
+
"""
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
# Helper functions for theme generation and API calls
|
| 519 |
+
|
| 520 |
+
|
| 521 |
+
def _generate_color_palette(
|
| 522 |
+
primary_color: str, style: str, accent_color: Optional[str] = None
|
| 523 |
+
) -> Dict[str, str]:
|
| 524 |
+
"""Generate a complete color palette based on primary color and style"""
|
| 525 |
+
try:
|
| 526 |
+
# Convert primary color to RGB for calculations
|
| 527 |
+
rgb = _hex_to_rgb(primary_color)
|
| 528 |
+
hsl = _rgb_to_hsl(rgb)
|
| 529 |
+
|
| 530 |
+
# Generate base colors based on style
|
| 531 |
+
if style == "dark":
|
| 532 |
+
base_100 = "#1a1a1a"
|
| 533 |
+
base_200 = "#2d2d2d"
|
| 534 |
+
base_300 = "#404040"
|
| 535 |
+
base_content = "#ffffff"
|
| 536 |
+
else: # light style
|
| 537 |
+
base_100 = "#ffffff"
|
| 538 |
+
base_200 = "#f8fafc"
|
| 539 |
+
base_300 = "#e2e8f0"
|
| 540 |
+
base_content = "#1e293b"
|
| 541 |
+
|
| 542 |
+
# Generate accent color if not provided
|
| 543 |
+
if not accent_color:
|
| 544 |
+
accent_color = _generate_complementary_color(primary_color, 0.1)
|
| 545 |
+
|
| 546 |
+
# Generate secondary color (triadic)
|
| 547 |
+
secondary_color = _generate_triadic_color(primary_color)
|
| 548 |
+
|
| 549 |
+
# Create complete color palette
|
| 550 |
+
colors = {
|
| 551 |
+
"base-100": base_100,
|
| 552 |
+
"base-200": base_200,
|
| 553 |
+
"base-300": base_300,
|
| 554 |
+
"base-content": base_content,
|
| 555 |
+
"primary": primary_color,
|
| 556 |
+
"primary-content": "#ffffff" if style == "light" else "#000000",
|
| 557 |
+
"secondary": secondary_color,
|
| 558 |
+
"secondary-content": "#ffffff",
|
| 559 |
+
"accent": accent_color,
|
| 560 |
+
"accent-content": "#ffffff",
|
| 561 |
+
"neutral": "#374151",
|
| 562 |
+
"neutral-content": "#ffffff",
|
| 563 |
+
"error": "#ef4444",
|
| 564 |
+
"error-content": "#ffffff",
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
return colors
|
| 568 |
+
|
| 569 |
+
except Exception:
|
| 570 |
+
# Fallback to default colors
|
| 571 |
+
return {
|
| 572 |
+
"base-100": "#ffffff",
|
| 573 |
+
"base-200": "#f8fafc",
|
| 574 |
+
"base-300": "#e2e8f0",
|
| 575 |
+
"base-content": "#1e293b",
|
| 576 |
+
"primary": primary_color,
|
| 577 |
+
"primary-content": "#ffffff",
|
| 578 |
+
"secondary": "#64748b",
|
| 579 |
+
"secondary-content": "#ffffff",
|
| 580 |
+
"accent": "#f59e0b",
|
| 581 |
+
"accent-content": "#000000",
|
| 582 |
+
"neutral": "#374151",
|
| 583 |
+
"neutral-content": "#ffffff",
|
| 584 |
+
"error": "#ef4444",
|
| 585 |
+
"error-content": "#ffffff",
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
|
| 589 |
+
def _query_ai_api(prompt: str, user_token: str = "", model_choice: str = "qwen"):
|
| 590 |
+
"""Query AI API - Use Nebius if token provided, otherwise HF Zero inference"""
|
| 591 |
+
model_config = AVAILABLE_MODELS.get(model_choice, AVAILABLE_MODELS["qwen"])
|
| 592 |
+
|
| 593 |
+
if user_token and user_token.strip():
|
| 594 |
+
# Use Nebius Studio API with provided token
|
| 595 |
+
headers = {
|
| 596 |
+
"Authorization": f"Bearer {user_token.strip()}",
|
| 597 |
+
"Content-Type": "application/json",
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
payload = {
|
| 601 |
+
"model": model_config["nebius_model"],
|
| 602 |
+
"messages": [{"role": "user", "content": prompt}],
|
| 603 |
+
"max_tokens": 1000,
|
| 604 |
+
"temperature": 0.3,
|
| 605 |
+
"top_p": 0.9,
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
response = requests.post(NEBIUS_API_URL, headers=headers, json=payload)
|
| 609 |
+
return response.json()
|
| 610 |
+
|
| 611 |
+
else:
|
| 612 |
+
# Use HuggingFace Zero Inference API (no token required)
|
| 613 |
+
hf_inference_url = (
|
| 614 |
+
f"https://api-inference.huggingface.co/models/{model_config['hf_model']}"
|
| 615 |
+
)
|
| 616 |
+
|
| 617 |
+
headers = {"Content-Type": "application/json"}
|
| 618 |
+
|
| 619 |
+
payload = {
|
| 620 |
+
"inputs": prompt,
|
| 621 |
+
"parameters": {
|
| 622 |
+
"max_new_tokens": 1000,
|
| 623 |
+
"temperature": 0.3,
|
| 624 |
+
"top_p": 0.9,
|
| 625 |
+
"do_sample": True,
|
| 626 |
+
"return_full_text": False,
|
| 627 |
+
},
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
response = requests.post(hf_inference_url, headers=headers, json=payload)
|
| 631 |
+
return response.json()
|
| 632 |
+
|
| 633 |
+
|
| 634 |
+
def _extract_json_from_response(generated_text: str) -> str:
|
| 635 |
+
"""Extract and clean JSON from AI response"""
|
| 636 |
+
json_part = generated_text.strip()
|
| 637 |
+
|
| 638 |
+
# Handle the case where AI returns explanation + JSON
|
| 639 |
+
if "Here is the JSON theme configuration" in json_part:
|
| 640 |
+
json_start = json_part.find("{")
|
| 641 |
+
if json_start != -1:
|
| 642 |
+
json_part = json_part[json_start:]
|
| 643 |
+
|
| 644 |
+
# Remove code block markers
|
| 645 |
+
if json_part.startswith("```json"):
|
| 646 |
+
json_part = json_part[7:]
|
| 647 |
+
elif json_part.startswith("```"):
|
| 648 |
+
json_part = json_part[3:]
|
| 649 |
+
if json_part.endswith("```"):
|
| 650 |
+
json_part = json_part[:-3]
|
| 651 |
+
json_part = json_part.strip()
|
| 652 |
+
|
| 653 |
+
# Find the first { and last } to extract clean JSON
|
| 654 |
+
start_idx = json_part.find("{")
|
| 655 |
+
if start_idx == -1:
|
| 656 |
+
return f"β No valid JSON found in response.\n\n**Raw response:**\n{generated_text}"
|
| 657 |
+
|
| 658 |
+
# Find the matching closing brace
|
| 659 |
+
brace_count = 0
|
| 660 |
+
end_idx = -1
|
| 661 |
+
for i in range(start_idx, len(json_part)):
|
| 662 |
+
if json_part[i] == "{":
|
| 663 |
+
brace_count += 1
|
| 664 |
+
elif json_part[i] == "}":
|
| 665 |
+
brace_count -= 1
|
| 666 |
+
if brace_count == 0:
|
| 667 |
+
end_idx = i + 1
|
| 668 |
+
break
|
| 669 |
+
|
| 670 |
+
if end_idx == -1:
|
| 671 |
+
return f"β Incomplete JSON found.\n\n**Raw response:**\n{generated_text}"
|
| 672 |
+
|
| 673 |
+
return json_part[start_idx:end_idx]
|
| 674 |
+
|
| 675 |
+
|
| 676 |
+
def _hex_to_rgb(hex_color: str) -> Tuple[int, int, int]:
|
| 677 |
+
"""Convert hex color to RGB tuple"""
|
| 678 |
+
hex_color = hex_color.lstrip("#")
|
| 679 |
+
return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
|
| 680 |
+
|
| 681 |
+
|
| 682 |
+
def _rgb_to_hsl(rgb: Tuple[int, int, int]) -> Tuple[float, float, float]:
|
| 683 |
+
"""Convert RGB to HSL"""
|
| 684 |
+
r, g, b = [x / 255.0 for x in rgb]
|
| 685 |
+
return colorsys.rgb_to_hls(r, g, b)
|
| 686 |
+
|
| 687 |
+
|
| 688 |
+
def _hsl_to_rgb(hsl: Tuple[float, float, float]) -> Tuple[int, int, int]:
|
| 689 |
+
"""Convert HSL to RGB"""
|
| 690 |
+
h, l, s = hsl
|
| 691 |
+
r, g, b = colorsys.hls_to_rgb(h, l, s)
|
| 692 |
+
return tuple(int(x * 255) for x in (r, g, b))
|
| 693 |
+
|
| 694 |
+
|
| 695 |
+
def _rgb_to_hex(rgb: Tuple[int, int, int]) -> str:
|
| 696 |
+
"""Convert RGB tuple to hex color"""
|
| 697 |
+
return f"#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}"
|
| 698 |
+
|
| 699 |
+
|
| 700 |
+
def _generate_complementary_color(base_color: str, lightness_adjust: float = 0) -> str:
|
| 701 |
+
"""Generate complementary color"""
|
| 702 |
+
try:
|
| 703 |
+
rgb = _hex_to_rgb(base_color)
|
| 704 |
+
h, l, s = _rgb_to_hsl(rgb)
|
| 705 |
+
|
| 706 |
+
# Shift hue by 180 degrees for complementary
|
| 707 |
+
comp_h = (h + 0.5) % 1.0
|
| 708 |
+
comp_l = max(0, min(1, l + lightness_adjust))
|
| 709 |
+
|
| 710 |
+
comp_rgb = _hsl_to_rgb((comp_h, comp_l, s))
|
| 711 |
+
return _rgb_to_hex(comp_rgb)
|
| 712 |
+
except:
|
| 713 |
+
return "#f59e0b" # Fallback color
|
| 714 |
+
|
| 715 |
+
|
| 716 |
+
def _generate_triadic_color(base_color: str) -> str:
|
| 717 |
+
"""Generate triadic color (120 degrees hue shift)"""
|
| 718 |
+
try:
|
| 719 |
+
rgb = _hex_to_rgb(base_color)
|
| 720 |
+
h, l, s = _rgb_to_hsl(rgb)
|
| 721 |
+
|
| 722 |
+
# Shift hue by 120 degrees for triadic
|
| 723 |
+
triadic_h = (h + 0.33) % 1.0
|
| 724 |
+
|
| 725 |
+
triadic_rgb = _hsl_to_rgb((triadic_h, l, s))
|
| 726 |
+
return _rgb_to_hex(triadic_rgb)
|
| 727 |
+
except:
|
| 728 |
+
return "#64748b" # Fallback color
|
page.py
ADDED
|
@@ -0,0 +1,568 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from gradio.themes.utils import fonts
|
| 3 |
+
import json
|
| 4 |
+
import random
|
| 5 |
+
import subprocess
|
| 6 |
+
import sys
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import requests
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
# CRITICAL: Enable MCP server mode (as per GRADIO_MCP_HF_SPACES_GUIDE.md)
|
| 12 |
+
os.environ["GRADIO_MCP_SERVER"] = "True"
|
| 13 |
+
|
| 14 |
+
# Import MCP tools from mcp_tools.py to make them available for the MCP server
|
| 15 |
+
try:
|
| 16 |
+
from mcp_tools import (
|
| 17 |
+
setup_package,
|
| 18 |
+
generate_theme,
|
| 19 |
+
convert_css_to_theme,
|
| 20 |
+
generate_app_code,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
print("β
MCP tools imported successfully")
|
| 24 |
+
except ImportError as e:
|
| 25 |
+
print(f"β οΈ Could not import MCP tools: {e}")
|
| 26 |
+
|
| 27 |
+
# New imports for HuggingFace Inference API
|
| 28 |
+
try:
|
| 29 |
+
import requests
|
| 30 |
+
|
| 31 |
+
HF_REQUESTS_AVAILABLE = True
|
| 32 |
+
print("β
HuggingFace Inference API available")
|
| 33 |
+
except ImportError:
|
| 34 |
+
HF_REQUESTS_AVAILABLE = False
|
| 35 |
+
print("β οΈ requests library not available")
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
from gradio_themer import GradioThemer
|
| 39 |
+
|
| 40 |
+
THEMER_AVAILABLE = True
|
| 41 |
+
print("β
Using renamed GradioThemer package (gradio_themer-0.1.0)")
|
| 42 |
+
except ImportError:
|
| 43 |
+
print("β οΈ GradioThemer not available, themes will not work")
|
| 44 |
+
THEMER_AVAILABLE = False
|
| 45 |
+
|
| 46 |
+
# Model API configuration
|
| 47 |
+
# Available models for both HF Zero and Nebius inference
|
| 48 |
+
AVAILABLE_MODELS = {
|
| 49 |
+
"qwen": {
|
| 50 |
+
"hf_model": "Qwen/Qwen2.5-Coder-7B",
|
| 51 |
+
"nebius_model": "Qwen/Qwen2.5-Coder-7B",
|
| 52 |
+
"name": "Qwen2.5-Coder-7B",
|
| 53 |
+
},
|
| 54 |
+
"llama": {
|
| 55 |
+
"hf_model": "meta-llama/Meta-Llama-3.1-8B-Instruct",
|
| 56 |
+
"nebius_model": "meta-llama/Meta-Llama-3.1-8B-Instruct",
|
| 57 |
+
"name": "Meta-Llama-3.1-8B-Instruct",
|
| 58 |
+
},
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
# Nebius Studio API configuration
|
| 62 |
+
NEBIUS_API_URL = "https://api.studio.nebius.ai/v1/chat/completions"
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def query_ai_api(prompt, user_token=None, model_choice="qwen"):
|
| 66 |
+
"""Query AI API - Use Nebius if token provided, otherwise HF Zero inference"""
|
| 67 |
+
|
| 68 |
+
model_config = AVAILABLE_MODELS.get(model_choice, AVAILABLE_MODELS["qwen"])
|
| 69 |
+
|
| 70 |
+
if user_token and user_token.strip():
|
| 71 |
+
# Use Nebius Studio API with provided token
|
| 72 |
+
headers = {
|
| 73 |
+
"Authorization": f"Bearer {user_token.strip()}",
|
| 74 |
+
"Content-Type": "application/json",
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
payload = {
|
| 78 |
+
"model": model_config["nebius_model"],
|
| 79 |
+
"messages": [{"role": "user", "content": prompt}],
|
| 80 |
+
"max_tokens": 1000,
|
| 81 |
+
"temperature": 0.3,
|
| 82 |
+
"top_p": 0.9,
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
response = requests.post(NEBIUS_API_URL, headers=headers, json=payload)
|
| 86 |
+
return response.json()
|
| 87 |
+
|
| 88 |
+
else:
|
| 89 |
+
# Use HuggingFace Zero Inference API (no token required)
|
| 90 |
+
hf_inference_url = (
|
| 91 |
+
f"https://api-inference.huggingface.co/models/{model_config['hf_model']}"
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
headers = {"Content-Type": "application/json"}
|
| 95 |
+
|
| 96 |
+
payload = {
|
| 97 |
+
"inputs": prompt,
|
| 98 |
+
"parameters": {
|
| 99 |
+
"max_new_tokens": 1000,
|
| 100 |
+
"temperature": 0.3,
|
| 101 |
+
"top_p": 0.9,
|
| 102 |
+
"do_sample": True,
|
| 103 |
+
"return_full_text": False,
|
| 104 |
+
},
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
response = requests.post(hf_inference_url, headers=headers, json=payload)
|
| 108 |
+
return response.json()
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def convert_css_to_theme_json_ai(input_text, user_token="", model_choice="qwen"):
|
| 112 |
+
# Internal UI function - not for MCP discovery
|
| 113 |
+
if not input_text.strip():
|
| 114 |
+
return "Please provide CSS code or describe your desired style."
|
| 115 |
+
|
| 116 |
+
if not HF_REQUESTS_AVAILABLE:
|
| 117 |
+
return "β requests library is required for API calls. Please install with: pip install requests"
|
| 118 |
+
|
| 119 |
+
try:
|
| 120 |
+
# Create the prompt with schema definition
|
| 121 |
+
SCHEMA = """{
|
| 122 |
+
"themes": {
|
| 123 |
+
"generated_theme": {
|
| 124 |
+
"name": "Generated Theme",
|
| 125 |
+
"colors": {
|
| 126 |
+
"base-100": "#ffffff",
|
| 127 |
+
"base-200": "#f8fafc",
|
| 128 |
+
"base-300": "#e2e8f0",
|
| 129 |
+
"base-content": "#1e293b",
|
| 130 |
+
"primary": "#3b82f6",
|
| 131 |
+
"primary-content": "#ffffff",
|
| 132 |
+
"secondary": "#64748b",
|
| 133 |
+
"secondary-content": "#ffffff",
|
| 134 |
+
"accent": "#f59e0b",
|
| 135 |
+
"accent-content": "#000000",
|
| 136 |
+
"neutral": "#374151",
|
| 137 |
+
"neutral-content": "#ffffff",
|
| 138 |
+
"error": "#ef4444",
|
| 139 |
+
"error-content": "#ffffff"
|
| 140 |
+
},
|
| 141 |
+
"background": "#f1f5f9",
|
| 142 |
+
"font": {
|
| 143 |
+
"family": "Inter",
|
| 144 |
+
"type": "google_font",
|
| 145 |
+
"name": "Inter"
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
}"""
|
| 150 |
+
|
| 151 |
+
ALPACA_PROMPT = """Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.
|
| 152 |
+
|
| 153 |
+
### Instruction:
|
| 154 |
+
Convert the provided CSS code or style description into a JSON theme configuration that follows the exact schema structure. Extract colors from CSS variables, class names, or generate appropriate colors based on the description. Return ONLY valid JSON that matches the schema format.
|
| 155 |
+
|
| 156 |
+
Expected JSON Schema:
|
| 157 |
+
{schema}
|
| 158 |
+
|
| 159 |
+
### Input:
|
| 160 |
+
{input_text}
|
| 161 |
+
|
| 162 |
+
### Response:
|
| 163 |
+
"""
|
| 164 |
+
|
| 165 |
+
prompt = ALPACA_PROMPT.format(schema=SCHEMA, input_text=input_text)
|
| 166 |
+
|
| 167 |
+
model_name = AVAILABLE_MODELS[model_choice]["name"]
|
| 168 |
+
api_type = "Nebius" if user_token and user_token.strip() else "HuggingFace Zero"
|
| 169 |
+
print(f"π€ Generating theme with {model_name} via {api_type}...")
|
| 170 |
+
|
| 171 |
+
try:
|
| 172 |
+
result = query_ai_api(prompt, user_token, model_choice)
|
| 173 |
+
|
| 174 |
+
# Handle different response formats
|
| 175 |
+
generated_text = ""
|
| 176 |
+
|
| 177 |
+
if user_token and user_token.strip():
|
| 178 |
+
# Handle Nebius API response (OpenAI format)
|
| 179 |
+
if isinstance(result, dict):
|
| 180 |
+
if "error" in result or "detail" in result:
|
| 181 |
+
error_msg = result.get(
|
| 182 |
+
"error", result.get("detail", "Unknown error")
|
| 183 |
+
)
|
| 184 |
+
if (
|
| 185 |
+
"authentication" in str(error_msg).lower()
|
| 186 |
+
or "unauthorized" in str(error_msg).lower()
|
| 187 |
+
or "invalid authentication token" in str(error_msg).lower()
|
| 188 |
+
):
|
| 189 |
+
return "β Invalid Nebius API token provided. Please check your Nebius API key and try again."
|
| 190 |
+
else:
|
| 191 |
+
return f"β Nebius API error: {error_msg}"
|
| 192 |
+
elif "choices" in result and len(result["choices"]) > 0:
|
| 193 |
+
generated_text = (
|
| 194 |
+
result["choices"][0].get("message", {}).get("content", "")
|
| 195 |
+
)
|
| 196 |
+
else:
|
| 197 |
+
return "β Unexpected Nebius API response format"
|
| 198 |
+
else:
|
| 199 |
+
return "β Unexpected Nebius API response format"
|
| 200 |
+
else:
|
| 201 |
+
# Handle HuggingFace API response
|
| 202 |
+
if isinstance(result, list) and len(result) > 0:
|
| 203 |
+
generated_text = result[0].get("generated_text", "")
|
| 204 |
+
elif isinstance(result, dict):
|
| 205 |
+
if "error" in result:
|
| 206 |
+
if "loading" in str(result["error"]).lower():
|
| 207 |
+
return "β³ Model is loading on HuggingFace. Please try again in a few moments."
|
| 208 |
+
return f"β HuggingFace API error: {result['error']}"
|
| 209 |
+
else:
|
| 210 |
+
generated_text = result.get("generated_text", "")
|
| 211 |
+
|
| 212 |
+
if not generated_text.strip():
|
| 213 |
+
return "β No response received from AI model. Please try again."
|
| 214 |
+
|
| 215 |
+
# Try to extract JSON from the response
|
| 216 |
+
try:
|
| 217 |
+
# Look for JSON in the response
|
| 218 |
+
start_idx = generated_text.find("{")
|
| 219 |
+
if start_idx == -1:
|
| 220 |
+
return f"β No valid JSON found in response:\n\n{generated_text}"
|
| 221 |
+
|
| 222 |
+
# Find the matching closing brace
|
| 223 |
+
brace_count = 0
|
| 224 |
+
end_idx = -1
|
| 225 |
+
for i in range(start_idx, len(generated_text)):
|
| 226 |
+
if generated_text[i] == "{":
|
| 227 |
+
brace_count += 1
|
| 228 |
+
elif generated_text[i] == "}":
|
| 229 |
+
brace_count -= 1
|
| 230 |
+
if brace_count == 0:
|
| 231 |
+
end_idx = i + 1
|
| 232 |
+
break
|
| 233 |
+
|
| 234 |
+
if end_idx == -1:
|
| 235 |
+
return f"β Incomplete JSON in response:\n\n{generated_text}"
|
| 236 |
+
|
| 237 |
+
json_str = generated_text[start_idx:end_idx]
|
| 238 |
+
|
| 239 |
+
# Validate JSON
|
| 240 |
+
parsed_json = json.loads(json_str)
|
| 241 |
+
|
| 242 |
+
# Pretty format the JSON
|
| 243 |
+
formatted_json = json.dumps(parsed_json, indent=2)
|
| 244 |
+
|
| 245 |
+
return formatted_json
|
| 246 |
+
|
| 247 |
+
except json.JSONDecodeError as e:
|
| 248 |
+
return f"β Invalid JSON generated:\n\nJSON Error: {str(e)}\n\nRaw Response:\n{generated_text}"
|
| 249 |
+
|
| 250 |
+
except requests.exceptions.RequestException as e:
|
| 251 |
+
return f"β Network error: {str(e)}"
|
| 252 |
+
except Exception as e:
|
| 253 |
+
return f"β Unexpected error during AI processing: {str(e)}"
|
| 254 |
+
|
| 255 |
+
except Exception as e:
|
| 256 |
+
return f"β Error: {str(e)}"
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def apply_random_theme():
|
| 260 |
+
# Internal UI function - not for MCP discovery
|
| 261 |
+
# Load the actual theme data to get all available themes
|
| 262 |
+
themes_file = Path(__file__).parent / "user_themes.json"
|
| 263 |
+
|
| 264 |
+
try:
|
| 265 |
+
with open(themes_file, "r", encoding="utf-8") as f:
|
| 266 |
+
theme_data = json.load(f)
|
| 267 |
+
|
| 268 |
+
if not theme_data.get("themes"):
|
| 269 |
+
return (
|
| 270 |
+
{"currentTheme": "default", "type": "builtin"},
|
| 271 |
+
"β No themes found in user_themes.json",
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
# Get all available theme keys
|
| 275 |
+
theme_keys = list(theme_data["themes"].keys())
|
| 276 |
+
if not theme_keys:
|
| 277 |
+
return (
|
| 278 |
+
{"currentTheme": "default", "type": "builtin"},
|
| 279 |
+
"β No themes available",
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
# Select a random theme
|
| 283 |
+
random_theme_key = random.choice(theme_keys)
|
| 284 |
+
random_theme = theme_data["themes"][random_theme_key]
|
| 285 |
+
|
| 286 |
+
print(f"π² Randomly selected theme: {random_theme_key}")
|
| 287 |
+
|
| 288 |
+
# Update the component state
|
| 289 |
+
new_state = {
|
| 290 |
+
"currentTheme": random_theme_key,
|
| 291 |
+
"type": "custom",
|
| 292 |
+
"themeConfig": random_theme,
|
| 293 |
+
"font": random_theme.get("font", {"family": "Inter", "weights": ["400"]}),
|
| 294 |
+
"removeBorders": True,
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
return (
|
| 298 |
+
new_state,
|
| 299 |
+
f"β
Applied random theme: {random_theme.get('name', random_theme_key)}",
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
except FileNotFoundError:
|
| 303 |
+
return (
|
| 304 |
+
{"currentTheme": "default", "type": "builtin"},
|
| 305 |
+
"β Theme file not found: user_themes.json",
|
| 306 |
+
)
|
| 307 |
+
except json.JSONDecodeError as e:
|
| 308 |
+
return (
|
| 309 |
+
{"currentTheme": "default", "type": "builtin"},
|
| 310 |
+
f"β Invalid JSON in theme file: {str(e)}",
|
| 311 |
+
)
|
| 312 |
+
except Exception as e:
|
| 313 |
+
return (
|
| 314 |
+
{"currentTheme": "default", "type": "builtin"},
|
| 315 |
+
f"β Error loading theme: {str(e)}",
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
# def start_mcp_server():
|
| 320 |
+
# """Start the MCP server in background"""
|
| 321 |
+
# try:
|
| 322 |
+
# mcp_process = subprocess.Popen(
|
| 323 |
+
# [sys.executable, "mcp_server.py"],
|
| 324 |
+
# cwd=Path(__file__).parent,
|
| 325 |
+
# stdout=subprocess.PIPE,
|
| 326 |
+
# stderr=subprocess.PIPE,
|
| 327 |
+
# )
|
| 328 |
+
# return "β
MCP Server started successfully! Use stdio transport to connect."
|
| 329 |
+
# except Exception as e:
|
| 330 |
+
# return f"β Failed to start MCP server: {str(e)}"
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
# Custom CSS for minimal design (no @import allowed in Gradio)
|
| 334 |
+
custom_css = """
|
| 335 |
+
.header-container {
|
| 336 |
+
text-align: center;
|
| 337 |
+
padding: 1rem 1rem;
|
| 338 |
+
border-bottom: 1px solid #e5e7eb;
|
| 339 |
+
margin-bottom: 1rem;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.main-sections {
|
| 343 |
+
max-width: 800px;
|
| 344 |
+
margin: 0 auto;
|
| 345 |
+
padding: 0 1rem 0rem 1rem;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.section {
|
| 349 |
+
margin-bottom: 0.75rem;
|
| 350 |
+
padding: 1rem;
|
| 351 |
+
border: 1px solid #e5e7eb;
|
| 352 |
+
border-radius: 8px;
|
| 353 |
+
background: #fafafa;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
.mcp-info {
|
| 357 |
+
background: #f0f9ff;
|
| 358 |
+
border-color: #0ea5e9;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
/* Override Gradio's default spacing and remove nested borders */
|
| 362 |
+
.gradio-container .block {
|
| 363 |
+
margin: 0.5rem 0 !important;
|
| 364 |
+
border: none !important;
|
| 365 |
+
box-shadow: none !important;
|
| 366 |
+
background: transparent !important;
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
.gradio-container .gr-column {
|
| 370 |
+
gap: 0.5rem !important;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
/* Remove nested styling from textbox containers */
|
| 374 |
+
.section .gradio-container .gr-textbox,
|
| 375 |
+
.section .gradio-container .gr-code {
|
| 376 |
+
border: 1px solid #d1d5db !important;
|
| 377 |
+
border-radius: 6px !important;
|
| 378 |
+
background: white !important;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
/* Ensure code output is selectable and copy button works */
|
| 382 |
+
.gr-code .language-json,
|
| 383 |
+
.gr-code pre,
|
| 384 |
+
.gr-code code {
|
| 385 |
+
user-select: text !important;
|
| 386 |
+
-webkit-user-select: text !important;
|
| 387 |
+
-moz-user-select: text !important;
|
| 388 |
+
-ms-user-select: text !important;
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
/* Make sure copy button is visible and functional */
|
| 392 |
+
.gr-code .copy-button,
|
| 393 |
+
.gr-code button[aria-label*="copy"],
|
| 394 |
+
.gr-code button[title*="copy"] {
|
| 395 |
+
opacity: 1 !important;
|
| 396 |
+
visibility: visible !important;
|
| 397 |
+
pointer-events: auto !important;
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
/* Add margin to bottom footer/button area */
|
| 401 |
+
.gradio-container footer,
|
| 402 |
+
.gradio-container .footer,
|
| 403 |
+
.gradio-container > div:last-child,
|
| 404 |
+
footer {
|
| 405 |
+
margin-bottom: 2rem !important;
|
| 406 |
+
padding-bottom: 2rem !important;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
/* Add bottom margin to the entire Gradio container */
|
| 410 |
+
.gradio-container {
|
| 411 |
+
margin-bottom: 3rem !important;
|
| 412 |
+
padding-bottom: 2rem !important;
|
| 413 |
+
}
|
| 414 |
+
"""
|
| 415 |
+
|
| 416 |
+
# Build the demo interface with Gradio's built-in font system
|
| 417 |
+
with gr.Blocks(css=custom_css, title="Gradio Themer - Demo & MCP Server") as demo:
|
| 418 |
+
|
| 419 |
+
# Header
|
| 420 |
+
with gr.Column(elem_classes="header-container"):
|
| 421 |
+
gr.Markdown("# π¨ Gradio Themer")
|
| 422 |
+
gr.Markdown(
|
| 423 |
+
"**Demo & MCP Server** - Dynamic theme system for Gradio applications"
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
# Add the working GradioThemer component
|
| 427 |
+
if THEMER_AVAILABLE:
|
| 428 |
+
themer = GradioThemer(
|
| 429 |
+
value={
|
| 430 |
+
"currentTheme": "corporate",
|
| 431 |
+
"type": "builtin",
|
| 432 |
+
"font": {"family": "Poppins", "weights": ["400", "500", "600", "700"]},
|
| 433 |
+
"removeBorders": True,
|
| 434 |
+
"themeInput": "",
|
| 435 |
+
"themeConfig": None,
|
| 436 |
+
"generatedCSS": "",
|
| 437 |
+
},
|
| 438 |
+
visible=False,
|
| 439 |
+
label="Theme Controller (for debugging)",
|
| 440 |
+
scale=1,
|
| 441 |
+
)
|
| 442 |
+
else:
|
| 443 |
+
themer = gr.HTML(visible=False) # Dummy component if themer not available
|
| 444 |
+
|
| 445 |
+
# Main content
|
| 446 |
+
with gr.Column(elem_classes="main-sections"):
|
| 447 |
+
|
| 448 |
+
# Top section - Random theme
|
| 449 |
+
with gr.Column(elem_classes="section"):
|
| 450 |
+
gr.Markdown("### π² Random Theme")
|
| 451 |
+
|
| 452 |
+
random_btn = gr.Button("Apply Random Theme", variant="primary", size="lg")
|
| 453 |
+
theme_status = gr.Textbox(
|
| 454 |
+
label="Theme Status",
|
| 455 |
+
placeholder="Click button to apply random theme",
|
| 456 |
+
interactive=False,
|
| 457 |
+
)
|
| 458 |
+
|
| 459 |
+
# Demo components to show theming
|
| 460 |
+
with gr.Column(elem_classes="section"):
|
| 461 |
+
gr.Markdown("### π¨ Live Theme Preview")
|
| 462 |
+
|
| 463 |
+
with gr.Row():
|
| 464 |
+
gr.Button("Primary Button", variant="primary")
|
| 465 |
+
gr.Button("Secondary Button", variant="secondary")
|
| 466 |
+
gr.Button("Stop Button", variant="stop")
|
| 467 |
+
|
| 468 |
+
with gr.Row():
|
| 469 |
+
gr.Textbox("Sample text input", label="Text Input")
|
| 470 |
+
gr.Slider(0, 100, value=50, label="Slider")
|
| 471 |
+
|
| 472 |
+
with gr.Row():
|
| 473 |
+
gr.Dropdown(["Option 1", "Option 2", "Option 3"], label="Dropdown")
|
| 474 |
+
gr.Radio(["Choice A", "Choice B"], label="Radio")
|
| 475 |
+
|
| 476 |
+
# Middle section - CSS converter
|
| 477 |
+
with gr.Column(elem_classes="section"):
|
| 478 |
+
gr.Markdown(
|
| 479 |
+
"""### π AI-Powered CSS to Theme Converter
|
| 480 |
+
π€ Convert CSS code or describe your style to generate JSON themes using AI models"""
|
| 481 |
+
)
|
| 482 |
+
with gr.Row():
|
| 483 |
+
model_selector = gr.Dropdown(
|
| 484 |
+
label="AI Model",
|
| 485 |
+
choices=[
|
| 486 |
+
("Qwen2.5-Coder-7B", "qwen"),
|
| 487 |
+
("Meta-Llama-3.1-8B-Instruct", "llama"),
|
| 488 |
+
],
|
| 489 |
+
value="qwen",
|
| 490 |
+
info="Choose AI model for theme generation",
|
| 491 |
+
scale=1,
|
| 492 |
+
)
|
| 493 |
+
token_input = gr.Textbox(
|
| 494 |
+
label="Nebius API Token (Optional)",
|
| 495 |
+
placeholder="Leave empty for HF Zero inference, or provide Nebius token for better performance",
|
| 496 |
+
type="password",
|
| 497 |
+
lines=1,
|
| 498 |
+
scale=2,
|
| 499 |
+
)
|
| 500 |
+
|
| 501 |
+
css_input = gr.Textbox(
|
| 502 |
+
label="Describe your style or paste CSS code here",
|
| 503 |
+
placeholder="Examples:\nβ’ 'Dark purple theme with neon accents'\nβ’ 'Corporate blue and white design'\nβ’ CSS code:\n.my-theme {\n --primary: #3b82f6;\n --background: #f8fafc;\n}",
|
| 504 |
+
lines=8,
|
| 505 |
+
max_lines=15,
|
| 506 |
+
)
|
| 507 |
+
|
| 508 |
+
convert_btn = gr.Button("Generate JSON", variant="primary", size="lg")
|
| 509 |
+
|
| 510 |
+
json_output = gr.Code(
|
| 511 |
+
label="Generated Theme JSON", language="json", lines=20
|
| 512 |
+
)
|
| 513 |
+
|
| 514 |
+
# Bottom section - MCP Server info
|
| 515 |
+
with gr.Column(elem_classes="section mcp-info"):
|
| 516 |
+
gr.Markdown("### π€ MCP Server Integration")
|
| 517 |
+
gr.Markdown(
|
| 518 |
+
"""
|
| 519 |
+
This Space functions as both a **Demo** and **MCP Server** for AI agents.
|
| 520 |
+
|
| 521 |
+
**Available MCP Tools:**
|
| 522 |
+
|
| 523 |
+
1. **`setup_package`** - Install and verify gradio-themer package
|
| 524 |
+
2. **`generate_theme`** - Create theme JSON configuration
|
| 525 |
+
3. **`convert_css_to_theme`** - Convert CSS to standardized JSON format (uses HF LLM)
|
| 526 |
+
4. **`generate_app_code`** - Generate complete Gradio app with theming
|
| 527 |
+
|
| 528 |
+
**For AI Agents:**
|
| 529 |
+
- Connect to this Space as an MCP server
|
| 530 |
+
- Use tools to help users create themed Gradio applications
|
| 531 |
+
- Automate theme generation and application setup
|
| 532 |
+
|
| 533 |
+
**For Developers:**
|
| 534 |
+
- Install: `pip install gradio-themer`
|
| 535 |
+
- Use the component in your Gradio apps
|
| 536 |
+
- Create custom themes with JSON configuration
|
| 537 |
+
|
| 538 |
+
**MCP Endpoint:** `/gradio_api/mcp/sse`
|
| 539 |
+
"""
|
| 540 |
+
)
|
| 541 |
+
|
| 542 |
+
# Event handlers
|
| 543 |
+
random_btn.click(
|
| 544 |
+
fn=apply_random_theme,
|
| 545 |
+
outputs=[themer, theme_status],
|
| 546 |
+
show_api=False, # Hide from API docs but keep UI functional
|
| 547 |
+
)
|
| 548 |
+
|
| 549 |
+
convert_btn.click(
|
| 550 |
+
fn=convert_css_to_theme_json_ai,
|
| 551 |
+
inputs=[css_input, token_input, model_selector],
|
| 552 |
+
outputs=[json_output],
|
| 553 |
+
show_api=False, # Hide from API docs but keep UI functional
|
| 554 |
+
)
|
| 555 |
+
|
| 556 |
+
|
| 557 |
+
# Launch the demo
|
| 558 |
+
if __name__ == "__main__":
|
| 559 |
+
# Configure for Google Fonts support and MCP server (as per GRADIO_MCP_HF_SPACES_GUIDE.md)
|
| 560 |
+
demo.launch(
|
| 561 |
+
mcp_server=True, # CRITICAL: Enable MCP server functionality
|
| 562 |
+
server_name="0.0.0.0",
|
| 563 |
+
server_port=7860,
|
| 564 |
+
share=False,
|
| 565 |
+
debug=True,
|
| 566 |
+
# Allow external resources like Google Fonts
|
| 567 |
+
allowed_paths=["./"],
|
| 568 |
+
)
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Requirements for MCP Server - HF Spaces Deployment
|
| 2 |
+
gradio>=5.31.0
|
| 3 |
+
gradio-themer>=0.1.0
|
| 4 |
+
requests>=2.25.0
|
| 5 |
+
transformers>=4.30.0
|
| 6 |
+
torch>=2.0.0
|
| 7 |
+
accelerate>=0.20.0
|
| 8 |
+
sentencepiece>=0.1.95
|
user_themes.json
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"themes": {
|
| 3 |
+
"dark_emerald": {
|
| 4 |
+
"name": "Dark Emerald",
|
| 5 |
+
"colors": {
|
| 6 |
+
"base-100": "#064e3b",
|
| 7 |
+
"base-200": "#065f46",
|
| 8 |
+
"base-300": "#047857",
|
| 9 |
+
"base-content": "#d1fae5",
|
| 10 |
+
"primary": "#10b981",
|
| 11 |
+
"primary-content": "#ffffff",
|
| 12 |
+
"secondary": "#6b7280",
|
| 13 |
+
"secondary-content": "#ffffff",
|
| 14 |
+
"accent": "#34d399",
|
| 15 |
+
"accent-content": "#000000",
|
| 16 |
+
"neutral": "#374151",
|
| 17 |
+
"neutral-content": "#ffffff",
|
| 18 |
+
"error": "#ef4444",
|
| 19 |
+
"error-content": "#ffffff"
|
| 20 |
+
},
|
| 21 |
+
"background": "#022c22",
|
| 22 |
+
"font": {
|
| 23 |
+
"family": "Helvetica",
|
| 24 |
+
"type": "system_font",
|
| 25 |
+
"name": "Helvetica"
|
| 26 |
+
}
|
| 27 |
+
},
|
| 28 |
+
"ocean_breeze": {
|
| 29 |
+
"name": "Ocean Breeze",
|
| 30 |
+
"colors": {
|
| 31 |
+
"base-100": "#f0f9ff",
|
| 32 |
+
"base-200": "#e0f2fe",
|
| 33 |
+
"base-300": "#bae6fd",
|
| 34 |
+
"base-content": "#0c4a6e",
|
| 35 |
+
"primary": "#0ea5e9",
|
| 36 |
+
"primary-content": "#ffffff",
|
| 37 |
+
"secondary": "#64748b",
|
| 38 |
+
"secondary-content": "#ffffff",
|
| 39 |
+
"accent": "#06b6d4",
|
| 40 |
+
"accent-content": "#ffffff",
|
| 41 |
+
"neutral": "#374151",
|
| 42 |
+
"neutral-content": "#ffffff",
|
| 43 |
+
"error": "#ef4444",
|
| 44 |
+
"error-content": "#ffffff"
|
| 45 |
+
},
|
| 46 |
+
"background": "#bae6fd",
|
| 47 |
+
"font": {
|
| 48 |
+
"family": "Helvetica",
|
| 49 |
+
"type": "system_font",
|
| 50 |
+
"name": "Helvetica"
|
| 51 |
+
}
|
| 52 |
+
},
|
| 53 |
+
"sunset_orange": {
|
| 54 |
+
"name": "Sunset Orange",
|
| 55 |
+
"colors": {
|
| 56 |
+
"base-100": "#fff7ed",
|
| 57 |
+
"base-200": "#ffedd5",
|
| 58 |
+
"base-300": "#fed7aa",
|
| 59 |
+
"base-content": "#9a3412",
|
| 60 |
+
"primary": "#ea580c",
|
| 61 |
+
"primary-content": "#ffffff",
|
| 62 |
+
"secondary": "#64748b",
|
| 63 |
+
"secondary-content": "#ffffff",
|
| 64 |
+
"accent": "#f59e0b",
|
| 65 |
+
"accent-content": "#ffffff",
|
| 66 |
+
"neutral": "#374151",
|
| 67 |
+
"neutral-content": "#ffffff",
|
| 68 |
+
"error": "#ef4444",
|
| 69 |
+
"error-content": "#ffffff"
|
| 70 |
+
},
|
| 71 |
+
"background": "#fed7aa",
|
| 72 |
+
"font": {
|
| 73 |
+
"family": "Verdana",
|
| 74 |
+
"type": "system_font",
|
| 75 |
+
"name": "Verdana"
|
| 76 |
+
}
|
| 77 |
+
},
|
| 78 |
+
"forest_green": {
|
| 79 |
+
"name": "Forest Green",
|
| 80 |
+
"colors": {
|
| 81 |
+
"base-100": "#f0fdf4",
|
| 82 |
+
"base-200": "#dcfce7",
|
| 83 |
+
"base-300": "#bbf7d0",
|
| 84 |
+
"base-content": "#14532d",
|
| 85 |
+
"primary": "#16a34a",
|
| 86 |
+
"primary-content": "#ffffff",
|
| 87 |
+
"secondary": "#64748b",
|
| 88 |
+
"secondary-content": "#ffffff",
|
| 89 |
+
"accent": "#22c55e",
|
| 90 |
+
"accent-content": "#ffffff",
|
| 91 |
+
"neutral": "#374151",
|
| 92 |
+
"neutral-content": "#ffffff",
|
| 93 |
+
"error": "#ef4444",
|
| 94 |
+
"error-content": "#ffffff"
|
| 95 |
+
},
|
| 96 |
+
"background": "#bbf7d0",
|
| 97 |
+
"font": {
|
| 98 |
+
"family": "Times New Roman",
|
| 99 |
+
"type": "system_font",
|
| 100 |
+
"name": "Times New Roman"
|
| 101 |
+
}
|
| 102 |
+
},
|
| 103 |
+
"royal_purple": {
|
| 104 |
+
"name": "Royal Purple",
|
| 105 |
+
"colors": {
|
| 106 |
+
"base-100": "#faf5ff",
|
| 107 |
+
"base-200": "#f3e8ff",
|
| 108 |
+
"base-300": "#e9d5ff",
|
| 109 |
+
"base-content": "#581c87",
|
| 110 |
+
"primary": "#9333ea",
|
| 111 |
+
"primary-content": "#ffffff",
|
| 112 |
+
"secondary": "#64748b",
|
| 113 |
+
"secondary-content": "#ffffff",
|
| 114 |
+
"accent": "#a855f7",
|
| 115 |
+
"accent-content": "#ffffff",
|
| 116 |
+
"neutral": "#374151",
|
| 117 |
+
"neutral-content": "#ffffff",
|
| 118 |
+
"error": "#ef4444",
|
| 119 |
+
"error-content": "#ffffff"
|
| 120 |
+
},
|
| 121 |
+
"background": "#e9d5ff",
|
| 122 |
+
"font": {
|
| 123 |
+
"family": "Tahoma",
|
| 124 |
+
"type": "system_font",
|
| 125 |
+
"name": "Tahoma"
|
| 126 |
+
}
|
| 127 |
+
},
|
| 128 |
+
"midnight_blue": {
|
| 129 |
+
"name": "Midnight Blue",
|
| 130 |
+
"colors": {
|
| 131 |
+
"base-100": "#1e293b",
|
| 132 |
+
"base-200": "#334155",
|
| 133 |
+
"base-300": "#475569",
|
| 134 |
+
"base-content": "#f1f5f9",
|
| 135 |
+
"primary": "#3b82f6",
|
| 136 |
+
"primary-content": "#ffffff",
|
| 137 |
+
"secondary": "#64748b",
|
| 138 |
+
"secondary-content": "#ffffff",
|
| 139 |
+
"accent": "#06b6d4",
|
| 140 |
+
"accent-content": "#ffffff",
|
| 141 |
+
"neutral": "#374151",
|
| 142 |
+
"neutral-content": "#ffffff",
|
| 143 |
+
"error": "#ef4444",
|
| 144 |
+
"error-content": "#ffffff"
|
| 145 |
+
},
|
| 146 |
+
"background": "#0f172a",
|
| 147 |
+
"font": {
|
| 148 |
+
"family": "Courier New",
|
| 149 |
+
"type": "system_font",
|
| 150 |
+
"name": "Courier New"
|
| 151 |
+
}
|
| 152 |
+
},
|
| 153 |
+
"warm_beige": {
|
| 154 |
+
"name": "Warm Beige",
|
| 155 |
+
"colors": {
|
| 156 |
+
"base-100": "#fef7f0",
|
| 157 |
+
"base-200": "#fde8d7",
|
| 158 |
+
"base-300": "#fbd5ae",
|
| 159 |
+
"base-content": "#92400e",
|
| 160 |
+
"primary": "#d97706",
|
| 161 |
+
"primary-content": "#ffffff",
|
| 162 |
+
"secondary": "#64748b",
|
| 163 |
+
"secondary-content": "#ffffff",
|
| 164 |
+
"accent": "#f59e0b",
|
| 165 |
+
"accent-content": "#ffffff",
|
| 166 |
+
"neutral": "#374151",
|
| 167 |
+
"neutral-content": "#ffffff",
|
| 168 |
+
"error": "#ef4444",
|
| 169 |
+
"error-content": "#ffffff"
|
| 170 |
+
},
|
| 171 |
+
"background": "#f7e6d3",
|
| 172 |
+
"font": {
|
| 173 |
+
"family": "Georgia",
|
| 174 |
+
"type": "system_font",
|
| 175 |
+
"name": "Georgia"
|
| 176 |
+
}
|
| 177 |
+
},
|
| 178 |
+
"dark_obsidian": {
|
| 179 |
+
"name": "Dark Obsidian",
|
| 180 |
+
"colors": {
|
| 181 |
+
"base-100": "#0f0f23",
|
| 182 |
+
"base-200": "#1a1a2e",
|
| 183 |
+
"base-300": "#16213e",
|
| 184 |
+
"base-content": "#e2e8f0",
|
| 185 |
+
"primary": "#8b5cf6",
|
| 186 |
+
"primary-content": "#ffffff",
|
| 187 |
+
"secondary": "#64748b",
|
| 188 |
+
"secondary-content": "#ffffff",
|
| 189 |
+
"accent": "#06b6d4",
|
| 190 |
+
"accent-content": "#ffffff",
|
| 191 |
+
"neutral": "#374151",
|
| 192 |
+
"neutral-content": "#ffffff",
|
| 193 |
+
"error": "#ef4444",
|
| 194 |
+
"error-content": "#ffffff"
|
| 195 |
+
},
|
| 196 |
+
"background": "#0a0a1a",
|
| 197 |
+
"font": {
|
| 198 |
+
"family": "Arial",
|
| 199 |
+
"type": "system_font",
|
| 200 |
+
"name": "Arial"
|
| 201 |
+
}
|
| 202 |
+
},
|
| 203 |
+
"dark_crimson": {
|
| 204 |
+
"name": "Dark Crimson",
|
| 205 |
+
"colors": {
|
| 206 |
+
"base-100": "#450a0a",
|
| 207 |
+
"base-200": "#7f1d1d",
|
| 208 |
+
"base-300": "#991b1b",
|
| 209 |
+
"base-content": "#fecaca",
|
| 210 |
+
"primary": "#dc2626",
|
| 211 |
+
"primary-content": "#ffffff",
|
| 212 |
+
"secondary": "#6b7280",
|
| 213 |
+
"secondary-content": "#ffffff",
|
| 214 |
+
"accent": "#f87171",
|
| 215 |
+
"accent-content": "#000000",
|
| 216 |
+
"neutral": "#374151",
|
| 217 |
+
"neutral-content": "#ffffff",
|
| 218 |
+
"error": "#ef4444",
|
| 219 |
+
"error-content": "#ffffff"
|
| 220 |
+
},
|
| 221 |
+
"background": "#1f0404",
|
| 222 |
+
"font": {
|
| 223 |
+
"family": "Verdana",
|
| 224 |
+
"type": "system_font",
|
| 225 |
+
"name": "Verdana"
|
| 226 |
+
}
|
| 227 |
+
},
|
| 228 |
+
"dark_slate": {
|
| 229 |
+
"name": "Dark Slate",
|
| 230 |
+
"colors": {
|
| 231 |
+
"base-100": "#1e293b",
|
| 232 |
+
"base-200": "#334155",
|
| 233 |
+
"base-300": "#475569",
|
| 234 |
+
"base-content": "#f1f5f9",
|
| 235 |
+
"primary": "#3b82f6",
|
| 236 |
+
"primary-content": "#ffffff",
|
| 237 |
+
"secondary": "#64748b",
|
| 238 |
+
"secondary-content": "#ffffff",
|
| 239 |
+
"accent": "#06b6d4",
|
| 240 |
+
"accent-content": "#ffffff",
|
| 241 |
+
"neutral": "#374151",
|
| 242 |
+
"neutral-content": "#ffffff",
|
| 243 |
+
"error": "#ef4444",
|
| 244 |
+
"error-content": "#ffffff"
|
| 245 |
+
},
|
| 246 |
+
"background": "#0f172a",
|
| 247 |
+
"font": {
|
| 248 |
+
"family": "Tahoma",
|
| 249 |
+
"type": "system_font",
|
| 250 |
+
"name": "Tahoma"
|
| 251 |
+
}
|
| 252 |
+
},
|
| 253 |
+
"dark_carbon": {
|
| 254 |
+
"name": "Generated Theme",
|
| 255 |
+
"colors": {
|
| 256 |
+
"base-100": "#FFF5DC",
|
| 257 |
+
"base-200": "#FEF3C7",
|
| 258 |
+
"base-300": "#FDE68A",
|
| 259 |
+
"base-content": "#1F2937",
|
| 260 |
+
"primary": "#4F46E5",
|
| 261 |
+
"primary-content": "#FFFFFF",
|
| 262 |
+
"secondary": "#8B5CF6",
|
| 263 |
+
"secondary-content": "#FFFFFF",
|
| 264 |
+
"accent": "#EAB308",
|
| 265 |
+
"accent-content": "#000000",
|
| 266 |
+
"neutral": "#374151",
|
| 267 |
+
"neutral-content": "#FFFFFF",
|
| 268 |
+
"error": "#EF4444",
|
| 269 |
+
"error-content": "#FFFFFF"
|
| 270 |
+
},
|
| 271 |
+
"background": "#F3E8FF"
|
| 272 |
+
},
|
| 273 |
+
"dark_violet": {
|
| 274 |
+
"name": "Dark Violet",
|
| 275 |
+
"colors": {
|
| 276 |
+
"base-100": "#2e1065",
|
| 277 |
+
"base-200": "#3730a3",
|
| 278 |
+
"base-300": "#4338ca",
|
| 279 |
+
"base-content": "#e0e7ff",
|
| 280 |
+
"primary": "#8b5cf6",
|
| 281 |
+
"primary-content": "#ffffff",
|
| 282 |
+
"secondary": "#6b7280",
|
| 283 |
+
"secondary-content": "#ffffff",
|
| 284 |
+
"accent": "#a78bfa",
|
| 285 |
+
"accent-content": "#000000",
|
| 286 |
+
"neutral": "#374151",
|
| 287 |
+
"neutral-content": "#ffffff",
|
| 288 |
+
"error": "#ef4444",
|
| 289 |
+
"error-content": "#ffffff"
|
| 290 |
+
},
|
| 291 |
+
"background": "#1e1b4b",
|
| 292 |
+
"font": {
|
| 293 |
+
"family": "Georgia",
|
| 294 |
+
"type": "system_font",
|
| 295 |
+
"name": "Georgia"
|
| 296 |
+
}
|
| 297 |
+
},
|
| 298 |
+
"hacker_matrix": {
|
| 299 |
+
"name": "Dark Theme",
|
| 300 |
+
"colors": {
|
| 301 |
+
"base-100": "#2d3748",
|
| 302 |
+
"base-200": "#1a1d23",
|
| 303 |
+
"base-300": "#0f172a",
|
| 304 |
+
"base-content": "#ffffff",
|
| 305 |
+
"primary": "#6c5ce7",
|
| 306 |
+
"primary-content": "#ffffff",
|
| 307 |
+
"secondary": "#4d5d7e",
|
| 308 |
+
"secondary-content": "#ffffff",
|
| 309 |
+
"accent": "#f7dc6f",
|
| 310 |
+
"accent-content": "#000000",
|
| 311 |
+
"neutral": "#3b3f4e",
|
| 312 |
+
"neutral-content": "#ffffff",
|
| 313 |
+
"error": "#f87171",
|
| 314 |
+
"error-content": "#ffffff"
|
| 315 |
+
},
|
| 316 |
+
"background": "#2c3e50"
|
| 317 |
+
}
|
| 318 |
+
},
|
| 319 |
+
"default_theme": "ocean_breeze",
|
| 320 |
+
"default_font": "Helvetica"
|
| 321 |
+
}
|