Rahul-Samedavar commited on
Commit
5628f48
·
1 Parent(s): 35b28aa

ready for demo

Browse files
Files changed (14) hide show
  1. .env.example +29 -0
  2. .gitignore +5 -0
  3. Dockerfile +13 -0
  4. asset/.gitkeep +0 -0
  5. config.py +124 -0
  6. file_processor.py +199 -0
  7. main.py +283 -0
  8. model_managers.py +307 -0
  9. models.py +35 -0
  10. prompts.py +99 -0
  11. requirements.txt +10 -0
  12. static/index.html +549 -0
  13. static/script.js +1084 -0
  14. static/style.css +1371 -0
.env.example ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # Instructions:
3
+ # 1. Rename this file to `.env`
4
+ # 2. Fill in the required values below.
5
+ # 3. Keep the .env file private and do not commit it to version control.
6
+ # -----------------------------------------------------------------------------
7
+
8
+ # --- Primary AI Model (Gemini) ---
9
+ # Your Google AI Studio (Gemini) API keys.
10
+ # You can provide multiple keys separated by commas for automatic rotation and fallback.
11
+ # Example: GEMINI_API_KEYS=key1,key2,key3
12
+ GEMINI_API_KEYS="YOUR_GEMINI_API_KEY_HERE"
13
+
14
+ # The name of the primary Gemini model to use.
15
+ # Defaults to "gemini-1.5-flash-latest" if not set.
16
+ PRIMARY_AI_MODEL_NAME="gemini-1.5-flash-latest"
17
+
18
+
19
+ # --- Fallback AI Model (Requesty Router) ---
20
+ # This model is used if all Gemini keys fail.
21
+ # You MUST provide a Requesty API key. Get one from requesty.ai
22
+ REQUESTY_API_KEY="YOUR_REQUESTY_API_KEY_HERE"
23
+
24
+ # The name of the fallback AI model to use via the Requesty router.
25
+ # Defaults to "gemini-1.5-pro-latest" if not set.
26
+ AI_MODEL_NAME="coding/gemini-2.5-flash"
27
+
28
+
29
+ CORS_ALLOW_ORIGINS="*"
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ __pycache__
2
+ env
3
+ .env
4
+ static/script.js.txt
5
+ test*
Dockerfile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9
2
+
3
+ RUN useradd -m -u 1000 user
4
+ USER user
5
+ ENV PATH="/home/user/.local/bin:$PATH"
6
+
7
+ WORKDIR /app
8
+
9
+ COPY --chown=user ./requirements.txt requirements.txt
10
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
11
+
12
+ COPY --chown=user . /app
13
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
asset/.gitkeep ADDED
File without changes
config.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration management for the AI Web Visualization Generator.
3
+
4
+ This module handles all application settings using Pydantic for validation
5
+ and type safety. Settings are loaded from environment variables and .env files.
6
+ """
7
+
8
+ from pathlib import Path
9
+ from typing import List
10
+
11
+ from pydantic import Field, field_validator
12
+ from pydantic_settings import BaseSettings, SettingsConfigDict
13
+
14
+
15
+ class AppSettings(BaseSettings):
16
+ """
17
+ Manages application settings using Pydantic for validation.
18
+
19
+ All settings can be configured via environment variables or a .env file.
20
+ Settings are validated at startup to ensure proper configuration.
21
+
22
+ Attributes:
23
+ primary_model_name: Name of the primary AI model (default: Gemini Flash)
24
+ gemini_api_keys: Comma-separated list of Gemini API keys
25
+ fallback_model_name: Name of the fallback AI model (default: Gemini Pro)
26
+ requesty_api_key: API key for Requesty service (required)
27
+ requesty_site_url: URL of the site using Requesty
28
+ requesty_site_name: Name of the site for Requesty headers
29
+ cors_allow_origins: Comma-separated list of allowed CORS origins
30
+ static_dir: Directory containing static frontend files
31
+ """
32
+
33
+ model_config = SettingsConfigDict(
34
+ env_file='.env',
35
+ env_file_encoding='utf-8',
36
+ extra='ignore'
37
+ )
38
+
39
+ # AI Model Configuration
40
+ primary_model_name: str = Field(
41
+ default="gemini-1.5-flash-latest",
42
+ alias="PRIMARY_AI_MODEL_NAME",
43
+ description="Primary AI model to use for generation"
44
+ )
45
+
46
+ gemini_api_keys: str = Field(
47
+ default="",
48
+ alias="GEMINI_API_KEYS",
49
+ description="Comma-separated list of Gemini API keys"
50
+ )
51
+
52
+ fallback_model_name: str = Field(
53
+ default="gemini-1.5-pro-latest",
54
+ alias="AI_MODEL_NAME",
55
+ description="Fallback AI model name"
56
+ )
57
+
58
+ # Requesty Configuration
59
+ requesty_api_key: str = Field(
60
+ ...,
61
+ alias="REQUESTY_API_KEY",
62
+ description="API key for Requesty service (required)"
63
+ )
64
+
65
+ requesty_site_url: str = Field(
66
+ default="",
67
+ alias="REQUESTY_SITE_URL",
68
+ description="Site URL for Requesty headers"
69
+ )
70
+
71
+ requesty_site_name: str = Field(
72
+ default="AI Visualization Generator",
73
+ alias="REQUESTY_SITE_NAME",
74
+ description="Site name for Requesty headers"
75
+ )
76
+
77
+ # Server Configuration
78
+ cors_allow_origins: str = Field(
79
+ default="*",
80
+ alias="CORS_ALLOW_ORIGINS",
81
+ description="Comma-separated list of allowed CORS origins"
82
+ )
83
+
84
+ static_dir: Path = Field(
85
+ default=Path("static"),
86
+ alias="STATIC_DIR",
87
+ description="Directory containing static frontend files"
88
+ )
89
+
90
+ @property
91
+ def index_file(self) -> Path:
92
+ """Path to the main index.html file."""
93
+ return self.static_dir / "index.html"
94
+
95
+ @property
96
+ def gemini_api_keys_list(self) -> List[str]:
97
+ """
98
+ Parse comma-separated Gemini API keys into a list.
99
+
100
+ Returns:
101
+ List of API keys, empty list if none provided
102
+ """
103
+ if isinstance(self.gemini_api_keys, str):
104
+ return [key.strip() for key in self.gemini_api_keys.split(',') if key.strip()]
105
+ return []
106
+
107
+
108
+ def load_settings() -> AppSettings:
109
+ """
110
+ Load and validate application settings.
111
+
112
+ Returns:
113
+ AppSettings: Validated application settings
114
+
115
+ Raises:
116
+ RuntimeError: If configuration is invalid or missing required values
117
+ """
118
+ try:
119
+ return AppSettings()
120
+ except Exception as e:
121
+ raise RuntimeError(
122
+ f"FATAL: Configuration error. Is your .env file set up correctly? "
123
+ f"Details: {e}"
124
+ )
file_processor.py ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ File processing utilities for the AI Web Visualization Generator.
3
+
4
+ This module handles the processing of various file types (text, PDF, CSV, Excel)
5
+ uploaded by users and converts them into text descriptions suitable for LLM prompts.
6
+ """
7
+
8
+ import io
9
+ from pathlib import Path
10
+ from typing import List
11
+
12
+ import pandas as pd
13
+ from fastapi import UploadFile
14
+ from pypdf import PdfReader
15
+
16
+
17
+ class FileProcessor:
18
+ """
19
+ Processes uploaded files and extracts their content for LLM prompts.
20
+
21
+ Supports multiple file formats including text files, PDFs, CSVs, and Excel files.
22
+ Binary files (images, audio, etc.) are identified but not processed.
23
+ """
24
+
25
+ # Maximum content length to include in prompt (to avoid huge prompts)
26
+ MAX_CONTENT_LENGTH = 4000
27
+
28
+ # Supported text-based file extensions
29
+ TEXT_EXTENSIONS = {'.txt', '.md', '.py', '.js', '.html', '.css', '.json'}
30
+
31
+ # Supported spreadsheet extensions
32
+ EXCEL_EXTENSIONS = {'.xlsx', '.xls'}
33
+
34
+ async def process_uploaded_files(self, files: List[UploadFile]) -> str:
35
+ """
36
+ Process multiple uploaded files and create a comprehensive text description.
37
+
38
+ Args:
39
+ files: List of uploaded files to process
40
+
41
+ Returns:
42
+ str: Formatted text description of all file contents
43
+ """
44
+ if not files:
45
+ return "No files were provided."
46
+
47
+ file_contexts = []
48
+
49
+ for file in files:
50
+ context = await self._process_single_file(file)
51
+ file_contexts.append(context)
52
+
53
+ return (
54
+ "The user has provided the following files. "
55
+ "Use their content as context for your response:\n\n"
56
+ + "\n\n".join(file_contexts)
57
+ )
58
+
59
+ async def _process_single_file(self, file: UploadFile) -> str:
60
+ """
61
+ Process a single uploaded file.
62
+
63
+ Args:
64
+ file: The file to process
65
+
66
+ Returns:
67
+ str: Formatted description of the file content
68
+ """
69
+ file_description = f"--- START OF FILE: {file.filename} ---"
70
+ content_summary = (
71
+ "Content: This is a binary file (e.g., image, audio). "
72
+ "It cannot be displayed as text but should be referenced in the "
73
+ "code by its filename."
74
+ )
75
+
76
+ file_extension = Path(file.filename).suffix.lower()
77
+
78
+ try:
79
+ content_bytes = await file.read()
80
+ content_summary = await self._extract_content(
81
+ content_bytes,
82
+ file_extension,
83
+ file.filename
84
+ )
85
+ except Exception as e:
86
+ print(f"Could not process file {file.filename}: {e}")
87
+ # Keep the default binary file message
88
+ finally:
89
+ await file.seek(0) # Reset file pointer for potential reuse
90
+
91
+ return (
92
+ f"{file_description}\n"
93
+ f"{content_summary}\n"
94
+ f"--- END OF FILE: {file.filename} ---"
95
+ )
96
+
97
+ async def _extract_content(
98
+ self,
99
+ content_bytes: bytes,
100
+ file_extension: str,
101
+ filename: str
102
+ ) -> str:
103
+ """
104
+ Extract text content from file bytes based on file type.
105
+
106
+ Args:
107
+ content_bytes: Raw file content
108
+ file_extension: File extension (e.g., '.pdf', '.csv')
109
+ filename: Original filename
110
+
111
+ Returns:
112
+ str: Extracted and possibly truncated content
113
+ """
114
+ content = None
115
+
116
+ # Text-based files
117
+ if file_extension in self.TEXT_EXTENSIONS:
118
+ content = content_bytes.decode('utf-8', errors='replace')
119
+
120
+ # CSV files
121
+ elif file_extension == '.csv':
122
+ content = self._process_csv(content_bytes)
123
+
124
+ # PDF files
125
+ elif file_extension == '.pdf':
126
+ content = self._process_pdf(content_bytes)
127
+
128
+ # Excel files
129
+ elif file_extension in self.EXCEL_EXTENSIONS:
130
+ content = self._process_excel(content_bytes)
131
+
132
+ # If no specific handler, return default message
133
+ if content is None:
134
+ return (
135
+ "Content: This is a binary file (e.g., image, audio). "
136
+ "It cannot be displayed as text but should be referenced in the "
137
+ "code by its filename."
138
+ )
139
+
140
+ # Truncate if necessary
141
+ if len(content) > self.MAX_CONTENT_LENGTH:
142
+ content = content[:self.MAX_CONTENT_LENGTH] + "\n... (content truncated)"
143
+
144
+ return content
145
+
146
+ def _process_csv(self, content_bytes: bytes) -> str:
147
+ """
148
+ Process CSV file content.
149
+
150
+ Args:
151
+ content_bytes: Raw CSV file bytes
152
+
153
+ Returns:
154
+ str: CSV content as text
155
+ """
156
+ df = pd.read_csv(io.BytesIO(content_bytes))
157
+ return "File content represented as CSV:\n" + df.to_csv(index=False)
158
+
159
+ def _process_pdf(self, content_bytes: bytes) -> str:
160
+ """
161
+ Process PDF file content and extract text.
162
+
163
+ Args:
164
+ content_bytes: Raw PDF file bytes
165
+
166
+ Returns:
167
+ str: Extracted text from all PDF pages
168
+ """
169
+ reader = PdfReader(io.BytesIO(content_bytes))
170
+ text_parts = [
171
+ page.extract_text()
172
+ for page in reader.pages
173
+ if page.extract_text()
174
+ ]
175
+ return "Extracted text from PDF:\n" + "\n".join(text_parts)
176
+
177
+ def _process_excel(self, content_bytes: bytes) -> str:
178
+ """
179
+ Process Excel file content.
180
+
181
+ Args:
182
+ content_bytes: Raw Excel file bytes
183
+
184
+ Returns:
185
+ str: Content from all sheets as CSV format
186
+ """
187
+ xls = pd.ExcelFile(io.BytesIO(content_bytes))
188
+ text_parts = []
189
+
190
+ for sheet_name in xls.sheet_names:
191
+ df = pd.read_excel(xls, sheet_name=sheet_name)
192
+ text_parts.append(
193
+ f"Sheet: '{sheet_name}'\n{df.to_csv(index=False)}"
194
+ )
195
+
196
+ return (
197
+ "File content represented as CSV for each sheet:\n"
198
+ + "\n\n".join(text_parts)
199
+ )
main.py ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Main application entry point for the AI Web Visualization Generator.
3
+
4
+ This FastAPI application provides endpoints for generating, modifying, and
5
+ explaining HTML visualizations using AI models. It supports file uploads,
6
+ streaming responses, and automatic fallback between AI providers.
7
+ """
8
+
9
+ import io
10
+ import zipfile
11
+ from typing import List
12
+
13
+ from fastapi import FastAPI, Request, Form, UploadFile, File
14
+ from fastapi.middleware.cors import CORSMiddleware
15
+ from fastapi.responses import HTMLResponse, StreamingResponse, PlainTextResponse
16
+ from fastapi.staticfiles import StaticFiles
17
+
18
+ from config import load_settings
19
+ from file_processor import FileProcessor
20
+ from model_managers import MultiModelManager
21
+ from models import ExplainRequest
22
+ from prompts import (
23
+ INSTRUCTIONS_FORMAT,
24
+ PROMPT_GENERATE,
25
+ PROMPT_MODIFY,
26
+ PROMPT_FOLLOW_UP
27
+ )
28
+
29
+
30
+ # --- Application Setup ---
31
+
32
+ # Load configuration
33
+ settings = load_settings()
34
+
35
+ # Initialize FastAPI application
36
+ app = FastAPI(
37
+ title="AI Web Visualization Generator",
38
+ version="3.3.0",
39
+ description="Generate and modify HTML visualizations using AI"
40
+ )
41
+
42
+ # Configure CORS
43
+ app.add_middleware(
44
+ CORSMiddleware,
45
+ allow_origins=settings.cors_allow_origins.split(","),
46
+ allow_credentials=True,
47
+ allow_methods=["*"],
48
+ allow_headers=["*"],
49
+ )
50
+
51
+ # Initialize services
52
+ model_manager = MultiModelManager(settings)
53
+ file_processor = FileProcessor()
54
+
55
+ # Mount static files if directory exists
56
+ if settings.static_dir.exists() and settings.static_dir.is_dir():
57
+ app.mount(
58
+ "/static",
59
+ StaticFiles(directory=str(settings.static_dir)),
60
+ name="static"
61
+ )
62
+
63
+
64
+ # --- API Endpoints ---
65
+
66
+ @app.get("/", response_class=HTMLResponse)
67
+ async def read_root():
68
+ """
69
+ Serve the main frontend page.
70
+
71
+ Returns the index.html file if it exists, otherwise returns
72
+ a basic HTML page indicating the static frontend is missing.
73
+ """
74
+ if settings.index_file.exists():
75
+ return HTMLResponse(
76
+ content=settings.index_file.read_text(encoding="utf-8")
77
+ )
78
+
79
+ return HTMLResponse(
80
+ "<h1>AI Visualization Generator Backend</h1>"
81
+ "<p>Static frontend not found.</p>"
82
+ )
83
+
84
+
85
+ @app.get("/healthz", response_class=PlainTextResponse)
86
+ async def healthz():
87
+ """
88
+ Health check endpoint.
89
+
90
+ Returns a simple "ok" response to indicate the service is running.
91
+ Used by container orchestration systems and load balancers.
92
+ """
93
+ return PlainTextResponse("ok")
94
+
95
+
96
+ @app.post("/generate")
97
+ async def generate_visualization(
98
+ request: Request,
99
+ prompt: str = Form(..., description="User's request for visualization"),
100
+ files: List[UploadFile] = File(
101
+ default=[],
102
+ description="Optional files to include as context"
103
+ )
104
+ ):
105
+ """
106
+ Generate a new HTML visualization from a user prompt.
107
+
108
+ This endpoint processes the user's request and any uploaded files,
109
+ then streams back an AI-generated HTML visualization.
110
+
111
+ Args:
112
+ request: FastAPI request object
113
+ prompt: User's description of what to generate
114
+ files: Optional files to use as context (images, data, etc.)
115
+
116
+ Returns:
117
+ StreamingResponse: AI-generated HTML code with metadata
118
+ """
119
+ # Process uploaded files into context
120
+ file_context = await file_processor.process_uploaded_files(files)
121
+
122
+ # Build the complete prompt
123
+ final_prompt = PROMPT_GENERATE.format(
124
+ INSTRUCTIONS_FORMAT=INSTRUCTIONS_FORMAT,
125
+ user_prompt=prompt,
126
+ file_context=file_context
127
+ )
128
+
129
+ # Stream the response
130
+ return StreamingResponse(
131
+ model_manager.generate_content_streaming(final_prompt, request),
132
+ media_type="text/plain; charset=utf-8"
133
+ )
134
+
135
+
136
+ @app.post("/modify")
137
+ async def modify_visualization(
138
+ request: Request,
139
+ prompt: str = Form(..., description="Modification request"),
140
+ current_code: str = Form(..., description="Current HTML code"),
141
+ console_logs: str = Form(default="", description="Browser console logs"),
142
+ prompt_history: List[str] = Form(
143
+ default=[],
144
+ description="Previous prompts in conversation"
145
+ ),
146
+ files: List[UploadFile] = File(
147
+ default=[],
148
+ description="Optional new files to include"
149
+ )
150
+ ):
151
+ """
152
+ Modify an existing HTML visualization based on user feedback.
153
+
154
+ This endpoint takes the current code and a modification request,
155
+ along with conversation history and optional new files, then
156
+ streams back an updated version.
157
+
158
+ Args:
159
+ request: FastAPI request object
160
+ prompt: User's modification request
161
+ current_code: The current HTML code to modify
162
+ console_logs: Browser console logs for debugging context
163
+ prompt_history: List of previous prompts in the conversation
164
+ files: Optional new files to add to the project
165
+
166
+ Returns:
167
+ StreamingResponse: Modified HTML code with metadata
168
+ """
169
+ # Format conversation history
170
+ history_str = (
171
+ "\n".join(f"- {p}" for p in prompt_history)
172
+ if prompt_history
173
+ else "No history provided."
174
+ )
175
+
176
+ # Process uploaded files
177
+ file_context = await file_processor.process_uploaded_files(files)
178
+
179
+ # Build the complete prompt
180
+ final_prompt = PROMPT_MODIFY.format(
181
+ user_prompt=prompt,
182
+ current_code=current_code,
183
+ console_logs=console_logs or "No console logs provided.",
184
+ prompt_history=history_str,
185
+ file_context=file_context,
186
+ INSTRUCTIONS_FORMAT=INSTRUCTIONS_FORMAT,
187
+ )
188
+
189
+ # Stream the response
190
+ return StreamingResponse(
191
+ model_manager.generate_content_streaming(final_prompt, request),
192
+ media_type="text/plain; charset=utf-8"
193
+ )
194
+
195
+
196
+ @app.post("/explain")
197
+ async def explain_code(req: ExplainRequest, request: Request):
198
+ """
199
+ Explain or answer questions about existing code.
200
+
201
+ This endpoint allows users to ask questions about their visualization
202
+ without modifying it. The AI provides explanations and guidance based
203
+ on the current code.
204
+
205
+ Args:
206
+ req: Request containing the question and current code
207
+ request: FastAPI request object
208
+
209
+ Returns:
210
+ StreamingResponse: AI explanation in Markdown format
211
+ """
212
+ final_prompt = PROMPT_FOLLOW_UP.format(
213
+ user_question=req.question,
214
+ code_to_explain=req.current_code
215
+ )
216
+
217
+ return StreamingResponse(
218
+ model_manager.generate_content_streaming(final_prompt, request),
219
+ media_type="text/plain; charset=utf-8"
220
+ )
221
+
222
+
223
+ @app.post("/download_zip")
224
+ async def download_zip(
225
+ html_content: str = Form(..., description="HTML code to package"),
226
+ files: List[UploadFile] = File(
227
+ default=[],
228
+ description="Assets to include in the zip"
229
+ )
230
+ ):
231
+ """
232
+ Package HTML code and assets into a downloadable ZIP file.
233
+
234
+ Creates a ZIP archive containing the HTML file as index.html and
235
+ all uploaded assets in an 'assets' subdirectory, ready for deployment.
236
+
237
+ Args:
238
+ html_content: The complete HTML code
239
+ files: Asset files to include (images, data files, etc.)
240
+
241
+ Returns:
242
+ StreamingResponse: ZIP file download
243
+ """
244
+ # Create ZIP file in memory
245
+ zip_buffer = io.BytesIO()
246
+
247
+ with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
248
+ # Add HTML file
249
+ zip_file.writestr("index.html", html_content)
250
+
251
+ # Add asset files if provided
252
+ if files:
253
+ assets_dir_in_zip = "assets"
254
+ for file in files:
255
+ file_content = await file.read()
256
+ zip_file.writestr(
257
+ f"{assets_dir_in_zip}/{file.filename}",
258
+ file_content
259
+ )
260
+
261
+ # Reset buffer position for reading
262
+ zip_buffer.seek(0)
263
+
264
+ return StreamingResponse(
265
+ zip_buffer,
266
+ media_type="application/zip",
267
+ headers={
268
+ "Content-Disposition": "attachment; filename=prompt-lab-project.zip"
269
+ }
270
+ )
271
+
272
+
273
+ # --- Application Entry Point ---
274
+
275
+ if __name__ == "__main__":
276
+ import uvicorn
277
+
278
+ uvicorn.run(
279
+ "main:app",
280
+ host="0.0.0.0",
281
+ port=8000,
282
+ reload=True
283
+ )
model_managers.py ADDED
@@ -0,0 +1,307 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AI model management for the AI Web Visualization Generator.
3
+
4
+ This module handles interactions with multiple AI providers (Gemini and Requesty),
5
+ including API key rotation, fallback mechanisms, and streaming response generation.
6
+ """
7
+
8
+ import itertools
9
+ from typing import AsyncGenerator, List, Optional
10
+
11
+ import openai
12
+ import google.generativeai as genai
13
+ from fastapi import Request
14
+
15
+ from config import AppSettings
16
+
17
+
18
+ class GeminiModelManager:
19
+ """
20
+ Manages Gemini API interactions with support for multiple API keys.
21
+
22
+ This class handles API key rotation and provides streaming content generation
23
+ using Google's Generative AI models. If one API key fails, it automatically
24
+ tries the next available key.
25
+
26
+ Attributes:
27
+ model_name: Name of the Gemini model to use
28
+ keys: List of Gemini API keys for rotation
29
+ generation_config: Configuration for text generation
30
+ """
31
+
32
+ def __init__(self, config: AppSettings):
33
+ """
34
+ Initialize the Gemini model manager.
35
+
36
+ Args:
37
+ config: Application settings containing API keys and model name
38
+
39
+ Raises:
40
+ ValueError: If no Gemini API keys are provided
41
+ """
42
+ self.model_name = config.primary_model_name
43
+ self.keys = config.gemini_api_keys_list
44
+
45
+ if not self.keys:
46
+ raise ValueError(
47
+ "GeminiModelManager initialized but no GEMINI_API_KEYS were provided."
48
+ )
49
+
50
+ self.key_cycler = itertools.cycle(self.keys)
51
+ self.generation_config = genai.GenerationConfig(
52
+ temperature=0.7,
53
+ top_p=1,
54
+ top_k=1
55
+ )
56
+
57
+ print(
58
+ f"Gemini Manager initialized for model: {self.model_name} "
59
+ f"with {len(self.keys)} API key(s)."
60
+ )
61
+
62
+ async def generate_content_streaming_with_key(
63
+ self,
64
+ prompt: str,
65
+ api_key: str
66
+ ) -> AsyncGenerator[str, None]:
67
+ """
68
+ Generate streaming content using a specific API key.
69
+
70
+ Args:
71
+ prompt: The prompt to send to the model
72
+ api_key: The Gemini API key to use
73
+
74
+ Yields:
75
+ str: Chunks of generated text
76
+
77
+ Raises:
78
+ Exception: If the API call fails
79
+ """
80
+ genai.configure(api_key=api_key)
81
+ model = genai.GenerativeModel(self.model_name)
82
+
83
+ print(
84
+ f"[Gemini] Attempting generation with model {self.model_name} "
85
+ f"using key ending in ...{api_key[-4:]}"
86
+ )
87
+
88
+ stream = await model.generate_content_async(
89
+ prompt,
90
+ stream=True,
91
+ generation_config=self.generation_config
92
+ )
93
+
94
+ async for chunk in stream:
95
+ if chunk.text:
96
+ yield chunk.text
97
+
98
+ print(
99
+ f"[Gemini] Successfully generated response "
100
+ f"with key ending in ...{api_key[-4:]}"
101
+ )
102
+
103
+ async def try_all_keys_streaming(
104
+ self,
105
+ prompt: str
106
+ ) -> AsyncGenerator[str, None]:
107
+ """
108
+ Attempt to generate content using all available API keys.
109
+
110
+ Tries each API key in sequence until one succeeds. If all keys fail,
111
+ raises the last exception encountered.
112
+
113
+ Args:
114
+ prompt: The prompt to send to the model
115
+
116
+ Yields:
117
+ str: Chunks of generated text
118
+
119
+ Raises:
120
+ Exception: If all API keys fail
121
+ """
122
+ last_exception = None
123
+
124
+ for i, api_key in enumerate(self.keys):
125
+ try:
126
+ print(
127
+ f"[Gemini] Trying key {i+1}/{len(self.keys)} "
128
+ f"(ending in ...{api_key[-4:]})"
129
+ )
130
+
131
+ async for chunk in self.generate_content_streaming_with_key(
132
+ prompt,
133
+ api_key
134
+ ):
135
+ yield chunk
136
+
137
+ # If we got here, generation succeeded
138
+ return
139
+
140
+ except Exception as e:
141
+ last_exception = e
142
+ print(f"[Gemini] Key {i+1}/{len(self.keys)} failed: {str(e)}")
143
+ continue
144
+
145
+ # All keys failed
146
+ print(
147
+ f"[Gemini] All {len(self.keys)} API keys failed. "
148
+ f"Last error: {last_exception}"
149
+ )
150
+ raise last_exception or Exception("All Gemini API keys failed")
151
+
152
+
153
+ class RequestyModelManager:
154
+ """
155
+ Manages Requesty API interactions as a fallback provider.
156
+
157
+ This class provides a fallback mechanism when Gemini API is unavailable
158
+ or all API keys have been exhausted. It uses the Requesty router service
159
+ with OpenAI-compatible API.
160
+
161
+ Attributes:
162
+ model_name: Name of the model to use via Requesty
163
+ client: Async OpenAI client configured for Requesty
164
+ """
165
+
166
+ def __init__(self, config: AppSettings):
167
+ """
168
+ Initialize the Requesty model manager.
169
+
170
+ Args:
171
+ config: Application settings containing API key and site info
172
+ """
173
+ self.model_name = config.fallback_model_name
174
+
175
+ # Build headers for Requesty service
176
+ headers = {
177
+ "HTTP-Referer": config.requesty_site_url,
178
+ "X-Title": config.requesty_site_name
179
+ }
180
+
181
+ # Filter out empty header values
182
+ headers = {k: v for k, v in headers.items() if v}
183
+
184
+ self.client = openai.AsyncOpenAI(
185
+ api_key=config.requesty_api_key,
186
+ base_url="https://router.requesty.ai/v1",
187
+ default_headers=headers
188
+ )
189
+
190
+ print(f"Requesty Fallback Manager initialized for model: {self.model_name}")
191
+
192
+ async def generate_content_streaming(
193
+ self,
194
+ prompt: str,
195
+ request: Request
196
+ ) -> AsyncGenerator[str, None]:
197
+ """
198
+ Generate streaming content using Requesty API.
199
+
200
+ Args:
201
+ prompt: The prompt to send to the model
202
+ request: FastAPI request object (for disconnect detection)
203
+
204
+ Yields:
205
+ str: Chunks of generated text
206
+ """
207
+ print(f"[Requesty] Attempting generation with model {self.model_name}")
208
+
209
+ stream = await self.client.chat.completions.create(
210
+ model=self.model_name,
211
+ messages=[{"role": "user", "content": prompt}],
212
+ stream=True
213
+ )
214
+
215
+ async for chunk in stream:
216
+ # Check if client disconnected
217
+ if await request.is_disconnected():
218
+ print("[Requesty] Client disconnected. Cancelling stream.")
219
+ break
220
+
221
+ content = chunk.choices[0].delta.content
222
+ if content:
223
+ yield content
224
+
225
+ print("[Requesty] Successfully generated response.")
226
+
227
+
228
+ class MultiModelManager:
229
+ """
230
+ Orchestrates multiple AI model providers with fallback logic.
231
+
232
+ This class manages the coordination between primary (Gemini) and fallback
233
+ (Requesty) AI providers. It attempts to use Gemini first with all available
234
+ API keys, then falls back to Requesty if all Gemini attempts fail.
235
+
236
+ Attributes:
237
+ gemini_manager: Optional Gemini model manager
238
+ requesty_manager: Requesty model manager (always available)
239
+ """
240
+
241
+ def __init__(self, config: AppSettings):
242
+ """
243
+ Initialize the multi-model manager.
244
+
245
+ Args:
246
+ config: Application settings for all providers
247
+ """
248
+ self.gemini_manager: Optional[GeminiModelManager] = None
249
+
250
+ # Try to initialize Gemini manager if API keys are available
251
+ if config.gemini_api_keys_list:
252
+ try:
253
+ self.gemini_manager = GeminiModelManager(config)
254
+ except ValueError as e:
255
+ print(f"Warning: Could not initialize Gemini Manager. {e}")
256
+
257
+ # Always initialize Requesty as fallback
258
+ self.requesty_manager = RequestyModelManager(config)
259
+
260
+ async def generate_content_streaming(
261
+ self,
262
+ prompt: str,
263
+ request: Request
264
+ ) -> AsyncGenerator[str, None]:
265
+ """
266
+ Generate content with automatic fallback between providers.
267
+
268
+ First attempts to use Gemini with all available API keys. If all fail
269
+ or Gemini is not available, falls back to Requesty. Sends a special
270
+ [STREAM_RESTART] marker when switching providers.
271
+
272
+ Args:
273
+ prompt: The prompt to send to the model
274
+ request: FastAPI request object
275
+
276
+ Yields:
277
+ str: Chunks of generated text
278
+ """
279
+ # Try Gemini first if available
280
+ if self.gemini_manager:
281
+ try:
282
+ print(
283
+ "[Orchestrator] Attempting generation with all "
284
+ "available Gemini keys..."
285
+ )
286
+
287
+ async for chunk in self.gemini_manager.try_all_keys_streaming(prompt):
288
+ yield chunk
289
+
290
+ # If we got here, generation succeeded
291
+ return
292
+
293
+ except Exception as e:
294
+ print(
295
+ f"[Orchestrator] All Gemini keys failed with final error: {e}. "
296
+ f"Falling back to Requesty."
297
+ )
298
+ # Send restart marker to indicate provider switch
299
+ yield "[STREAM_RESTART]\n"
300
+
301
+ # Use Requesty fallback
302
+ print("[Orchestrator] Using fallback: Requesty.")
303
+ async for chunk in self.requesty_manager.generate_content_streaming(
304
+ prompt,
305
+ request
306
+ ):
307
+ yield chunk
models.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pydantic models for API request/response validation.
3
+
4
+ This module defines the data models used for API endpoints, ensuring
5
+ proper validation and documentation of request/response formats.
6
+ """
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ class ExplainRequest(BaseModel):
12
+ """
13
+ Request model for the /explain endpoint.
14
+
15
+ Used when users ask questions about existing code.
16
+
17
+ Attributes:
18
+ question: The user's question about the code
19
+ current_code: The HTML code to explain or discuss
20
+ """
21
+
22
+ question: str = Field(
23
+ ...,
24
+ min_length=1,
25
+ max_length=1000,
26
+ description="User's question about the code",
27
+ examples=["How does the animation work?"]
28
+ )
29
+
30
+ current_code: str = Field(
31
+ ...,
32
+ min_length=1,
33
+ description="The current HTML code for context",
34
+ examples=["<!DOCTYPE html><html>...</html>"]
35
+ )
prompts.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Prompt templates for the AI Web Visualization Generator.
3
+
4
+ This module contains all prompt templates used for generating, modifying,
5
+ and explaining code. Templates are formatted as immutable strings to ensure
6
+ consistency across the application.
7
+ """
8
+
9
+
10
+ INSTRUCTIONS_FORMAT = """
11
+ **YOU ARE A CODE GENERATOR. YOUR SOLE TASK IS TO GENERATE A COMPLETE HTML FILE IN THE FORMAT SPECIFIED BELOW. DO NOT DEVIATE.**
12
+
13
+ **CRITICAL: Response Format**
14
+ Your entire response MUST strictly follow this structure. Do not add any extra text or explanations outside of these sections.
15
+
16
+ 1. **[ANALYSIS]...[END_ANALYSIS]**: Explain your game idea and highlevel overview of how it will be implemented.
17
+ 2. **[CHANGES]...[END_CHANGES]**: List changes made. For the first generation, write "Initial generation."
18
+ 3. **[INSTRUCTIONS]...[END_INSTRUCTIONS]**: Write user-facing notes about how to *interact with the final webpage*.
19
+ 4. **HTML Code**: Immediately after `[END_INSTRUCTIONS]`, the complete HTML code MUST begin, starting with `<!DOCTYPE html>`.
20
+
21
+ **CRITICAL: Asset Handling**
22
+ - If the user provides assets (e.g., `heart.png`), you MUST reference them in your HTML using a relative path like `assets/heart.png`.
23
+ - **DO NOT** write instructions on how to save the file or create folders. The user's environment handles this automatically. Assume the `assets` folder exists.
24
+
25
+ **Here is a short example of a perfect response:**
26
+
27
+ [ANALYSIS]
28
+ The user wants a simple red square. I will create a div and style it with CSS inside the HTML file.
29
+ [END_ANALYSIS]
30
+ [CHANGES]
31
+ Initial generation.
32
+ [END_CHANGES]
33
+ [INSTRUCTIONS]
34
+ This is a simple red square. There is no interaction.
35
+ [END_INSTRUCTIONS]
36
+ <!DOCTYPE html>
37
+ <html>
38
+ <head><title>Red Square</title><style>div{width:100px;height:100px;background:red;}</style></head>
39
+ <body><div></div></body>
40
+ </html>
41
+ """.strip()
42
+
43
+
44
+ PROMPT_GENERATE = """
45
+ You are an expert web developer tasked with generating a complete, single-file HTML web page.
46
+ You must adhere to the formatting rules and instructions provided below.
47
+ You have creative freedom while designing and developing so make it functional, logical and maitaning asthetics but stick on to the format of response.
48
+
49
+
50
+ User's Request: "{user_prompt}"
51
+
52
+ File Context (assets provided by the user):
53
+ {file_context}
54
+ {INSTRUCTIONS_FORMAT}
55
+
56
+ Generate the complete response now.
57
+ """.strip()
58
+
59
+
60
+ PROMPT_MODIFY = """
61
+ You are an expert web developer tasked with modifying an existing HTML file based on the user's new request.
62
+ You must adhere to the formatting rules and instructions provided below.
63
+ You have creative freedom while designing and developing so make it functional, logical and maitaning asthetics but stick on to the format of response.
64
+
65
+ Conversation History:
66
+ {prompt_history}
67
+
68
+ Current Project Code:
69
+ ```html
70
+ {current_code}
71
+ ```
72
+
73
+ User's New Request:
74
+ "{user_prompt}"
75
+
76
+ File Context (new or existing assets provided by the user):
77
+ {file_context}
78
+ {INSTRUCTIONS_FORMAT}
79
+
80
+ Generate the complete and updated response now.
81
+ """.strip()
82
+
83
+
84
+ PROMPT_FOLLOW_UP = """
85
+ You are a versatile AI assistant and expert web developer. The user has an existing web application/visualization and is asking a follow-up question about it.
86
+ User's Question: "{user_question}"
87
+ The Current Code for Context:
88
+ ```html
89
+ {code_to_explain}
90
+ ```
91
+
92
+ Your Task:
93
+ - Analyze the user's question.
94
+ - Provide a concise answer without markdown format.
95
+ - Refer to specific parts of the code to make your answer concrete and helpful.
96
+ - Maintain a helpful, collaborative tone.
97
+
98
+ Generate your helpful response now.
99
+ """.strip()
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ python-dotenv
4
+ openai
5
+ pydantic-settings
6
+ google-generativeai
7
+ python-multipart
8
+ pypdf
9
+ openpyxl
10
+ pandas
static/index.html ADDED
@@ -0,0 +1,549 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>CodeCanvas - AI Code Generation Studio</title>
7
+ <link rel="stylesheet" href="static/style.css" />
8
+ <link
9
+ href="https://fonts.googleapis.com/css2?family=Work+Sans:wght@400;600;700&family=Inter:wght@300;400;500;600&display=swap"
10
+ rel="stylesheet"
11
+ />
12
+ <script type="module" src="https://md-block.verou.me/md-block.js"></script>
13
+ <link
14
+ rel="stylesheet"
15
+ href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
16
+ />
17
+ <!-- Removed Prism.js and added Monaco Editor -->
18
+ <script src="https://cdn.jsdelivr.net/npm/markdown-it@14.0.0/dist/markdown-it.min.js"></script>
19
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
20
+ <script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs/loader.js"></script>
21
+ </head>
22
+ <body>
23
+ <!-- Particle Background -->
24
+ <div class="particle-background">
25
+ <canvas id="particle-canvas"></canvas>
26
+ </div>
27
+
28
+ <!-- Header -->
29
+ <header class="header">
30
+ <div class="header-content">
31
+ <!-- Added hamburger menu for mobile -->
32
+ <button class="hamburger-btn" id="hamburger-btn">
33
+ <span></span>
34
+ <span></span>
35
+ <span></span>
36
+ </button>
37
+
38
+ <div class="logo">
39
+ <i class="fas fa-palette"></i>
40
+ <span>CodeCanvas</span>
41
+ </div>
42
+ <div class="header-actions">
43
+ <!-- Separated load and save session buttons -->
44
+ <button class="session-btn" id="load-session-btn">
45
+ <svg
46
+ width="16"
47
+ height="16"
48
+ viewBox="0 0 24 24"
49
+ fill="none"
50
+ stroke="currentColor"
51
+ stroke-width="2"
52
+ >
53
+ <path
54
+ d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
55
+ />
56
+ <polyline points="14,2 14,8 20,8" />
57
+ <line x1="16" y1="13" x2="8" y2="13" />
58
+ <line x1="16" y1="17" x2="8" y2="17" />
59
+ <polyline points="10,9 9,9 8,9" />
60
+ </svg>
61
+ <span>Load</span>
62
+ </button>
63
+ <button class="session-btn" id="save-session-btn">
64
+ <svg
65
+ width="16"
66
+ height="16"
67
+ viewBox="0 0 24 24"
68
+ fill="none"
69
+ stroke="currentColor"
70
+ stroke-width="2"
71
+ >
72
+ <path
73
+ d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"
74
+ />
75
+ <polyline points="17,21 17,13 7,13 7,21" />
76
+ <polyline points="7,3 7,8 15,8" />
77
+ </svg>
78
+ <span>Save</span>
79
+ </button>
80
+ <button class="theme-toggle" id="theme-toggle">
81
+ <i class="fas fa-moon"></i>
82
+ </button>
83
+ </div>
84
+ </div>
85
+ </header>
86
+
87
+ <!-- ... existing popups ... -->
88
+ <!-- Separate Load Session Popup -->
89
+ <div class="session-popup hidden" id="load-session-popup">
90
+ <div class="popup-overlay"></div>
91
+ <div class="popup-content">
92
+ <div class="popup-header">
93
+ <h3>
94
+ <svg
95
+ width="20"
96
+ height="20"
97
+ viewBox="0 0 24 24"
98
+ fill="none"
99
+ stroke="currentColor"
100
+ stroke-width="2"
101
+ >
102
+ <path
103
+ d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
104
+ />
105
+ <polyline points="14,2 14,8 20,8" />
106
+ </svg>
107
+ Load Session
108
+ </h3>
109
+ <button class="popup-close" id="load-session-popup-close">
110
+ <i class="fas fa-times"></i>
111
+ </button>
112
+ </div>
113
+ <div class="popup-body">
114
+ <select id="session-select" class="select-input">
115
+ <option value="">No sessions found</option>
116
+ </select>
117
+ <div class="button-row">
118
+ <button id="load-session-action-btn" class="btn btn-primary">
119
+ <i class="fas fa-upload"></i> Load Session
120
+ </button>
121
+ <button id="delete-session-btn" class="btn btn-destructive">
122
+ <i class="fas fa-trash"></i> Delete
123
+ </button>
124
+ </div>
125
+ <button id="new-session-btn" class="btn btn-outline">
126
+ <i class="fas fa-plus"></i> New Session
127
+ </button>
128
+ </div>
129
+ </div>
130
+ </div>
131
+
132
+ <!-- Separate Save Session Popup -->
133
+ <div class="session-popup hidden" id="save-session-popup">
134
+ <div class="popup-overlay"></div>
135
+ <div class="popup-content">
136
+ <div class="popup-header">
137
+ <h3>
138
+ <svg
139
+ width="20"
140
+ height="20"
141
+ viewBox="0 0 24 24"
142
+ fill="none"
143
+ stroke="currentColor"
144
+ stroke-width="2"
145
+ >
146
+ <path
147
+ d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"
148
+ />
149
+ <polyline points="17,21 17,13 7,13 7,21" />
150
+ <polyline points="7,3 7,8 15,8" />
151
+ </svg>
152
+ Save Session
153
+ </h3>
154
+ <button class="popup-close" id="save-session-popup-close">
155
+ <i class="fas fa-times"></i>
156
+ </button>
157
+ </div>
158
+ <div class="popup-body">
159
+ <div class="input-group">
160
+ <input
161
+ type="text"
162
+ id="session-name-input"
163
+ class="text-input"
164
+ placeholder="Enter session name..."
165
+ />
166
+ <button id="save-session-action-btn" class="btn btn-accent">
167
+ <i class="fas fa-save"></i> Save
168
+ </button>
169
+ </div>
170
+ </div>
171
+ </div>
172
+ </div>
173
+
174
+ <!-- Main Container -->
175
+ <div class="main-container">
176
+ <!-- Added mobile overlay for sidebar -->
177
+ <div class="sidebar-overlay" id="sidebar-overlay"></div>
178
+
179
+ <!-- Redesigned sidebar with better organization -->
180
+ <aside class="sidebar" id="sidebar">
181
+ <div class="sidebar-content">
182
+ <!-- Info Panels moved to top -->
183
+ <div class="info-panels">
184
+ <details
185
+ id="analysis-container"
186
+ class="info-panel info-panel-dull glass-effect hidden"
187
+ >
188
+ <summary class="info-header">
189
+ <i class="fas fa-brain"></i>
190
+ <span>AI Thoughts</span>
191
+ <i class="fas fa-chevron-down"></i>
192
+ </summary>
193
+ <div id="ai-analysis" class="info-content auto-scroll"></div>
194
+ </details>
195
+
196
+ <details
197
+ id="changes-container"
198
+ class="info-panel info-panel-dull glass-effect hidden"
199
+ >
200
+ <summary class="info-header">
201
+ <i class="fas fa-list-ul"></i>
202
+ <span>Changes Summary</span>
203
+ <i class="fas fa-chevron-down"></i>
204
+ </summary>
205
+ <div
206
+ id="summary-of-changes"
207
+ class="info-content auto-scroll"
208
+ ></div>
209
+ </details>
210
+
211
+ <details
212
+ id="instructions-container"
213
+ class="info-panel glass-effect hidden"
214
+ >
215
+ <summary class="info-header">
216
+ <i class="fas fa-lightbulb"></i>
217
+ <span>Instructions</span>
218
+ <i class="fas fa-chevron-down"></i>
219
+ </summary>
220
+ <div
221
+ id="game-instructions"
222
+ class="info-content auto-scroll"
223
+ ></div>
224
+ </details>
225
+ </div>
226
+
227
+ <!-- Loading Spinner -->
228
+ <div class="spinner-container hidden">
229
+ <div class="spinner">
230
+ <div class="spinner-ring"></div>
231
+ <div class="spinner-ring"></div>
232
+ <div class="spinner-ring"></div>
233
+ </div>
234
+ <p class="spinner-text" style="width: 100%; text-align: center">
235
+ Summoning ideas
236
+ </p>
237
+ </div>
238
+
239
+ <!-- Initial Generation moved down -->
240
+ <div
241
+ id="initial-generation"
242
+ class="panel generation-panel glass-effect"
243
+ >
244
+ <div class="panel-header">
245
+ <h3><i class="fas fa-magic"></i> Create</h3>
246
+ </div>
247
+ <div class="panel-content">
248
+ <textarea
249
+ id="prompt-input"
250
+ class="textarea-input"
251
+ placeholder="Describe your idea... e.g., A bouncing ball simulation, interactive data visualization, animated landing page..."
252
+ ></textarea>
253
+
254
+ <!-- Redesigned file upload with smaller button -->
255
+ <div class="file-upload-container">
256
+ <label
257
+ for="generate-file-input"
258
+ class="file-upload-label-small"
259
+ >
260
+ <svg
261
+ width="16"
262
+ height="16"
263
+ viewBox="0 0 24 24"
264
+ fill="none"
265
+ stroke="currentColor"
266
+ stroke-width="2"
267
+ >
268
+ <path
269
+ d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66L9.64 16.2a2 2 0 0 1-2.83-2.83l8.49-8.48"
270
+ />
271
+ </svg>
272
+ Attach Files
273
+ </label>
274
+ <input
275
+ type="file"
276
+ id="generate-file-input"
277
+ multiple
278
+ class="file-upload-input"
279
+ />
280
+ <div id="generate-file-list" class="file-list"></div>
281
+ </div>
282
+
283
+ <button
284
+ id="generate-btn"
285
+ class="btn btn-primary btn-large pulse-on-hover"
286
+ >
287
+ <i class="fas fa-wand-magic-sparkles"></i>
288
+ Generate Code
289
+ </button>
290
+ </div>
291
+ </div>
292
+
293
+ <!-- Modification Panel moved down -->
294
+ <div
295
+ id="modification-panel"
296
+ class="panel modification-panel glass-effect hidden"
297
+ >
298
+ <div class="panel-header">
299
+ <h3><i class="fas fa-edit"></i> Modify</h3>
300
+ </div>
301
+ <div class="panel-content">
302
+ <textarea
303
+ id="modification-input"
304
+ class="textarea-input"
305
+ placeholder="Describe your changes... e.g., Make it more colorful, add animations, change the layout..."
306
+ ></textarea>
307
+
308
+ <!-- Redesigned file upload with smaller button -->
309
+ <div class="file-upload-container">
310
+ <label for="modify-file-input" class="file-upload-label-small">
311
+ <svg
312
+ width="16"
313
+ height="16"
314
+ viewBox="0 0 24 24"
315
+ fill="none"
316
+ stroke="currentColor"
317
+ stroke-width="2"
318
+ >
319
+ <path
320
+ d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66L9.64 16.2a2 2 0 0 1-2.83-2.83l8.49-8.48"
321
+ />
322
+ </svg>
323
+ Attach Files
324
+ </label>
325
+ <input
326
+ type="file"
327
+ id="modify-file-input"
328
+ multiple
329
+ class="file-upload-input"
330
+ />
331
+ <div id="modify-file-list" class="file-list"></div>
332
+ </div>
333
+
334
+ <!-- Console Output -->
335
+ <div id="console-container" class="console-container hidden">
336
+ <label class="console-label">
337
+ <i class="fas fa-terminal"></i> Console Output
338
+ </label>
339
+ <textarea
340
+ id="console-output"
341
+ class="console-output"
342
+ readonly
343
+ ></textarea>
344
+ </div>
345
+
346
+ <div class="button-grid">
347
+ <button id="modify-btn" class="btn btn-primary pulse-on-hover">
348
+ <i class="fas fa-sync-alt"></i> Apply Changes
349
+ </button>
350
+ </div>
351
+ </div>
352
+ </div>
353
+ </div>
354
+ </aside>
355
+
356
+ <!-- Main Content -->
357
+ <main class="main-content">
358
+ <div class="content-header glass-effect">
359
+ <div class="view-tabs">
360
+ <button id="toggle-preview-btn" class="tab-btn active">
361
+ <i class="fas fa-eye"></i>
362
+ <span>Live Preview</span>
363
+ </button>
364
+ <button id="toggle-code-btn" class="tab-btn">
365
+ <i class="fas fa-code"></i>
366
+ <span>Source Code</span>
367
+ </button>
368
+ </div>
369
+ <!-- Moved download and open buttons to content header with SVG icons -->
370
+ <div class="content-actions">
371
+ <!-- Version History moved from sidebar -->
372
+ <div
373
+ id="version-history-controls"
374
+ class="version-history-header-compact hidden"
375
+ >
376
+ <svg
377
+ width="18"
378
+ height="18"
379
+ viewBox="0 0 24 24"
380
+ fill="none"
381
+ stroke="currentColor"
382
+ stroke-width="2"
383
+ >
384
+ <path d="M3 3v5h5" />
385
+ <path d="M3.05 13A9 9 0 1 0 6 5.3L3 8" />
386
+ <path d="M12 7v5l4 2" />
387
+ </svg>
388
+ <select
389
+ id="version-history-select"
390
+ class="select-input-header"
391
+ ></select>
392
+ </div>
393
+
394
+ <button id="copy-code-btn" class="action-btn" title="Copy Code">
395
+ <svg
396
+ width="18"
397
+ height="18"
398
+ viewBox="0 0 24 24"
399
+ fill="none"
400
+ stroke="currentColor"
401
+ stroke-width="2"
402
+ >
403
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
404
+ <path
405
+ d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
406
+ />
407
+ </svg>
408
+ </button>
409
+ <!-- Moved download button from sidebar to header -->
410
+ <button id="download-btn" class="action-btn" title="Download Code">
411
+ <svg
412
+ width="18"
413
+ height="18"
414
+ viewBox="0 0 24 24"
415
+ fill="none"
416
+ stroke="currentColor"
417
+ stroke-width="2"
418
+ >
419
+ <path <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
420
+ <polyline points="7,10 12,15 17,10" />
421
+ <line x1="12" y1="15" x2="12" y2="3" />
422
+ </svg>
423
+ </button>
424
+ <!-- Moved open tab button from sidebar to header -->
425
+ <button
426
+ id="open-tab-btn"
427
+ class="action-btn"
428
+ title="Open in New Window"
429
+ >
430
+ <svg
431
+ width="18"
432
+ height="18"
433
+ viewBox="0 0 24 24"
434
+ fill="none"
435
+ stroke="currentColor"
436
+ stroke-width="2"
437
+ >
438
+ <path
439
+ d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"
440
+ />
441
+ <polyline points="15,3 21,3 21,9" />
442
+ <line x1="10" y1="14" x2="21" y2="3" />
443
+ </svg>
444
+ </button>
445
+ <button
446
+ id="refresh-preview-btn"
447
+ class="action-btn hidden"
448
+ title="Refresh Preview"
449
+ >
450
+ <svg
451
+ width="18"
452
+ height="18"
453
+ viewBox="0 0 24 24"
454
+ fill="none"
455
+ stroke="currentColor"
456
+ stroke-width="2"
457
+ >
458
+ <polyline points="23,4 23,10 17,10" />
459
+ <polyline points="1,20 1,14 7,14" />
460
+ <path
461
+ d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"
462
+ />
463
+ </svg>
464
+ </button>
465
+ </div>
466
+ </div>
467
+
468
+ <div class="content-body">
469
+ <!-- Enhanced code editor with Monaco Editor -->
470
+ <div class="code-container hidden">
471
+ <div class="code-editor-wrapper glass-effect">
472
+ <!-- Replaced overlapping textarea/pre with single Monaco container -->
473
+ <div id="monaco-editor" class="monaco-editor-container"></div>
474
+ </div>
475
+ </div>
476
+
477
+ <!-- Enhanced preview with better scrolling -->
478
+ <div class="preview-container">
479
+ <div class="preview-wrapper glass-effect">
480
+ <iframe
481
+ id="game-iframe"
482
+ class="preview-iframe"
483
+ sandbox="allow-scripts allow-pointer-lock allow-same-origin"
484
+ title="Generated Visualization"
485
+ >
486
+ </iframe>
487
+ <div class="preview-placeholder">
488
+ <div class="placeholder-content">
489
+ <i class="fas fa-palette floating-icon"></i>
490
+ <h3>Ready to Create</h3>
491
+ <p>
492
+ Describe your idea in the sidebar to get started with AI
493
+ code generation
494
+ </p>
495
+ </div>
496
+ </div>
497
+ </div>
498
+ </div>
499
+ </div>
500
+ </main>
501
+ </div>
502
+
503
+ <!-- Fixed follow-up floating button -->
504
+ <div class="followup-float" id="followup-float">
505
+ <button class="followup-btn" id="followup-btn">
506
+ <i class="fas fa-question-circle"></i>
507
+ </button>
508
+ <div class="followup-tooltip">Ask AI</div>
509
+ </div>
510
+
511
+ <div class="followup-popup hidden" id="followup-popup">
512
+ <div class="popup-overlay"></div>
513
+ <div class="popup-content">
514
+ <div class="popup-header">
515
+ <h3><i class="fas fa-question-circle"></i> Ask AI</h3>
516
+ <button class="popup-close" id="followup-popup-close">
517
+ <i class="fas fa-times"></i>
518
+ </button>
519
+ </div>
520
+ <div class="popup-body">
521
+ <textarea
522
+ id="follow-up-input"
523
+ class="textarea-input"
524
+ placeholder="Ask about the code... e.g., How does this work? Can you explain this function?"
525
+ ></textarea>
526
+ <button id="follow-up-btn" class="btn btn-accent pulse-on-hover">
527
+ <i class="fas fa-paper-plane"></i> Ask Question
528
+ </button>
529
+
530
+ <div id="follow-up-spinner" class="spinner-container hidden">
531
+ <div class="spinner-small">
532
+ <div class="spinner-ring"></div>
533
+ </div>
534
+ <p class="spinner-text">Thinking...</p>
535
+ </div>
536
+
537
+ <div id="follow-up-output-container" class="follow-up-output hidden">
538
+ <div class="output-header">
539
+ <i class="fas fa-robot"></i> AI Response
540
+ </div>
541
+ <div id="follow-up-output" class="output-content"></div>
542
+ </div>
543
+ </div>
544
+ </div>
545
+ </div>
546
+
547
+ <script src="static/script.js"></script>
548
+ </body>
549
+ </html>
static/script.js ADDED
@@ -0,0 +1,1084 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener("DOMContentLoaded", () => {
2
+ // --- Element Selectors ---
3
+ const promptInput = document.getElementById("prompt-input");
4
+ const generateBtn = document.getElementById("generate-btn");
5
+ const modificationInput = document.getElementById("modification-input");
6
+ const modifyBtn = document.getElementById("modify-btn");
7
+ const downloadBtn = document.getElementById("download-btn");
8
+ const openTabBtn = document.getElementById("open-tab-btn");
9
+ const copyCodeBtn = document.getElementById("copy-code-btn");
10
+
11
+ const togglePreviewBtn = document.getElementById("toggle-preview-btn");
12
+ const toggleCodeBtn = document.getElementById("toggle-code-btn");
13
+ const refreshPreviewBtn = document.getElementById("refresh-preview-btn");
14
+
15
+ const codeContainer = document.querySelector(".code-container");
16
+ const previewContainer = document.querySelector(".preview-container");
17
+ const monacoContainer = document.getElementById("monaco-editor");
18
+ const gameIframe = document.getElementById("game-iframe");
19
+
20
+ const initialGenerationPanel = document.getElementById("initial-generation");
21
+ const modificationPanel = document.getElementById("modification-panel");
22
+ const spinner = document.querySelector(".spinner-container");
23
+
24
+ const analysisContainer = document.getElementById("analysis-container");
25
+ const aiAnalysis = document.getElementById("ai-analysis");
26
+ const changesContainer = document.getElementById("changes-container");
27
+ const summaryOfChanges = document.getElementById("summary-of-changes");
28
+ const instructionsContainer = document.getElementById(
29
+ "instructions-container"
30
+ );
31
+ const gameInstructions = document.getElementById("game-instructions");
32
+
33
+ const consoleContainer = document.getElementById("console-container");
34
+ const consoleOutput = document.getElementById("console-output");
35
+
36
+ const followUpInput = document.getElementById("follow-up-input");
37
+ const followUpBtn = document.getElementById("follow-up-btn");
38
+ const followUpSpinner = document.getElementById("follow-up-spinner");
39
+ const followUpOutputContainer = document.getElementById(
40
+ "follow-up-output-container"
41
+ );
42
+ const followUpOutput = document.getElementById("follow-up-output");
43
+
44
+ const versionHistoryControls = document.getElementById(
45
+ "version-history-controls"
46
+ );
47
+ const versionHistorySelect = document.getElementById(
48
+ "version-history-select"
49
+ );
50
+
51
+ const loadSessionBtn = document.getElementById("load-session-btn");
52
+ const saveSessionBtn = document.getElementById("save-session-btn");
53
+ const loadSessionPopup = document.getElementById("load-session-popup");
54
+ const saveSessionPopup = document.getElementById("save-session-popup");
55
+ const loadSessionPopupClose = document.getElementById(
56
+ "load-session-popup-close"
57
+ );
58
+ const saveSessionPopupClose = document.getElementById(
59
+ "save-session-popup-close"
60
+ );
61
+
62
+ const followupFloat = document.getElementById("followup-float");
63
+ const followupBtn = document.getElementById("followup-btn");
64
+ const followupPopup = document.getElementById("followup-popup");
65
+ const followupPopupClose = document.getElementById("followup-popup-close");
66
+
67
+ const sessionSelect = document.getElementById("session-select");
68
+ const sessionNameInput = document.getElementById("session-name-input");
69
+ const loadSessionActionBtn = document.getElementById(
70
+ "load-session-action-btn"
71
+ );
72
+ const saveSessionActionBtn = document.getElementById(
73
+ "save-session-action-btn"
74
+ );
75
+ const deleteSessionBtn = document.getElementById("delete-session-btn");
76
+ const newSessionBtn = document.getElementById("new-session-btn");
77
+
78
+ const hamburgerBtn = document.getElementById("hamburger-btn");
79
+ const sidebar = document.getElementById("sidebar");
80
+ const sidebarOverlay = document.getElementById("sidebar-overlay");
81
+ const themeToggle = document.getElementById("theme-toggle");
82
+
83
+ const generateFileInput = document.getElementById("generate-file-input");
84
+ const generateFileList = document.getElementById("generate-file-list");
85
+ const modifyFileInput = document.getElementById("modify-file-input");
86
+ const modifyFileList = document.getElementById("modify-file-list");
87
+
88
+ // --- State Variables ---
89
+ let promptHistory = [];
90
+ let consoleLogs = [];
91
+ let abortController = null;
92
+ let versionHistory = [];
93
+ let currentSessionId = null;
94
+ const clientSideAssets = new Map();
95
+ const DB_NAME = "CodeCanvasDB";
96
+ const DB_VERSION = 1;
97
+ const STORE_NAME = "sessions";
98
+ let marked = null;
99
+ let db = null;
100
+ let monacoEditor = null;
101
+ let monaco = null;
102
+ let lastScrollPosition = { lineNumber: 1, column: 1 };
103
+ let hasGeneratedContent = false;
104
+ const mainContainer = document.querySelector(".main-container");
105
+
106
+ let autoScrollEnabled = true;
107
+
108
+ // --- IndexedDB Functions ---
109
+ function initDB() {
110
+ return new Promise((resolve, reject) => {
111
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
112
+ request.onerror = () => reject(request.error);
113
+ request.onsuccess = () => {
114
+ db = request.result;
115
+ resolve(db);
116
+ };
117
+ request.onupgradeneeded = (event) => {
118
+ db = event.target.result;
119
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
120
+ const store = db.createObjectStore(STORE_NAME, { keyPath: "id" });
121
+ store.createIndex("name", "name", { unique: false });
122
+ store.createIndex("savedAt", "savedAt", { unique: false });
123
+ }
124
+ };
125
+ });
126
+ }
127
+
128
+ async function saveSessionToDB(sessionData) {
129
+ if (!db) await initDB();
130
+ return new Promise((resolve, reject) => {
131
+ const transaction = db.transaction([STORE_NAME], "readwrite");
132
+ const store = transaction.objectStore(STORE_NAME);
133
+ const request = store.put(sessionData);
134
+ request.onsuccess = () => resolve(request.result);
135
+ request.onerror = () => reject(request.error);
136
+ });
137
+ }
138
+
139
+ async function getSessionsFromDB() {
140
+ if (!db) await initDB();
141
+ return new Promise((resolve, reject) => {
142
+ const transaction = db.transaction([STORE_NAME], "readonly");
143
+ const store = transaction.objectStore(STORE_NAME);
144
+ const request = store.getAll();
145
+ request.onsuccess = () => resolve(request.result || []);
146
+ request.onerror = () => reject(request.error);
147
+ });
148
+ }
149
+
150
+ async function deleteSessionFromDB(sessionId) {
151
+ if (!db) await initDB();
152
+ return new Promise((resolve, reject) => {
153
+ const transaction = db.transaction([STORE_NAME], "readwrite");
154
+ const store = transaction.objectStore(STORE_NAME);
155
+ const request = store.delete(sessionId);
156
+ request.onsuccess = () => resolve();
157
+ request.onerror = () => reject(request.error);
158
+ });
159
+ }
160
+
161
+ // --- Core Application Logic ---
162
+
163
+ class StreamParser {
164
+ constructor() {
165
+ this.reset();
166
+ }
167
+
168
+ reset() {
169
+ this.analysis = "";
170
+ this.changes = "";
171
+ this.instructions = "";
172
+ this.html = "";
173
+ this.currentSection = null;
174
+ this.buffer = "";
175
+ }
176
+
177
+ processChunk(chunk) {
178
+ this.buffer += chunk;
179
+ const lines = this.buffer.split("\n");
180
+ this.buffer = lines.pop();
181
+
182
+ for (const line of lines) {
183
+ const trimmedLine = line.trim();
184
+
185
+ if (trimmedLine === "[ANALYSIS]") {
186
+ this.currentSection = "ANALYSIS";
187
+ continue;
188
+ }
189
+ if (trimmedLine === "[END_ANALYSIS]") {
190
+ this.currentSection = null;
191
+ continue;
192
+ }
193
+ if (trimmedLine === "[CHANGES]") {
194
+ this.currentSection = "CHANGES";
195
+ continue;
196
+ }
197
+ if (trimmedLine === "[END_CHANGES]") {
198
+ this.currentSection = null;
199
+ continue;
200
+ }
201
+ if (trimmedLine === "[INSTRUCTIONS]") {
202
+ this.currentSection = "INSTRUCTIONS";
203
+ continue;
204
+ }
205
+ if (trimmedLine === "[END_INSTRUCTIONS]") {
206
+ this.currentSection = "HTML";
207
+ continue;
208
+ }
209
+
210
+ switch (this.currentSection) {
211
+ case "ANALYSIS":
212
+ this.analysis += line + "\n";
213
+ break;
214
+ case "CHANGES":
215
+ this.changes += line + "\n";
216
+ break;
217
+ case "INSTRUCTIONS":
218
+ this.instructions += line + "\n";
219
+ break;
220
+ case "HTML":
221
+ this.html += line + "\n";
222
+ break;
223
+ default:
224
+ if (
225
+ trimmedLine.startsWith("<!DOCTYPE html>") ||
226
+ this.currentSection === "HTML"
227
+ ) {
228
+ this.currentSection = "HTML";
229
+ this.html += line + "\n";
230
+ }
231
+ break;
232
+ }
233
+ }
234
+ }
235
+
236
+ /**
237
+ * BUGFIX: Returns the current state NON-DESTRUCTIVELY for live UI updates.
238
+ * It does NOT process the buffer, preventing duplication errors.
239
+ */
240
+ getCurrentState() {
241
+ return {
242
+ analysis: this.analysis.trim(),
243
+ changes: this.changes.trim(),
244
+ instructions: this.instructions.trim(),
245
+ html: this.cleanHtml(this.html),
246
+ };
247
+ }
248
+
249
+ /**
250
+ * BUGFIX: Finalizes the stream by processing the remaining buffer.
251
+ * This should ONLY be called once at the very end.
252
+ */
253
+ finalize() {
254
+ if (this.buffer) {
255
+ // Assume any remaining buffer content belongs to the last active section, defaulting to HTML
256
+ const targetSection = this.currentSection || "HTML";
257
+ switch (targetSection) {
258
+ case "ANALYSIS":
259
+ this.analysis += this.buffer;
260
+ break;
261
+ case "CHANGES":
262
+ this.changes += this.buffer;
263
+ break;
264
+ case "INSTRUCTIONS":
265
+ this.instructions += this.buffer;
266
+ break;
267
+ case "HTML":
268
+ default:
269
+ this.html += this.buffer;
270
+ break;
271
+ }
272
+ this.buffer = ""; // Clear the buffer after finalizing
273
+ }
274
+ return this.getCurrentState();
275
+ }
276
+
277
+ cleanHtml(htmlString) {
278
+ return htmlString.replace(/^```html\s*|\s*```$/g, "").trim();
279
+ }
280
+ }
281
+
282
+ async function streamResponse(url, body, targetElement) {
283
+ setLoading(true, url);
284
+ const parser = new StreamParser();
285
+
286
+ if (!targetElement) {
287
+ setView("code");
288
+ if (monacoEditor) monacoEditor.setValue("");
289
+ clearInfoPanels();
290
+ }
291
+
292
+ if (abortController) abortController.abort();
293
+ abortController = new AbortController();
294
+
295
+ try {
296
+ const isFormData = body instanceof FormData;
297
+ const response = await fetch(url, {
298
+ method: "POST",
299
+ body: isFormData ? body : JSON.stringify(body),
300
+ headers: isFormData ? {} : { "Content-Type": "application/json" },
301
+ signal: abortController.signal,
302
+ });
303
+
304
+ if (!response.ok)
305
+ throw new Error(`HTTP error! status: ${response.status}`);
306
+ const reader = response.body.getReader();
307
+ const decoder = new TextDecoder();
308
+
309
+ while (true) {
310
+ const { done, value } = await reader.read();
311
+ if (done) break;
312
+ let chunk = decoder.decode(value, { stream: true });
313
+
314
+ if (chunk.includes("[STREAM_RESTART]")) {
315
+ console.warn("Stream restart signal received.");
316
+ showNotification(
317
+ "Primary model failed; switching to fallback.",
318
+ "warning"
319
+ );
320
+ parser.reset();
321
+ clearInfoPanels();
322
+ if (monacoEditor) monacoEditor.setValue("");
323
+ chunk = chunk.replace(/\[STREAM_RESTART\]\s*\n?/, "");
324
+ }
325
+
326
+ if (targetElement) {
327
+ targetElement.textContent += chunk;
328
+ targetElement.scrollTop = targetElement.scrollHeight;
329
+ } else {
330
+ parser.processChunk(chunk);
331
+ // BUGFIX: Use the non-destructive getter for live updates
332
+ const currentData = parser.getCurrentState();
333
+ updateUIFromStream(currentData);
334
+ }
335
+ }
336
+
337
+ if (!targetElement) {
338
+ const prompt = isFormData ? body.get("prompt") : body.prompt;
339
+ // BUGFIX: Call finalize() only once at the end of the stream
340
+ const finalData = parser.finalize();
341
+
342
+ // Final UI update with the fully parsed data
343
+ updateUIFromStream(finalData);
344
+ if (monacoEditor) {
345
+ const totalLines = monacoEditor.getModel().getLineCount();
346
+ monacoEditor.revealLineNearTop(
347
+ totalLines,
348
+ monaco.editor.ScrollType.Smooth
349
+ );
350
+ }
351
+
352
+ // Update iframe and save version with the final, clean data
353
+ updateIframe(finalData.html);
354
+ saveVersion({ ...finalData, prompt });
355
+ showModificationPanel();
356
+ setTimeout(minimizeInfoPanels, 2000);
357
+ }
358
+ } catch (error) {
359
+ if (error.name !== "AbortError") {
360
+ console.error("Streaming failed:", error);
361
+ const errorMessage = `Error: Failed to get response. Details: ${error.message}`;
362
+ if (targetElement) {
363
+ targetElement.textContent = "Error: Could not get response.";
364
+ } else if (monacoEditor) {
365
+ monacoEditor.setValue(errorMessage);
366
+ setView("code");
367
+ }
368
+ }
369
+ } finally {
370
+ setLoading(false, url);
371
+ abortController = null;
372
+ }
373
+ }
374
+
375
+ function updateUIFromStream({ analysis, changes, instructions, html }) {
376
+ if (analysis) {
377
+ analysisContainer.classList.remove("hidden");
378
+ aiAnalysis.innerHTML = marked ? marked.parse(analysis) : analysis;
379
+ aiAnalysis.scrollTop = aiAnalysis.scrollHeight;
380
+ }
381
+
382
+ if (changes) {
383
+ changesContainer.classList.remove("hidden");
384
+ summaryOfChanges.innerHTML = marked ? marked.parse(changes) : changes;
385
+ summaryOfChanges.scrollTop = summaryOfChanges.scrollHeight;
386
+ }
387
+
388
+ if (instructions) {
389
+ instructionsContainer.classList.remove("hidden");
390
+ gameInstructions.innerHTML = marked
391
+ ? marked.parse(instructions)
392
+ : instructions;
393
+ gameInstructions.scrollTop = gameInstructions.scrollHeight;
394
+ }
395
+
396
+ if (html && monacoEditor) {
397
+ const currentContent = monacoEditor.getValue();
398
+ if (html !== currentContent) {
399
+ const model = monacoEditor.getModel();
400
+
401
+ monacoEditor.executeEdits(null, [
402
+ { range: model.getFullModelRange(), text: html },
403
+ ]);
404
+
405
+ // --- Force scroll to bottom every time ---
406
+ const scrollHeight = monacoEditor.getScrollHeight();
407
+ monacoEditor.setScrollTop(scrollHeight);
408
+ }
409
+ }
410
+ }
411
+
412
+ // --- Client-Side Asset Handling ---
413
+ function handleFileSelection(event, fileListElement) {
414
+ for (const file of event.target.files) {
415
+ if (!clientSideAssets.has(file.name)) {
416
+ clientSideAssets.set(file.name, {
417
+ file: file,
418
+ blobUrl: URL.createObjectURL(file),
419
+ });
420
+ }
421
+ }
422
+ renderSelectedFiles(fileListElement);
423
+ event.target.value = "";
424
+ }
425
+
426
+ function renderSelectedFiles(fileListElement) {
427
+ fileListElement.innerHTML = "";
428
+ fileListElement.classList.toggle("hidden", clientSideAssets.size === 0);
429
+ clientSideAssets.forEach((asset, fileName) => {
430
+ const fileItem = document.createElement("div");
431
+ fileItem.className = "file-item";
432
+ const nameSpan = document.createElement("span");
433
+ nameSpan.textContent = fileName;
434
+ const removeBtn = document.createElement("button");
435
+ removeBtn.innerHTML = "&times;";
436
+ removeBtn.className = "file-remove-btn";
437
+ removeBtn.onclick = () => {
438
+ URL.revokeObjectURL(asset.blobUrl);
439
+ clientSideAssets.delete(fileName);
440
+ renderSelectedFiles(generateFileList);
441
+ renderSelectedFiles(modifyFileList);
442
+ };
443
+ fileItem.appendChild(nameSpan);
444
+ fileItem.appendChild(removeBtn);
445
+ fileListElement.appendChild(fileItem);
446
+ });
447
+ }
448
+
449
+ async function deserializeAssets(assets) {
450
+ clientSideAssets.clear();
451
+ for (const assetData of assets) {
452
+ try {
453
+ const blob = new Blob([assetData.data], { type: assetData.type });
454
+ const file = new File([blob], assetData.fileName, {
455
+ type: assetData.type,
456
+ });
457
+ clientSideAssets.set(assetData.fileName, {
458
+ file: file,
459
+ blobUrl: URL.createObjectURL(blob),
460
+ });
461
+ } catch (error) {
462
+ console.error(`Failed to restore asset ${assetData.fileName}:`, error);
463
+ }
464
+ }
465
+ renderSelectedFiles(generateFileList);
466
+ renderSelectedFiles(modifyFileList);
467
+ }
468
+
469
+ // --- Event Handlers ---
470
+ loadSessionBtn.addEventListener("click", () => {
471
+ populateSessionDropdown();
472
+ openPopup(loadSessionPopup);
473
+ });
474
+
475
+ saveSessionBtn.addEventListener("click", () => {
476
+ openPopup(saveSessionPopup);
477
+ });
478
+
479
+ generateFileInput.addEventListener("change", (e) =>
480
+ handleFileSelection(e, generateFileList)
481
+ );
482
+ modifyFileInput.addEventListener("change", (e) =>
483
+ handleFileSelection(e, modifyFileList)
484
+ );
485
+
486
+ generateBtn.addEventListener("click", async () => {
487
+ const prompt = promptInput.value.trim();
488
+ if (!prompt) {
489
+ showNotification("Please enter a prompt", "error");
490
+ return;
491
+ }
492
+ hasGeneratedContent = true;
493
+ updateLayoutState();
494
+ versionHistory = [];
495
+ promptHistory = [];
496
+ currentSessionId = null;
497
+ sessionNameInput.value = prompt.substring(0, 50);
498
+ const formData = new FormData();
499
+ formData.append("prompt", prompt);
500
+ clientSideAssets.forEach((asset) =>
501
+ formData.append("files", asset.file, asset.file.name)
502
+ );
503
+ streamResponse("/generate", formData);
504
+ if (window.innerWidth <= 768) closeSidebar();
505
+ });
506
+
507
+ modifyBtn.addEventListener("click", async () => {
508
+ const modification = modificationInput.value.trim();
509
+ if (!modification) {
510
+ showNotification("Please enter a modification request", "error");
511
+ return;
512
+ }
513
+ hasGeneratedContent = true;
514
+ updateLayoutState();
515
+ const currentHtml = monacoEditor ? monacoEditor.getValue() : "";
516
+ if (!currentHtml) return alert("There is no code to modify!");
517
+ const formData = new FormData();
518
+ formData.append("prompt", modification);
519
+ formData.append("current_code", currentHtml);
520
+ formData.append("console_logs", consoleLogs.join("\n"));
521
+ promptHistory.forEach((p) => formData.append("prompt_history", p));
522
+ clientSideAssets.forEach((asset) =>
523
+ formData.append("files", asset.file, asset.file.name)
524
+ );
525
+ streamResponse("/modify", formData);
526
+ if (window.innerWidth <= 768) closeSidebar();
527
+ });
528
+
529
+ async function streamResponseMarkdown(url, body, targetElement) {
530
+ setLoading(true, url);
531
+ if (abortController) abortController.abort();
532
+ abortController = new AbortController();
533
+
534
+ let accumulatedText = "";
535
+
536
+ try {
537
+ const isFormData = body instanceof FormData;
538
+ const response = await fetch(url, {
539
+ method: "POST",
540
+ body: isFormData ? body : JSON.stringify(body),
541
+ headers: isFormData ? {} : { "Content-Type": "application/json" },
542
+ signal: abortController.signal,
543
+ });
544
+ if (!response.ok)
545
+ throw new Error(`HTTP error! status: ${response.status}`);
546
+ const reader = response.body.getReader();
547
+ const decoder = new TextDecoder();
548
+
549
+ // Show loading indicator while streaming
550
+ targetElement.innerHTML = '<div class="streaming-indicator">Receiving response...</div>';
551
+
552
+ while (true) {
553
+ const { done, value } = await reader.read();
554
+ if (done) break;
555
+ let chunk = decoder.decode(value, { stream: true });
556
+ accumulatedText += chunk;
557
+ }
558
+
559
+ // Render markdown only once after all text is received
560
+ const md = window.markdownit();
561
+ targetElement.innerHTML = md.render(accumulatedText);
562
+ targetElement.scrollTop = targetElement.scrollHeight;
563
+
564
+ } catch (error) {
565
+ if (error.name !== "AbortError") {
566
+ console.error("Streaming failed:", error);
567
+ targetElement.textContent = "Error: Could not get response.";
568
+ }
569
+ } finally {
570
+ setLoading(false, url);
571
+ abortController = null;
572
+ }
573
+ }
574
+
575
+ followUpBtn.addEventListener("click", () => {
576
+ const question = followUpInput.value.trim();
577
+ if (!question) return alert("Please ask a question!");
578
+ const currentHtml = monacoEditor ? monacoEditor.getValue() : "";
579
+ if (!currentHtml) return alert("There is no code to ask about!");
580
+ followUpOutputContainer.classList.remove("hidden");
581
+ followUpOutput.innerHTML = "";
582
+ streamResponseMarkdown(
583
+ "/explain",
584
+ { question, current_code: currentHtml },
585
+ followUpOutput
586
+ );
587
+ });
588
+
589
+ downloadBtn.addEventListener("click", async () => {
590
+ const html = monacoEditor ? monacoEditor.getValue() : "";
591
+ if (!html) return alert("No code to download!");
592
+ if (clientSideAssets.size === 0) {
593
+ const blob = new Blob([html], { type: "text/html" });
594
+ const url = URL.createObjectURL(blob);
595
+ const a = document.createElement("a");
596
+ a.href = url;
597
+ a.download = "creation.html";
598
+ a.click();
599
+ URL.revokeObjectURL(url);
600
+ return;
601
+ }
602
+ const formData = new FormData();
603
+ formData.append("html_content", html);
604
+ clientSideAssets.forEach((asset) =>
605
+ formData.append("files", asset.file, asset.file.name)
606
+ );
607
+ try {
608
+ const response = await fetch("/download_zip", {
609
+ method: "POST",
610
+ body: formData,
611
+ });
612
+ if (!response.ok)
613
+ throw new Error(`Failed to create zip file: ${response.statusText}`);
614
+ const blob = await response.blob();
615
+ const url = URL.createObjectURL(blob);
616
+ const a = document.createElement("a");
617
+ a.href = url;
618
+ a.download = "promptlab-project.zip";
619
+ a.click();
620
+ URL.revokeObjectURL(url);
621
+ } catch (error) {
622
+ console.error("Download failed:", error);
623
+ alert("Failed to download project as a zip. Check the console.");
624
+ }
625
+ });
626
+
627
+ openTabBtn.addEventListener("click", () => {
628
+ const html = monacoEditor ? monacoEditor.getValue() : "";
629
+ if (!html) return alert("No code to open!");
630
+ const processedHtml = replaceAssetPathsWithBlobs(html);
631
+ const blob = new Blob([processedHtml], { type: "text/html" });
632
+ const url = URL.createObjectURL(blob);
633
+ window.open(url, "_blank");
634
+ });
635
+
636
+ copyCodeBtn.addEventListener("click", async () => {
637
+ const code = monacoEditor ? monacoEditor.getValue() : "";
638
+ if (!code) return;
639
+ try {
640
+ await navigator.clipboard.writeText(code);
641
+ const originalContent = copyCodeBtn.innerHTML;
642
+ copyCodeBtn.innerHTML =
643
+ '<i class="fas fa-check"></i> <span>Copied!</span>';
644
+ setTimeout(() => {
645
+ copyCodeBtn.innerHTML = originalContent;
646
+ }, 2000);
647
+ } catch (err) {
648
+ console.error("Failed to copy text: ", err);
649
+ }
650
+ });
651
+
652
+ refreshPreviewBtn.addEventListener("click", () => {
653
+ const code = monacoEditor ? monacoEditor.getValue() : "";
654
+ updateIframe(code);
655
+ setView("preview");
656
+ });
657
+
658
+ togglePreviewBtn.addEventListener("click", () => setView("preview"));
659
+ toggleCodeBtn.addEventListener("click", () => setView("code"));
660
+
661
+ // --- UI and State Management Functions ---
662
+ function replaceAssetPathsWithBlobs(htmlContent) {
663
+ if (clientSideAssets.size === 0) return htmlContent;
664
+ let processedHtml = htmlContent;
665
+ clientSideAssets.forEach((asset, fileName) => {
666
+ const safeFileName = fileName.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
667
+ const regex = new RegExp(`(['"])assets/${safeFileName}\\1`, "g");
668
+ processedHtml = processedHtml.replace(regex, `$1${asset.blobUrl}$1`);
669
+ });
670
+ return processedHtml;
671
+ }
672
+
673
+ function updateIframe(htmlContent) {
674
+ const consoleLoggerScript = `<script>
675
+ const originalConsole = { ...window.console };
676
+ const postLog = (type, args) => { try { const message = args.map(arg => (typeof arg === 'object' && arg !== null) ? JSON.stringify(arg) : String(arg)).join(' '); window.parent.postMessage({ type: 'console', level: type, message }, '*'); } catch (e) {} };
677
+ window.console.log = (...args) => { postLog('log', args); originalConsole.log(...args); };
678
+ window.console.error = (...args) => { postLog('error', args); originalConsole.error(...args); };
679
+ window.console.warn = (...args) => { postLog('warn', args); originalConsole.warn(...args); };
680
+ window.addEventListener('error', e => postLog('error', [e.message, 'at', e.filename + ':' + e.lineno]));
681
+ </script>`;
682
+ consoleLogs = [];
683
+ updateConsoleDisplay();
684
+ const processedHtml = replaceAssetPathsWithBlobs(htmlContent);
685
+ const enhancedHtml = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>html, body { margin: 0; padding: 0; overflow: auto; min-height: 100vh; box-sizing: border-box; } * { box-sizing: border-box; }</style>${consoleLoggerScript}</head><body>${processedHtml}</body></html>`;
686
+ gameIframe.srcdoc = enhancedHtml;
687
+ }
688
+
689
+ window.addEventListener("message", (event) => {
690
+ if (event.data && event.data.type === "console") {
691
+ const { level, message } = event.data;
692
+ consoleLogs.push(`[${level.toUpperCase()}] ${message}`);
693
+ updateConsoleDisplay();
694
+ }
695
+ });
696
+
697
+ function updateConsoleDisplay() {
698
+ consoleContainer.classList.toggle("hidden", consoleLogs.length === 0);
699
+ if (consoleLogs.length > 0) {
700
+ consoleOutput.value = consoleLogs.join("\n");
701
+ consoleOutput.scrollTop = consoleOutput.scrollHeight;
702
+ }
703
+ }
704
+
705
+ function setView(view) {
706
+ if (view === "code" && monacoEditor) {
707
+ lastScrollPosition = monacoEditor.getPosition() || {
708
+ lineNumber: 1,
709
+ column: 1,
710
+ };
711
+ }
712
+ previewContainer.classList.toggle("hidden", view !== "preview");
713
+ codeContainer.classList.toggle("hidden", view === "preview");
714
+ togglePreviewBtn.classList.toggle("active", view === "preview");
715
+ toggleCodeBtn.classList.toggle("active", view !== "preview");
716
+ refreshPreviewBtn.classList.toggle("hidden", view === "preview");
717
+
718
+ if (view === "code" && monacoEditor) {
719
+ setTimeout(() => {
720
+ monacoEditor.layout();
721
+ monacoEditor.setPosition(lastScrollPosition);
722
+ monacoEditor.revealPosition(lastScrollPosition);
723
+ }, 100);
724
+ }
725
+ }
726
+
727
+ function setLoading(isLoading, url) {
728
+ if (url === "/explain") {
729
+ followUpSpinner.classList.toggle("hidden", !isLoading);
730
+ followUpBtn.disabled = isLoading;
731
+ } else {
732
+ spinner.classList.toggle("hidden", !isLoading);
733
+ generateBtn.disabled = isLoading;
734
+ modifyBtn.disabled = isLoading;
735
+ }
736
+ }
737
+
738
+ function showModificationPanel() {
739
+ initialGenerationPanel.classList.add("hidden");
740
+ modificationPanel.classList.remove("hidden");
741
+ followupFloat.classList.add("show");
742
+ renderSelectedFiles(generateFileList);
743
+ renderSelectedFiles(modifyFileList);
744
+ }
745
+
746
+ function clearInfoPanels() {
747
+ analysisContainer.classList.add("hidden");
748
+ changesContainer.classList.add("hidden");
749
+ instructionsContainer.classList.add("hidden");
750
+ aiAnalysis.innerHTML = "";
751
+ summaryOfChanges.innerHTML = "";
752
+ gameInstructions.innerHTML = "";
753
+ }
754
+
755
+ function minimizeInfoPanels() {
756
+ [analysisContainer, changesContainer, instructionsContainer].forEach(
757
+ (panel) => {
758
+ if (!panel.classList.contains("hidden")) {
759
+ panel.open = false;
760
+ }
761
+ }
762
+ );
763
+ }
764
+
765
+ function updateLayoutState() {
766
+ mainContainer.classList.toggle("centered", !hasGeneratedContent);
767
+ }
768
+
769
+ // --- Session, Version, and UI Initialization ---
770
+ function saveVersion(versionData) {
771
+ versionHistory.push(versionData);
772
+ promptHistory = versionHistory.map((v) => v.prompt);
773
+ updateVersionHistoryUI();
774
+ }
775
+
776
+ function updateVersionHistoryUI() {
777
+ versionHistoryControls.classList.toggle(
778
+ "hidden",
779
+ versionHistory.length === 0
780
+ );
781
+ versionHistorySelect.innerHTML = versionHistory
782
+ .map(
783
+ (version, index) =>
784
+ `<option value="${index}">V${index + 1}: ${version.prompt.substring(
785
+ 0,
786
+ 50
787
+ )}...</option>`
788
+ )
789
+ .reverse()
790
+ .join("");
791
+ versionHistorySelect.value = versionHistory.length - 1;
792
+ }
793
+
794
+ function loadVersion(index) {
795
+ const version = versionHistory[index];
796
+ if (!version) return;
797
+ if (monacoEditor) {
798
+ monacoEditor.setValue(version.html || "");
799
+ }
800
+ updateIframe(version.html || "");
801
+ clearInfoPanels();
802
+ if (version.analysis) {
803
+ analysisContainer.classList.remove("hidden");
804
+ aiAnalysis.innerHTML = marked.parse(version.analysis);
805
+ }
806
+ if (version.changes) {
807
+ changesContainer.classList.remove("hidden");
808
+ summaryOfChanges.innerHTML = marked.parse(version.changes);
809
+ }
810
+ if (version.instructions) {
811
+ instructionsContainer.classList.remove("hidden");
812
+ gameInstructions.innerHTML = marked.parse(version.instructions);
813
+ }
814
+ promptHistory = versionHistory.slice(0, index + 1).map((v) => v.prompt);
815
+ }
816
+
817
+ versionHistorySelect.addEventListener("change", (e) =>
818
+ loadVersion(Number(e.target.value))
819
+ );
820
+
821
+ async function populateSessionDropdown() {
822
+ try {
823
+ const sessions = await getSessionsFromDB();
824
+ if (sessions.length === 0) {
825
+ sessionSelect.innerHTML = '<option value="">No sessions found</option>';
826
+ return;
827
+ }
828
+ sessionSelect.innerHTML = sessions
829
+ .sort((a, b) => new Date(b.savedAt) - new Date(a.savedAt))
830
+ .map(
831
+ (s) =>
832
+ `<option value="${s.id}">${s.name} - ${new Date(
833
+ s.savedAt
834
+ ).toLocaleDateString()}</option>`
835
+ )
836
+ .join("");
837
+ } catch (error) {
838
+ console.error("Error loading sessions:", error);
839
+ sessionSelect.innerHTML =
840
+ '<option value="">Error loading sessions</option>';
841
+ }
842
+ }
843
+
844
+ async function saveCurrentSession() {
845
+ const sessionName = sessionNameInput.value.trim();
846
+ if (!sessionName) return alert("Please enter a session name.");
847
+ if (versionHistory.length === 0) return alert("Nothing to save.");
848
+
849
+ try {
850
+ saveSessionActionBtn.disabled = true;
851
+ saveSessionActionBtn.innerHTML =
852
+ '<i class="fas fa-spinner fa-spin"></i> Saving...';
853
+
854
+ const assets = [];
855
+ for (const [fileName, asset] of clientSideAssets.entries()) {
856
+ const arrayBuffer = await asset.file.arrayBuffer();
857
+ assets.push({ fileName, data: arrayBuffer, type: asset.file.type });
858
+ }
859
+
860
+ const sessionData = {
861
+ id: currentSessionId || Date.now(),
862
+ name: sessionName,
863
+ history: versionHistory,
864
+ assets,
865
+ savedAt: new Date().toISOString(),
866
+ };
867
+
868
+ await saveSessionToDB(sessionData);
869
+ currentSessionId = sessionData.id;
870
+
871
+ saveSessionActionBtn.innerHTML = '<i class="fas fa-check"></i> Saved!';
872
+ setTimeout(() => {
873
+ closePopup(saveSessionPopup);
874
+ saveSessionActionBtn.disabled = false;
875
+ saveSessionActionBtn.innerHTML = '<i class="fas fa-save"></i> Save';
876
+ }, 1500);
877
+ } catch (error) {
878
+ console.error("Error saving session:", error);
879
+ alert(`Failed to save session: ${error.message}`);
880
+ saveSessionActionBtn.disabled = false;
881
+ saveSessionActionBtn.innerHTML = '<i class="fas fa-save"></i> Save';
882
+ }
883
+ }
884
+
885
+ async function loadSelectedSession() {
886
+ const sessionId = sessionSelect.value;
887
+ if (!sessionId) return alert("Please select a session to load.");
888
+ try {
889
+ const sessions = await getSessionsFromDB();
890
+ const session = sessions.find((s) => s.id === Number(sessionId));
891
+ if (!session) return alert("Session not found.");
892
+
893
+ versionHistory = session.history || [];
894
+ currentSessionId = session.id;
895
+ sessionNameInput.value = session.name;
896
+
897
+ clientSideAssets.forEach((asset) => URL.revokeObjectURL(asset.blobUrl));
898
+ clientSideAssets.clear();
899
+ if (session.assets) {
900
+ await deserializeAssets(session.assets);
901
+ }
902
+
903
+ updateVersionHistoryUI();
904
+ if (versionHistory.length > 0) {
905
+ loadVersion(versionHistory.length - 1);
906
+ }
907
+ showModificationPanel();
908
+ hasGeneratedContent = true;
909
+ updateLayoutState();
910
+ closePopup(loadSessionPopup);
911
+ } catch (error) {
912
+ console.error("Failed to load session:", error);
913
+ alert(`Failed to load session: ${error.message}`);
914
+ }
915
+ }
916
+
917
+ async function deleteSelectedSession() {
918
+ const sessionId = sessionSelect.value;
919
+ if (
920
+ !sessionId ||
921
+ !confirm(
922
+ "Are you sure you want to delete this session? This cannot be undone."
923
+ )
924
+ )
925
+ return;
926
+ try {
927
+ await deleteSessionFromDB(Number(sessionId));
928
+ if (currentSessionId === Number(sessionId)) {
929
+ startNewSession();
930
+ }
931
+ await populateSessionDropdown();
932
+ } catch (error) {
933
+ console.error("Error deleting session:", error);
934
+ alert(`Failed to delete session: ${error.message}`);
935
+ }
936
+ }
937
+
938
+ function startNewSession() {
939
+ versionHistory = [];
940
+ promptHistory = [];
941
+ currentSessionId = null;
942
+ sessionNameInput.value = "";
943
+ promptInput.value = "";
944
+ modificationInput.value = "";
945
+ clientSideAssets.forEach((asset) => URL.revokeObjectURL(asset.blobUrl));
946
+ clientSideAssets.clear();
947
+ renderSelectedFiles(generateFileList);
948
+ renderSelectedFiles(modifyFileList);
949
+ if (monacoEditor) monacoEditor.setValue("");
950
+ updateIframe("");
951
+ clearInfoPanels();
952
+ initialGenerationPanel.classList.remove("hidden");
953
+ modificationPanel.classList.add("hidden");
954
+ followupFloat.classList.remove("show");
955
+ versionHistoryControls.classList.add("hidden");
956
+ hasGeneratedContent = false;
957
+ updateLayoutState();
958
+ closePopup(loadSessionPopup);
959
+ }
960
+
961
+ saveSessionActionBtn.addEventListener("click", saveCurrentSession);
962
+ loadSessionActionBtn.addEventListener("click", loadSelectedSession);
963
+ deleteSessionBtn.addEventListener("click", deleteSelectedSession);
964
+ newSessionBtn.addEventListener("click", startNewSession);
965
+
966
+ // --- Popups and Mobile Menu ---
967
+ function openPopup(popup) {
968
+ popup.classList.remove("hidden");
969
+ }
970
+ function closePopup(popup) {
971
+ popup.classList.add("hidden");
972
+ }
973
+
974
+ loadSessionPopupClose.addEventListener("click", () =>
975
+ closePopup(loadSessionPopup)
976
+ );
977
+ saveSessionPopupClose.addEventListener("click", () =>
978
+ closePopup(saveSessionPopup)
979
+ );
980
+ followupPopupClose.addEventListener("click", () => closePopup(followupPopup));
981
+
982
+ [loadSessionPopup, saveSessionPopup, followupPopup].forEach((popup) => {
983
+ popup.addEventListener("click", (e) => {
984
+ if (e.target.classList.contains("popup-overlay")) closePopup(popup);
985
+ });
986
+ });
987
+
988
+ document.addEventListener("keydown", (e) => {
989
+ if (e.key === "Escape") {
990
+ [loadSessionPopup, saveSessionPopup, followupPopup].forEach(closePopup);
991
+ }
992
+ });
993
+
994
+ hamburgerBtn.addEventListener("click", () => {
995
+ sidebar.classList.toggle("open");
996
+ sidebarOverlay.classList.toggle("show");
997
+ });
998
+ sidebarOverlay.addEventListener("click", () => {
999
+ sidebar.classList.remove("open");
1000
+ sidebarOverlay.classList.remove("show");
1001
+ });
1002
+
1003
+ // --- Theme ---
1004
+ function initTheme() {
1005
+ const theme = localStorage.getItem("theme") || "dark";
1006
+ document.documentElement.setAttribute("data-theme", theme);
1007
+ updateThemeIcon(theme);
1008
+ updateMonacoTheme(theme);
1009
+ }
1010
+
1011
+ function toggleTheme() {
1012
+ const current = document.documentElement.getAttribute("data-theme");
1013
+ const next = current === "dark" ? "light" : "dark";
1014
+ document.documentElement.setAttribute("data-theme", next);
1015
+ localStorage.setItem("theme", next);
1016
+ updateThemeIcon(next);
1017
+ updateMonacoTheme(next);
1018
+ }
1019
+
1020
+ function updateThemeIcon(theme) {
1021
+ themeToggle.querySelector("i").className =
1022
+ theme === "dark" ? "fas fa-sun" : "fas fa-moon";
1023
+ }
1024
+
1025
+ function updateMonacoTheme(theme) {
1026
+ if (monaco && monaco.editor) {
1027
+ monaco.editor.setTheme(theme === "dark" ? "vs-dark" : "vs");
1028
+ }
1029
+ }
1030
+
1031
+ themeToggle.addEventListener("click", toggleTheme);
1032
+
1033
+ // --- Initialization ---
1034
+ function initializeMonacoEditor() {
1035
+ if (typeof require === "undefined") {
1036
+ console.error("Monaco Editor loader not available.");
1037
+ return;
1038
+ }
1039
+ require.config({
1040
+ paths: { vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs" },
1041
+ });
1042
+ require(["vs/editor/editor.main"], () => {
1043
+ monaco = window.monaco;
1044
+ const theme = localStorage.getItem("theme") || "dark";
1045
+ monacoEditor = monaco.editor.create(monacoContainer, {
1046
+ value:
1047
+ "// AI code will appear here. Describe your idea to get started!",
1048
+ language: "html",
1049
+ theme: theme === "dark" ? "vs-dark" : "vs",
1050
+ automaticLayout: true,
1051
+ minimap: { enabled: false },
1052
+ scrollBeyondLastLine: false,
1053
+ wordWrap: "on",
1054
+ });
1055
+
1056
+ monacoEditor.onDidScrollChange((e) => {
1057
+ const { scrollTop, scrollHeight, height } = e;
1058
+ autoScrollEnabled = scrollTop + height >= scrollHeight - 50;
1059
+ });
1060
+ });
1061
+ }
1062
+
1063
+ async function initializeApp() {
1064
+ if (window.marked) {
1065
+ marked = window.marked;
1066
+ } else {
1067
+ console.warn("Marked.js not loaded. AI thoughts will be unformatted.");
1068
+ }
1069
+ await initDB();
1070
+ initTheme();
1071
+ initializeMonacoEditor();
1072
+ updateLayoutState();
1073
+ populateSessionDropdown();
1074
+ }
1075
+
1076
+ followupBtn.addEventListener("click", () => openPopup(followupPopup));
1077
+
1078
+ function showNotification(message, type) {
1079
+ // A simple alert, can be replaced with a more sophisticated notification system
1080
+ alert(`[${type.toUpperCase()}] ${message}`);
1081
+ }
1082
+
1083
+ initializeApp();
1084
+ });
static/style.css ADDED
@@ -0,0 +1,1371 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Reset and Base Styles */
2
+ * {
3
+ margin: 0;
4
+ padding: 0;
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ :root {
9
+ /* Design System Colors */
10
+ --bg-primary: #f9fafb;
11
+ --bg-secondary: #ffffff;
12
+ --bg-tertiary: #f3f4f6;
13
+ --text-primary: #1f2937;
14
+ --text-secondary: #6b7280;
15
+ --text-muted: #9ca3af;
16
+ --primary: #0891b2;
17
+ --primary-hover: #0e7490;
18
+ --accent: #f59e0b;
19
+ --accent-hover: #d97706;
20
+ --success: #10b981;
21
+ --danger: #ef4444;
22
+ --warning: #f59e0b;
23
+ --border: #e5e7eb;
24
+ --border-light: #f3f4f6;
25
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
26
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
27
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1),
28
+ 0 4px 6px -4px rgb(0 0 0 / 0.1);
29
+ --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1),
30
+ 0 8px 10px -6px rgb(0 0 0 / 0.1);
31
+ --radius: 8px;
32
+ --radius-sm: 4px;
33
+ --radius-lg: 12px;
34
+ --transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
35
+ --transition-slow: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
36
+ --muted-foreground: #9ca3af;
37
+ --foreground: #1f2937;
38
+ --card: #ffffff;
39
+ }
40
+
41
+ [data-theme="dark"] {
42
+ --bg-primary: #0f172a;
43
+ --bg-secondary: #1e293b;
44
+ --bg-tertiary: #334155;
45
+ --text-primary: #f1f5f9;
46
+ --text-secondary: #cbd5e1;
47
+ --text-muted: #94a3b8;
48
+ --primary: #0ea5e9;
49
+ --primary-hover: #0284c7;
50
+ --accent: #fbbf24;
51
+ --accent-hover: #f59e0b;
52
+ --border: #334155;
53
+ --border-light: #475569;
54
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
55
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.3);
56
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.3),
57
+ 0 4px 6px -4px rgb(0 0 0 / 0.3);
58
+ --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.3),
59
+ 0 8px 10px -6px rgb(0 0 0 / 0.3);
60
+ --radius: 8px;
61
+ --radius-sm: 4px;
62
+ --radius-lg: 12px;
63
+ --transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
64
+ --transition-slow: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
65
+ --muted-foreground: #94a3b8;
66
+ --foreground: #f1f5f9;
67
+ --card: #1e293b;
68
+ }
69
+
70
+ body {
71
+ font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
72
+ sans-serif;
73
+ background: var(--bg-primary);
74
+ color: var(--text-primary);
75
+ line-height: 1.6;
76
+ overflow-x: hidden;
77
+ }
78
+
79
+ /* Header */
80
+ .header {
81
+ position: sticky;
82
+ top: 0;
83
+ z-index: 100;
84
+ background: rgba(255, 255, 255, 0.8);
85
+ backdrop-filter: blur(12px);
86
+ border-bottom: 1px solid var(--border);
87
+ transition: var(--transition);
88
+ }
89
+
90
+ [data-theme="dark"] .header {
91
+ background: rgba(15, 23, 42, 0.8);
92
+ }
93
+
94
+ .header-content {
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: space-between;
98
+ padding: 1rem 2rem;
99
+ max-width: 100%;
100
+ }
101
+
102
+ .logo {
103
+ display: flex;
104
+ align-items: center;
105
+ gap: 0.75rem;
106
+ font-family: "Work Sans", sans-serif;
107
+ font-weight: 700;
108
+ font-size: 1.5rem;
109
+ color: var(--primary);
110
+ }
111
+
112
+ .logo i {
113
+ font-size: 1.75rem;
114
+ background: linear-gradient(135deg, var(--primary), var(--accent));
115
+ -webkit-background-clip: text;
116
+ -webkit-text-fill-color: transparent;
117
+ background-clip: text;
118
+ }
119
+
120
+ .header-actions {
121
+ display: flex;
122
+ align-items: center;
123
+ gap: 0.5rem;
124
+ }
125
+
126
+ .theme-toggle {
127
+ background: var(--bg-secondary);
128
+ border: 1px solid var(--border);
129
+ border-radius: var(--radius);
130
+ padding: 0.5rem;
131
+ cursor: pointer;
132
+ transition: var(--transition);
133
+ color: var(--text-secondary);
134
+ }
135
+
136
+ .theme-toggle:hover {
137
+ background: var(--bg-tertiary);
138
+ color: var(--text-primary);
139
+ transform: translateY(-1px);
140
+ }
141
+
142
+ .session-btn {
143
+ display: inline-flex;
144
+ align-items: center;
145
+ gap: 0.5rem;
146
+ padding: 0.5rem 0.75rem;
147
+ background: rgba(255, 255, 255, 0.1);
148
+ border: 1px solid rgba(255, 255, 255, 0.2);
149
+ border-radius: var(--radius);
150
+ color: var(--text-primary);
151
+ font-size: 0.875rem;
152
+ cursor: pointer;
153
+ transition: var(--transition);
154
+ }
155
+
156
+ .session-btn:hover {
157
+ background: rgba(255, 255, 255, 0.2);
158
+ transform: translateY(-1px);
159
+ }
160
+
161
+ .session-btn svg {
162
+ width: 16px;
163
+ height: 16px;
164
+ }
165
+
166
+ [data-theme="dark"] .session-btn {
167
+ background: rgba(30, 41, 59, 0.3);
168
+ border: 1px solid rgba(148, 163, 184, 0.2);
169
+ }
170
+
171
+ [data-theme="dark"] .session-btn:hover {
172
+ background: rgba(30, 41, 59, 0.5);
173
+ }
174
+
175
+ /* Main Layout */
176
+ .main-container {
177
+ display: grid;
178
+ grid-template-columns: 400px 1fr;
179
+ height: calc(100vh - 73px);
180
+ gap: 0;
181
+ transition: grid-template-columns 0.3s ease;
182
+ }
183
+
184
+ .main-container.centered {
185
+ grid-template-columns: 1fr;
186
+ justify-items: center;
187
+ }
188
+
189
+ .main-container.centered .main-content {
190
+ display: none;
191
+ }
192
+
193
+ .main-container.centered .sidebar {
194
+ max-width: 500px;
195
+ width: 100%;
196
+ border-right: none;
197
+ border-radius: var(--radius-lg);
198
+ margin: 2rem;
199
+ }
200
+
201
+ /* Sidebar */
202
+ .sidebar {
203
+ background: rgba(255, 255, 255, 0.05);
204
+ backdrop-filter: blur(10px);
205
+ border-right: 1px solid rgba(255, 255, 255, 0.1);
206
+ overflow-y: auto;
207
+ scrollbar-width: thin;
208
+ scrollbar-color: var(--border) transparent;
209
+ }
210
+
211
+ [data-theme="dark"] .sidebar {
212
+ background: rgba(30, 41, 59, 0.1);
213
+ border-right: 1px solid rgba(148, 163, 184, 0.1);
214
+ }
215
+
216
+ .sidebar::-webkit-scrollbar {
217
+ width: 6px;
218
+ }
219
+
220
+ .sidebar::-webkit-scrollbar-track {
221
+ background: transparent;
222
+ }
223
+
224
+ .sidebar::-webkit-scrollbar-thumb {
225
+ background: var(--border);
226
+ border-radius: 3px;
227
+ }
228
+
229
+ .sidebar-content {
230
+ padding: 1.5rem;
231
+ display: flex;
232
+ flex-direction: column;
233
+ gap: 1.5rem;
234
+ }
235
+
236
+ /* Panels */
237
+ .panel {
238
+ background: var(--bg-secondary);
239
+ border: 1px solid var(--border);
240
+ border-radius: var(--radius-lg);
241
+ overflow: hidden;
242
+ transition: var(--transition);
243
+ box-shadow: var(--shadow-sm);
244
+ animation: fadeIn 0.3s ease-out;
245
+ transform: translateY(0);
246
+ }
247
+
248
+ [data-theme="dark"] .panel {
249
+ background: var(--bg-tertiary);
250
+ }
251
+
252
+ .panel:hover {
253
+ box-shadow: var(--shadow-md);
254
+ transform: translateY(-2px);
255
+ }
256
+
257
+ .panel-header {
258
+ background: var(--bg-tertiary);
259
+ padding: 1rem 1.25rem;
260
+ border-bottom: 1px solid var(--border);
261
+ }
262
+
263
+ .panel-header h3 {
264
+ font-family: "Work Sans", sans-serif;
265
+ font-weight: 600;
266
+ font-size: 1rem;
267
+ color: var(--text-primary);
268
+ display: flex;
269
+ align-items: center;
270
+ gap: 0.5rem;
271
+ }
272
+
273
+ .panel-header i {
274
+ color: var(--primary);
275
+ font-size: 0.875rem;
276
+ }
277
+
278
+ .panel-content {
279
+ padding: 1.25rem;
280
+ display: flex;
281
+ flex-direction: column;
282
+ gap: 1rem;
283
+ }
284
+
285
+ /* Form Elements */
286
+ .text-input,
287
+ .textarea-input,
288
+ .select-input {
289
+ width: 100%;
290
+ padding: 0.75rem;
291
+ border: 1px solid var(--border);
292
+ border-radius: var(--radius);
293
+ background: var(--bg-primary);
294
+ color: var(--text-primary);
295
+ font-size: 0.875rem;
296
+ transition: var(--transition);
297
+ font-family: inherit;
298
+ }
299
+
300
+ .text-input:focus,
301
+ .textarea-input:focus,
302
+ .select-input:focus {
303
+ outline: none;
304
+ border-color: var(--primary);
305
+ box-shadow: 0 0 0 3px rgba(8, 145, 178, 0.1);
306
+ }
307
+
308
+ .textarea-input {
309
+ resize: vertical;
310
+ min-height: 100px;
311
+ line-height: 1.5;
312
+ }
313
+
314
+ .select-input {
315
+ cursor: pointer;
316
+ }
317
+
318
+ /* Buttons */
319
+ .btn {
320
+ display: inline-flex;
321
+ align-items: center;
322
+ justify-content: center;
323
+ gap: 0.5rem;
324
+ padding: 0.75rem 1rem;
325
+ border: none;
326
+ border-radius: var(--radius);
327
+ font-size: 0.875rem;
328
+ font-weight: 500;
329
+ cursor: pointer;
330
+ transition: var(--transition);
331
+ text-decoration: none;
332
+ font-family: inherit;
333
+ white-space: nowrap;
334
+ }
335
+
336
+ .btn:disabled {
337
+ opacity: 0.5;
338
+ cursor: not-allowed;
339
+ transform: none !important;
340
+ }
341
+
342
+ .btn-primary {
343
+ background: var(--primary);
344
+ color: white;
345
+ }
346
+
347
+ .btn-primary:hover:not(:disabled) {
348
+ background: var(--primary-hover);
349
+ transform: translateY(-1px);
350
+ box-shadow: var(--shadow-md);
351
+ }
352
+
353
+ .btn-secondary {
354
+ background: var(--bg-tertiary);
355
+ color: var(--text-primary);
356
+ border: 1px solid var(--border);
357
+ }
358
+
359
+ .btn-secondary:hover:not(:disabled) {
360
+ background: var(--border);
361
+ transform: translateY(-1px);
362
+ }
363
+
364
+ .btn-accent {
365
+ background: var(--accent);
366
+ color: white;
367
+ }
368
+
369
+ .btn-accent:hover:not(:disabled) {
370
+ background: var(--accent-hover);
371
+ transform: translateY(-1px);
372
+ box-shadow: var(--shadow-md);
373
+ }
374
+
375
+ .btn-destructive {
376
+ background: var(--danger);
377
+ color: white;
378
+ }
379
+
380
+ .btn-destructive:hover:not(:disabled) {
381
+ background: #dc2626;
382
+ transform: translateY(-1px);
383
+ }
384
+
385
+ .btn-outline {
386
+ background: transparent;
387
+ color: var(--text-primary);
388
+ border: 1px solid var(--border);
389
+ }
390
+
391
+ .btn-outline:hover:not(:disabled) {
392
+ background: var(--bg-tertiary);
393
+ transform: translateY(-1px);
394
+ }
395
+
396
+ .btn-ghost {
397
+ background: transparent;
398
+ color: var(--text-secondary);
399
+ border: none;
400
+ }
401
+
402
+ .btn-ghost:hover:not(:disabled) {
403
+ background: var(--bg-tertiary);
404
+ color: var(--text-primary);
405
+ }
406
+
407
+ .btn-large {
408
+ padding: 1rem 1.5rem;
409
+ font-size: 1rem;
410
+ font-weight: 600;
411
+ }
412
+
413
+ /* Button Layouts */
414
+ .button-row {
415
+ display: flex;
416
+ gap: 0.75rem;
417
+ }
418
+
419
+ .button-row .btn {
420
+ flex: 1;
421
+ }
422
+
423
+ .button-grid {
424
+ display: grid;
425
+ grid-template-columns: 1fr 1fr;
426
+ gap: 0.75rem;
427
+ }
428
+
429
+ .button-grid .btn:first-child {
430
+ grid-column: 1 / -1;
431
+ }
432
+
433
+ .input-group {
434
+ display: flex;
435
+ gap: 0.75rem;
436
+ }
437
+
438
+ .input-group .text-input {
439
+ flex: 1;
440
+ }
441
+
442
+ /* Console */
443
+ .console-container {
444
+ margin-top: 0.5rem;
445
+ }
446
+
447
+ .console-label {
448
+ display: flex;
449
+ align-items: center;
450
+ gap: 0.5rem;
451
+ font-size: 0.75rem;
452
+ color: var(--text-muted);
453
+ margin-bottom: 0.5rem;
454
+ font-weight: 500;
455
+ }
456
+
457
+ .console-output {
458
+ width: 100%;
459
+ height: 120px;
460
+ padding: 0.75rem;
461
+ background: #1a1a1a;
462
+ color: #00ff00;
463
+ border: 1px solid var(--border);
464
+ border-radius: var(--radius);
465
+ font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
466
+ font-size: 0.75rem;
467
+ line-height: 1.4;
468
+ resize: vertical;
469
+ }
470
+
471
+ /* Version History */
472
+ .version-history {
473
+ margin-bottom: 1rem;
474
+ }
475
+
476
+ .version-history-compact {
477
+ margin-bottom: 1rem;
478
+ padding: 0.75rem;
479
+ background: rgba(255, 255, 255, 0.05);
480
+ border: 1px solid rgba(255, 255, 255, 0.1);
481
+ border-radius: 0.5rem;
482
+ backdrop-filter: blur(10px);
483
+ }
484
+
485
+ .version-history-header {
486
+ display: flex;
487
+ align-items: center;
488
+ gap: 0.5rem;
489
+ margin-bottom: 0.5rem;
490
+ font-size: 0.875rem;
491
+ font-weight: 500;
492
+ color: var(--muted-foreground);
493
+ }
494
+
495
+ .version-history-header svg {
496
+ color: var(--primary);
497
+ }
498
+
499
+ .select-input-compact {
500
+ width: 100%;
501
+ padding: 0.5rem;
502
+ background: rgba(255, 255, 255, 0.05);
503
+ border: 1px solid rgba(255, 255, 255, 0.1);
504
+ border-radius: 0.375rem;
505
+ color: var(--foreground);
506
+ font-size: 0.875rem;
507
+ transition: all 0.2s ease;
508
+ }
509
+
510
+ .select-input-compact:focus {
511
+ outline: none;
512
+ border-color: var(--primary);
513
+ box-shadow: 0 0 0 2px rgba(var(--primary), 0.2);
514
+ }
515
+
516
+ /* Info Panels */
517
+ .info-panels {
518
+ display: flex;
519
+ flex-direction: column;
520
+ gap: 0.75rem;
521
+ margin-bottom: 1rem;
522
+ }
523
+
524
+ .info-panel {
525
+ border-radius: var(--radius);
526
+ overflow: hidden;
527
+ transition: all 0.3s ease;
528
+ }
529
+
530
+ .info-panel.minimized {
531
+ opacity: 0.7;
532
+ }
533
+
534
+ .info-panel.minimized:not([open]) {
535
+ transform: scale(0.98);
536
+ }
537
+
538
+ .info-panel-dull {
539
+ opacity: 0.8;
540
+ }
541
+
542
+ .info-header {
543
+ display: flex;
544
+ align-items: center;
545
+ gap: 0.75rem;
546
+ padding: 0.75rem 1rem;
547
+ background: var(--bg-secondary);
548
+ border: 1px solid var(--border);
549
+ cursor: pointer;
550
+ font-weight: 500;
551
+ color: var(--text-primary);
552
+ transition: all 0.2s ease;
553
+ }
554
+
555
+ .info-header:hover {
556
+ background: var(--bg-tertiary);
557
+ border-color: var(--primary);
558
+ }
559
+
560
+ .info-header i:last-child {
561
+ margin-left: auto;
562
+ transition: transform 0.2s ease;
563
+ }
564
+
565
+ .info-panel[open] .info-header i:last-child {
566
+ transform: rotate(180deg);
567
+ }
568
+
569
+ .info-content {
570
+ padding: 1rem;
571
+ background: var(--bg-primary);
572
+ border: 1px solid var(--border);
573
+ border-top: none;
574
+ font-size: 0.875rem;
575
+ line-height: 1.6;
576
+ padding-left: 2rem;
577
+ }
578
+
579
+ /* Auto-scroll styles for info panels */
580
+ .auto-scroll {
581
+ max-height: 200px;
582
+ overflow-y: auto;
583
+ scrollbar-width: thin;
584
+ scrollbar-color: var(--border) transparent;
585
+ }
586
+
587
+ .auto-scroll::-webkit-scrollbar {
588
+ width: 4px;
589
+ }
590
+
591
+ .auto-scroll::-webkit-scrollbar-track {
592
+ background: transparent;
593
+ }
594
+
595
+ .auto-scroll::-webkit-scrollbar-thumb {
596
+ background: var(--border);
597
+ border-radius: 2px;
598
+ }
599
+
600
+ /* Main Content */
601
+ .main-content {
602
+ display: flex;
603
+ flex-direction: column;
604
+ background: var(--bg-primary);
605
+ overflow: hidden;
606
+ }
607
+
608
+ .content-header {
609
+ background: rgba(255, 255, 255, 0.1);
610
+ backdrop-filter: blur(10px);
611
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
612
+ display: flex;
613
+ align-items: center;
614
+ justify-content: space-between;
615
+ padding: 1rem 2rem;
616
+ }
617
+
618
+ [data-theme="dark"] .content-header {
619
+ background: rgba(30, 41, 59, 0.3);
620
+ border-bottom: 1px solid rgba(148, 163, 184, 0.1);
621
+ }
622
+
623
+ .view-tabs {
624
+ display: flex;
625
+ background: var(--bg-primary);
626
+ border-radius: var(--radius);
627
+ padding: 0.25rem;
628
+ gap: 0.25rem;
629
+ }
630
+
631
+ .tab-btn {
632
+ display: flex;
633
+ align-items: center;
634
+ gap: 0.5rem;
635
+ padding: 0.5rem 1rem;
636
+ background: transparent;
637
+ border: none;
638
+ border-radius: var(--radius-sm);
639
+ color: var(--text-secondary);
640
+ font-size: 0.875rem;
641
+ font-weight: 500;
642
+ cursor: pointer;
643
+ transition: var(--transition);
644
+ font-family: inherit;
645
+ }
646
+
647
+ .tab-btn:hover {
648
+ background: var(--bg-tertiary);
649
+ color: var(--text-primary);
650
+ }
651
+
652
+ .tab-btn.active {
653
+ background: var(--primary);
654
+ color: white;
655
+ box-shadow: var(--shadow-sm);
656
+ }
657
+
658
+ .content-actions {
659
+ display: flex;
660
+ align-items: center;
661
+ gap: 0.5rem;
662
+ }
663
+
664
+ .action-btn {
665
+ display: inline-flex;
666
+ align-items: center;
667
+ justify-content: center;
668
+ width: 40px;
669
+ height: 40px;
670
+ background: rgba(255, 255, 255, 0.1);
671
+ border: 1px solid rgba(255, 255, 255, 0.2);
672
+ border-radius: var(--radius);
673
+ color: var(--text-secondary);
674
+ cursor: pointer;
675
+ transition: var(--transition);
676
+ }
677
+
678
+ .action-btn:hover {
679
+ background: rgba(255, 255, 255, 0.2);
680
+ color: var(--text-primary);
681
+ transform: translateY(-1px);
682
+ }
683
+
684
+ .action-btn svg {
685
+ width: 18px;
686
+ height: 18px;
687
+ }
688
+
689
+ [data-theme="dark"] .action-btn {
690
+ background: rgba(30, 41, 59, 0.3);
691
+ border: 1px solid rgba(148, 163, 184, 0.2);
692
+ }
693
+
694
+ [data-theme="dark"] .action-btn:hover {
695
+ background: rgba(30, 41, 59, 0.5);
696
+ }
697
+
698
+ .content-body {
699
+ flex: 1;
700
+ position: relative;
701
+ overflow: hidden;
702
+ }
703
+
704
+ /* Code Editor */
705
+ .code-container {
706
+ position: absolute;
707
+ inset: 0;
708
+ display: flex;
709
+ flex-direction: column;
710
+ overflow: hidden;
711
+ }
712
+
713
+ .code-editor-wrapper {
714
+ flex: 1;
715
+ position: relative;
716
+ background: #1e1e1e;
717
+ border-radius: var(--radius);
718
+ margin: 1rem;
719
+ overflow: hidden;
720
+ box-shadow: var(--shadow-lg);
721
+ min-height: 0;
722
+ height: 100%;
723
+ min-height: 500px;
724
+ max-height: calc(100vh - 200px);
725
+ }
726
+
727
+ .monaco-editor-container {
728
+ width: 100%;
729
+ height: 100%;
730
+ min-height: 500px;
731
+ max-height: calc(100vh - 200px);
732
+ overflow: hidden;
733
+ }
734
+
735
+ /* Preview */
736
+ .preview-container {
737
+ position: absolute;
738
+ inset: 0;
739
+ display: flex;
740
+ flex-direction: column;
741
+ overflow: hidden;
742
+ }
743
+
744
+ .preview-wrapper {
745
+ flex: 1;
746
+ position: relative;
747
+ background: white;
748
+ border-radius: var(--radius);
749
+ margin: 1rem;
750
+ overflow: hidden;
751
+ box-shadow: var(--shadow-lg);
752
+ min-height: 0;
753
+ height: 100%;
754
+ min-height: 500px;
755
+ max-height: calc(100vh - 200px);
756
+ }
757
+
758
+ .preview-iframe {
759
+ width: 100%;
760
+ height: 100%;
761
+ border: none;
762
+ background: white;
763
+ overflow: auto;
764
+ min-height: 500px;
765
+ max-height: calc(100vh - 200px);
766
+ display: block;
767
+ scrollbar-width: thin;
768
+ scrollbar-color: #888 #f1f1f1;
769
+ }
770
+
771
+ .preview-iframe::-webkit-scrollbar {
772
+ width: 8px;
773
+ }
774
+
775
+ .preview-iframe::-webkit-scrollbar-track {
776
+ background: #f1f1f1;
777
+ }
778
+
779
+ .preview-iframe::-webkit-scrollbar-thumb {
780
+ background: #888;
781
+ border-radius: 4px;
782
+ }
783
+
784
+ .preview-iframe::-webkit-scrollbar-thumb:hover {
785
+ background: #555;
786
+ }
787
+
788
+ .preview-placeholder {
789
+ position: absolute;
790
+ inset: 0;
791
+ display: flex;
792
+ align-items: center;
793
+ justify-content: center;
794
+ background: var(--bg-secondary);
795
+ z-index: 1;
796
+ pointer-events: none;
797
+ }
798
+
799
+ .preview-iframe:not([src=""]) + .preview-placeholder,
800
+ .preview-iframe[srcdoc]:not([srcdoc=""]) + .preview-placeholder {
801
+ display: none;
802
+ }
803
+
804
+ .placeholder-content {
805
+ text-align: center;
806
+ color: var(--text-muted);
807
+ }
808
+
809
+ .placeholder-content i {
810
+ font-size: 3rem;
811
+ color: var(--primary);
812
+ margin-bottom: 1rem;
813
+ opacity: 0.5;
814
+ }
815
+
816
+ .placeholder-content h3 {
817
+ font-size: 1.25rem;
818
+ font-weight: 600;
819
+ color: var(--text-secondary);
820
+ margin-bottom: 0.5rem;
821
+ }
822
+
823
+ .placeholder-content p {
824
+ font-size: 0.875rem;
825
+ max-width: 300px;
826
+ }
827
+
828
+ /* Utility Classes */
829
+ .hidden {
830
+ display: none !important;
831
+ }
832
+
833
+ /* Responsive Design */
834
+ @media (max-width: 1024px) {
835
+ .main-container {
836
+ grid-template-columns: 350px 1fr;
837
+ }
838
+
839
+ .header-content {
840
+ padding: 1rem;
841
+ }
842
+
843
+ .content-header {
844
+ padding: 1rem;
845
+ }
846
+ }
847
+
848
+ @media (max-width: 768px) {
849
+ /* Show hamburger menu on mobile */
850
+ .hamburger-btn {
851
+ display: flex;
852
+ }
853
+
854
+ .main-container {
855
+ grid-template-columns: 1fr;
856
+ }
857
+
858
+ /* Mobile sidebar as overlay */
859
+ .sidebar {
860
+ position: fixed;
861
+ top: 0;
862
+ left: -100%;
863
+ width: 320px;
864
+ height: 100vh;
865
+ z-index: 200;
866
+ transition: left 0.3s ease;
867
+ border-right: 1px solid var(--border);
868
+ border-bottom: none;
869
+ max-height: none;
870
+ }
871
+
872
+ .sidebar.open {
873
+ left: 0;
874
+ }
875
+
876
+ .sidebar-overlay.show {
877
+ display: block;
878
+ }
879
+
880
+ .content-header {
881
+ flex-direction: column;
882
+ gap: 1rem;
883
+ align-items: stretch;
884
+ }
885
+
886
+ .view-tabs {
887
+ justify-content: center;
888
+ }
889
+
890
+ .content-actions {
891
+ justify-content: center;
892
+ }
893
+
894
+ /* Hide text labels on mobile for session buttons */
895
+ .session-btn span {
896
+ display: none;
897
+ }
898
+
899
+ .session-btn {
900
+ padding: 0.5rem;
901
+ min-width: 40px;
902
+ }
903
+
904
+ .followup-float {
905
+ bottom: 1rem;
906
+ right: 1rem;
907
+ }
908
+
909
+ .followup-btn {
910
+ width: 48px;
911
+ height: 48px;
912
+ font-size: 1.1rem;
913
+ }
914
+ }
915
+
916
+ @media (max-width: 480px) {
917
+ .header-content {
918
+ padding: 0.75rem 1rem;
919
+ }
920
+
921
+ .logo span {
922
+ display: none;
923
+ }
924
+
925
+ .sidebar {
926
+ width: 280px;
927
+ }
928
+
929
+ .action-btn {
930
+ width: 36px;
931
+ height: 36px;
932
+ }
933
+
934
+ .action-btn svg {
935
+ width: 16px;
936
+ height: 16px;
937
+ }
938
+ }
939
+
940
+ /* Animations */
941
+ @keyframes fadeIn {
942
+ from {
943
+ opacity: 0;
944
+ transform: translateY(10px);
945
+ }
946
+ to {
947
+ opacity: 1;
948
+ transform: translateY(0);
949
+ }
950
+ }
951
+
952
+ @keyframes slideIn {
953
+ from {
954
+ transform: translateX(-100%);
955
+ }
956
+ to {
957
+ transform: translateX(0);
958
+ }
959
+ }
960
+
961
+ @keyframes slideInUp {
962
+ from {
963
+ opacity: 0;
964
+ transform: translateY(30px);
965
+ }
966
+ to {
967
+ opacity: 1;
968
+ transform: translateY(0);
969
+ }
970
+ }
971
+
972
+ @keyframes bounceIn {
973
+ 0% {
974
+ opacity: 0;
975
+ transform: scale(0.3);
976
+ }
977
+ 50% {
978
+ opacity: 1;
979
+ transform: scale(1.05);
980
+ }
981
+ 70% {
982
+ transform: scale(0.9);
983
+ }
984
+ 100% {
985
+ opacity: 1;
986
+ transform: scale(1);
987
+ }
988
+ }
989
+
990
+ @keyframes float {
991
+ 0%,
992
+ 100% {
993
+ transform: translateY(0px);
994
+ }
995
+ 50% {
996
+ transform: translateY(-10px);
997
+ }
998
+ }
999
+
1000
+ /* Focus Styles */
1001
+ .btn:focus-visible,
1002
+ .text-input:focus,
1003
+ .textarea-input:focus,
1004
+ .select-input:focus {
1005
+ outline: 2px solid var(--primary);
1006
+ outline-offset: 2px;
1007
+ }
1008
+
1009
+ /* High Contrast Mode */
1010
+ @media (prefers-contrast: high) {
1011
+ :root {
1012
+ --border: #000000;
1013
+ --text-secondary: #000000;
1014
+ }
1015
+
1016
+ [data-theme="dark"] {
1017
+ --border: #ffffff;
1018
+ --text-secondary: #ffffff;
1019
+ }
1020
+ }
1021
+
1022
+ /* Reduced Motion */
1023
+ @media (prefers-reduced-motion: reduce) {
1024
+ *,
1025
+ *::before,
1026
+ *::after {
1027
+ animation-duration: 0.01ms !important;
1028
+ animation-iteration-count: 1 !important;
1029
+ transition-duration: 0.01ms !important;
1030
+ }
1031
+ }
1032
+
1033
+ /* Particle Background Styles */
1034
+ .particle-background {
1035
+ position: fixed;
1036
+ top: 0;
1037
+ left: 0;
1038
+ width: 100%;
1039
+ height: 100%;
1040
+ z-index: -1;
1041
+ pointer-events: none;
1042
+ }
1043
+
1044
+ #particle-canvas {
1045
+ width: 100%;
1046
+ height: 100%;
1047
+ }
1048
+
1049
+ /* Glassmorphism Effect */
1050
+ .glass-effect {
1051
+ background: rgba(255, 255, 255, 0.1);
1052
+ backdrop-filter: blur(10px);
1053
+ border: 1px solid rgba(255, 255, 255, 0.2);
1054
+ box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
1055
+ }
1056
+
1057
+ [data-theme="dark"] .glass-effect {
1058
+ background: rgba(30, 41, 59, 0.3);
1059
+ border: 1px solid rgba(148, 163, 184, 0.2);
1060
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
1061
+ }
1062
+
1063
+ /* Session Popup Styles */
1064
+ .session-popup,
1065
+ .followup-popup {
1066
+ position: fixed;
1067
+ top: 0;
1068
+ left: 0;
1069
+ width: 100%;
1070
+ height: 100%;
1071
+ z-index: 1000;
1072
+ display: flex;
1073
+ align-items: center;
1074
+ justify-content: center;
1075
+ animation: fadeIn 0.3s ease-out;
1076
+ }
1077
+
1078
+ .popup-overlay {
1079
+ position: absolute;
1080
+ top: 0;
1081
+ left: 0;
1082
+ width: 100%;
1083
+ height: 100%;
1084
+ background: rgba(0, 0, 0, 0.5);
1085
+ backdrop-filter: blur(5px);
1086
+ }
1087
+
1088
+ .popup-content {
1089
+ position: relative;
1090
+ background: var(--bg-secondary);
1091
+ border-radius: var(--radius-lg);
1092
+ box-shadow: var(--shadow-xl);
1093
+ max-width: 500px;
1094
+ width: 90%;
1095
+ max-height: 80vh;
1096
+ overflow: hidden;
1097
+ animation: slideInUp 0.3s ease-out;
1098
+ }
1099
+
1100
+ .popup-header {
1101
+ display: flex;
1102
+ align-items: center;
1103
+ justify-content: space-between;
1104
+ padding: 1.5rem;
1105
+ background: var(--bg-tertiary);
1106
+ border-bottom: 1px solid var(--border);
1107
+ }
1108
+
1109
+ .popup-header h3 {
1110
+ font-family: "Work Sans", sans-serif;
1111
+ font-weight: 600;
1112
+ font-size: 1.125rem;
1113
+ color: var(--text-primary);
1114
+ display: flex;
1115
+ align-items: center;
1116
+ gap: 0.75rem;
1117
+ }
1118
+
1119
+ .popup-close {
1120
+ background: none;
1121
+ border: none;
1122
+ color: var(--text-muted);
1123
+ cursor: pointer;
1124
+ padding: 0.5rem;
1125
+ border-radius: var(--radius);
1126
+ transition: var(--transition);
1127
+ }
1128
+
1129
+ .popup-close:hover {
1130
+ background: var(--bg-primary);
1131
+ color: var(--text-primary);
1132
+ }
1133
+
1134
+ .popup-body {
1135
+ padding: 1.5rem;
1136
+ display: flex;
1137
+ flex-direction: column;
1138
+ gap: 1rem;
1139
+ max-height: 60vh;
1140
+ overflow-y: auto;
1141
+ }
1142
+
1143
+ /* Floating Follow-up Button */
1144
+ .followup-float {
1145
+ position: fixed;
1146
+ bottom: 2rem;
1147
+ right: 2rem;
1148
+ z-index: 100;
1149
+ opacity: 0;
1150
+ visibility: hidden;
1151
+ transform: translateY(20px);
1152
+ transition: all 0.3s ease;
1153
+ }
1154
+
1155
+ .followup-float.show {
1156
+ opacity: 1;
1157
+ visibility: visible;
1158
+ transform: translateY(0);
1159
+ }
1160
+
1161
+ .followup-btn {
1162
+ width: 56px;
1163
+ height: 56px;
1164
+ border-radius: 50%;
1165
+ background: var(--primary);
1166
+ color: white;
1167
+ border: none;
1168
+ cursor: pointer;
1169
+ display: flex;
1170
+ align-items: center;
1171
+ justify-content: center;
1172
+ font-size: 1.25rem;
1173
+ box-shadow: var(--shadow-lg);
1174
+ transition: all 0.2s ease;
1175
+ }
1176
+
1177
+ .followup-btn:hover {
1178
+ transform: scale(1.1);
1179
+ box-shadow: var(--shadow-xl);
1180
+ }
1181
+
1182
+ .followup-tooltip {
1183
+ position: absolute;
1184
+ bottom: 100%;
1185
+ right: 0;
1186
+ margin-bottom: 0.5rem;
1187
+ padding: 0.5rem 0.75rem;
1188
+ background: var(--bg-primary);
1189
+ border: 1px solid var(--border);
1190
+ border-radius: var(--radius);
1191
+ font-size: 0.875rem;
1192
+ white-space: nowrap;
1193
+ opacity: 0;
1194
+ visibility: hidden;
1195
+ transform: translateY(10px);
1196
+ transition: all 0.2s ease;
1197
+ }
1198
+
1199
+ .followup-float:hover .followup-tooltip {
1200
+ opacity: 1;
1201
+ visibility: visible;
1202
+ transform: translateY(0);
1203
+ }
1204
+
1205
+ /* Sidebar Overlay for Mobile */
1206
+ .sidebar-overlay {
1207
+ display: none;
1208
+ position: fixed;
1209
+ top: 0;
1210
+ left: 0;
1211
+ width: 100%;
1212
+ height: 100%;
1213
+ background: rgba(0, 0, 0, 0.5);
1214
+ z-index: 150;
1215
+ }
1216
+
1217
+ /* Hamburger Menu Styles */
1218
+ .hamburger-btn {
1219
+ display: none;
1220
+ flex-direction: column;
1221
+ justify-content: space-around;
1222
+ width: 24px;
1223
+ height: 24px;
1224
+ background: transparent;
1225
+ border: none;
1226
+ cursor: pointer;
1227
+ padding: 0;
1228
+ z-index: 10;
1229
+ }
1230
+
1231
+ .hamburger-btn span {
1232
+ display: block;
1233
+ height: 2px;
1234
+ width: 100%;
1235
+ background: var(--text-primary);
1236
+ border-radius: 1px;
1237
+ transition: var(--transition);
1238
+ }
1239
+
1240
+ .hamburger-btn.active span:nth-child(1) {
1241
+ transform: rotate(45deg) translate(5px, 5px);
1242
+ }
1243
+
1244
+ .hamburger-btn.active span:nth-child(2) {
1245
+ opacity: 0;
1246
+ }
1247
+
1248
+ .hamburger-btn.active span:nth-child(3) {
1249
+ transform: rotate(-45deg) translate(7px, -6px);
1250
+ }
1251
+
1252
+ /* Added version history styling for content header */
1253
+ .version-history-header-compact {
1254
+ display: flex;
1255
+ align-items: center;
1256
+ gap: 0.5rem;
1257
+ margin-right: 1rem;
1258
+ }
1259
+
1260
+ .version-history-header-compact svg {
1261
+ color: var(--primary);
1262
+ }
1263
+
1264
+ .select-input-header {
1265
+ padding: 0.375rem 0.5rem;
1266
+ background: rgba(255, 255, 255, 0.1);
1267
+ border: 1px solid rgba(255, 255, 255, 0.2);
1268
+ border-radius: var(--radius);
1269
+ color: var(--foreground);
1270
+ font-size: 0.875rem;
1271
+ min-width: 120px;
1272
+ transition: all 0.2s ease;
1273
+ }
1274
+
1275
+ .select-input-header:hover {
1276
+ background: rgba(255, 255, 255, 0.15);
1277
+ }
1278
+
1279
+ [data-theme="dark"] .select-input-header {
1280
+ background: rgba(30, 41, 59, 0.3);
1281
+ border: 1px solid rgba(148, 163, 184, 0.2);
1282
+ }
1283
+
1284
+ [data-theme="dark"] .select-input-header:hover {
1285
+ background: rgba(30, 41, 59, 0.5);
1286
+ }
1287
+
1288
+ /* File Upload Styles */
1289
+ .file-upload-container {
1290
+ margin-top: 0.5rem;
1291
+ display: flex;
1292
+ flex-direction: column;
1293
+ gap: 0.75rem;
1294
+ }
1295
+
1296
+ .file-upload-input {
1297
+ display: none;
1298
+ }
1299
+
1300
+ .file-upload-label {
1301
+ width: 100%;
1302
+ text-align: center;
1303
+ }
1304
+
1305
+ .file-upload-label-small {
1306
+ display: inline-flex;
1307
+ align-items: center;
1308
+ gap: 0.5rem;
1309
+ padding: 0.5rem 0.75rem;
1310
+ background: var(--bg-secondary);
1311
+ border: 1px solid var(--border);
1312
+ border-radius: var(--radius);
1313
+ color: var(--text-secondary);
1314
+ font-size: 0.875rem;
1315
+ cursor: pointer;
1316
+ transition: all 0.2s ease;
1317
+ text-decoration: none;
1318
+ }
1319
+
1320
+ .file-upload-label-small:hover {
1321
+ background: var(--bg-tertiary);
1322
+ border-color: var(--primary);
1323
+ color: var(--primary);
1324
+ }
1325
+
1326
+ .file-upload-label-small svg {
1327
+ width: 16px;
1328
+ height: 16px;
1329
+ }
1330
+
1331
+ .file-list {
1332
+ display: flex;
1333
+ flex-direction: column;
1334
+ gap: 0.5rem;
1335
+ max-height: 150px;
1336
+ overflow-y: auto;
1337
+ padding-right: 5px; /* for scrollbar */
1338
+ }
1339
+
1340
+ .file-item {
1341
+ display: flex;
1342
+ justify-content: space-between;
1343
+ align-items: center;
1344
+ padding: 0.5rem 0.75rem;
1345
+ background: var(--bg-tertiary);
1346
+ border-radius: var(--radius-sm);
1347
+ font-size: 0.8rem;
1348
+ color: var(--text-secondary);
1349
+ animation: fadeIn 0.2s ease-out;
1350
+ }
1351
+
1352
+ .file-item span {
1353
+ white-space: nowrap;
1354
+ overflow: hidden;
1355
+ text-overflow: ellipsis;
1356
+ margin-right: 0.5rem;
1357
+ }
1358
+
1359
+ .file-remove-btn {
1360
+ background: none;
1361
+ border: none;
1362
+ color: var(--text-muted);
1363
+ font-size: 1rem;
1364
+ cursor: pointer;
1365
+ padding: 0 0.25rem;
1366
+ line-height: 1;
1367
+ }
1368
+
1369
+ .file-remove-btn:hover {
1370
+ color: var(--danger);
1371
+ }