Faaz commited on
Commit
4386567
·
1 Parent(s): c596620

Session 5: Rebuild frontend as Vite+React 3-panel website builder IDE

Browse files

- Replaced vanilla JS chat with professional IDE (Bolt.new style)
- 3-panel layout: Sidebar (file tree + agent steps) | Editor (animated) | Preview (always-on)
- Plan Modal: asks tech stack + design style before generating
- Prompt Enhancer: transforms raw input into structured prompts
- Code animation: line-by-line fade-in at 15ms intervals
- File tree: real-time population during generation
- Live preview: always-visible iframe with console panel
- Demo fallback: landing page + dashboard demos when API quota exceeded
- Settings: API URL, HF token, temperature, max tokens (localStorage)
- ZeroGPU quota detection: isQuotaError() + isQuotaException() -> auto-fallback
- npm run build: 222KB JS (70KB gzip), 3.25s
- Updated context.md: full architecture + next steps for future sessions
- Updated README.md: current project state

README.md CHANGED
@@ -1,40 +1,37 @@
1
  # MINDI 1.5 Vision-Coder
2
 
3
- **Built by [MINDIGENOUS.AI](https://mindigenous.ai)**
4
-
5
- **Builder:** Faaz ([@Mindigenous](https://huggingface.co/Mindigenous) on HuggingFace)
6
-
7
- **Started:** April 14, 2026
8
-
9
- **Target Launch:** May 5, 2026
10
 
11
  ---
12
 
13
  ## What is MINDI 1.5?
14
 
15
- MINDI 1.5 Vision-Coder is a multimodal agentic AI coding model that:
 
 
 
 
 
 
16
 
17
- - Generates production-ready Next.js 14 + Tailwind CSS + TypeScript code
18
- - Sees its own output via vision capabilities (CLIP ViT-L/14)
19
- - Critiques its own UI/UX design and iterates
20
- - Searches the internet for latest packages and documentation
21
- - Tests code in an isolated sandbox environment
22
- - Fixes its own errors automatically
23
- - Suggests improvements to the user
24
 
25
- ## Architecture
 
 
 
 
26
 
27
- - **Base Model:** Open-source coding model (3B-7B parameters, Apache 2.0 / MIT)
28
- - **Fine-tuning:** LoRA on AMD MI300X 192GB VRAM
29
- - **Vision Encoder:** CLIP ViT-L/14
30
- - **Agents:** Search + Sandbox + UI Critic + Code Generation
31
- - **Training Data:** 500,000+ curated examples
32
- - **Backend:** FastAPI
33
- - **Output Format:** Next.js 14 + Tailwind CSS + TypeScript
34
 
35
  ## HuggingFace
36
 
37
- Final model will be published at: `Mindigenous/MINDI-1.5-Vision-Coder`
 
 
38
 
39
  ## License
40
 
 
1
  # MINDI 1.5 Vision-Coder
2
 
3
+ **Built by [MINDIGENOUS.AI](https://mindigenous.ai)**
4
+ **Builder:** Faaz ([@Mindigenous](https://huggingface.co/Mindigenous) on HuggingFace)
5
+ **Started:** April 14, 2026 | **Training Complete:** April 28, 2026 | **Frontend v2:** May 2, 2026
 
 
 
 
6
 
7
  ---
8
 
9
  ## What is MINDI 1.5?
10
 
11
+ MINDI 1.5 Vision-Coder is a multimodal AI model that generates frontend code (HTML/CSS/JS, React, Next.js, Tailwind) from text prompts and UI screenshots.
12
+
13
+ - **Architecture:** Qwen2.5-Coder-7B-Instruct + LoRA + CLIP ViT-L/14 + Vision-Language Fusion
14
+ - **Training:** 10,000 steps across 3 phases on AMD MI300X 192GB
15
+ - **Live API:** [Mindigenous/mindi-chat](https://huggingface.co/spaces/Mindigenous/mindi-chat) on HuggingFace Spaces
16
+
17
+ ## Frontend — AI Website Builder
18
 
19
+ A professional 3-panel IDE (Bolt.new-style) for interacting with MINDI:
 
 
 
 
 
 
20
 
21
+ ```powershell
22
+ cd frontend
23
+ npm install
24
+ npm run dev # → http://localhost:5173
25
+ ```
26
 
27
+ Features: Plan modal, prompt enhancement, code animation, live preview, file tree, demo fallback.
28
+ **Read `context.md` for full architecture and next steps.**
 
 
 
 
 
29
 
30
  ## HuggingFace
31
 
32
+ - **Model:** `Mindigenous/MINDI-1.5-Vision-Coder` (private)
33
+ - **Dataset:** `Mindigenous/MINDI-1.5-training-data` (private)
34
+ - **Space:** `Mindigenous/mindi-chat` (live, ZeroGPU)
35
 
36
  ## License
37
 
context.md CHANGED
@@ -1,8 +1,8 @@
1
  # MINDI 1.5 Vision-Coder — Complete Project Context
2
 
3
- > **Last updated:** April 30, 2026 (Session 4)
4
- > **Purpose:** This file contains ALL context needed to continue development with any AI assistant.
5
- > It covers architecture decisions, errors encountered, fixes applied, training state, and exact next steps.
6
 
7
  ---
8
 
@@ -21,791 +21,481 @@
21
  - **GitHub:** `https://github.com/Faaz345/MINDI-1.5-Vision-Coder.git` (branch: `master`)
22
  - **HuggingFace Model:** `Mindigenous/MINDI-1.5-Vision-Coder` (private, push as `master:main`)
23
  - **HuggingFace Dataset:** `Mindigenous/MINDI-1.5-training-data` (private)
 
24
  - **HF Token:** Set as `HF_TOKEN` environment variable (stored separately, not in repo)
25
 
26
  ---
27
 
28
- ## 2. DIRECTORY STRUCTURE
29
 
30
- ```
31
- MINDI-1.5-Vision-Coder/
32
- ├── src/
33
- │ ├── model/
34
- │ │ ├── architecture.py # Qwen2.5-Coder + LoRA wrapper (NOT nn.Module)
35
- │ │ ├── mindi_model.py # MINDI15 main class (nn.Module)
36
- │ │ ├── vision_encoder.py # CLIP ViT-L/14 (frozen) + trainable projection
37
- │ │ ├── fusion_layer.py # VisionLanguageFusion with text_gate
38
- │ │ └── __init__.py
39
- │ ├── training/
40
- │ │ ├── mindi_trainer.py # MINDITrainer: 3-phase loop, streaming data
41
- │ │ ├── data_pipeline.py # Data processing pipeline
42
- │ │ └── __init__.py
43
- │ ├── agents/ # Agentic pipeline (orchestrator, error fixer, UI critic)
44
- │ ├── inference/ # Generation pipeline
45
- │ ├── evaluation/ # Evaluation framework
46
- │ ├── search/ # Tavily search agent
47
- │ ├── sandbox/ # E2B/Docker code execution
48
- │ ├── tokenizer/ # MINDI tokenizer wrapper
49
- │ └── utils/ # Config & env loaders
50
- ├── scripts/
51
- │ ├── train.py # Master training launcher (--dry_run, --phase, --resume)
52
- │ ├── download_websight.py # Download WebSight v0.2 from HF
53
- │ ├── upload_websight_images.py # Upload images to HF in batches (10K/dir limit)
54
- │ ├── gpu_diagnostic.py # 6-stage GPU test for MI300X
55
- │ └── ... (data processing scripts)
56
- ├── configs/
57
- │ ├── training_config.yaml # Training hyperparameters
58
- │ ├── model_config.yaml # Model architecture config
59
- │ ├── data_config.yaml # Data sources and processing
60
- │ └── search_config.yaml # Tavily search settings
61
- ├── data/
62
- │ ├── processed/ # Text training data (train.jsonl, val.jsonl, test.jsonl)
63
- │ ├── websight/ # Vision data (52,500 images in subdirs + JSONL)
64
- │ │ ├── train.jsonl # 50,000 vision-code pairs
65
- │ │ ├── val.jsonl # 2,500 vision-code pairs
66
- │ │ └── images/
67
- │ │ ├── 00/ # ws_0000000.jpg - ws_0009999.jpg (10K each)
68
- │ │ ├── 01/
69
- │ │ ├── 02/
70
- │ │ ├── 03/
71
- │ │ ├── 04/
72
- │ │ └── 05/ # ws_0050000.jpg - ws_0052499.jpg (2,500)
73
- │ ├── tokenizer/
74
- │ │ ├── mindi_tokenizer/ # Custom tokenizer (vocab 151,685)
75
- │ │ └── base_tokenizer/ # Original Qwen tokenizer
76
- │ └── raw/ # Raw downloaded data sources
77
- ├── api/ # FastAPI endpoints
78
- ├── checkpoints/ # Model checkpoints
79
- ├── logs/ # Training logs
80
- ├── requirements.txt # Full requirements
81
- ├── requirements-training.txt # Lean MI300X Docker requirements
82
- ├── setup_mi300x.sh # MI300X Docker setup script
83
- ├── .gitattributes # LFS tracking for large tokenizer files
84
- └── .gitignore
85
- ```
86
-
87
- ---
88
-
89
- ## 3. ARCHITECTURE DETAILS
90
-
91
- ### 3.1 Model Components
92
-
93
- | Component | Class | File | Params | Trainable |
94
- |-----------|-------|------|--------|-----------|
95
- | Base LLM | `MINDIArchitecture` | `architecture.py` | 7.62B | No (frozen) |
96
- | LoRA | via PEFT | `architecture.py` | 161.5M | Yes |
97
- | CLIP Vision | `VisionEncoder` | `vision_encoder.py` | 304M | 4.2M (projection only) |
98
- | Fusion | `VisionLanguageFusion` | `fusion_layer.py` | 16.8M | Yes |
99
- | **Total** | `MINDI15` | `mindi_model.py` | **8.1B** | **182.5M (2.25%)** |
100
 
101
- ### 3.2 CRITICAL Architecture Notes
 
 
 
 
 
102
 
103
- 1. **`MINDIArchitecture` is NOT an `nn.Module`** — it's a plain Python wrapper class. The actual trainable PeftModel is accessed via `self.architecture.get_model()` and registered as `self.llm` in `MINDI15.__init__()`.
 
 
104
 
105
- 2. **`self.llm = self.architecture.get_model()`** — This line in `mindi_model.py` registers the PeftModel as a proper submodule so `model.parameters()` can find LoRA params. Without this, the optimizer gets zero trainable parameters.
 
 
 
106
 
107
- 3. **Vision encoder uses `float32` projection** — CLIP backbone is frozen, only `self.projection` (Linear 1024→4096) trains. The projection operates in float32 for stability even though the rest is bf16.
108
 
109
- 4. **Fusion layer has `text_gate`** A learnable scalar parameter (init=0) that creates a residual path for text-only inputs. This ensures gradients flow to the fusion layer during Phase 2 even when processing text-only batches (which have no vision tokens and would otherwise be pure passthrough with no gradient).
110
 
111
- ### 3.3 Forward Pass Flow
 
 
 
112
 
113
- ```
114
- Image → CLIP (frozen) → 256 patches (1024) → projection (4096) → visual_tokens
115
- Text → tokenizer → input_ids → LLM embedding layer → text_embeds
116
 
117
- With image: fusion = [gated_visual_tokens; text_embeds] (prepend)
118
- Without image: fusion = text_embeds + sigmoid(text_gate) * (transformed - text_embeds)
 
 
 
 
119
 
120
- fusion LLM layers (with LoRA) → logits → loss (cross-entropy, labels=-100 for padding)
 
 
 
121
  ```
122
 
123
- ### 3.4 LoRA Configuration
 
 
 
 
124
 
 
125
  ```python
126
- LoraConfig(
127
- r=64,
128
- lora_alpha=128,
129
- lora_dropout=0.05,
130
- target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
131
- bias="none",
132
- task_type=TaskType.CAUSAL_LM,
133
- )
134
- ```
135
-
136
- ### 3.5 MINDI Special Tokens (22 total, 11 pairs)
137
-
138
- ```
139
- <|think_start|> / <|think_end|> — Internal reasoning
140
- <|code_start|> / <|code_end|> — Generated code blocks
141
- <|file_start|> / <|file_end|> — File references
142
- <|critique_start|> / <|critique_end|> — Self-critique
143
- <|suggest_start|> / <|suggest_end|> — Suggestions
144
- <|search_start|> / <|search_end|> — Search context
145
- <|error_start|> / <|error_end|> — Error messages
146
- <|fix_start|> / <|fix_end|> — Fix attempts
147
- <|vision_start|> / <|vision_end|> — Vision input markers
148
- <|sandbox_start|> / <|sandbox_end|> — Sandbox execution
149
- <|context_start|> / <|context_end|> — Context block
150
  ```
151
 
152
  ---
153
 
154
- ## 4. TRAINING PIPELINE
155
-
156
- ### 4.1 Three-Phase Training Strategy
157
 
158
- | Phase | Name | Steps | LR | Batch | Components | Data | Purpose |
159
- |-------|------|-------|-----|-------|-----------|------|---------|
160
- | 1 | `phase1_lora` | 5,000 | 2e-4 | 16 | LoRA only | Text-only code | Teach coding patterns |
161
- | 2 | `phase2_vision_bridge` | 2,500 | 1e-5 | 8 | Vision+Fusion | WebSight images | Align visual tokens |
162
- | 3 | `phase3_all` | 2,500 | 5e-5 | 12 | All trainable | Mixed text+vision | Joint fine-tuning |
163
 
164
- **Total: 10,000 steps**
165
 
166
- ### 4.2 Training Data
167
 
168
- **Text data (Phase 1 + Phase 3):**
169
- - `data/processed/train.jsonl` — 1,304,486 examples, 4.18 GB
170
- - `data/processed/val.jsonl` — 72,471 examples
171
- - Sources: CodeAlpaca, CodeFeedback, EvolCode, MagicCoder, StarCoder (5 langs), Synthetic Next.js
172
 
173
- **Vision data (Phase 2 + Phase 3):**
174
- - `data/websight/train.jsonl` 50,000 image+code pairs, 114 MB JSONL
175
- - `data/websight/val.jsonl` 2,500 image+code pairs, 5.7 MB JSONL
176
- - `data/websight/images/` 52,500 JPG screenshots in 6 subdirectories (11.6 GB)
177
- - Source: HuggingFaceM4/WebSight v0.2 (UI screenshot → HTML/CSS pairs)
178
-
179
- **WebSight JSONL format:**
180
- ```json
181
- {
182
- "id": "websight_0000001",
183
- "type": "vision_code",
184
- "source": "websight_v0.2",
185
- "image_path": "data/websight/images/00/ws_0000001.jpg",
186
- "messages": [
187
- {"role": "system", "content": "You are MINDI 1.5 Vision-Coder..."},
188
- {"role": "user", "content": "<|vision_start|><|vision_end|>\nGenerate the HTML/CSS code for this UI screenshot."},
189
- {"role": "assistant", "content": "<|think_start|>...<|think_end|>\n<|code_start|>\n...HTML/CSS...\n<|code_end|>"}
190
- ],
191
- "metadata": {"dataset": "websight", "version": "v0.2"}
192
- }
193
  ```
194
 
195
- **IMPORTANT:** Images are organized in subdirectories of ≤10,000 files each because HuggingFace has a 10K files/directory limit. The JSONL `image_path` fields reference the subdirectory structure (e.g., `data/websight/images/00/ws_0000001.jpg`).
196
 
197
- ### 4.3 Data Loading
198
-
199
- - **`StreamingJSONLDataset`** (in `mindi_trainer.py`) Streams from disk line-by-line, tokenizes on-the-fly
200
- - **Shuffle buffer** of 10,000 examples (reservoir-style)
201
- - **Image loading** via `_load_image()` — loads PIL images from relative paths
202
- - **Custom collate function** stacks tensors, keeps images as a list
203
- - **Phase routing** — Phase 1 uses text data, Phase 2 uses WebSight, Phase 3 uses text (with inline images if present)
204
-
205
- ### 4.4 Key Training Features
206
-
207
- - **bf16 precision** Required for MI300X stability (NOT fp16)
208
- - **Gradient checkpointing** — Enabled even with 192GB VRAM
209
- - **torch.compile()** Optional, works on ROCm
210
- - **Cosine LR with warmup** Per-phase schedules
211
- - **Gradient accumulation** Configurable per phase (default: 4)
212
- - **Emergency checkpoint** Saved on Ctrl+C
213
- - **Crash checkpoint** Saved on unhandled exceptions
214
-
215
- ---
216
-
217
- ## 5. TRAINING HISTORY & RESULTS
218
-
219
- ### 5.1 Phase 1 Dry Run SUCCESS ✅
220
-
221
- **Date:** April 15, 2026 (on DigitalOcean MI300X)
222
- **Command:** `python3 scripts/train.py --dry_run --no_wandb`
223
- **Result:** Loss dropped from 1.94 → 0.85 in 10 steps, completed in 12.1 minutes
224
- **VRAM usage:** ~14.3 GB
225
-
226
- ### 5.2 Phase 2 — First Attempt FAILED ❌
227
-
228
- **Error:** `element 0 of tensors does not require grad and does not have a grad_fn`
229
- **Root cause:** Phase 2 trains vision+fusion with LoRA frozen. Text-only data means fusion is pure passthrough (no gradient path). The fusion layer was getting zero gradients because without vision tokens, the text-only path was `return text_embeds, attention_mask` — a pure passthrough with no learnable operation.
230
- **Fix:** Added `text_gate` learnable residual parameter to `VisionLanguageFusion`. Text-only path changed to: `text_embeds + sigmoid(text_gate) * (transformed - text_embeds)`. Also built the WebSight vision data pipeline to provide actual image+code pairs for Phase 2.
231
-
232
- ### 5.3 Full 3-Phase Dry Run — NOT YET COMPLETED
233
-
234
- The MI300X GPU kept hanging/wedging (see Section 6). Phase 2 and 3 with the new WebSight data pipeline have NOT been tested yet.
235
-
236
- ---
237
-
238
- ## 6. ERRORS & FIXES — COMPLETE HISTORY
239
-
240
- ### 6.1 GPU Hang #1 — HSA_OVERRIDE_GFX_VERSION
241
-
242
- **Symptom:** GPU completely unresponsive. `torch.cuda.get_device_name(0)` returns blank, any CUDA operation hangs.
243
- **Root cause:** `HSA_OVERRIDE_GFX_VERSION=11.0.0` was set in the Docker container. This conflicts with ROCm 7.0's native MI300X/gfx942 support.
244
- **Fix:** Do NOT set `HSA_OVERRIDE_GFX_VERSION`. ROCm 7.0 natively supports gfx942. Remove it from all scripts/env.
245
- **Commit:** `4a33f96 Remove HSA_OVERRIDE_GFX_VERSION`
246
-
247
- ### 6.2 No Trainable Parameters in Optimizer
248
-
249
- **Symptom:** `RuntimeError: No trainable parameters in phase 'phase1_lora'`
250
- **Root cause:** `MINDIArchitecture` is a plain Python class (not `nn.Module`). When `MINDI15` calls `model.parameters()`, it doesn't find the LoRA parameters because the PeftModel isn't registered as a submodule.
251
- **Fix:** Added `self.llm = self.architecture.get_model()` in `MINDI15.__init__()` to register the PeftModel as a proper nn.Module submodule. Updated `forward()` and `generate()` to use `self.llm` instead of `self.architecture.get_model()`.
252
- **Commit:** `cdc806e Fix: register LLM as nn.Module submodule so optimizer finds LoRA params`
253
-
254
- ### 6.3 extra_special_tokens Format Error
255
-
256
- **Symptom:** `TypeError` when loading tokenizer — transformers 4.55 expects `extra_special_tokens` as a dict, not a list.
257
- **Fix:** Changed `data/tokenizer/mindi_tokenizer/tokenizer_config.json`: converted `extra_special_tokens` from list format to `{"token_name": {"content": "..."}}` dict format.
258
- **Commit:** `02eef51 Fix extra_special_tokens: list to dict for transformers 4.55`
259
-
260
- ### 6.4 Phase 2 Gradient Flow Crash
261
-
262
- **Symptom:** `element 0 of tensors does not require grad and does not have a grad_fn` during Phase 2
263
- **Root cause:** Text-only data → no vision tokens → fusion is pure passthrough → no gradient path to fusion parameters.
264
- **Fix:** (1) Added `text_gate` learnable residual gate in `VisionLanguageFusion` for text-only gradient flow. (2) Built WebSight vision data pipeline with actual image+code pairs.
265
- **Commit:** `4e9835e Fix Phase 2: fusion layer processes text-only via learnable residual gate`
266
-
267
- ### 6.5 Git LFS Issues
268
 
269
- **Symptom:** `tokenizer.json` files >10MB causing push failures to HuggingFace.
270
- **Fix:** Configured `.gitattributes` for LFS tracking. Ran `git lfs migrate import` to rewrite history. Force-pushed to both GitHub and HF.
271
- **Commit:** `161c946 Track large tokenizer files with Git LFS`
272
 
273
- ### 6.6 HuggingFace Auth for MI300X Clone
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
- **Symptom:** `git clone` from HF failed with auth error in Docker container.
276
- **Fix:** Use token as both username and password: `https://hf_TOKEN:hf_TOKEN@huggingface.co/Mindigenous/MINDI-1.5-Vision-Coder.git`
277
- Also needed: `apt-get install -y git-lfs && git lfs install`
278
 
279
- ### 6.7 GPU Hang #2 Driver Wedge After Heavy I/O
 
 
 
280
 
281
- **Symptom:** After interrupted HF upload + training attempt, GPU shows 100% utilization with 0% VRAM in `rocm-smi`. Even `torch.randn(device='cuda')` hangs. Docker restart insufficient.
282
- **Kernel log:** `amdgpu: GPU reset begin!` → `device wedged, but recovered through reset` → But GPU% stays at 100%.
283
- **Fix:**
284
- 1. `docker stop rocm`
285
- 2. `echo 1 > /sys/bus/pci/devices/0000:83:00.0/reset` (PCI address from `lspci | grep AMD`)
286
- 3. If GPU% still 100%: `modprobe -r amdgpu && modprobe amdgpu`
287
- 4. Verify `rocm-smi` shows GPU% = 0% before restarting Docker
288
- **Status:** Droplet was deleted. Session 2 is on `134.199.197.198`.
289
 
290
- ### 6.8 HuggingFace Upload Limits
291
 
292
- **Symptom:** `413 Payload Too Large` (25K files/commit) and `400 Bad Request` (10K files/directory)
293
- **Fix:** Reorganized 52,500 images into 6 subdirectories of ≤10K files (`00/` through `05/`). Upload in separate commits per subdirectory. Updated JSONL `image_path` fields to include subdirectory.
294
- **Script:** `scripts/upload_websight_images.py`
295
 
296
- ---
297
 
298
- ## 7. MI300X DEPLOYMENT
299
 
300
- ### 7.1 Infrastructure
301
 
302
- - **Provider:** DigitalOcean GPU Droplet
303
- - **GPU:** AMD Instinct MI300X (192GB HBM3 VRAM)
304
- - **Cost:** $1.99/hr
305
- - **Docker container:** Named `rocm`, accessed via `docker exec -it rocm /bin/bash`
306
- - **ROCm/HIP:** 7.0.51831-a3e329ad8
307
- - **PyTorch:** 2.9.0.dev20250821+rocm7.0.0
308
- - **Python:** 3.10
309
 
310
- ### 7.2 Critical Environment Variables
 
 
 
311
 
312
- ```bash
313
- export HF_TOKEN=<your-hf-token> # Get from HF settings page
314
- export HF_HUB_DISABLE_PROGRESS_BARS=1
315
- export PYTORCH_ROCM_ARCH=gfx942
316
- export TOKENIZERS_PARALLELISM=false
317
- # DO NOT SET: HSA_OVERRIDE_GFX_VERSION (causes GPU hang on ROCm 7.0)
318
  ```
319
 
320
- ### 7.3 Fresh Droplet Setup Procedure
321
-
322
- ```bash
323
- # 1. SSH into droplet
324
- ssh root@<DROPLET_IP>
325
-
326
- # 2. Verify GPU health on host (must show 0% GPU)
327
- rocm-smi
328
-
329
- # 3. Start Docker
330
- docker start rocm
331
- docker exec -it rocm /bin/bash
332
-
333
- # 4. Set environment (inside Docker)
334
- export HF_TOKEN=<your-hf-token> # Get from HF settings page
335
- export HF_HUB_DISABLE_PROGRESS_BARS=1
336
- export PYTORCH_ROCM_ARCH=gfx942
337
- export TOKENIZERS_PARALLELISM=false
338
-
339
- # 5. Quick GPU test
340
- python3 -c "import torch; print('GPU:', torch.cuda.get_device_name(0)); x=torch.randn(100,device='cuda'); print('OK:', x.sum().item())"
341
-
342
- # 6. Install git-lfs (ignore AMD artifactory DNS warning — harmless)
343
- apt-get update && apt-get install -y git-lfs
344
- git lfs install
345
-
346
- # 7. Clone code repo
347
- cd /workspace
348
- git clone https://$HF_TOKEN:$HF_TOKEN@huggingface.co/Mindigenous/MINDI-1.5-Vision-Coder.git
349
- cd MINDI-1.5-Vision-Coder
350
-
351
- # 8. Install requirements
352
- pip install -r requirements-training.txt
353
 
354
- # 9. Download training data from HF dataset repo
355
- # NOTE: Use git clone, NOT snapshot_download (which hits HTTP 429 rate limits)
356
- # NOTE: Must rm -rf data first code repo creates an empty data/ directory
357
- rm -rf data
358
- git clone https://$HF_TOKEN:$HF_TOKEN@huggingface.co/datasets/Mindigenous/MINDI-1.5-training-data data
359
 
360
- # 10. Verify data
361
- wc -l data/processed/train.jsonl data/processed/val.jsonl
362
- wc -l data/websight/train.jsonl data/websight/val.jsonl
363
- for d in data/websight/images/0*/; do echo "$d: $(ls $d | wc -l) files"; done
364
-
365
- # 11. Create output directories
366
- mkdir -p checkpoints/training checkpoints/best logs/training
 
 
 
367
 
368
- # 12. Run GPU diagnostic
369
- python3 scripts/gpu_diagnostic.py
370
 
371
- # 13. Dry run (test all 3 phases before full training)
372
- python3 scripts/train.py --dry_run --no_wandb
373
 
374
- # 14. Full training (background, survives SSH disconnect)
375
- nohup python3 scripts/train.py --no_wandb > /workspace/training.log 2>&1 &
376
  ```
377
-
378
- ### 7.4 GPU Hang Recovery (if it happens again)
379
-
380
- ```bash
381
- # From HOST (not inside Docker):
382
- docker stop rocm
383
- echo 1 > /sys/bus/pci/devices/0000:83:00.0/reset # PCI address may differ
384
- rocm-smi # Verify GPU% = 0%
385
- # If still 100%:
386
- modprobe -r amdgpu && modprobe amdgpu
387
- rocm-smi # Should show 0% now
388
- docker start rocm
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  ```
390
 
391
- ### 6.9 HuggingFace snapshot_download Rate Limit (HTTP 429)
392
 
393
- **Symptom:** `HTTP Error 429 thrown while requesting GET .../tree/main` during `snapshot_download()`. Retries endlessly.
394
- **Root cause:** The dataset has 52,500+ image files. `snapshot_download` paginates through the HF tree API listing all files, causing rate limiting.
395
- **Fix:** Use `git clone` instead of `snapshot_download` for the dataset:
396
- ```bash
397
- rm -rf data
398
- git clone https://$HF_TOKEN:$HF_TOKEN@huggingface.co/datasets/Mindigenous/MINDI-1.5-training-data data
399
- ```
400
- This downloads everything in a single git connection without hitting the API rate limiter.
401
- **Discovered:** April 16, 2026 — Session 2
402
 
403
- ### 6.10 Bash History Expansion with Exclamation Mark
404
 
405
- **Symptom:** `bash: !': event not found` when running `python3 -c "...print('Done!')"` in a single line.
406
- **Root cause:** Bash interprets `!'` inside double quotes as history expansion.
407
- **Fix:** Use multi-line python commands (with actual newlines between double quotes) instead of single-line. Or use single quotes around the python code.
408
- **Discovered:** April 16, 2026 Session 2
 
 
 
409
 
410
- ### 6.11 Data Directory Already Exists on Clone
411
 
412
- **Symptom:** `fatal: destination path 'data' already exists and is not an empty directory` when trying to `git clone ... data`.
413
- **Root cause:** The code repo clone creates an empty `data/` directory structure.
414
- **Fix:** `rm -rf data` before cloning the dataset repo.
415
- **Discovered:** April 16, 2026 — Session 2
416
 
417
- ---
418
 
419
- ## 8. HF DATASET REPO STRUCTURE
420
 
421
- **Repo:** `Mindigenous/MINDI-1.5-training-data` (private, type: dataset)
422
 
423
  ```
424
- ├── .gitattributes
425
- ├── README.md
426
- ├── processed/
427
- │ ├── train.jsonl # 1.3M text examples
428
- │ ├── val.jsonl
429
- │ ├── test.jsonl
430
- │ ├── filter_report.json
431
- │ ├── mindi_filtered.jsonl
432
- │ └── split_meta.json
433
- ├── raw/ # Original data sources (11 files)
434
- ├── tokenizer/
435
- │ ├── base_tokenizer/
436
- │ └── mindi_tokenizer/
437
- └── websight/
438
- ├── train.jsonl # 50K vision-code JSONL
439
- ├── val.jsonl # 2.5K vision-code JSONL
440
- └── images/
441
- ├── 00/ # 10,000 JPGs
442
- ├── 01/ # 10,000 JPGs
443
- ├── 02/ # 10,000 JPGs
444
- ├── 03/ # 10,000 JPGs
445
- ├── 04/ # 10,000 JPGs (uploading as of April 16)
446
- └── 05/ # 2,500 JPGs (uploading as of April 16)
447
  ```
448
 
449
- **NOTE:** As of April 16, 2026, subdirectories 00-03 are uploaded. 04 and 05 are being uploaded via `scripts/upload_websight_images.py`. If upload was interrupted, re-run the script — it skips already-uploaded subdirs.
450
-
451
  ---
452
 
453
- ## 9. GIT HISTORY (CHRONOLOGICAL)
454
 
455
- ```
456
- 553fbf7 feat: initial project scaffold for MINDI 1.5 Vision-Coder
457
- 11e0d89 Day 1 Complete: Tokenizer setup — 22 MINDI special tokens (vocab 151,685)
458
- 59c6c97 Day 2 COMPLETE: 1.48M examples processed, 6GB dataset, WebSight done
459
- 2ff5c54 Day 3 COMPLETE: Full model architecture (7 files)
460
- 1c36b28 Fix train.py: mem -> memory on line 225
461
- f04f58b Fix setup_mi300x.sh step 2 + add project context summary
462
- 35fd5fc Fix setup_mi300x.sh for Docker container on MI300X droplet
463
- 5fb9ec3 Add GPU diagnostic script, fix architecture loading with sync
464
- 161c946 Track large tokenizer files with Git LFS
465
- 4a33f96 Remove HSA_OVERRIDE_GFX_VERSION - ROCm 7.0 native MI300X support
466
- 24b5fb1 Add requirements-training.txt for MI300X Docker
467
- 02eef51 Fix extra_special_tokens: list to dict for transformers 4.55
468
- cdc806e Fix: register LLM as nn.Module submodule so optimizer finds LoRA params
469
- 4e9835e Fix Phase 2: fusion layer text_gate for gradient flow
470
- 672896a Add WebSight vision data pipeline: download, image-aware loader, phase routing
471
- ```
472
 
473
- ---
 
474
 
475
- ## 10. WHAT WORKS (VERIFIED) ✅
 
 
 
 
 
 
476
 
477
- 1. **Tokenizer** — 151,685 vocab with 22 MINDI special tokens, loads correctly
478
- 2. **Model initialization** — MINDI15 loads all 4 components, 182.5M trainable params
479
- 3. **GPU diagnostic** — All 6 tests pass (bf16 matmul, 1GB alloc, CPU→CUDA transfer, forward pass)
480
- 4. **Phase 1 dry run** — Loss 1.94 → 0.85 in 10 steps ✅
481
- 5. **WebSight download** — 52,500 images (11.6 GB) downloaded and organized
482
- 6. **Data format** — JSONL with image_path references, streaming dataset works
483
- 7. **Git LFS** — Large tokenizer files tracked correctly
484
- 8. **Code pushed** — All code on GitHub master + HF model repo main
 
 
 
 
 
 
 
485
 
486
  ---
487
 
488
- ## 11. WHAT REMAINS (TODO)
489
-
490
- 1. ~~**Complete WebSight upload to HF**~~ — Check if subdirs 04 and 05 are uploaded; re-run upload script if needed
491
- 2. **Full 3-phase dry run** Phase 2 (WebSight) and Phase 3 (mixed) NOT yet tested with the vision pipeline
492
- 3. **Full production training** — 10,000 steps total (Phase 1: 5K, Phase 2: 2.5K, Phase 3: 2.5K)
493
- 4. **Inference testing**Generate code from screenshots after training
494
- 5. **Commit `upload_websight_images.py` and `context.md`** These new files need to be pushed
495
-
496
- ### Session 2 Status (April 16, 2026)
497
- - Fresh droplet spun up at `134.199.197.198`
498
- - Docker container started, GPU healthy (0% util, 45°C)
499
- - Code repo cloned, dependencies installed
500
- - GPU diagnostic: All 6 tests passed (bf16 matmul, 1GB alloc, forward pass)
501
- - ⚠️ Data download: multiple rate limits (snapshot_download git clone git-lfs → hf_hub_download retries)
502
- - All data downloaded: 1.3M text + 50K WebSight JSONL + 52,500 images
503
- - Phase 1 dry run PASSED: loss 18.87 8.05 in 10 steps (10.8 min)
504
- - ✅ Phase 2 dry run PASSED: loss 1.46 → 1.19, val_loss 1.32 in 10 steps (6.2 min)
505
- - Phase 3 dry run PASSED: loss 14.10 → 9.71, val_loss 9.72 in 10 steps (8.2 min)
506
- - Checkpoint upload to HF fixed (.gitignore was blocking *.pt, *.safetensors — removed model file patterns)
507
- - ✅ Auto-push script running (pushes latest checkpoint to HF every 2 hours — fixed alphabetic sorting bug)
508
- - Resume bug fixed: train() now skips completed phases and resumes mid-phase correctly
509
- - Phase 1 training: step 4500/5000, val_loss 0.5372 on 3rd droplet (165.245.141.141)
510
- - Image download running: ~8300/52500 images (needed for Phase 2)
511
- - 💰 Budget: ~$91 on current account, more accounts available
512
- - 📋 Plan: finish Phase 1 Phase 2 Phase 3, auto-push checkpoints to HF
513
 
514
  ---
515
 
516
- ## 12. KNOWN ISSUES & GOTCHAS
517
 
518
- ### DO NOT:
519
- - Set `HSA_OVERRIDE_GFX_VERSION=11.0.0` — kills GPU on ROCm 7.0
520
- - Use `fp16` on MI300X use `bf16` for stability
521
- - Try to upload >10K files to a single HF directory split into subdirs
522
- - Try to commit >25K files in a single HF commit batch commits
523
- - Use the global Python (base env) on Windows use venv (global torch DLL is broken)
524
-
525
- ### WATCH OUT FOR:
526
- - GPU hanging after heavy I/O — check `rocm-smi` shows 0% GPU before training
527
- - Data paths — WebSight images use **relative paths** from project root in JSONL
528
- - `MINDIArchitecture` is NOT `nn.Module` — always use `self.llm` inside MINDI15
529
- - The `text_gate` in fusion starts at 0 (sigmoid=0.5) — this is intentional
530
- - On MI300X, Docker container named `rocm` — always `docker exec -it rocm /bin/bash`
531
 
532
  ---
533
 
534
- ## 13. COMMANDS REFERENCE
535
-
536
- ### Local (Windows, PowerShell, in venv):
537
- ```powershell
538
- # Activate venv
539
- & ".\venv\Scripts\Activate.ps1"
 
 
 
 
 
 
 
 
 
540
 
541
- # Download WebSight
542
- $env:HF_TOKEN="<your-hf-token>"
543
- python scripts/download_websight.py --num_train 50000 --num_val 2500
544
-
545
- # Upload WebSight images to HF (handles subdirs, retry, skip)
546
- python scripts/upload_websight_images.py
547
-
548
- # Push code to GitHub + HF
549
- git push origin master
550
- git push hf master:main
551
- ```
552
-
553
- ### MI300X (Linux, Docker, inside container):
554
- ```bash
555
- # Dry run (10 steps per phase)
556
- python3 scripts/train.py --dry_run --no_wandb
557
 
558
- # Full training
559
- python3 scripts/train.py --no_wandb
560
 
561
- # Single phase
562
- python3 scripts/train.py --phase 1 --no_wandb
563
- python3 scripts/train.py --phase 2 --no_wandb
564
- python3 scripts/train.py --phase 3 --no_wandb
565
 
566
- # Resume from checkpoint
567
- python3 scripts/train.py --resume checkpoints/training/phase1_lora_step5000 --no_wandb
 
 
568
 
569
- # GPU diagnostic
570
- python3 scripts/gpu_diagnostic.py
571
- ```
 
572
 
573
  ---
574
 
575
- ## 14. NEXT SESSION CHECKLIST
576
 
577
- When continuing with a new AI assistant:
578
 
579
- 1. **Open this directory** in your IDE
580
- 2. **Read this file first** to get full context
581
- 3. **Check WebSight upload status:**
582
  ```powershell
583
- python -c "import os; from huggingface_hub import HfApi; api=HfApi(token=os.environ['HF_TOKEN']); files=[f for f in api.list_repo_files('Mindigenous/MINDI-1.5-training-data', repo_type='dataset') if 'websight/images' in f]; print(f'{len(files)} images in HF repo')"
 
 
584
  ```
585
- 4. If <52,500: re-run `python scripts/upload_websight_images.py`
586
- 5. **Push any uncommitted files:**
587
- ```bash
588
- git add scripts/upload_websight_images.py context.md
589
- git commit -m "Add WebSight batch uploader and project context"
590
- git push origin master
591
- git push hf master:main
592
- ```
593
- 6. **Spin up fresh MI300X droplet** on DigitalOcean
594
- 7. **Follow Section 7.3** for setup procedure
595
- 8. **IMPORTANT:** Use `git clone` for data download (NOT `snapshot_download` — see Section 6.9)
596
- 9. **IMPORTANT:** `rm -rf data` before cloning dataset repo (see Section 6.11)
597
- 10. **Run dry run first** to verify all 3 phases work
598
- 11. **Then full training** — `nohup python3 scripts/train.py --no_wandb > /workspace/training.log 2>&1 &`
599
 
600
  ---
601
 
602
- ## 15. DATA FILE LOCATIONS ON HF DATASET REPO
603
-
604
- When cloning data on MI300X using `snapshot_download`, files will land at:
605
-
606
- | HF Repo Path | Local Path (relative to project root) |
607
- |---|---|
608
- | `processed/train.jsonl` | `data/processed/train.jsonl` |
609
- | `processed/val.jsonl` | `data/processed/val.jsonl` |
610
- | `websight/train.jsonl` | `data/websight/train.jsonl` |
611
- | `websight/val.jsonl` | `data/websight/val.jsonl` |
612
- | `websight/images/00/*.jpg` | `data/websight/images/00/*.jpg` |
613
- | `tokenizer/mindi_tokenizer/*` | `data/tokenizer/mindi_tokenizer/*` |
614
-
615
- The `snapshot_download(local_dir='data')` call places everything correctly because the HF repo structure mirrors the local `data/` directory.
616
-
617
- ---
618
 
619
- ## 16. APRIL 16, 2026 — MAIN TRAINING COMMANDS
 
 
 
 
620
 
621
- ### Data Download (git clone — NOT snapshot_download)
 
622
 
623
- ```bash
624
- # Inside Docker, after cloning code repo:
625
- rm -rf data
626
- git clone https://$HF_TOKEN:$HF_TOKEN@huggingface.co/datasets/Mindigenous/MINDI-1.5-training-data data
627
  ```
628
 
629
- ### Training — Background (Recommended, survives SSH disconnect)
630
-
631
- ```bash
632
- # From inside Docker:
633
- nohup python3 scripts/train.py --no_wandb > /workspace/training.log 2>&1 &
634
- echo $! > /workspace/training.pid
635
  ```
636
 
637
- Or from the **host** (also survives SSH disconnect):
638
-
639
- ```bash
640
- docker exec -d rocm bash -lc 'cd /workspace/MINDI-1.5-Vision-Coder && export HF_TOKEN=<your-hf-token> && export PYTORCH_ROCM_ARCH=gfx942 && python3 scripts/train.py --no_wandb > /workspace/training.log 2>&1'
 
 
641
  ```
642
 
643
- ### Training Interactive (Foreground)
644
-
645
  ```bash
646
- python3 scripts/train.py --no_wandb 2>&1 | tee /workspace/training.log
647
- ```
 
 
648
 
649
- ### Monitoring
 
650
 
651
- ```bash
652
- docker exec rocm tail -f /workspace/training.log # Live logs
653
- docker exec rocm rocm-smi # GPU usage
654
- docker exec rocm ps aux | grep train.py # Process check
655
  ```
656
 
657
- Notes:
658
- - Use the background command if you want the process detached from your SSH session.
659
- - The `scripts/train.py` launcher does not accept a `--log_file` flag; redirect output into `/workspace/training.log` instead.
660
- - Line-buffered stdout has been added to `src/training/mindi_trainer.py` so logs should appear in near real-time when using `tail -f`.
661
-
662
- ## 17. DROPLET HISTORY
663
-
664
- | Session | Date | Droplet IP | Status | Notes |
665
- |---------|------|-----------|--------|-------|
666
- | 1 | April 15, 2026 | `134.199.194.245` | Deleted | Phase 1 dry run passed. GPU hung during heavy I/O. |
667
- | 2 | April 16, 2026 | `134.199.197.198` | Deleted | Phase 1 steps 0→4250 completed. Credits exhausted. |
668
- | 3 | April 19, 2026 | `165.245.141.141` | Active | Phase 1 resumed at step 4250. Resume bug fixed. |
669
-
670
- ---
671
-
672
- *This context file was created on April 16, 2026 during Claude Opus 4.6 session to ensure project continuity.*
673
- *Updated on April 16, 2026 — Session 2: snapshot_download 429 fix, bash escaping, fresh droplet setup.*
674
- *Updated on April 28, 2026 — Training complete, frontend built, API deployed.*
675
- *Updated on April 30, 2026 — Session 4: Fixed critical frontend bugs, Gradio 5.x API protocol, ZeroGPU quota handling.*
676
-
677
  ---
678
 
679
- ## 22. SESSION 4 — April 30, 2026
680
-
681
- ### Bugs Found & Fixed
682
-
683
- **Bug 6.12: `handleSend` ReferenceError (app.js)**
684
- - **Symptom:** Agent integration broken on page load — `const _originalSend = handleSend` throws ReferenceError because `handleSend` was never defined (the actual function is `send`)
685
- - **Fix:** Changed to `let activeSend = send` pattern — init() overrides `activeSend = handleSendWithAgent` when MINDIAgent is available. Eliminated duplicate keydown event handlers.
686
- - **File:** `frontend/app.js`
687
-
688
- **Bug 6.13: Gradio 5.x API protocol mismatch**
689
- - **Symptom:** `POST /api/predict` returns 404 — the frontend used old Gradio 3.x API format
690
- - **Root cause:** HF Space runs Gradio 5.23.0 which uses SSE v3 protocol with `/gradio_api/call/{api_name}` (two-step: POST to submit → GET to stream result)
691
- - **Fix:** Rewrote `callGenerate()` to use the Gradio 5.x two-step flow: POST `/gradio_api/call/chat_fn` → get event_id → GET `/gradio_api/call/chat_fn/{event_id}` → parse SSE response for `event: complete` data
692
- - **File:** `frontend/app.js`
693
- - **Config reference:** `GET /config` returns `{"api_prefix": "/gradio_api", "protocol": "sse_v3", "dependencies": [{"api_name": "chat_fn"}]}`
694
-
695
- **Bug 6.14: Health check misdetects Gradio Space as offline**
696
- - **Symptom:** Status shows "Demo Mode" even when Space is running
697
- - **Root cause:** `pingHealth()` tried `/api/health` (doesn't exist on Gradio) then `/api/predict` (old format → 404)
698
- - **Fix:** For HF Spaces, use `fetch(base, {mode:'no-cors'})` which succeeds if the Space is reachable
699
- - **File:** `frontend/app.js`
700
-
701
- **Improvement: ZeroGPU quota error handling**
702
- - Reduced `@spaces.GPU(duration=120)` → `@spaces.GPU(duration=60)` (inference is fast after model cache)
703
- - Added try-except in `chat_fn()` to return clean JSON error instead of crashing when GPU quota exceeded
704
- - **File:** `hf_space/app.py`
705
-
706
- ### Session 4 Status
707
- - ✅ Frontend bugs fixed (handleSend reference, duplicate handlers)
708
- - ✅ Gradio 5.x API protocol implemented (SSE v3 two-step flow)
709
- - ✅ Health check fixed — shows green "MINDI · HF Space" status
710
- - ✅ Space updated on HF — `Mindigenous/mindi-chat`
711
- - ⚠️ ZeroGPU daily quota limit can block visitors — PRO users get 8x more quota
712
- - ✅ Agent system (agent.js + sandbox.js) scaffolded — Plan→Generate→Execute→Verify→Fix loop
713
- - 📋 Next: Wait for quota reset, then test full end-to-end flow with real model inference
714
-
715
- ### Training Summary
716
- All 3 phases of MINDI 1.5 Vision-Coder training are COMPLETE:
717
-
718
- | Phase | Steps | Status | Platform |
719
- |-------|-------|--------|----------|
720
- | Phase 1 (LoRA) | 5,000 | ✅ Complete | DigitalOcean MI300X |
721
- | Phase 2 (Vision Bridge) | 2,500 | ✅ Complete | DigitalOcean MI300X |
722
- | Phase 3 (Joint) steps 0-1500 | 1,500 | ✅ Complete | DigitalOcean MI300X |
723
- | Phase 3 (Joint) steps 1500-2500 | 1,000 | ✅ Complete | Modal A100-40GB |
724
-
725
- ### Modal Training Details
726
- - Resumed from step 1500 checkpoint on Modal A100-40GB ($2.10/hr)
727
- - Config patched at runtime: batch_size=2, max_length=2048 (from 6/4096)
728
- - Total Modal cost: ~$28 ($30 credits)
729
- - Final loss: 0.25–0.40 range
730
-
731
- ### HuggingFace Checkpoints (Mindigenous/MINDI-1.5-Vision-Coder)
732
- All checkpoints uploaded to `checkpoints/` directory:
733
- - Phase 1: 16 checkpoints (step250 → step5000)
734
- - Phase 2: 10 checkpoints (step250 → step2500)
735
- - Phase 3: `phase3_all_step500`, `step1000`, `step1500`, `step2000`, `phase3_all_step2500_final`, `phase3_final`
736
-
737
- ### Model Test Results (April 28, 2026)
738
- - ✅ Code generation (text-only): Matrix exponentiation fibonacci
739
- - ✅ HTML/CSS generation: Gradient + responsive design
740
- - ✅ Vision (image input): Processed dummy image
741
- - ✅ Agentic (bug fix): Identified subtraction→addition bug
742
- - VRAM usage: 17.2 GB (A100-40GB)
743
-
744
- ---
745
-
746
- ## 19. FRONTEND
747
-
748
- ### Location: `frontend/`
749
- - `index.html` — Three-panel layout (sidebar + chat + code preview)
750
- - `styles.css` — Premium dark theme with purple/blue gradients
751
- - `app.js` — Chat logic, image upload, code extraction, demo mode
752
-
753
- ### Features
754
- - Chat interface with code block rendering (Prism.js)
755
- - Image upload for vision-to-code
756
- - Code preview panel with tabs (Code / Preview / Sections)
757
- - Special token parsing (thinking, critique, fix, error)
758
- - Demo mode (works without API — simulated responses)
759
- - Settings modal (double-click MINDI logo) to configure API endpoint
760
- - Responsive design (mobile + desktop)
761
-
762
- ### To Run Locally
763
- ```bash
764
- cd frontend
765
- python -m http.server 8080
766
- # Open http://localhost:8080
767
  ```
768
 
769
- ---
770
 
771
- ## 20. MODAL API SERVER
772
 
773
- ### File: `modal_api.py`
774
- FastAPI web endpoint that:
775
- 1. Loads MINDI 1.5 from volume checkpoint on container startup
776
- 2. Exposes `/api/generate` (POST) and `/api/health` (GET)
777
- 3. Accepts text + optional base64 image
778
- 4. Returns response + parsed special token sections
779
- 5. CORS enabled for frontend
780
 
781
- ### Deployment
782
- ```bash
783
- modal deploy modal_api.py
784
- # Returns a URL like: https://mindigenous-ai--mindi-api-api.modal.run
785
- ```
786
 
787
- ### Cost
788
- - A100 @ $2.10/hr, scales to zero when idle
789
- - ~$0.01-0.05 per request
790
- - Container idle timeout: 5 minutes
 
 
791
 
792
- ### Connect Frontend to API
793
- 1. Open frontend at http://localhost:8080
794
- 2. Double-click the MINDI logo (top-left sidebar)
795
- 3. Enter the Modal API URL
796
- 4. Save settings
797
 
798
  ---
799
 
800
- ## 21. REMAINING BUDGET & NEXT STEPS
801
-
802
- ### Budget
803
- - Modal: $2.21 remaining (~1 hour A100 time)
804
- - DigitalOcean: exhausted
805
-
806
- ### Next Steps
807
- 1. Deploy API when more credits available
808
- 2. Host frontend on Vercel/GitHub Pages (free)
809
- 3. Consider HuggingFace Spaces (free T4) with 4-bit quantization as alternative
810
- 4. Push frontend to GitHub/HF repos
811
-
 
1
  # MINDI 1.5 Vision-Coder — Complete Project Context
2
 
3
+ > **Last updated:** May 2, 2026 (Session 5)
4
+ > **Purpose:** This file contains ALL context needed to continue development with any AI assistant.
5
+ > It covers architecture decisions, errors encountered, fixes applied, training state, frontend state, and exact next steps.
6
 
7
  ---
8
 
 
21
  - **GitHub:** `https://github.com/Faaz345/MINDI-1.5-Vision-Coder.git` (branch: `master`)
22
  - **HuggingFace Model:** `Mindigenous/MINDI-1.5-Vision-Coder` (private, push as `master:main`)
23
  - **HuggingFace Dataset:** `Mindigenous/MINDI-1.5-training-data` (private)
24
+ - **HuggingFace Space:** `Mindigenous/mindi-chat` — live Gradio 5.x Space (ZeroGPU)
25
  - **HF Token:** Set as `HF_TOKEN` environment variable (stored separately, not in repo)
26
 
27
  ---
28
 
29
+ ## 2. TRAINING STATUS — COMPLETE ✅
30
 
31
+ All 3 phases of MINDI 1.5 Vision-Coder training are COMPLETE:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
+ | Phase | Steps | Status | Platform |
34
+ |-------|-------|--------|----------|
35
+ | Phase 1 (LoRA) | 5,000 | ✅ Complete | DigitalOcean MI300X |
36
+ | Phase 2 (Vision Bridge) | 2,500 | ✅ Complete | DigitalOcean MI300X |
37
+ | Phase 3 (Joint) 0→1500 | 1,500 | ✅ Complete | DigitalOcean MI300X |
38
+ | Phase 3 (Joint) 1500→2500 | 1,000 | ✅ Complete | Modal A100-40GB |
39
 
40
+ **Final loss:** 0.25–0.40 range
41
+ **VRAM:** 17.2 GB on A100-40GB
42
+ **All checkpoints:** Uploaded to `checkpoints/` in HF model repo
43
 
44
+ ### HuggingFace Checkpoints (Mindigenous/MINDI-1.5-Vision-Coder)
45
+ - Phase 1: 16 checkpoints (step250 → step5000)
46
+ - Phase 2: 10 checkpoints (step250 → step2500)
47
+ - Phase 3: `phase3_all_step500`, `step1000`, `step1500`, `step2000`, `phase3_all_step2500_final`, `phase3_final`
48
 
49
+ ---
50
 
51
+ ## 3. LIVE APIHuggingFace SPACE
52
 
53
+ **Space URL:** `https://mindigenous-mindi-chat.hf.space`
54
+ **Space ID:** `Mindigenous/mindi-chat`
55
+ **Framework:** Gradio 5.23.0 (ZeroGPU)
56
+ **Protocol:** SSE v3 — two-step: POST to submit → GET to stream result
57
 
58
+ ### API Call Pattern (Gradio 5.x SSE v3)
 
 
59
 
60
+ ```javascript
61
+ // Step 1: Submit job
62
+ POST https://mindigenous-mindi-chat.hf.space/gradio_api/call/chat_fn
63
+ Headers: { "Content-Type": "application/json", "Authorization": "Bearer hf_..." }
64
+ Body: { "data": [prompt, imageArg, temperature, maxTokens, historyJson] }
65
+ Response: { "event_id": "..." }
66
 
67
+ // Step 2: Stream result
68
+ GET https://mindigenous-mindi-chat.hf.space/gradio_api/call/chat_fn/{event_id}
69
+ Parse SSE: find "event: complete" → next line "data: [...]"
70
+ Parse data[0] as JSON: { response: "...", sections: {} }
71
  ```
72
 
73
+ ### ZeroGPU Quota
74
+ - **Anonymous users:** Very low quota (hits "GPU task aborted" error quickly)
75
+ - **Authenticated users (HF token):** ~8× higher quota
76
+ - **Quota errors throw as exceptions** with message containing "GPU task aborted" or "zerogpu"
77
+ - **Fix:** Always send `Authorization: Bearer <HF_TOKEN>` header
78
 
79
+ ### Gradio Function Signature
80
  ```python
81
+ # hf_space/app.py — chat_fn
82
+ def chat_fn(prompt: str, image: dict|None, temperature: float, max_tokens: int, history_json: str) -> str:
83
+ # Returns JSON string: {"response": "...", "sections": {...}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  ```
85
 
86
  ---
87
 
88
+ ## 4. FRONTEND — NEW VITE + REACT WEBSITE BUILDER ⭐ (Session 5 Work)
 
 
89
 
90
+ ### What Was Built (May 2, 2026)
 
 
 
 
91
 
92
+ The old vanilla HTML/CSS/JS chat frontend was completely replaced with a **professional 3-panel website builder IDE** (similar to Bolt.new / v0.dev), built with Vite + React.
93
 
94
+ **The old frontend is backed up in:** `frontend/_legacy/`
95
 
96
+ ### How to Run
 
 
 
97
 
98
+ ```powershell
99
+ cd "d:\Desktop 31st Jan 2026\MINDI 1.5 vision-coder\frontend"
100
+ npm install # only first time
101
+ npm run dev # starts at http://localhost:5173
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  ```
103
 
104
+ ### New Frontend Structure
105
 
106
+ ```
107
+ frontend/
108
+ ├── index.html # Shell with Google Fonts (Inter + JetBrains Mono)
109
+ ├── package.json # Vite 8.x + React 19 + prismjs + lucide-react
110
+ ├── vite.config.js # Vite config
111
+ ├── _legacy/ # Old vanilla JS chat frontend (backed up)
112
+ └── src/
113
+ ├── main.jsx # React entry point
114
+ ├── index.css # Design system (CSS tokens, reset, animations)
115
+ ├── App.jsx # Main app — all state management + generation flow
116
+ ├── App.css # All layout + component styles (3-panel IDE)
117
+ ├── components/
118
+ │ ├── Sidebar.jsx # File tree + Agent Progress + status indicator
119
+ │ ├── Editor.jsx # Code editor with line-by-line animation + tabs
120
+ │ ├── Preview.jsx # Always-visible iframe preview + Console panel
121
+ │ ├── PromptBar.jsx # Bottom prompt input (auto-resize, send/stop)
122
+ │ ├── PlanModal.jsx # Clarifying questions (tech stack, design style)
123
+ │ ├── SettingsModal.jsx # API URL, HF token, temperature, max tokens
124
+ │ └── Toasts.jsx # Toast notifications
125
+ └── services/
126
+ ├── api.js # Gradio SSE v3 integration + auth + demo fallback
127
+ ├── promptEnhancer.js # Analyzes prompt → asks questions → structured prompt
128
+ └── fileParser.js # Extracts files from model response markdown
129
+ ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
+ ### Layout
 
 
132
 
133
+ ```
134
+ ┌──────────────┬─────────────────────────┬──────────────────┐
135
+ │ SIDEBAR │ CODE EDITOR │ LIVE PREVIEW │
136
+ │ (260px) │ (flex: 1) │ (420px) │
137
+ │ │ │ │
138
+ │ MINDI 1.5 │ 🌐 index.html │ ● Preview │
139
+ │ brand │ 1 <!DOCTYPE html> │ [Rendered HTML] │
140
+ │ │ 2 <html lang="en"> │ │
141
+ │ FILES (1) │ 3 <head> │ │
142
+ │ 🌐 index. │ ... │ CONSOLE │
143
+ │ html │ │ > Page rendered │
144
+ │ │ │ │
145
+ │ AGENT │ │ │
146
+ │ PROGRESS │ │ │
147
+ │ ✅ Enhancing│ │ │
148
+ │ ✅ Generating│ │ │
149
+ │ ✅ Complete │ │ │
150
+ │ │ │ │
151
+ │ ● MINDI · │ │ │
152
+ │ Connected │ │ │
153
+ ├──────────────┴─────────────────────────┴──────────────────┤
154
+ │ [Describe what you want to build...] [Send] │
155
+ │ MINDI 1.5 Vision-Coder Shift+Enter new line │
156
+ └───��────────────────────────────────────────────────────────┘
157
+ ```
158
 
159
+ ### Key Features
 
 
160
 
161
+ 1. **Plan Modal**When user submits prompt without specifying tech stack or theme, a "Configure Your Project" modal appears with:
162
+ - Tech stack: HTML+CSS+JS / React / Next.js / Vue
163
+ - Design style: Dark / Light / Gradient / Minimal
164
+ - "Skip & Generate" and "Generate ⚡" buttons
165
 
166
+ 2. **Prompt Enhancer** (`src/services/promptEnhancer.js`) Transforms raw input into structured prompts with design requirements, responsiveness rules, font choices, no-placeholder rules.
 
 
 
 
 
 
 
167
 
168
+ 3. **Code Animation** — Lines appear one by one at 15ms intervals with `line-appear` CSS animation as code generates.
169
 
170
+ 4. **File Tree** Files parsed from model response appear in sidebar with fade-in animation. Click to switch active file in editor.
 
 
171
 
172
+ 5. **Live Preview** — Always-visible iframe on right renders HTML output. "Open in new tab" and "Copy HTML" buttons.
173
 
174
+ 6. **Demo Fallback** — When API quota exceeded or any error occurs, pre-built demo responses for common prompts (landing page, dashboard) render automatically. No white screen.
175
 
176
+ 7. **Settings** — Click the MINDI logo (top-left) to open Settings: configure API URL, HF Token, Temperature, Max Tokens.
177
 
178
+ ### Error Handling in api.js
 
 
 
 
 
 
179
 
180
+ ```javascript
181
+ // Two separate detection mechanisms:
182
+ isQuotaError(result) // Response-level: checks result.response + result.sections.error
183
+ isQuotaException(errMsg) // Exception-level: checks thrown error message
184
 
185
+ // Both match: zerogpu | gpu quota | gpu task aborted | task aborted | unlogged user
 
 
 
 
 
186
  ```
187
 
188
+ When quota error detected immediately falls back to `generateDemo(prompt)` which returns pre-built HTML.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
+ ### Demo Responses Available
191
+ - `/landing|hero|page|website/i` Lumina landing page (Tailwind, gradient, features section)
192
+ - `/dashboard|chart|analytics|admin/i` Pulsegrid dashboard (sidebar, stat cards, bar chart)
193
+ - Default → Simple MINDI hello card
 
194
 
195
+ ### Settings Persistence
196
+ Saved in `localStorage` under key `mindi.builder.v1`:
197
+ ```json
198
+ {
199
+ "apiUrl": "https://mindigenous-mindi-chat.hf.space",
200
+ "hfToken": "hf_...",
201
+ "temperature": 0.7,
202
+ "maxTokens": 2048
203
+ }
204
+ ```
205
 
206
+ ---
 
207
 
208
+ ## 5. DIRECTORY STRUCTURE (Full Project)
 
209
 
 
 
210
  ```
211
+ MINDI-1.5-Vision-Coder/
212
+ ├── src/ # Model source code
213
+ │ ├── model/
214
+ │ │ ├── architecture.py # Qwen2.5-Coder + LoRA wrapper (NOT nn.Module)
215
+ │ │ ├── mindi_model.py # MINDI15 main class (nn.Module)
216
+ │ │ ├── vision_encoder.py # CLIP ViT-L/14 (frozen) + trainable projection
217
+ │ │ ├── fusion_layer.py # VisionLanguageFusion with text_gate
218
+ │ │ └── __init__.py
219
+ │ ├── training/
220
+ │ │ ├── mindi_trainer.py # MINDITrainer: 3-phase loop, streaming data
221
+ │ │ ├── data_pipeline.py # Data processing pipeline
222
+ │ │ └── __init__.py
223
+ │ └── ...
224
+ ├── scripts/
225
+ │ ├── train.py # Master training launcher
226
+ │ ├── download_websight.py
227
+ │ ├── upload_websight_images.py
228
+ │ └── gpu_diagnostic.py
229
+ ├── hf_space/
230
+ │ ├── app.py # Gradio Space — live at Mindigenous/mindi-chat
231
+ │ └── requirements.txt
232
+ ├── frontend/ # ⭐ NEW: Vite + React website builder
233
+ │ ├── index.html
234
+ │ ├── package.json
235
+ │ ├── _legacy/ # Old vanilla JS chat (backup)
236
+ │ └── src/ # (see Section 4 above)
237
+ ├── api/ # FastAPI endpoints (future)
238
+ ├── modal_api.py # Modal.com A100 API server
239
+ ├── modal_train.py # Modal.com training script
240
+ ├── data/ # Local training data
241
+ ├── configs/ # Training configs
242
+ ├── context.md # ← THIS FILE
243
+ └── ...
244
  ```
245
 
246
+ ---
247
 
248
+ ## 6. ARCHITECTURE DETAILS
 
 
 
 
 
 
 
 
249
 
250
+ ### 6.1 Model Components
251
 
252
+ | Component | Class | File | Params | Trainable |
253
+ |-----------|-------|------|--------|-----------|
254
+ | Base LLM | `MINDIArchitecture` | `architecture.py` | 7.62B | No (frozen) |
255
+ | LoRA | via PEFT | `architecture.py` | 161.5M | Yes |
256
+ | CLIP Vision | `VisionEncoder` | `vision_encoder.py` | 304M | 4.2M (projection only) |
257
+ | Fusion | `VisionLanguageFusion` | `fusion_layer.py` | 16.8M | Yes |
258
+ | **Total** | `MINDI15` | `mindi_model.py` | **8.1B** | **182.5M (2.25%)** |
259
 
260
+ ### 6.2 CRITICAL Architecture Notes
261
 
262
+ 1. **`MINDIArchitecture` is NOT an `nn.Module`** it's a plain Python wrapper. The actual trainable PeftModel is accessed via `self.architecture.get_model()` and registered as `self.llm` in `MINDI15.__init__()`.
 
 
 
263
 
264
+ 2. **`self.llm = self.architecture.get_model()`** — Required so `model.parameters()` finds LoRA params.
265
 
266
+ 3. **Fusion layer has `text_gate`** — Learnable scalar (init=0) for gradient flow during text-only batches.
267
 
268
+ ### 6.3 MINDI Special Tokens (22 total, 11 pairs)
269
 
270
  ```
271
+ <|think_start|> / <|think_end|> — Internal reasoning
272
+ <|code_start|> / <|code_end|> — Generated code blocks
273
+ <|file_start|> / <|file_end|> — File references
274
+ <|critique_start|> / <|critique_end|> — Self-critique
275
+ <|suggest_start|> / <|suggest_end|> — Suggestions
276
+ <|search_start|> / <|search_end|> — Search context
277
+ <|error_start|> / <|error_end|> — Error messages
278
+ <|fix_start|> / <|fix_end|> — Fix attempts
279
+ <|vision_start|> / <|vision_end|> — Vision input markers
280
+ <|sandbox_start|> / <|sandbox_end|> — Sandbox execution
281
+ <|context_start|> / <|context_end|> — Context block
 
 
 
 
 
 
 
 
 
 
 
 
282
  ```
283
 
 
 
284
  ---
285
 
286
+ ## 7. HF SPACE — app.py KEY DETAILS
287
 
288
+ **File:** `hf_space/app.py`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
 
290
+ ### System Prompt (no identity hallucination fix)
291
+ The system prompt explicitly states: "You are MINDI 1.5 Vision-Coder, created by Mindigenous. You are NOT GPT-4, Claude, or any other AI..."
292
 
293
+ ### chat_fn Signature
294
+ ```python
295
+ @spaces.GPU(duration=60)
296
+ def chat_fn(prompt, image, temperature, max_tokens, history_json):
297
+ # history_json is a JSON string of [{"role": ..., "content": ...}, ...]
298
+ # Returns: JSON string {"response": "...", "sections": {...}}
299
+ ```
300
 
301
+ ### Gradio Interface
302
+ ```python
303
+ gr.Interface(
304
+ fn=chat_fn,
305
+ inputs=[
306
+ gr.Textbox(label="Prompt"),
307
+ gr.Image(type="filepath", label="Image"),
308
+ gr.Slider(0, 2, value=0.7, label="Temperature"),
309
+ gr.Slider(128, 4096, value=2048, label="Max Tokens"),
310
+ gr.Textbox(label="History JSON", visible=False),
311
+ ],
312
+ outputs=gr.Textbox(label="Response"),
313
+ api_name="chat_fn"
314
+ )
315
+ ```
316
 
317
  ---
318
 
319
+ ## 8. KNOWN ERRORS & FIXES HISTORY
320
+
321
+ ### Training Errors (all fixed ✅)
322
+ | # | Error | Fix |
323
+ |---|-------|-----|
324
+ | 6.1 | GPU hang HSA_OVERRIDE_GFX_VERSION | Do NOT set this var on ROCm 7.0 |
325
+ | 6.2 | No trainable params in optimizer | `self.llm = self.architecture.get_model()` |
326
+ | 6.3 | extra_special_tokens format error | Changed from list to dict in tokenizer_config.json |
327
+ | 6.4 | Phase 2 gradient flow crash | Added `text_gate` residual in VisionLanguageFusion |
328
+ | 6.5 | Git LFS push failures | `.gitattributes` + `git lfs migrate import` |
329
+ | 6.6 | HF auth for MI300X clone | Use token as both username+password in git URL |
330
+ | 6.7 | GPU hang after heavy I/O | PCI reset: `echo 1 > /sys/bus/pci/devices/0000:83:00.0/reset` |
331
+ | 6.8 | HF upload limits (10K/dir, 25K/commit) | Reorganized images into 6 subdirs |
332
+ | 6.9 | snapshot_download HTTP 429 | Use `git clone` instead |
333
+ | 6.10 | Bash history expansion `!'` | Use multi-line python or single-quoted strings |
334
+ | 6.11 | Data dir already exists on clone | `rm -rf data` before cloning dataset repo |
335
+
336
+ ### Frontend API Errors (all fixed ✅)
337
+ | # | Error | Fix |
338
+ |---|-------|-----|
339
+ | 6.12 | `handleSend` ReferenceError in old app.js | `let activeSend = send` pattern (now in _legacy) |
340
+ | 6.13 | Gradio 3.x 5.x API mismatch (404 on /api/predict) | Rewrote to SSE v3 two-step flow |
341
+ | 6.14 | Health check misdetects Space as offline | Use `fetch(base, {mode:'no-cors'})` for HF Spaces |
342
+ | 6.15 | GPU quota blocks demo no fallback | `isQuotaError()` + `isQuotaException()` → auto demo |
343
+ | 6.16 | handlePlanSubmit catch had no demo fallback | Added demo fallback to all catch blocks in App.jsx |
344
 
345
  ---
346
 
347
+ ## 9. SESSION HISTORY
348
 
349
+ | Session | Date | Key Work |
350
+ |---------|------|----------|
351
+ | 1 | April 15, 2026 | Phase 1 dry run. GPU hang resolved. |
352
+ | 2 | April 16, 2026 | Phase 1 training 0→4250. WebSight data uploaded. |
353
+ | 3 | April 19–28, 2026 | Phase 1→2→3 complete. Model deployed to HF Space. |
354
+ | 4 | April 30, 2026 | Fixed Gradio API protocol. HF token auth. ZeroGPU quota handling. Agent scaffolded. |
355
+ | 5 | May 2, 2026 | **Rebuilt frontend as Vite+React 3-panel IDE.** Prompt enhancer, plan modal, code animation, live preview, file tree, demo fallback. |
 
 
 
 
 
 
356
 
357
  ---
358
 
359
+ ## 10. WHAT WORKS ✅
360
+
361
+ 1. **Model training** — All 3 phases complete, checkpoints on HF
362
+ 2. **HF Space** — Live at `Mindigenous/mindi-chat`, Gradio 5.x SSE v3
363
+ 3. **New Frontend (Vite+React)** — `http://localhost:5173`
364
+ - 3-panel IDE (Sidebar | Editor | Preview)
365
+ - Plan Modal (tech stack + design style questions)
366
+ - Prompt Enhancer (raw input → structured prompt)
367
+ - Code animation (line-by-line fade-in)
368
+ - File tree (real-time population during generation)
369
+ - Live preview (always-visible iframe)
370
+ - Demo fallback (landing page + dashboard demos)
371
+ - Settings modal (API URL, HF token, temperature)
372
+ - ZeroGPU quota detection + auto-fallback
373
+ 4. **Build** — `npm run build` → 222KB JS (70KB gzip), 3.25s
374
 
375
+ ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
 
377
+ ## 11. WHAT REMAINS ❌
 
378
 
379
+ ### High Priority
380
+ 1. **Add HF token to Settings** — Without token, demo fallback always used. Real MINDI output requires `hf_...` token in Settings modal.
381
+ 2. **Make suggestion pills clickable** — "Landing Page", "Dashboard" etc. chips on welcome screen should trigger generation when clicked.
382
+ 3. **Syntax highlighting** — Add Prism.js token coloring to the code editor.
383
 
384
+ ### Medium Priority
385
+ 4. **Vision loop** — Feed preview screenshots back to MINDI for automated visual QA (captureScreenshot → base64 → callMINDI).
386
+ 5. **Multi-file support** — Model generates single-file HTML currently. Add prompt instruction for `// filename:` markers to split into HTML/CSS/JS.
387
+ 6. **Download project button** — Let user download generated files as a ZIP.
388
 
389
+ ### Low Priority
390
+ 7. **WebContainer SDK** — For projects that need Node.js execution (Next.js, npm packages).
391
+ 8. **Fine-tuning for multi-file output** — Train on structured output format with `// filename:` markers.
392
+ 9. **Deploy frontend** — Host on Vercel or GitHub Pages (free).
393
 
394
  ---
395
 
396
+ ## 12. NEXT SESSION CHECKLIST
397
 
398
+ When starting a new AI assistant session:
399
 
400
+ 1. **Read this file** first (most important)
401
+ 2. **Run frontend:**
 
402
  ```powershell
403
+ cd "d:\Desktop 31st Jan 2026\MINDI 1.5 vision-coder\frontend"
404
+ npm run dev
405
+ # Opens at http://localhost:5173
406
  ```
407
+ 3. **Add HF token** in Settings (click MINDI logo → Settings → paste `hf_...` token)
408
+ 4. **Test with real MINDI model** — type "landing page", skip plan modal, verify real response comes back
409
+ 5. **Continue from "What Remains" section** above — start with suggestion chips or syntax highlighting
 
 
 
 
 
 
 
 
 
 
 
410
 
411
  ---
412
 
413
+ ## 13. COMMANDS REFERENCE
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
 
415
+ ### Frontend (Windows PowerShell)
416
+ ```powershell
417
+ # Run dev server
418
+ cd "d:\Desktop 31st Jan 2026\MINDI 1.5 vision-coder\frontend"
419
+ npm run dev # http://localhost:5173
420
 
421
+ # Build for production
422
+ npm run build # dist/ folder
423
 
424
+ # Check build
425
+ npx vite build 2>&1 | Select-Object -Last 10
 
 
426
  ```
427
 
428
+ ### Git
429
+ ```powershell
430
+ git add -A
431
+ git commit -m "..."
432
+ git push origin master # GitHub
433
+ git push hf master:main # HuggingFace
434
  ```
435
 
436
+ ### Local (Windows, PowerShell, in venv)
437
+ ```powershell
438
+ & ".\venv\Scripts\Activate.ps1"
439
+ $env:HF_TOKEN="<your-hf-token>"
440
+ python scripts/download_websight.py --num_train 50000 --num_val 2500
441
+ python scripts/upload_websight_images.py
442
  ```
443
 
444
+ ### MI300X (if spinning up again)
 
445
  ```bash
446
+ export HF_TOKEN=<your-hf-token>
447
+ export PYTORCH_ROCM_ARCH=gfx942
448
+ export TOKENIZERS_PARALLELISM=false
449
+ # DO NOT SET: HSA_OVERRIDE_GFX_VERSION
450
 
451
+ # GPU test
452
+ python3 -c "import torch; print('GPU:', torch.cuda.get_device_name(0)); x=torch.randn(100,device='cuda'); print('OK:', x.sum().item())"
453
 
454
+ # Full training
455
+ nohup python3 scripts/train.py --no_wandb > /workspace/training.log 2>&1 &
 
 
456
  ```
457
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
458
  ---
459
 
460
+ ## 14. DESIGN SYSTEM (Frontend)
461
+
462
+ CSS variables defined in `src/index.css`:
463
+
464
+ ```css
465
+ --bg-0: #07070c; /* Deepest background */
466
+ --bg-1: #0a0a12;
467
+ --panel: #111120; /* Sidebar, modals */
468
+ --border: rgba(255,255,255,.06);
469
+ --text: #ececf1;
470
+ --text-2: #b4b4c4;
471
+ --text-mute: #7a7a8c;
472
+ --purple: #7c3aed;
473
+ --purple-light: #a78bfa;
474
+ --blue: #2563eb;
475
+ --grad: linear-gradient(135deg, #7c3aed 0%, #2563eb 100%);
476
+ --sans: 'Inter', ...;
477
+ --mono: 'JetBrains Mono', ...;
478
+ --sidebar-w: 260px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
479
  ```
480
 
481
+ Key animations: `fadeIn`, `line-appear`, `float`, `pulse`, `spin`, `pop-in`, `toast-in`
482
 
483
+ ---
484
 
485
+ ## 15. MODEL QUALITY NOTES
 
 
 
 
 
 
486
 
487
+ MINDI 1.5 is a 7B model with ~10K training steps. Known characteristics:
 
 
 
 
488
 
489
+ | Issue | Status | Mitigation |
490
+ |-------|--------|-----------|
491
+ | Identity hallucination ("I am GPT-4") | ✅ Fixed via system prompt | Strong MINDI identity in `hf_space/app.py` |
492
+ | Basic/simple HTML output | ⚠️ Expected for 7B | Prompt enhancer adds design requirements |
493
+ | Weak image understanding | ⚠️ Only 2.5K vision steps | Prompt still works for text-only generation |
494
+ | No multi-file output | ⚠️ Not trained on it | Single complete file works fine |
495
 
496
+ **The prompt enhancer compensates for most quality issues** by structuring prompts with explicit design requirements (fonts, colors, responsiveness, no-placeholders rule, complete code requirement).
 
 
 
 
497
 
498
  ---
499
 
500
+ *Updated May 2, 2026 Session 5: Rebuilt frontend as Vite+React 3-panel website builder IDE.*
501
+ *Previous sessions: April 15–30, 2026 — Model training (3 phases), HF Space deployment, API fixes.*
 
 
 
 
 
 
 
 
 
 
frontend/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
frontend/.gitkeep DELETED
File without changes
frontend/README.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
frontend/agent.js DELETED
@@ -1,266 +0,0 @@
1
- /* =============================================================
2
- MINDI Agent — Orchestrator
3
- Plan → Generate → Execute → Verify → Fix loop.
4
- Turns raw MINDI model into an autonomous coding agent.
5
- ============================================================= */
6
-
7
- const MINDIAgent = (() => {
8
- 'use strict';
9
-
10
- const MAX_RETRIES = 3;
11
- const STEP_TYPES = {
12
- PLAN: 'plan',
13
- GENERATE: 'generate',
14
- EXECUTE: 'execute',
15
- VERIFY: 'verify',
16
- FIX: 'fix',
17
- DONE: 'done',
18
- ERROR: 'error',
19
- };
20
-
21
- const STATUS = { PENDING: 'pending', RUNNING: 'running', SUCCESS: 'success', FAILED: 'failed' };
22
-
23
- // ── Agent state ────────────────────────────────────────
24
- function createRun() {
25
- return {
26
- id: 'run-' + Date.now().toString(36),
27
- steps: [],
28
- currentCode: null,
29
- language: null,
30
- iteration: 0,
31
- startTime: Date.now(),
32
- status: 'running',
33
- };
34
- }
35
-
36
- function addStep(run, type, status = STATUS.RUNNING, detail = '') {
37
- const step = {
38
- id: run.steps.length,
39
- type,
40
- status,
41
- detail,
42
- startTime: Date.now(),
43
- endTime: null,
44
- };
45
- run.steps.push(step);
46
- return step;
47
- }
48
-
49
- function completeStep(step, status, detail = '') {
50
- step.status = status;
51
- step.endTime = Date.now();
52
- if (detail) step.detail = detail;
53
- }
54
-
55
- // ── Prompt templates ───────────────────────────────────
56
- function planPrompt(userRequest) {
57
- return `Break this coding request into clear, numbered implementation steps (max 5 steps). Only list the steps, nothing else.
58
-
59
- Request: ${userRequest}`;
60
- }
61
-
62
- function generatePrompt(userRequest, plan, previousCode, previousError) {
63
- let prompt = `Write COMPLETE, WORKING code for this request. Include ALL necessary HTML, CSS, and JavaScript in a single file. Do NOT leave any placeholders, TODOs, or "add more here" comments. Every feature must work.
64
-
65
- Request: ${userRequest}`;
66
-
67
- if (plan) prompt += `\n\nPlan:\n${plan}`;
68
-
69
- if (previousCode && previousError) {
70
- prompt += `\n\nPrevious code had this error:\n${previousError}\n\nPrevious code:\n\`\`\`\n${previousCode}\n\`\`\`\n\nFix the error and return the COMPLETE corrected code.`;
71
- }
72
-
73
- return prompt;
74
- }
75
-
76
- function verifyPrompt(code, output, errors, screenshotDescription) {
77
- let prompt = `Review this code and its execution result. Is it working correctly?
78
-
79
- Code:
80
- \`\`\`
81
- ${code.slice(0, 3000)}
82
- \`\`\`
83
-
84
- Console output: ${output || '(none)'}
85
- Errors: ${errors || '(none)'}`;
86
-
87
- if (screenshotDescription) {
88
- prompt += `\nScreenshot shows: ${screenshotDescription}`;
89
- }
90
-
91
- prompt += `\n\nRespond with either:
92
- - "PASS" if the code works correctly
93
- - "FAIL: <description of what's wrong>" if there are issues`;
94
-
95
- return prompt;
96
- }
97
-
98
- // ── Extract code from response ─────────────────────────
99
- function extractCode(response) {
100
- // Try fenced code blocks first
101
- const re = /```(\w+)?\s*\n([\s\S]*?)```/g;
102
- let last = null, m;
103
- while ((m = re.exec(response)) !== null) {
104
- last = { language: (m[1] || '').toLowerCase(), code: m[2] };
105
- }
106
- if (last) return last;
107
-
108
- // Try special tokens
109
- const codeMatch = response.match(/<\|code_start\|>([\s\S]*?)<\|code_end\|>/);
110
- if (codeMatch) {
111
- return { language: '', code: codeMatch[1].trim() };
112
- }
113
-
114
- return null;
115
- }
116
-
117
- // ── Main agent run ─────────────────────────────────────
118
- async function run(userPrompt, options = {}) {
119
- const {
120
- apiCall, // async (prompt, image?) => {response, sections}
121
- sandboxContainer, // DOM element for iframe preview
122
- onStep, // (run, step) => void — UI callback
123
- image = null, // optional image for vision
124
- } = options;
125
-
126
- const agentRun = createRun();
127
- const notify = (step) => onStep && onStep(agentRun, step);
128
-
129
- try {
130
- // ── Step 1: PLAN ──────────────────────────────────
131
- const planStep = addStep(agentRun, STEP_TYPES.PLAN);
132
- notify(planStep);
133
-
134
- let plan = null;
135
- try {
136
- const planResult = await apiCall(planPrompt(userPrompt), image);
137
- plan = planResult.response;
138
- completeStep(planStep, STATUS.SUCCESS, plan.split('\n').filter(l => /^\d/.test(l.trim())).length + ' steps identified');
139
- } catch (e) {
140
- completeStep(planStep, STATUS.FAILED, e.message);
141
- // Continue without plan
142
- }
143
- notify(planStep);
144
-
145
- // ── Step 2+: GENERATE → EXECUTE → VERIFY → FIX loop
146
- let previousCode = null;
147
- let previousError = null;
148
-
149
- for (let iteration = 0; iteration <= MAX_RETRIES; iteration++) {
150
- agentRun.iteration = iteration;
151
-
152
- // ── GENERATE ──────────────────────────────────
153
- const genStep = addStep(agentRun, iteration === 0 ? STEP_TYPES.GENERATE : STEP_TYPES.FIX);
154
- genStep.detail = iteration === 0 ? 'Generating code...' : `Fixing (attempt ${iteration}/${MAX_RETRIES})...`;
155
- notify(genStep);
156
-
157
- let codeResult;
158
- try {
159
- const genResult = await apiCall(
160
- generatePrompt(userPrompt, plan, previousCode, previousError),
161
- image
162
- );
163
- codeResult = extractCode(genResult.response);
164
-
165
- if (!codeResult) {
166
- // No code block found — use entire response as code
167
- codeResult = { language: '', code: genResult.response };
168
- }
169
-
170
- agentRun.currentCode = codeResult.code;
171
- agentRun.language = codeResult.language || CodeSandbox.detectLanguage(codeResult.code);
172
- const lines = codeResult.code.split('\n').length;
173
- completeStep(genStep, STATUS.SUCCESS, `${lines} lines of ${agentRun.language}`);
174
- } catch (e) {
175
- completeStep(genStep, STATUS.FAILED, e.message);
176
- notify(genStep);
177
- break;
178
- }
179
- notify(genStep);
180
-
181
- // ── EXECUTE ───────────────────────────────────
182
- const execStep = addStep(agentRun, STEP_TYPES.EXECUTE);
183
- execStep.detail = `Running ${agentRun.language} code...`;
184
- notify(execStep);
185
-
186
- let execResult;
187
- try {
188
- execResult = await CodeSandbox.execute(
189
- codeResult.code,
190
- agentRun.language,
191
- sandboxContainer
192
- );
193
-
194
- const output = execResult.logs.join('\n') || '(no output)';
195
- if (execResult.success) {
196
- completeStep(execStep, STATUS.SUCCESS, `Ran in ${execResult.duration}ms — ${output.slice(0, 100)}`);
197
- } else {
198
- completeStep(execStep, STATUS.FAILED, execResult.errors.join('\n').slice(0, 200));
199
- }
200
- } catch (e) {
201
- execResult = { success: false, errors: [e.message], logs: [] };
202
- completeStep(execStep, STATUS.FAILED, e.message);
203
- }
204
- notify(execStep);
205
-
206
- // ── VERIFY ────────────────────────────────────
207
- if (execResult.success) {
208
- const verifyStep = addStep(agentRun, STEP_TYPES.VERIFY);
209
- verifyStep.detail = 'Checking output...';
210
- notify(verifyStep);
211
-
212
- // Try to take a screenshot for visual verification
213
- let screenshot = null;
214
- if (execResult.iframe && agentRun.language === 'html') {
215
- try {
216
- screenshot = await CodeSandbox.captureScreenshot(execResult.iframe);
217
- } catch { /* ignore */ }
218
- }
219
-
220
- // Simple check: if no errors and has output, consider it passing
221
- // For a more thorough check, we'd send screenshot back to MINDI
222
- const hasOutput = execResult.logs.length > 0 || agentRun.language === 'html';
223
- if (hasOutput) {
224
- completeStep(verifyStep, STATUS.SUCCESS, 'Code runs without errors ✓');
225
- notify(verifyStep);
226
-
227
- // DONE!
228
- const doneStep = addStep(agentRun, STEP_TYPES.DONE);
229
- completeStep(doneStep, STATUS.SUCCESS, `Completed in ${iteration + 1} iteration(s)`);
230
- agentRun.status = 'success';
231
- notify(doneStep);
232
- break;
233
- } else {
234
- completeStep(verifyStep, STATUS.FAILED, 'No output produced');
235
- previousCode = codeResult.code;
236
- previousError = 'Code produced no output';
237
- notify(verifyStep);
238
- }
239
- } else {
240
- // Execution failed — prepare for retry
241
- previousCode = codeResult.code;
242
- previousError = execResult.errors.join('\n');
243
-
244
- if (iteration === MAX_RETRIES) {
245
- const errStep = addStep(agentRun, STEP_TYPES.ERROR);
246
- completeStep(errStep, STATUS.FAILED, `Failed after ${MAX_RETRIES + 1} attempts: ${previousError.slice(0, 200)}`);
247
- agentRun.status = 'failed';
248
- notify(errStep);
249
- }
250
- }
251
- }
252
- } catch (e) {
253
- const errStep = addStep(agentRun, STEP_TYPES.ERROR);
254
- completeStep(errStep, STATUS.FAILED, e.message);
255
- agentRun.status = 'failed';
256
- notify(errStep);
257
- }
258
-
259
- agentRun.endTime = Date.now();
260
- return agentRun;
261
- }
262
-
263
- return { run, STEP_TYPES, STATUS, extractCode };
264
- })();
265
-
266
- if (typeof module !== 'undefined') module.exports = MINDIAgent;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/app.js DELETED
@@ -1,2118 +0,0 @@
1
- /* =============================================================
2
- MINDI 1.5 — Vision-Coder · Frontend logic
3
- ============================================================= */
4
-
5
- (() => {
6
- 'use strict';
7
-
8
- // ----------------------------------------------------------------
9
- // Constants
10
- // ----------------------------------------------------------------
11
- const API_DEFAULT = 'https://mindigenous-mindi-chat.hf.space';
12
- const STORAGE_KEY = 'mindi.v1.state';
13
- const MAX_TEXTAREA = 200;
14
- const COLD_START_HINT_MS = 10_000; // show cold-start hint after 10s
15
-
16
- const SECTION_ORDER = ['thinking', 'code', 'critique', 'fix', 'error', 'suggest', 'file'];
17
- const SECTION_LABELS = {
18
- thinking: 'Thinking',
19
- code: 'Code',
20
- critique: 'Critique',
21
- fix: 'Fix',
22
- error: 'Error',
23
- suggest: 'Suggestion',
24
- file: 'File',
25
- };
26
- // mapping from raw token name → sections key
27
- const TOKEN_TO_KEY = {
28
- think: 'thinking',
29
- code: 'code',
30
- critique: 'critique',
31
- fix: 'fix',
32
- error: 'error',
33
- suggest: 'suggest',
34
- file: 'file',
35
- };
36
-
37
- // ----------------------------------------------------------------
38
- // State (persisted to localStorage)
39
- // ----------------------------------------------------------------
40
- const defaultState = () => ({
41
- apiUrl: API_DEFAULT,
42
- hfToken: '', // optional HF PRO token to bypass anonymous ZeroGPU quota
43
- visionEnabled: false, // default OFF — see notes in Settings; vision-language fusion is currently low-quality
44
- temperature: 0.7,
45
- maxTokens: 2048,
46
- chats: [], // [{id, title, createdAt, updatedAt, messages: [{role, content, images?}]}]
47
- currentId: null,
48
- });
49
-
50
- const state = loadState();
51
-
52
- function loadState() {
53
- try {
54
- const raw = localStorage.getItem(STORAGE_KEY);
55
- if (!raw) return defaultState();
56
- const parsed = JSON.parse(raw);
57
- return Object.assign(defaultState(), parsed);
58
- } catch {
59
- return defaultState();
60
- }
61
- }
62
-
63
- function saveState() {
64
- try {
65
- localStorage.setItem(STORAGE_KEY, JSON.stringify({
66
- apiUrl: state.apiUrl,
67
- hfToken: state.hfToken,
68
- temperature: state.temperature,
69
- maxTokens: state.maxTokens,
70
- chats: state.chats,
71
- currentId: state.currentId,
72
- }));
73
- } catch (e) {
74
- console.warn('[mindi] failed to save state', e);
75
- }
76
- }
77
-
78
- // Runtime-only state (not persisted)
79
- const runtime = {
80
- status: 'connecting', // connecting | online | demo | offline
81
- authBlocked: false, // true if last API call hit a quota/auth error
82
- pendingImages: [], // [{name, dataUrl}]
83
- isSending: false,
84
- lastCode: null, // {language, code}
85
- lastSections: null, // {thinking: [], ...}
86
- };
87
-
88
- // ----------------------------------------------------------------
89
- // DOM
90
- // ----------------------------------------------------------------
91
- const $ = (s) => document.querySelector(s);
92
- const $$ = (s) => Array.from(document.querySelectorAll(s));
93
-
94
- const els = {
95
- body: document.body,
96
- sidebar: $('#sidebar'),
97
- scrim: $('#scrim'),
98
- brand: $('#brand'),
99
- newChatBtn: $('#new-chat-btn'),
100
- search: $('#search'),
101
- history: $('#chat-history'),
102
- historyEmpty: $('#history-empty'),
103
- statusDot: $('#status-dot'),
104
- statusText: $('#status-text'),
105
-
106
- chat: $('#chat'),
107
- hamburger: $('#hamburger'),
108
- chatTitle: $('#chat-title'),
109
- togglePreview: $('#toggle-preview'),
110
-
111
- welcome: $('#welcome'),
112
- quickCards: $$('.quick-card'),
113
- messages: $('#messages'),
114
-
115
- composer: $('#composer'),
116
- composerImages: $('#composer-images'),
117
- attachBtn: $('#attach-btn'),
118
- fileInput: $('#file-input'),
119
- promptInput: $('#prompt-input'),
120
- sendBtn: $('#send-btn'),
121
-
122
- preview: $('#preview'),
123
- tabs: $$('.tab'),
124
- panes: $$('.preview-pane'),
125
- copyCode: $('#copy-code'),
126
- downloadCode: $('#download-code'),
127
- codeOut: $('#code-out'),
128
- codeOutInner: $('#code-out-inner'),
129
- emptyCode: $('#empty-code'),
130
- liveFrame: $('#live-frame'),
131
- emptyLive: $('#empty-live'),
132
- sections: $('#sections'),
133
- emptySections: $('#empty-sections'),
134
-
135
- settingsModal: $('#settings-modal'),
136
- settingsUrl: $('#settings-url'),
137
- settingsHfToken:$('#settings-hf-token'),
138
- hfTokenStatus: $('#hf-token-status'),
139
- settingsVision: $('#settings-vision'),
140
- settingsTemp: $('#settings-temp'),
141
- settingsTokens: $('#settings-tokens'),
142
- tempVal: $('#temp-val'),
143
- tokensVal: $('#tokens-val'),
144
- saveSettings: $('#save-settings'),
145
-
146
- toasts: $('#toasts'),
147
- };
148
-
149
- // ----------------------------------------------------------------
150
- // Utilities
151
- // ----------------------------------------------------------------
152
- function uid() {
153
- return 'c-' + Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
154
- }
155
- function escapeHtml(s) {
156
- return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
157
- .replace(/"/g, '&quot;').replace(/'/g, '&#039;');
158
- }
159
- function escapeAttr(s) {
160
- return escapeHtml(s);
161
- }
162
- function fileToDataUrl(file) {
163
- return new Promise((resolve, reject) => {
164
- const reader = new FileReader();
165
- reader.onload = () => resolve(reader.result);
166
- reader.onerror = reject;
167
- reader.readAsDataURL(file);
168
- });
169
- }
170
- function debounce(fn, ms) {
171
- let t = null;
172
- return (...args) => {
173
- clearTimeout(t);
174
- t = setTimeout(() => fn(...args), ms);
175
- };
176
- }
177
- function downloadFile(filename, content) {
178
- const blob = new Blob([content], { type: 'text/plain' });
179
- const url = URL.createObjectURL(blob);
180
- const a = document.createElement('a');
181
- a.href = url;
182
- a.download = filename;
183
- document.body.appendChild(a);
184
- a.click();
185
- document.body.removeChild(a);
186
- URL.revokeObjectURL(url);
187
- }
188
- function relativeDateGroup(ts) {
189
- const d = new Date(ts);
190
- const now = new Date();
191
- const start = (x) => { const z = new Date(x); z.setHours(0,0,0,0); return z; };
192
- const today = start(now);
193
- const yesterday = new Date(today); yesterday.setDate(today.getDate() - 1);
194
- const week = new Date(today); week.setDate(today.getDate() - 7);
195
-
196
- if (d >= today) return 'Today';
197
- if (d >= yesterday) return 'Yesterday';
198
- if (d >= week) return 'This Week';
199
- return 'Earlier';
200
- }
201
- function languageFromCode(code) {
202
- const trimmed = code.trim();
203
- if (/^<!doctype|^<html|^<\w+[\s>]/i.test(trimmed)) return 'markup';
204
- if (/^(import|from|def|class|print|if __name__)/m.test(trimmed)) return 'python';
205
- if (/^(import|export|const|function|class|let|var|=>)/m.test(trimmed)) return 'javascript';
206
- if (/^[\s\S]*\{[\s\S]*\}\s*$/.test(trimmed) && /^\s*"\w+"\s*:/m.test(trimmed)) return 'json';
207
- if (/(SELECT|INSERT|UPDATE|DELETE|CREATE TABLE)/i.test(trimmed)) return 'sql';
208
- if (/^[\.#]?[\w-]+\s*\{[^}]*:\s*[^;]+;/.test(trimmed)) return 'css';
209
- return 'plaintext';
210
- }
211
-
212
- // ----------------------------------------------------------------
213
- // Output cleaning + section parsing
214
- // ----------------------------------------------------------------
215
- // Strip all special tokens for chat display
216
- function cleanForDisplay(raw) {
217
- if (!raw) return '';
218
- let t = String(raw);
219
-
220
- // Section start/end tokens
221
- Object.keys(TOKEN_TO_KEY).forEach((tok) => {
222
- t = t.replace(new RegExp(`<\\|${tok}_start\\|>`, 'g'), '');
223
- t = t.replace(new RegExp(`<\\|${tok}_end\\|>`, 'g'), '');
224
- });
225
-
226
- // Conversation tokens
227
- t = t.replace(/<\|im_start\|>/g, '');
228
- t = t.replace(/<\|im_end\|>/g, '');
229
- t = t.replace(/<\|endoftext\|>/g, '');
230
-
231
- // Role prefixes at line starts
232
- t = t.replace(/^(system|user|assistant)\s*\n/gim, '');
233
-
234
- return t.trim();
235
- }
236
-
237
- // Parse the special token sections out of the raw response
238
- function parseSections(raw) {
239
- const sections = {};
240
- SECTION_ORDER.forEach((k) => { sections[k] = []; });
241
- if (!raw) return sections;
242
-
243
- const text = String(raw);
244
- Object.entries(TOKEN_TO_KEY).forEach(([tok, key]) => {
245
- const re = new RegExp(`<\\|${tok}_start\\|>([\\s\\S]*?)<\\|${tok}_end\\|>`, 'g');
246
- let m;
247
- while ((m = re.exec(text)) !== null) {
248
- const body = m[1].trim();
249
- if (body) sections[key].push(body);
250
- }
251
- });
252
- return sections;
253
- }
254
-
255
- // Merge API-provided sections with parsed ones (API wins, parsed fills gaps)
256
- function mergeSections(api, parsed) {
257
- const merged = {};
258
- SECTION_ORDER.forEach((k) => {
259
- const a = (api && Array.isArray(api[k])) ? api[k] : [];
260
- const p = (parsed && Array.isArray(parsed[k])) ? parsed[k] : [];
261
- merged[k] = a.length ? a : p;
262
- });
263
- return merged;
264
- }
265
-
266
- // ----------------------------------------------------------------
267
- // Cloud sandbox launcher (StackBlitz) — gives users real Next.js /
268
- // Node / React / HTML execution by handing the generated code off
269
- // to stackblitz.com via their public POST API. No backend required.
270
- // Docs: https://developer.stackblitz.com/docs/platform/post-api
271
- // ----------------------------------------------------------------
272
- function isCloudRunnable(code, lang) {
273
- const l = (lang || '').toLowerCase();
274
- if (['html', 'markup', 'jsx', 'tsx', 'javascript', 'js', 'typescript', 'ts', 'json'].includes(l)) return true;
275
- // Heuristic: short non-obvious snippets get the button if they parse
276
- // like a web project (so the model can ship a partial JS file too).
277
- return /<!doctype|<html|^\s*import |^\s*export |^\s*function |^\s*const |^\s*class /im.test(code);
278
- }
279
-
280
- // Detect the kind of project the model produced. Returns one of:
281
- // 'next' | 'react' | 'node' | 'html' | 'snippet'
282
- // The detection has to be permissive but ordered (Next.js before React
283
- // before Node) so a Next.js file with `import 'react'` doesn't mis-route.
284
- function detectProjectKind(code, lang) {
285
- const l = (lang || '').toLowerCase();
286
- const looksLikeNext = /from ['"]next\/|next\.config|app\/page\.[jt]sx|pages\/index|getServerSideProps|getStaticProps/i.test(code);
287
- const looksLikeReact = /from ['"]react['"]|ReactDOM\.|useState\(|useEffect\(|<\w+\s+\w+={/i.test(code);
288
- const looksLikeNode = /^\s*(?:const|import)\s+\w+\s*=?\s*require\(|process\.env|module\.exports/m.test(code);
289
- const isHtmlDoc = /<!doctype|<html/i.test(code);
290
-
291
- if (looksLikeNext) return 'next';
292
- if (looksLikeReact || l === 'jsx' || l === 'tsx') return 'react';
293
- if (looksLikeNode || l === 'json') return 'node';
294
- if (isHtmlDoc || l === 'html' || l === 'markup') return 'html';
295
- return 'snippet';
296
- }
297
-
298
- // Human-friendly label shown on the code-block header pill so users see
299
- // exactly what we detected (and why a launcher might open as HTML when
300
- // they asked for Next.js).
301
- function projectKindLabel(kind) {
302
- return ({ next: 'Next.js', react: 'React', node: 'Node.js', html: 'HTML', snippet: 'Snippet' })[kind] || kind;
303
- }
304
-
305
- // Decide which StackBlitz template + file layout to use based on what
306
- // the model produced. We try to be permissive — anything that looks
307
- // like a React/Next/Node project goes into the WebContainer-backed
308
- // 'node' template; raw HTML uses the static 'html' template.
309
- function buildStackBlitzProject(code, lang) {
310
- const kind = detectProjectKind(code, lang);
311
- const l = (lang || '').toLowerCase();
312
- const isHtmlDoc = /<!doctype|<html/i.test(code);
313
-
314
- const title = 'MINDI generated project';
315
- const description = 'Generated by MINDI 1.5 Vision-Coder';
316
-
317
- if (kind === 'next') {
318
- // Minimal Next.js 14 app-router project.
319
- return {
320
- title, description,
321
- template: 'node',
322
- files: {
323
- 'package.json': JSON.stringify({
324
- name: 'mindi-next-app',
325
- private: true,
326
- scripts: { dev: 'next dev', build: 'next build', start: 'next start' },
327
- dependencies: { next: '^14.2.5', react: '^18.3.1', 'react-dom': '^18.3.1' },
328
- }, null, 2),
329
- 'app/page.tsx': /export\s+default/i.test(code) ? code : `export default function Page() {\n return (\n <main>\n${code.split('\n').map(l => ' ' + l).join('\n')}\n </main>\n );\n}\n`,
330
- 'app/layout.tsx':
331
- `export default function RootLayout({ children }: { children: React.ReactNode }) {
332
- return (<html lang="en"><body>{children}</body></html>);
333
- }
334
- `,
335
- 'tsconfig.json': JSON.stringify({
336
- compilerOptions: {
337
- target: 'ES2020', lib: ['dom', 'dom.iterable', 'esnext'], jsx: 'preserve',
338
- module: 'esnext', moduleResolution: 'bundler', strict: true, esModuleInterop: true,
339
- skipLibCheck: true, allowJs: true, isolatedModules: true, noEmit: true,
340
- plugins: [{ name: 'next' }],
341
- },
342
- include: ['next-env.d.ts', '**/*.ts', '**/*.tsx'],
343
- }, null, 2),
344
- 'README.md': `# ${title}\n\n${description}\n\nRun:\n\n\`\`\`bash\nnpm install\nnpm run dev\n\`\`\`\n`,
345
- },
346
- };
347
- }
348
-
349
- if (kind === 'react') {
350
- // Vite + React project (faster boot in WebContainer than CRA).
351
- const ext = (l === 'tsx' || /\:\s*\w+(\[\])?/.test(code)) ? 'tsx' : 'jsx';
352
- return {
353
- title, description,
354
- template: 'node',
355
- files: {
356
- 'package.json': JSON.stringify({
357
- name: 'mindi-react-app',
358
- private: true,
359
- scripts: { dev: 'vite', build: 'vite build', preview: 'vite preview' },
360
- dependencies: { react: '^18.3.1', 'react-dom': '^18.3.1' },
361
- devDependencies: { '@vitejs/plugin-react': '^4.3.1', vite: '^5.4.1' },
362
- }, null, 2),
363
- 'vite.config.js':
364
- `import { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nexport default defineConfig({ plugins: [react()] });\n`,
365
- 'index.html':
366
- `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title></head><body><div id="root"></div><script type="module" src="/src/main.${ext}"></script></body></html>`,
367
- [`src/main.${ext}`]:
368
- `import React from 'react';\nimport { createRoot } from 'react-dom/client';\nimport App from './App.${ext}';\ncreateRoot(document.getElementById('root')).render(<App />);\n`,
369
- [`src/App.${ext}`]: /export\s+default/i.test(code) ? code : `export default function App() {\n return (<div>${'<pre>{`' + code.replace(/`/g, '\\`') + '`}</pre>'}</div>);\n}\n`,
370
- },
371
- };
372
- }
373
-
374
- if (kind === 'node') {
375
- return {
376
- title, description,
377
- template: 'node',
378
- files: {
379
- 'package.json': JSON.stringify({
380
- name: 'mindi-node-app', private: true,
381
- scripts: { start: 'node index.js' },
382
- }, null, 2),
383
- 'index.js': l === 'json' ? `console.log(${code});` : code,
384
- },
385
- };
386
- }
387
-
388
- if (kind === 'html') {
389
- return {
390
- title, description,
391
- template: 'html',
392
- files: {
393
- 'index.html': isHtmlDoc ? code : `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title></head><body>\n${code}\n</body></html>`,
394
- },
395
- };
396
- }
397
-
398
- // Fallback: static html with the code dropped into a <pre> tag so
399
- // the user at least sees their snippet rendered in the StackBlitz preview.
400
- return {
401
- title, description,
402
- template: 'html',
403
- files: {
404
- 'index.html': `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title></head><body><pre>${escapeHtml(code)}</pre></body></html>`,
405
- },
406
- };
407
- }
408
-
409
- // Pick the file the user most likely wants to land on when StackBlitz opens.
410
- function chooseEntryFile(proj) {
411
- const files = proj.files || {};
412
- const preferred = [
413
- 'app/page.tsx', 'app/page.jsx',
414
- 'src/App.tsx', 'src/App.jsx',
415
- 'pages/index.tsx', 'pages/index.jsx',
416
- 'index.html', 'index.js',
417
- ];
418
- for (const p of preferred) if (files[p]) return p;
419
- return Object.keys(files)[0] || 'index.html';
420
- }
421
-
422
- // Hand the project off to stackblitz.com. We prefer the official SDK
423
- // (https://developer.stackblitz.com/platform/api/javascript-sdk) because
424
- // the bare /run form-POST endpoint silently rejects some payloads and
425
- // falls back to opening their default Next.js starter — exactly what
426
- // the user reported. The SDK uses an iframe handshake that's more
427
- // reliable across browser SameSite / referrer policies.
428
- // Form POST is kept as a fallback if the SDK script fails to load.
429
- function launchInStackBlitz(code, lang) {
430
- const proj = buildStackBlitzProject(code, lang);
431
- const sdk = window.StackBlitzSDK;
432
-
433
- if (sdk && typeof sdk.openProject === 'function') {
434
- try {
435
- sdk.openProject(
436
- {
437
- title: proj.title,
438
- description: proj.description,
439
- template: proj.template,
440
- files: proj.files,
441
- },
442
- { newWindow: true, openFile: chooseEntryFile(proj) }
443
- );
444
- return;
445
- } catch (err) {
446
- console.warn('StackBlitz SDK launch failed, falling back to form POST:', err);
447
- }
448
- }
449
-
450
- // Fallback: classic form POST. Less reliable but works without the SDK
451
- // script (useful if it's blocked by an offline / strict-CSP environment).
452
- const form = document.createElement('form');
453
- form.action = 'https://stackblitz.com/run';
454
- form.method = 'POST';
455
- form.target = '_blank';
456
- form.rel = 'noopener';
457
- form.style.display = 'none';
458
-
459
- const add = (name, value) => {
460
- const input = document.createElement('input');
461
- input.type = 'hidden';
462
- input.name = name;
463
- input.value = value;
464
- form.appendChild(input);
465
- };
466
- add('project[title]', proj.title);
467
- add('project[description]', proj.description);
468
- add('project[template]', proj.template);
469
- Object.entries(proj.files).forEach(([path, content]) => {
470
- add(`project[files][${path}]`, content);
471
- });
472
-
473
- document.body.appendChild(form);
474
- form.submit();
475
- setTimeout(() => form.remove(), 0);
476
- }
477
-
478
- // ----------------------------------------------------------------
479
- // Cloud sandbox launcher (CodeSandbox) — second cloud IDE option.
480
- // Uses the public Define API which returns a sandbox_id we redirect to.
481
- // Docs: https://codesandbox.io/docs/learn/sandboxes/cli-api#define-api
482
- // We reuse buildStackBlitzProject() for the file shape since both IDEs
483
- // accept the same package.json / file layout; CodeSandbox auto-detects
484
- // the template from package.json dependencies.
485
- // ----------------------------------------------------------------
486
- function buildCodeSandboxFiles(code, lang) {
487
- const proj = buildStackBlitzProject(code, lang);
488
- const files = {};
489
- Object.entries(proj.files).forEach(([path, content]) => {
490
- files[path] = { content };
491
- });
492
- // For raw HTML projects (StackBlitz template='html'), nudge CodeSandbox
493
- // toward its 'static' template so it serves index.html as-is instead of
494
- // trying to npm install nothing.
495
- if (proj.template === 'html' && !files['package.json']) {
496
- files['sandbox.config.json'] = { content: JSON.stringify({ template: 'static' }, null, 2) };
497
- }
498
- return files;
499
- }
500
-
501
- async function launchInCodeSandbox(code, lang) {
502
- const files = buildCodeSandboxFiles(code, lang);
503
- try {
504
- const res = await fetch('https://codesandbox.io/api/v1/sandboxes/define?json=1', {
505
- method: 'POST',
506
- headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
507
- body: JSON.stringify({ files }),
508
- });
509
- if (!res.ok) {
510
- const txt = await res.text().catch(() => '');
511
- throw new Error(`HTTP ${res.status}: ${txt.slice(0, 160)}`);
512
- }
513
- const data = await res.json();
514
- if (!data || !data.sandbox_id) throw new Error('No sandbox_id in response');
515
- window.open(`https://codesandbox.io/s/${data.sandbox_id}`, '_blank', 'noopener');
516
- } catch (err) {
517
- toast(`CodeSandbox launch failed: ${err.message || err}`, 'error');
518
- }
519
- }
520
-
521
- // Extract last fenced code block from the response text
522
- function extractLastCodeBlock(text) {
523
- if (!text) return null;
524
- const re = /```(\w+)?\s*\n([\s\S]*?)```/g;
525
- let last = null, m;
526
- while ((m = re.exec(text)) !== null) {
527
- last = { language: (m[1] || '').toLowerCase() || null, code: m[2] };
528
- }
529
- if (last) {
530
- if (!last.language) last.language = languageFromCode(last.code);
531
- return last;
532
- }
533
- return null;
534
- }
535
-
536
- // ----------------------------------------------------------------
537
- // Markdown renderer (limited: paragraphs, fenced code, inline code, bold/italic)
538
- // ----------------------------------------------------------------
539
- function renderMarkdown(text) {
540
- if (!text) return '';
541
-
542
- // Tokenize: split into fenced-code parts and text parts
543
- const segments = [];
544
- const re = /```(\w+)?\s*\n([\s\S]*?)```/g;
545
- let lastIdx = 0, m;
546
- while ((m = re.exec(text)) !== null) {
547
- if (m.index > lastIdx) {
548
- segments.push({ type: 'text', value: text.slice(lastIdx, m.index) });
549
- }
550
- segments.push({ type: 'code', lang: (m[1] || '').toLowerCase() || null, value: m[2] });
551
- lastIdx = re.lastIndex;
552
- }
553
- if (lastIdx < text.length) {
554
- segments.push({ type: 'text', value: text.slice(lastIdx) });
555
- }
556
-
557
- return segments.map((seg) => {
558
- if (seg.type === 'code') {
559
- const lang = seg.lang || languageFromCode(seg.value);
560
- const safe = escapeHtml(seg.value);
561
- const dataCode = escapeAttr(seg.value);
562
- const runnable = isCloudRunnable(seg.value, lang);
563
- const kind = runnable ? detectProjectKind(seg.value, lang) : null;
564
- const kindPill = kind
565
- ? `<span class="md-kind md-kind--${kind}" title="Project kind detected from the generated code. The launchers below open this exact code, even if it doesn't match what you originally asked for.">${projectKindLabel(kind)}</span>`
566
- : '';
567
- const launchBtns = runnable
568
- ? (
569
- `<button class="md-run" data-code="${dataCode}" data-lang="${escapeAttr(lang)}" type="button" title="Run this code on stackblitz.com (real Node.js / WebContainer sandbox, supports Next.js / React / Node)">\u25B6 StackBlitz</button>` +
570
- `<button class="md-sandbox" data-code="${dataCode}" data-lang="${escapeAttr(lang)}" type="button" title="Open this code in codesandbox.io (cloud IDE with live preview)">\u25B6 CodeSandbox</button>`
571
- )
572
- : '';
573
- return (
574
- `<pre class="md-code-block">` +
575
- `<div class="md-code-head">` +
576
- `<span class="md-lang">${escapeHtml(lang)}${kindPill}</span>` +
577
- `<div class="md-code-actions">` +
578
- launchBtns +
579
- `<button class="md-copy" data-code="${dataCode}" type="button">Copy</button>` +
580
- `</div>` +
581
- `</div>` +
582
- `<code class="language-${escapeHtml(lang)}">${safe}</code>` +
583
- `</pre>`
584
- );
585
- }
586
- // text segment
587
- let h = seg.value.trim();
588
- if (!h) return '';
589
- h = escapeHtml(h);
590
- h = h.replace(/`([^`\n]+)`/g, '<code class="md-inline">$1</code>');
591
- h = h.replace(/\*\*([^*\n]+)\*\*/g, '<strong>$1</strong>');
592
- h = h.replace(/(^|[\s(])\*([^*\n]+)\*(?=[\s).,!?:;]|$)/g, '$1<em>$2</em>');
593
- return h.split(/\n{2,}/)
594
- .map((p) => '<p>' + p.replace(/\n/g, '<br>') + '</p>')
595
- .join('');
596
- }).join('');
597
- }
598
-
599
- // ----------------------------------------------------------------
600
- // API client
601
- // ----------------------------------------------------------------
602
- function authHeaders(extra) {
603
- const h = Object.assign({}, extra || {});
604
- if (state.hfToken) h['Authorization'] = `Bearer ${state.hfToken}`;
605
- return h;
606
- }
607
-
608
- // Convert a data: URL into a Blob (used for Gradio image uploads).
609
- function dataUrlToBlob(dataUrl) {
610
- const match = /^data:([^;]+);base64,(.+)$/.exec(dataUrl || '');
611
- if (!match) throw new Error('Invalid image data URL');
612
- const mime = match[1];
613
- const b64 = match[2];
614
- const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
615
- return { blob: new Blob([bytes], { type: mime }), mime };
616
- }
617
-
618
- // Upload an image to a Gradio HF Space via /gradio_api/upload.
619
- // Returns the server-side file path that can be referenced as
620
- // {path: ..., meta: {_type: "gradio.FileData"}} in a chat_fn data array.
621
- // This is REQUIRED — gr.Image(type="pil") on the backend cannot decode
622
- // a raw data URL string.
623
- async function uploadImageToGradio(base, dataUrl, signal) {
624
- const { blob, mime } = dataUrlToBlob(dataUrl);
625
- const ext = (mime.split('/')[1] || 'png').replace('+xml', '').split(';')[0];
626
- const filename = `mindi-upload-${Date.now()}.${ext}`;
627
- const formData = new FormData();
628
- formData.append('files', blob, filename);
629
-
630
- // Don't pre-set Content-Type — the browser sets the multipart boundary.
631
- const headers = authHeaders({});
632
- delete headers['Content-Type'];
633
-
634
- const res = await fetch(`${base}/gradio_api/upload`, {
635
- method: 'POST',
636
- headers,
637
- body: formData,
638
- signal,
639
- });
640
- if (!res.ok) {
641
- const txt = await res.text().catch(() => '');
642
- throw new Error(`Image upload ${res.status}: ${txt.slice(0, 200) || 'failed'}`);
643
- }
644
- const result = await res.json();
645
- // Gradio 5.x returns ["/tmp/gradio/.../filename.png"]
646
- const filePath = Array.isArray(result) ? result[0] : (result && result.files && result.files[0]);
647
- if (!filePath || typeof filePath !== 'string') {
648
- throw new Error(`Unexpected upload response: ${JSON.stringify(result).slice(0, 200)}`);
649
- }
650
- return filePath;
651
- }
652
-
653
- // Detect responses that came back as a quota / auth error from the
654
- // backend's chat_fn try/except, so we can show actionable UX.
655
- function detectAuthError(result) {
656
- if (!result) return null;
657
- const text = String(result.response || '');
658
- const errs = (result.sections && result.sections.error) || [];
659
- const blob = (text + ' ' + errs.join(' ')).toLowerCase();
660
- if (/zerogpu|gpu quota|out of .* quota|exceeded .* quota|unlogged user/.test(blob)) {
661
- return state.hfToken
662
- ? 'Your HF token hit its ZeroGPU quota. Wait for the daily reset or use a PRO token.'
663
- : 'Anonymous ZeroGPU quota exhausted. Open Settings (double-click the MINDI logo) and paste your HF token.';
664
- }
665
- return null;
666
- }
667
-
668
- // Build a friendly assistant message shown inline in the chat when the
669
- // request is (or would be) blocked by the ZeroGPU quota / auth wall.
670
- // ZeroGPU quota is per-user — there is no way to bypass it without a
671
- // logged-in HF token. See https://huggingface.co/docs/hub/en/spaces-zerogpu
672
- function makeAuthBlockedResponse() {
673
- if (state.hfToken) {
674
- return {
675
- response:
676
- `**Your HF token hit its daily ZeroGPU quota.**
677
-
678
- ZeroGPU enforces a per-user GPU-time budget that resets every 24 hours after first use. Free HF accounts get a small daily allowance, **PRO accounts get 8\u00d7 more**, and PRO/Team/Enterprise can also top up with [pre-paid credits](https://huggingface.co/settings/billing) at \\$1 per 10 GPU-minutes.
679
-
680
- **What to do:**
681
- - Wait for the daily reset (24h after your first call today), **or**
682
- - Top up credits in your HF billing settings, **or**
683
- - Open **Settings** (double-click the MINDI logo) and paste a different PRO token.
684
-
685
- I'll keep further messages local until you update the token \u2014 sending them now would just hit the same wall.`,
686
- sections: {},
687
- };
688
- }
689
- return {
690
- response:
691
- `**Sign-in needed to use the live model.**
692
-
693
- The MINDI 1.5 backend runs on HuggingFace ZeroGPU, which gives every IP a tiny anonymous quota (~3 minutes / day) before blocking further requests. That's why the first message worked but the next one didn't.
694
-
695
- **To unlock real generation:**
696
- 1. Get a token at [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) (free account is fine; **PRO** gives 8\u00d7 more).
697
- 2. **Double-click the MINDI logo** \u2192 paste the token in the *HuggingFace token* field \u2192 *Save settings*.
698
- 3. Re-send your message.
699
-
700
- Your token is stored only in this browser's local storage and sent as an \`Authorization: Bearer\` header to the Space.`,
701
- sections: {},
702
- };
703
- }
704
-
705
- async function pingHealth() {
706
- // If a previous request was blocked by ZeroGPU quota / auth, stay in
707
- // 'Auth required' until the user adds a token (applySettings clears it).
708
- // Otherwise this would silently flip back to 'online' every 60s and the
709
- // next user message would hit the same quota wall.
710
- if (runtime.authBlocked) {
711
- setStatus('demo', state.hfToken ? 'Quota exhausted' : 'Auth required');
712
- return;
713
- }
714
- if (!state.apiUrl) {
715
- setStatus('demo', 'Demo Mode');
716
- return;
717
- }
718
- try {
719
- const base = state.apiUrl.replace(/\/$/, '');
720
- const isGradio = base.includes('hf.space') || base.includes('huggingface.co');
721
-
722
- if (isGradio) {
723
- // For Gradio/HF Spaces: check the root URL which returns the Gradio page
724
- const res = await fetch(base, { method: 'HEAD', mode: 'no-cors' }).catch(() => null);
725
- // no-cors always returns opaque response, so we check for network errors
726
- if (res) {
727
- setStatus('online', 'MINDI · HF Space');
728
- } else {
729
- setStatus('demo', 'Demo Mode (Space unreachable)');
730
- }
731
- } else {
732
- // Direct REST API health check
733
- try {
734
- const res = await fetch(`${base}/api/health`, { method: 'GET', headers: { 'Accept': 'application/json' } });
735
- if (res.ok) {
736
- const d = await res.json().catch(() => ({}));
737
- setStatus('online', `${d.model || 'MINDI'} · online`);
738
- } else {
739
- setStatus('demo', 'Demo Mode');
740
- }
741
- } catch {
742
- setStatus('demo', 'Demo Mode');
743
- }
744
- }
745
- } catch {
746
- setStatus('demo', 'Demo Mode');
747
- }
748
- }
749
-
750
- // Build a Qwen-style history list from the current chat, EXCLUDING the
751
- // user message that's about to be sent (which is `prompt` itself) and any
752
- // in-flight loading placeholder. Capped to keep the request small.
753
- function buildHistory() {
754
- const chat = currentChat();
755
- if (!chat) return [];
756
- const msgs = chat.messages || [];
757
- // Drop trailing loading messages and the most-recent user message,
758
- // since send() already pushed it just before calling us.
759
- let end = msgs.length;
760
- while (end > 0 && msgs[end - 1].loading) end--;
761
- if (end > 0 && msgs[end - 1].role === 'user') end--;
762
- const slice = msgs.slice(Math.max(0, end - 20), end);
763
- return slice
764
- .filter((m) => (m.role === 'user' || m.role === 'assistant') && !m.loading)
765
- .map((m) => ({
766
- role: m.role,
767
- content: typeof m.content === 'string' ? m.content : String(m.content || ''),
768
- }))
769
- .filter((m) => m.content.trim().length > 0);
770
- }
771
-
772
- async function callGenerate(prompt, image, signal) {
773
- const base = state.apiUrl.replace(/\/$/, '');
774
- const history = buildHistory();
775
- // Gradio expects each input as its own positional element. We pass
776
- // history as a JSON-encoded string because the backend's chat_fn input
777
- // is a Textbox (not a JSON component) — _coerce_history() decodes it.
778
- const historyJson = history.length ? JSON.stringify(history) : '';
779
-
780
- // Detect if this is a Gradio HF Space
781
- const isGradio = base.includes('hf.space') || base.includes('huggingface.co/spaces');
782
-
783
- if (isGradio) {
784
- // Gradio 5.x SSE v3 protocol — two-step:
785
- // 1. POST /gradio_api/call/{api_name} → get event_id
786
- // 2. GET /gradio_api/call/{api_name}/{event_id} → stream result
787
-
788
- // ── Image: upload first, then reference by path ──
789
- // gr.Image(type="pil") cannot decode a raw data: URL — it expects a
790
- // FileData reference produced by /gradio_api/upload. We do this
791
- // unconditionally when an image is supplied so the backend's CLIP
792
- // path actually receives pixels. If vision is disabled in settings,
793
- // send() drops the image before calling us.
794
- let imageArg = null;
795
- if (image && typeof image === 'string' && image.startsWith('data:')) {
796
- try {
797
- const filePath = await uploadImageToGradio(base, image, signal);
798
- imageArg = {
799
- path: filePath,
800
- meta: { _type: 'gradio.FileData' },
801
- orig_name: filePath.split('/').pop() || 'image.png',
802
- };
803
- } catch (e) {
804
- console.warn('[mindi] Image upload to Gradio failed:', e);
805
- toast(`Image upload failed: ${e.message || e}. Sending text only.`, 'error', 5000);
806
- imageArg = null;
807
- }
808
- }
809
-
810
- // Step 1: Submit the request
811
- const submitRes = await fetch(`${base}/gradio_api/call/chat_fn`, {
812
- method: 'POST',
813
- headers: authHeaders({ 'Content-Type': 'application/json' }),
814
- body: JSON.stringify({
815
- data: [prompt, imageArg, state.temperature, state.maxTokens, historyJson],
816
- }),
817
- signal,
818
- });
819
- if (!submitRes.ok) {
820
- const txt = await submitRes.text().catch(() => '');
821
- throw new Error(`API submit ${submitRes.status}: ${txt.slice(0, 200) || 'request failed'}`);
822
- }
823
- const { event_id } = await submitRes.json();
824
- if (!event_id) {
825
- throw new Error('No event_id returned from Gradio API');
826
- }
827
-
828
- // Step 2: Get the result via SSE stream
829
- const resultRes = await fetch(`${base}/gradio_api/call/chat_fn/${event_id}`, {
830
- method: 'GET',
831
- headers: authHeaders(),
832
- signal,
833
- });
834
- if (!resultRes.ok) {
835
- const txt = await resultRes.text().catch(() => '');
836
- throw new Error(`API result ${resultRes.status}: ${txt.slice(0, 200) || 'request failed'}`);
837
- }
838
-
839
- // Parse SSE response — look for the "complete" event with data
840
- const sseText = await resultRes.text();
841
- const lines = sseText.split('\n');
842
- let raw = null;
843
- for (let i = 0; i < lines.length; i++) {
844
- if (lines[i].startsWith('event: complete')) {
845
- // Next line(s) starting with "data: " contain the result
846
- const dataLine = lines[i + 1];
847
- if (dataLine && dataLine.startsWith('data: ')) {
848
- try {
849
- const parsed = JSON.parse(dataLine.slice(6));
850
- // Gradio wraps in array
851
- raw = Array.isArray(parsed) ? parsed[0] : parsed;
852
- } catch {
853
- raw = dataLine.slice(6);
854
- }
855
- }
856
- break;
857
- }
858
- if (lines[i].startsWith('event: error')) {
859
- const dataLine = lines[i + 1];
860
- const errMsg = dataLine?.startsWith('data: ') ? dataLine.slice(6) : 'Unknown Gradio error';
861
- throw new Error(`Gradio error: ${errMsg.slice(0, 300)}`);
862
- }
863
- }
864
-
865
- if (raw === null) {
866
- throw new Error('No complete event found in Gradio SSE response');
867
- }
868
-
869
- // raw is a JSON string from our chat_fn
870
- try {
871
- return JSON.parse(raw);
872
- } catch {
873
- return { response: String(raw), sections: {} };
874
- }
875
-
876
- } else {
877
- // Direct REST API (Modal or custom)
878
- const body = {
879
- prompt,
880
- temperature: state.temperature,
881
- max_tokens: state.maxTokens,
882
- history,
883
- };
884
- if (image) body.image = image;
885
- const res = await fetch(`${base}/api/generate`, {
886
- method: 'POST',
887
- headers: authHeaders({ 'Content-Type': 'application/json', 'Accept': 'application/json' }),
888
- body: JSON.stringify(body),
889
- signal,
890
- });
891
- if (!res.ok) {
892
- const txt = await res.text().catch(() => '');
893
- throw new Error(`API ${res.status}: ${txt.slice(0, 200) || 'request failed'}`);
894
- }
895
- return res.json();
896
- }
897
- }
898
-
899
- // ----------------------------------------------------------------
900
- // Demo / Fallback responses
901
- // ----------------------------------------------------------------
902
- const DEMO_RESPONSES = [
903
- {
904
- match: /landing|hero|next\.?js/i,
905
- response:
906
- `Here's a clean Next.js landing page using Tailwind CSS:
907
-
908
- \`\`\`tsx
909
- // app/page.tsx
910
- export default function Home() {
911
- return (
912
- <main className="min-h-screen bg-gradient-to-b from-slate-950 to-slate-900 text-white">
913
- <section className="max-w-6xl mx-auto px-6 py-24 text-center">
914
- <span className="inline-block px-3 py-1 rounded-full bg-violet-500/15 text-violet-300 text-xs font-mono tracking-widest uppercase mb-6">
915
- Now in beta
916
- </span>
917
- <h1 className="text-5xl md:text-7xl font-semibold tracking-tight">
918
- Build faster.<br/>
919
- <span className="bg-gradient-to-r from-violet-400 to-blue-400 bg-clip-text text-transparent">
920
- Ship sooner.
921
- </span>
922
- </h1>
923
- <p className="mt-6 text-lg text-slate-300 max-w-2xl mx-auto">
924
- The frontend you'd build if you had unlimited time, in a single prompt.
925
- </p>
926
- <div className="mt-10 flex justify-center gap-3">
927
- <a className="px-6 py-3 rounded-full bg-gradient-to-r from-violet-600 to-blue-600 font-medium" href="#cta">
928
- Get started
929
- </a>
930
- <a className="px-6 py-3 rounded-full border border-white/10 hover:bg-white/5" href="#features">
931
- See features
932
- </a>
933
- </div>
934
- </section>
935
- </main>
936
- );
937
- }
938
- \`\`\`
939
-
940
- This sets up a hero section with a gradient headline, a kicker badge, and two CTAs. Drop in an \`<Image>\` background or particle layer next.`,
941
- sections: {
942
- thinking: ['User wants a Next.js landing page. Producing a single-file app/page.tsx using Tailwind for the hero, with two CTAs and accessible markup.'],
943
- code: ['app/page.tsx generated with hero section + gradient headline.'],
944
- critique: [],
945
- fix: [],
946
- },
947
- },
948
- {
949
- match: /dashboard|chart|analytics/i,
950
- response:
951
- `Here's a self-contained dashboard UI in vanilla HTML/CSS:
952
-
953
- \`\`\`html
954
- <!DOCTYPE html>
955
- <html lang="en">
956
- <head>
957
- <meta charset="UTF-8" />
958
- <title>Pulsegrid · Dashboard</title>
959
- <style>
960
- :root { --bg:#0b0b14; --panel:#14141f; --border:rgba(255,255,255,.08); --text:#ececf1; --mute:#8b94a7; --acc:#7c3aed; }
961
- * { box-sizing:border-box; margin:0; padding:0; }
962
- body { background:var(--bg); color:var(--text); font:14px/1.55 'Inter', sans-serif; min-height:100vh; display:grid; grid-template-columns:240px 1fr; }
963
- aside { background:var(--panel); border-right:1px solid var(--border); padding:20px; }
964
- aside h1 { font-size:18px; background:linear-gradient(135deg,#7c3aed,#2563eb); -webkit-background-clip:text; color:transparent; margin-bottom:24px; }
965
- nav a { display:block; padding:10px 12px; border-radius:8px; color:var(--mute); text-decoration:none; margin-bottom:2px; }
966
- nav a.active { background:rgba(124,58,237,.15); color:#fff; }
967
- main { padding:24px; overflow-y:auto; }
968
- .stats { display:grid; grid-template-columns:repeat(4,1fr); gap:14px; margin-bottom:20px; }
969
- .stat { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:16px; }
970
- .stat .v { font-size:24px; font-weight:600; margin-top:6px; }
971
- .stat .l { color:var(--mute); font-size:12px; text-transform:uppercase; letter-spacing:.1em; }
972
- .chart { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:18px; height:260px; display:flex; align-items:end; gap:8px; }
973
- .bar { flex:1; background:linear-gradient(180deg,#7c3aed,#2563eb); border-radius:6px 6px 0 0; }
974
- </style>
975
- </head>
976
- <body>
977
- <aside>
978
- <h1>Pulsegrid</h1>
979
- <nav>
980
- <a class="active">Overview</a>
981
- <a>Customers</a>
982
- <a>Revenue</a>
983
- <a>Settings</a>
984
- </nav>
985
- </aside>
986
- <main>
987
- <div class="stats">
988
- <div class="stat"><div class="l">Revenue</div><div class="v">$48,210</div></div>
989
- <div class="stat"><div class="l">Active users</div><div class="v">12,840</div></div>
990
- <div class="stat"><div class="l">Conversion</div><div class="v">4.2%</div></div>
991
- <div class="stat"><div class="l">Churn</div><div class="v">1.1%</div></div>
992
- </div>
993
- <div class="chart">
994
- <div class="bar" style="height:40%"></div>
995
- <div class="bar" style="height:65%"></div>
996
- <div class="bar" style="height:30%"></div>
997
- <div class="bar" style="height:80%"></div>
998
- <div class="bar" style="height:55%"></div>
999
- <div class="bar" style="height:90%"></div>
1000
- <div class="bar" style="height:70%"></div>
1001
- </div>
1002
- </main>
1003
- </body>
1004
- </html>
1005
- \`\`\`
1006
-
1007
- Hit the **Preview** tab to see it rendered live.`,
1008
- sections: {
1009
- thinking: ['User wants a dashboard. Building a self-contained HTML page with sidebar nav, stat cards, and a CSS-only bar chart so it can render in the iframe preview.'],
1010
- code: ['Single-file dashboard.html with grid layout, stats, sidebar.'],
1011
- critique: ['No real charting library — bars are static. For production, swap in Recharts/Chart.js.'],
1012
- fix: [],
1013
- },
1014
- },
1015
- {
1016
- match: /api|fastapi|backend|jwt|postgres/i,
1017
- response:
1018
- `Here's a minimal but production-shaped FastAPI service:
1019
-
1020
- \`\`\`python
1021
- # main.py
1022
- from datetime import datetime, timedelta
1023
- from fastapi import FastAPI, Depends, HTTPException, status
1024
- from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
1025
- from sqlalchemy.orm import Session
1026
- from passlib.context import CryptContext
1027
- from jose import jwt
1028
- from pydantic import BaseModel
1029
-
1030
- from .database import SessionLocal, engine
1031
- from . import models, schemas
1032
-
1033
- models.Base.metadata.create_all(bind=engine)
1034
-
1035
- app = FastAPI(title="Notes API")
1036
- SECRET_KEY = "change-me"
1037
- ALGORITHM = "HS256"
1038
- EXPIRES = timedelta(hours=24)
1039
- pwd = CryptContext(schemes=["bcrypt"])
1040
- oauth2 = OAuth2PasswordBearer(tokenUrl="/auth/login")
1041
-
1042
- def get_db():
1043
- db = SessionLocal()
1044
- try: yield db
1045
- finally: db.close()
1046
-
1047
- def make_token(sub: str) -> str:
1048
- return jwt.encode({"sub": sub, "exp": datetime.utcnow() + EXPIRES}, SECRET_KEY, ALGORITHM)
1049
-
1050
- @app.post("/auth/login")
1051
- def login(form: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
1052
- user = db.query(models.User).filter_by(email=form.username).first()
1053
- if not user or not pwd.verify(form.password, user.hashed_password):
1054
- raise HTTPException(401, "Invalid credentials")
1055
- return {"access_token": make_token(user.email), "token_type": "bearer"}
1056
-
1057
- @app.get("/notes", response_model=list[schemas.Note])
1058
- def list_notes(db: Session = Depends(get_db), token: str = Depends(oauth2)):
1059
- payload = jwt.decode(token, SECRET_KEY, [ALGORITHM])
1060
- return db.query(models.Note).filter_by(owner_email=payload["sub"]).all()
1061
- \`\`\`
1062
-
1063
- Pair this with \`models.py\`, \`schemas.py\`, and a \`.env\` containing \`DATABASE_URL=postgresql://…\`.`,
1064
- sections: {
1065
- thinking: ['User asked for FastAPI + JWT + Postgres. Sketching the service entrypoint with auth flow and a protected GET endpoint.'],
1066
- code: ['main.py with /auth/login and /notes endpoints.'],
1067
- critique: ['SECRET_KEY hardcoded — move to env. bcrypt rounds are default — increase for production.'],
1068
- suggest: ['Add Alembic migrations and pytest fixtures next.'],
1069
- },
1070
- },
1071
- {
1072
- match: /bug|fix|debug|divide/i,
1073
- response:
1074
- `Two bugs in that snippet:
1075
-
1076
- 1. **No zero-divisor guard** — \`divide_list([1,2], 0)\` throws \`ZeroDivisionError\`.
1077
- 2. **No type validation** — non-numeric items raise \`TypeError\` deep in the loop.
1078
-
1079
- Here's the fix:
1080
-
1081
- \`\`\`python
1082
- def divide_list(numbers, divisor):
1083
- if divisor == 0:
1084
- raise ValueError("divisor must be non-zero")
1085
- result = []
1086
- for n in numbers:
1087
- if not isinstance(n, (int, float)):
1088
- raise TypeError(f"non-numeric item: {n!r}")
1089
- result.append(n / divisor)
1090
- return result
1091
- \`\`\`
1092
-
1093
- For very large lists, switch to a generator (\`yield\` instead of \`append\`) to keep memory flat.`,
1094
- sections: {
1095
- thinking: ['Two issues: zero-divisor crash, non-numeric crash. Adding explicit guards and a clearer error message.'],
1096
- critique: ['Function silently coerces booleans because bool ⊂ int in Python — may want to exclude them explicitly.'],
1097
- fix: ['Added divisor==0 guard, added isinstance validation, kept the original signature.'],
1098
- code: ['Patched divide_list with safe input checking.'],
1099
- },
1100
- },
1101
- ];
1102
-
1103
- const DEFAULT_DEMO = {
1104
- response:
1105
- `I'm running in **Demo Mode** because the live API isn't reachable.
1106
-
1107
- Try one of the quick-action prompts on the welcome screen, or open Settings (double-click the brand logo) and paste your MINDI API URL.
1108
-
1109
- \`\`\`javascript
1110
- // You sent a prompt and I'm a placeholder.
1111
- // Connect the real API to see actual generations.
1112
- console.log("MINDI 1.5 — awaiting connection");
1113
- \`\`\``,
1114
- sections: {
1115
- thinking: ['No API URL configured or the endpoint is unreachable. Returning a demo response.'],
1116
- code: ['Stub response.'],
1117
- },
1118
- };
1119
-
1120
- function pickDemo(prompt) {
1121
- const found = DEMO_RESPONSES.find((d) => d.match.test(prompt));
1122
- return found || DEFAULT_DEMO;
1123
- }
1124
-
1125
- async function generateDemo(prompt) {
1126
- await new Promise((r) => setTimeout(r, 1200 + Math.random() * 700));
1127
- const demo = pickDemo(prompt);
1128
- return { response: demo.response, sections: demo.sections || {} };
1129
- }
1130
-
1131
- // ----------------------------------------------------------------
1132
- // Status
1133
- // ----------------------------------------------------------------
1134
- function setStatus(status, label) {
1135
- runtime.status = status;
1136
- els.statusDot.classList.remove('status-dot--gray', 'status-dot--green', 'status-dot--yellow', 'status-dot--red');
1137
- const map = { connecting: 'gray', online: 'green', demo: 'yellow', offline: 'red' };
1138
- els.statusDot.classList.add(`status-dot--${map[status] || 'gray'}`);
1139
- els.statusText.textContent = label;
1140
- }
1141
-
1142
- // ----------------------------------------------------------------
1143
- // Toasts
1144
- // ----------------------------------------------------------------
1145
- function toast(msg, kind = 'info', ms = 2400) {
1146
- const el = document.createElement('div');
1147
- el.className = `toast toast--${kind}`;
1148
- el.innerHTML = `<span class="toast-icon"></span><span>${escapeHtml(msg)}</span>`;
1149
- els.toasts.appendChild(el);
1150
- setTimeout(() => {
1151
- el.classList.add('is-leaving');
1152
- setTimeout(() => el.remove(), 260);
1153
- }, ms);
1154
- }
1155
-
1156
- // ----------------------------------------------------------------
1157
- // Chat data helpers
1158
- // ----------------------------------------------------------------
1159
- function currentChat() {
1160
- return state.chats.find((c) => c.id === state.currentId) || null;
1161
- }
1162
- function ensureChat() {
1163
- let chat = currentChat();
1164
- if (!chat) {
1165
- chat = { id: uid(), title: 'New chat', createdAt: Date.now(), updatedAt: Date.now(), messages: [] };
1166
- state.chats.unshift(chat);
1167
- state.currentId = chat.id;
1168
- }
1169
- return chat;
1170
- }
1171
- function deriveTitle(text) {
1172
- const t = (text || '').replace(/\s+/g, ' ').trim();
1173
- if (!t) return 'New chat';
1174
- return t.length > 42 ? t.slice(0, 42).trim() + '…' : t;
1175
- }
1176
-
1177
- // ----------------------------------------------------------------
1178
- // Render: messages
1179
- // ----------------------------------------------------------------
1180
- function renderMessages() {
1181
- const chat = currentChat();
1182
- const hasMessages = !!(chat && chat.messages.length);
1183
- els.chat.classList.toggle('has-messages', hasMessages);
1184
- els.messages.innerHTML = '';
1185
- if (!hasMessages) return;
1186
-
1187
- chat.messages.forEach((m) => els.messages.appendChild(renderMessageEl(m)));
1188
-
1189
- // Highlight after insertion
1190
- if (window.Prism) {
1191
- try { Prism.highlightAllUnder(els.messages); } catch { /* noop */ }
1192
- }
1193
- scrollMessagesToBottom();
1194
- }
1195
-
1196
- function renderMessageEl(m) {
1197
- const wrap = document.createElement('div');
1198
- wrap.className = `msg msg--${m.role === 'user' ? 'user' : 'asst'}`;
1199
-
1200
- const avatar = document.createElement('div');
1201
- avatar.className = 'msg-avatar';
1202
- avatar.textContent = m.role === 'user' ? 'You'.slice(0,1) : 'M';
1203
-
1204
- const body = document.createElement('div');
1205
- body.className = 'msg-body';
1206
-
1207
- const meta = document.createElement('div');
1208
- meta.className = 'msg-meta';
1209
- meta.innerHTML = `<span class="msg-meta-name">${m.role === 'user' ? 'You' : 'MINDI 1.5'}</span>`;
1210
- body.appendChild(meta);
1211
-
1212
- if (Array.isArray(m.images) && m.images.length) {
1213
- const imgsWrap = document.createElement('div');
1214
- imgsWrap.className = 'msg-images';
1215
- m.images.forEach((src) => {
1216
- const img = document.createElement('img');
1217
- img.src = src;
1218
- img.alt = 'Attached image';
1219
- imgsWrap.appendChild(img);
1220
- });
1221
- body.appendChild(imgsWrap);
1222
- }
1223
-
1224
- const bubble = document.createElement('div');
1225
- bubble.className = 'msg-bubble';
1226
- if (m.loading) {
1227
- bubble.innerHTML = `<span>${escapeHtml(m.content || 'Thinking')}</span><span class="dots"><span></span><span></span><span></span></span>`;
1228
- wrap.classList.add('msg-loading');
1229
- } else {
1230
- bubble.innerHTML = renderMarkdown(cleanForDisplay(m.content));
1231
- }
1232
- body.appendChild(bubble);
1233
-
1234
- wrap.appendChild(avatar);
1235
- wrap.appendChild(body);
1236
- return wrap;
1237
- }
1238
-
1239
- function scrollMessagesToBottom() {
1240
- requestAnimationFrame(() => {
1241
- els.messages.scrollTop = els.messages.scrollHeight;
1242
- });
1243
- }
1244
-
1245
- // ----------------------------------------------------------------
1246
- // Render: history sidebar
1247
- // ----------------------------------------------------------------
1248
- function renderHistory() {
1249
- const q = (els.search.value || '').toLowerCase().trim();
1250
- const filtered = q
1251
- ? state.chats.filter((c) => c.title.toLowerCase().includes(q) ||
1252
- c.messages.some((m) => (m.content || '').toLowerCase().includes(q)))
1253
- : state.chats;
1254
-
1255
- els.history.innerHTML = '';
1256
-
1257
- if (!filtered.length) {
1258
- els.history.innerHTML = `
1259
- <div class="history-empty">
1260
- <p>${q ? 'No matches.' : 'No chats yet.'}</p>
1261
- <p class="muted">${q ? 'Try a different search.' : 'Start a conversation to see it here.'}</p>
1262
- </div>`;
1263
- return;
1264
- }
1265
-
1266
- // Group by date
1267
- const groupOrder = ['Today', 'Yesterday', 'This Week', 'Earlier'];
1268
- const groups = {};
1269
- filtered.forEach((c) => {
1270
- const g = relativeDateGroup(c.updatedAt || c.createdAt);
1271
- (groups[g] ||= []).push(c);
1272
- });
1273
-
1274
- groupOrder.forEach((g) => {
1275
- if (!groups[g]) return;
1276
- const wrap = document.createElement('div');
1277
- wrap.className = 'history-group';
1278
- wrap.innerHTML = `<div class="history-group-title">${g}</div>`;
1279
- groups[g].forEach((c) => {
1280
- const btn = document.createElement('button');
1281
- btn.className = 'history-item';
1282
- if (c.id === state.currentId) btn.classList.add('is-active');
1283
- btn.textContent = c.title || 'New chat';
1284
- btn.title = c.title;
1285
- btn.addEventListener('click', () => loadChat(c.id));
1286
- wrap.appendChild(btn);
1287
- });
1288
- els.history.appendChild(wrap);
1289
- });
1290
- }
1291
-
1292
- function loadChat(id) {
1293
- state.currentId = id;
1294
- const chat = currentChat();
1295
- if (chat) {
1296
- els.chatTitle.textContent = chat.title || 'New chat';
1297
- // recompute preview panels from last assistant message
1298
- const lastAssistant = [...chat.messages].reverse().find((m) => m.role === 'assistant' && !m.loading);
1299
- if (lastAssistant) updatePreviewFromAssistant(lastAssistant);
1300
- else clearPreview();
1301
- }
1302
- renderMessages();
1303
- renderHistory();
1304
- saveState();
1305
- closeMobileSidebar();
1306
- }
1307
-
1308
- // ----------------------------------------------------------------
1309
- // Preview panel updates
1310
- // ----------------------------------------------------------------
1311
- function clearPreview() {
1312
- runtime.lastCode = null;
1313
- runtime.lastSections = null;
1314
- els.codeOut.hidden = true;
1315
- els.emptyCode.hidden = false;
1316
- els.liveFrame.hidden = true;
1317
- els.emptyLive.hidden = false;
1318
- els.sections.hidden = true;
1319
- els.emptySections.hidden = false;
1320
- els.sections.innerHTML = '';
1321
- els.codeOutInner.textContent = '';
1322
- }
1323
-
1324
- function updatePreviewFromAssistant(msg) {
1325
- const cleaned = cleanForDisplay(msg.content);
1326
- const block = extractLastCodeBlock(cleaned);
1327
- runtime.lastCode = block;
1328
- if (block) renderCodeOut(block);
1329
- else { els.codeOut.hidden = true; els.emptyCode.hidden = false; }
1330
-
1331
- // Live HTML preview
1332
- if (block && /^(markup|html)$/i.test(block.language || '')) {
1333
- renderLivePreview(block.code);
1334
- } else {
1335
- els.liveFrame.hidden = true;
1336
- els.emptyLive.hidden = false;
1337
- }
1338
-
1339
- // Sections
1340
- const apiSections = msg.sections || {};
1341
- const parsedSections = parseSections(msg.content);
1342
- runtime.lastSections = mergeSections(apiSections, parsedSections);
1343
- renderSections(runtime.lastSections);
1344
- }
1345
-
1346
- function renderCodeOut(block) {
1347
- const lang = block.language || 'plaintext';
1348
- els.codeOutInner.className = `language-${lang}`;
1349
- els.codeOutInner.textContent = block.code;
1350
- els.emptyCode.hidden = true;
1351
- els.codeOut.hidden = false;
1352
- if (window.Prism) {
1353
- try { Prism.highlightElement(els.codeOutInner); } catch { /* noop */ }
1354
- }
1355
- }
1356
-
1357
- function renderLivePreview(html) {
1358
- els.emptyLive.hidden = true;
1359
- els.liveFrame.hidden = false;
1360
- const doc = els.liveFrame.contentDocument || els.liveFrame.contentWindow.document;
1361
- doc.open();
1362
- doc.write(html);
1363
- doc.close();
1364
- }
1365
-
1366
- function renderSections(sections) {
1367
- const hasAny = SECTION_ORDER.some((k) => (sections[k] || []).length);
1368
- if (!hasAny) {
1369
- els.sections.hidden = true;
1370
- els.emptySections.hidden = false;
1371
- els.sections.innerHTML = '';
1372
- return;
1373
- }
1374
- els.emptySections.hidden = true;
1375
- els.sections.hidden = false;
1376
- els.sections.innerHTML = '';
1377
-
1378
- SECTION_ORDER.forEach((kind) => {
1379
- const items = sections[kind] || [];
1380
- items.forEach((body, i) => {
1381
- const card = document.createElement('div');
1382
- card.className = 'section-card';
1383
- card.dataset.kind = kind;
1384
- card.innerHTML = `
1385
- <div class="section-card-head">
1386
- <span class="section-tag">${SECTION_LABELS[kind]}</span>
1387
- <span>${items.length > 1 ? `${i + 1} / ${items.length}` : ''}</span>
1388
- </div>
1389
- <div class="section-card-body">${escapeHtml(body)}</div>
1390
- `;
1391
- els.sections.appendChild(card);
1392
- });
1393
- });
1394
- }
1395
-
1396
- // ----------------------------------------------------------------
1397
- // Send flow
1398
- // ----------------------------------------------------------------
1399
- async function send() {
1400
- if (runtime.isSending) return;
1401
- const text = els.promptInput.value.trim();
1402
- if (!text && !runtime.pendingImages.length) return;
1403
-
1404
- const chat = ensureChat();
1405
- const wasEmpty = chat.messages.length === 0;
1406
-
1407
- const userMsg = {
1408
- role: 'user',
1409
- content: text,
1410
- images: runtime.pendingImages.map((p) => p.dataUrl),
1411
- ts: Date.now(),
1412
- };
1413
- chat.messages.push(userMsg);
1414
- chat.updatedAt = Date.now();
1415
-
1416
- if (wasEmpty) {
1417
- chat.title = deriveTitle(text);
1418
- els.chatTitle.textContent = chat.title;
1419
- }
1420
-
1421
- // Reset input.
1422
- // If vision is disabled in Settings, drop the image before calling
1423
- // the API so we don't waste an upload round-trip on something the
1424
- // backend will ignore. The image still appears in the user message
1425
- // for the chat record.
1426
- const imageForApi = state.visionEnabled
1427
- ? (runtime.pendingImages[0]?.dataUrl || null)
1428
- : null;
1429
- if (!state.visionEnabled && runtime.pendingImages.length) {
1430
- toast('Vision is disabled \u2014 image attached for record only. Enable it in Settings to send to the model.', 'info', 4500);
1431
- }
1432
- els.promptInput.value = '';
1433
- autosizeTextarea();
1434
- clearPendingImages();
1435
- updateSendEnabled();
1436
-
1437
- // Loading message
1438
- const loadingMsg = { role: 'assistant', content: 'Thinking', loading: true, ts: Date.now() };
1439
- chat.messages.push(loadingMsg);
1440
- renderMessages();
1441
- renderHistory();
1442
- saveState();
1443
-
1444
- runtime.isSending = true;
1445
- let coldStartTimer = setTimeout(() => {
1446
- loadingMsg.content = 'Cold start — booting the GPU. First request can take ~4 minutes';
1447
- renderMessages();
1448
- }, COLD_START_HINT_MS);
1449
-
1450
- let result, errored = null;
1451
- try {
1452
- // If we already know auth is blocked, don't call the API again — the
1453
- // request would just consume more anonymous quota and return the same
1454
- // error. Show the inline 'add your token' card instead.
1455
- if (runtime.authBlocked) {
1456
- result = makeAuthBlockedResponse();
1457
- } else if (runtime.status === 'demo' || !state.apiUrl) {
1458
- result = await generateDemo(text);
1459
- } else {
1460
- result = await callGenerate(text, imageForApi);
1461
- }
1462
- } catch (e) {
1463
- errored = e;
1464
- // Auto-fallback to demo so the UI never feels dead
1465
- result = await generateDemo(text).catch(() => ({ response: 'Generation failed.' }));
1466
- if (!/cold|abort|signal/i.test(String(e?.message || ''))) {
1467
- toast('API error — falling back to demo', 'error', 3500);
1468
- }
1469
- } finally {
1470
- clearTimeout(coldStartTimer);
1471
- runtime.isSending = false;
1472
- }
1473
-
1474
- // If the API returned a quota / auth error, surface it clearly and
1475
- // stop calling the API on subsequent messages until the user adds a token.
1476
- const authMsg = detectAuthError(result);
1477
- if (authMsg) {
1478
- runtime.authBlocked = true;
1479
- toast(authMsg, 'error', 7000);
1480
- setStatus('demo', state.hfToken ? 'Quota exhausted' : 'Auth required');
1481
- // Replace the raw backend error with a friendlier inline card so the
1482
- // chat doesn't show a wall of quota-error text.
1483
- result = makeAuthBlockedResponse();
1484
- }
1485
-
1486
- // Remove loading, push assistant
1487
- const idx = chat.messages.indexOf(loadingMsg);
1488
- if (idx !== -1) chat.messages.splice(idx, 1);
1489
-
1490
- const assistantMsg = {
1491
- role: 'assistant',
1492
- content: result?.response || '(no response)',
1493
- sections: result?.sections || null,
1494
- ts: Date.now(),
1495
- };
1496
- chat.messages.push(assistantMsg);
1497
- chat.updatedAt = Date.now();
1498
-
1499
- renderMessages();
1500
- renderHistory();
1501
- updatePreviewFromAssistant(assistantMsg);
1502
- saveState();
1503
-
1504
- if (errored) console.warn('[mindi] generate error:', errored);
1505
- }
1506
-
1507
- // ----------------------------------------------------------------
1508
- // Composer interactions
1509
- // ----------------------------------------------------------------
1510
- function autosizeTextarea() {
1511
- const ta = els.promptInput;
1512
- ta.style.height = 'auto';
1513
- const next = Math.min(ta.scrollHeight, MAX_TEXTAREA);
1514
- ta.style.height = next + 'px';
1515
- }
1516
- function updateSendEnabled() {
1517
- const has = els.promptInput.value.trim().length > 0 || runtime.pendingImages.length > 0;
1518
- els.sendBtn.disabled = !has || runtime.isSending;
1519
- }
1520
- function clearPendingImages() {
1521
- runtime.pendingImages = [];
1522
- renderPendingImages();
1523
- }
1524
- function renderPendingImages() {
1525
- if (!runtime.pendingImages.length) {
1526
- els.composerImages.hidden = true;
1527
- els.composerImages.innerHTML = '';
1528
- return;
1529
- }
1530
- els.composerImages.hidden = false;
1531
- els.composerImages.innerHTML = '';
1532
- runtime.pendingImages.forEach((p, i) => {
1533
- const tile = document.createElement('div');
1534
- tile.className = 'composer-image';
1535
- tile.style.backgroundImage = `url("${p.dataUrl}")`;
1536
- tile.title = p.name;
1537
- const rm = document.createElement('button');
1538
- rm.className = 'composer-image-remove';
1539
- rm.setAttribute('aria-label', 'Remove image');
1540
- rm.textContent = '×';
1541
- rm.addEventListener('click', () => {
1542
- runtime.pendingImages.splice(i, 1);
1543
- renderPendingImages();
1544
- updateSendEnabled();
1545
- });
1546
- tile.appendChild(rm);
1547
- els.composerImages.appendChild(tile);
1548
- });
1549
- }
1550
-
1551
- async function handleFileChosen(file) {
1552
- if (!file || !file.type.startsWith('image/')) {
1553
- toast('Only image files are supported.', 'error');
1554
- return;
1555
- }
1556
- if (file.size > 6 * 1024 * 1024) {
1557
- toast('Image too large (max 6 MB).', 'error');
1558
- return;
1559
- }
1560
- try {
1561
- const dataUrl = await fileToDataUrl(file);
1562
- runtime.pendingImages = [{ name: file.name, dataUrl }]; // single image per request
1563
- renderPendingImages();
1564
- updateSendEnabled();
1565
- } catch {
1566
- toast('Could not read that image.', 'error');
1567
- }
1568
- }
1569
-
1570
- // ----------------------------------------------------------------
1571
- // Tabs
1572
- // ----------------------------------------------------------------
1573
- function activateTab(tabName) {
1574
- els.tabs.forEach((t) => {
1575
- const on = t.dataset.tab === tabName;
1576
- t.classList.toggle('is-active', on);
1577
- t.setAttribute('aria-selected', on ? 'true' : 'false');
1578
- });
1579
- els.panes.forEach((p) => {
1580
- p.classList.toggle('is-active', p.dataset.pane === tabName);
1581
- });
1582
- }
1583
-
1584
- // ----------------------------------------------------------------
1585
- // Settings modal
1586
- // ----------------------------------------------------------------
1587
- function maskToken(t) {
1588
- if (!t) return 'none';
1589
- if (t.length <= 8) return 'set';
1590
- return `${t.slice(0, 4)}…${t.slice(-4)}`;
1591
- }
1592
- function refreshTokenStatus() {
1593
- if (els.hfTokenStatus) els.hfTokenStatus.textContent = maskToken(state.hfToken);
1594
- }
1595
- function openSettings() {
1596
- els.settingsUrl.value = state.apiUrl || '';
1597
- if (els.settingsHfToken) els.settingsHfToken.value = state.hfToken || '';
1598
- if (els.settingsVision) els.settingsVision.checked = !!state.visionEnabled;
1599
- els.settingsTemp.value = state.temperature;
1600
- els.settingsTokens.value = state.maxTokens;
1601
- els.tempVal.textContent = Number(state.temperature).toFixed(2);
1602
- els.tokensVal.textContent = state.maxTokens;
1603
- refreshTokenStatus();
1604
- els.settingsModal.hidden = false;
1605
- setTimeout(() => els.settingsUrl.focus(), 50);
1606
- }
1607
- function closeSettings() {
1608
- els.settingsModal.hidden = true;
1609
- }
1610
- function applySettings() {
1611
- const url = els.settingsUrl.value.trim();
1612
- const token = els.settingsHfToken ? els.settingsHfToken.value.trim() : '';
1613
- const vision = !!(els.settingsVision && els.settingsVision.checked);
1614
- const temp = parseFloat(els.settingsTemp.value);
1615
- const tokens = parseInt(els.settingsTokens.value, 10);
1616
- const tokenChanged = token !== state.hfToken;
1617
- state.apiUrl = url || API_DEFAULT;
1618
- state.hfToken = token;
1619
- state.visionEnabled = vision;
1620
- state.temperature = isFinite(temp) ? temp : 0.7;
1621
- state.maxTokens = isFinite(tokens) ? tokens : 2048;
1622
- // If the user just saved a new (non-empty) token, give the API another shot.
1623
- if (tokenChanged && token) {
1624
- runtime.authBlocked = false;
1625
- }
1626
- saveState();
1627
- refreshTokenStatus();
1628
- closeSettings();
1629
- toast(tokenChanged && token ? 'Token saved — retrying API' : 'Settings saved', 'success');
1630
- pingHealth();
1631
- }
1632
-
1633
- // ----------------------------------------------------------------
1634
- // Mobile sidebar / preview toggling
1635
- // ----------------------------------------------------------------
1636
- function openMobileSidebar() { els.body.classList.add('sidebar-open'); }
1637
- function closeMobileSidebar() { els.body.classList.remove('sidebar-open'); }
1638
- function togglePreview() {
1639
- if (window.matchMedia('(max-width: 1024px)').matches) {
1640
- els.body.classList.toggle('preview-open');
1641
- } else {
1642
- els.body.classList.toggle('preview-hidden');
1643
- }
1644
- }
1645
-
1646
- // ----------------------------------------------------------------
1647
- // Copy / download from preview panel
1648
- // ----------------------------------------------------------------
1649
- async function copyLastCode() {
1650
- if (!runtime.lastCode) {
1651
- toast('No code to copy yet', 'info');
1652
- return;
1653
- }
1654
- try {
1655
- await navigator.clipboard.writeText(runtime.lastCode.code);
1656
- toast('Copied to clipboard', 'success', 1600);
1657
- } catch {
1658
- toast('Clipboard unavailable', 'error');
1659
- }
1660
- }
1661
- function downloadLastCode() {
1662
- if (!runtime.lastCode) {
1663
- toast('No code to download yet', 'info');
1664
- return;
1665
- }
1666
- const ext = (() => {
1667
- const m = { javascript: 'js', typescript: 'ts', tsx: 'tsx', jsx: 'jsx',
1668
- python: 'py', markup: 'html', html: 'html', css: 'css',
1669
- json: 'json', sql: 'sql', bash: 'sh' };
1670
- return m[runtime.lastCode.language] || 'txt';
1671
- })();
1672
- downloadFile(`mindi-output.${ext}`, runtime.lastCode.code);
1673
- }
1674
-
1675
- // ----------------------------------------------------------------
1676
- // Bind events
1677
- // ----------------------------------------------------------------
1678
- // The active send handler — overridden by agent init if available
1679
- let activeSend = send;
1680
-
1681
- function bind() {
1682
- // Composer
1683
- els.promptInput.addEventListener('input', () => { autosizeTextarea(); updateSendEnabled(); });
1684
- els.promptInput.addEventListener('keydown', (e) => {
1685
- if (e.key === 'Enter' && !e.shiftKey) {
1686
- e.preventDefault();
1687
- activeSend();
1688
- }
1689
- });
1690
- els.sendBtn.addEventListener('click', () => activeSend());
1691
-
1692
- // Attach
1693
- els.attachBtn.addEventListener('click', () => els.fileInput.click());
1694
- els.fileInput.addEventListener('change', (e) => {
1695
- const f = e.target.files?.[0];
1696
- if (f) handleFileChosen(f);
1697
- e.target.value = '';
1698
- });
1699
-
1700
- // Drag & drop on composer
1701
- ['dragenter', 'dragover'].forEach((ev) => {
1702
- els.composer.addEventListener(ev, (e) => { e.preventDefault(); els.composer.style.borderColor = 'rgba(124, 58, 237, .6)'; });
1703
- });
1704
- ['dragleave', 'drop'].forEach((ev) => {
1705
- els.composer.addEventListener(ev, (e) => { e.preventDefault(); els.composer.style.borderColor = ''; });
1706
- });
1707
- els.composer.addEventListener('drop', (e) => {
1708
- const file = e.dataTransfer?.files?.[0];
1709
- if (file) handleFileChosen(file);
1710
- });
1711
-
1712
- // Quick action cards
1713
- els.quickCards.forEach((card) => {
1714
- card.addEventListener('click', () => {
1715
- els.promptInput.value = card.dataset.prompt || '';
1716
- autosizeTextarea();
1717
- updateSendEnabled();
1718
- els.promptInput.focus();
1719
- });
1720
- });
1721
-
1722
- // Sidebar
1723
- els.newChatBtn.addEventListener('click', () => {
1724
- state.currentId = null;
1725
- ensureChat();
1726
- els.chatTitle.textContent = 'New chat';
1727
- clearPreview();
1728
- renderMessages();
1729
- renderHistory();
1730
- saveState();
1731
- els.promptInput.focus();
1732
- closeMobileSidebar();
1733
- });
1734
- els.search.addEventListener('input', debounce(renderHistory, 120));
1735
- els.hamburger.addEventListener('click', openMobileSidebar);
1736
- els.scrim.addEventListener('click', () => { closeMobileSidebar(); els.body.classList.remove('preview-open'); });
1737
- els.togglePreview.addEventListener('click', togglePreview);
1738
-
1739
- // Tabs
1740
- els.tabs.forEach((t) => t.addEventListener('click', () => activateTab(t.dataset.tab)));
1741
-
1742
- // Code copy / download
1743
- els.copyCode.addEventListener('click', copyLastCode);
1744
- els.downloadCode.addEventListener('click', downloadLastCode);
1745
-
1746
- // Delegated click handler for code-block action buttons inside messages
1747
- // (Copy, Run-in-StackBlitz, Open-in-CodeSandbox).
1748
- els.messages.addEventListener('click', async (e) => {
1749
- const copyBtn = e.target.closest('.md-copy');
1750
- if (copyBtn) {
1751
- try {
1752
- await navigator.clipboard.writeText(copyBtn.dataset.code || '');
1753
- const prev = copyBtn.textContent;
1754
- copyBtn.textContent = 'Copied!';
1755
- setTimeout(() => { copyBtn.textContent = prev; }, 1400);
1756
- } catch {
1757
- toast('Clipboard unavailable', 'error');
1758
- }
1759
- return;
1760
- }
1761
-
1762
- const runBtn = e.target.closest('.md-run');
1763
- if (runBtn) {
1764
- try {
1765
- launchInStackBlitz(runBtn.dataset.code || '', runBtn.dataset.lang || '');
1766
- } catch (err) {
1767
- toast(`StackBlitz launch failed: ${err.message || err}`, 'error');
1768
- }
1769
- return;
1770
- }
1771
-
1772
- const sbxBtn = e.target.closest('.md-sandbox');
1773
- if (sbxBtn) {
1774
- const prev = sbxBtn.textContent;
1775
- sbxBtn.disabled = true;
1776
- sbxBtn.textContent = '\u25B6 Opening\u2026';
1777
- try {
1778
- await launchInCodeSandbox(sbxBtn.dataset.code || '', sbxBtn.dataset.lang || '');
1779
- } finally {
1780
- sbxBtn.textContent = prev;
1781
- sbxBtn.disabled = false;
1782
- }
1783
- return;
1784
- }
1785
- });
1786
-
1787
- // Brand → settings (double-click)
1788
- els.brand.addEventListener('dblclick', openSettings);
1789
-
1790
- // Settings modal
1791
- els.settingsModal.addEventListener('click', (e) => {
1792
- if (e.target.matches('[data-close]') || e.target.closest('[data-close]')) closeSettings();
1793
- });
1794
- els.settingsTemp.addEventListener('input', () => {
1795
- els.tempVal.textContent = Number(els.settingsTemp.value).toFixed(2);
1796
- });
1797
- els.settingsTokens.addEventListener('input', () => {
1798
- els.tokensVal.textContent = els.settingsTokens.value;
1799
- });
1800
- els.saveSettings.addEventListener('click', applySettings);
1801
-
1802
- // Esc to close modal / mobile drawers
1803
- document.addEventListener('keydown', (e) => {
1804
- if (e.key === 'Escape') {
1805
- if (!els.settingsModal.hidden) closeSettings();
1806
- closeMobileSidebar();
1807
- els.body.classList.remove('preview-open');
1808
- }
1809
- // Cmd/Ctrl + K → focus search
1810
- if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
1811
- e.preventDefault();
1812
- els.search.focus();
1813
- }
1814
- });
1815
-
1816
- // Keep textarea sized after window resize (font reflow)
1817
- window.addEventListener('resize', autosizeTextarea);
1818
- }
1819
-
1820
- // ----------------------------------------------------------------
1821
- // Agent integration
1822
- // ----------------------------------------------------------------
1823
- const agentEls = {
1824
- log: document.getElementById('agent-log'),
1825
- sandbox: document.getElementById('agent-sandbox'),
1826
- console: document.getElementById('agent-console'),
1827
- consoleBody: document.getElementById('agent-console-body'),
1828
- emptyAgent: document.getElementById('empty-agent'),
1829
- };
1830
-
1831
- const STEP_ICONS = {
1832
- plan: '📋',
1833
- generate: '⚡',
1834
- execute: '▶️',
1835
- verify: '✅',
1836
- fix: '🔧',
1837
- done: '🎉',
1838
- error: '❌',
1839
- };
1840
-
1841
- const STEP_LABELS = {
1842
- plan: 'Planning',
1843
- generate: 'Generating Code',
1844
- execute: 'Executing',
1845
- verify: 'Verifying Output',
1846
- fix: 'Fixing Error',
1847
- done: 'Complete',
1848
- error: 'Error',
1849
- };
1850
-
1851
- function isCodeRequest(text) {
1852
- return /\b(build|create|make|write|generate|code|html|css|app|page|website|component|function|class|api|dashboard|landing|todo|form|navbar|button|layout|design)\b/i.test(text);
1853
- }
1854
-
1855
- function renderAgentStep(run, step) {
1856
- if (!agentEls.log) return;
1857
-
1858
- // Show agent panel, hide empty state
1859
- agentEls.emptyAgent && (agentEls.emptyAgent.hidden = true);
1860
- agentEls.log.hidden = false;
1861
-
1862
- // Switch to agent tab
1863
- const agentTab = document.querySelector('.tab[data-tab="agent"]');
1864
- if (agentTab && !agentTab.classList.contains('is-active')) {
1865
- agentTab.click();
1866
- }
1867
-
1868
- // Find or create step element
1869
- let el = agentEls.log.querySelector(`[data-step-id="${step.id}"]`);
1870
- if (!el) {
1871
- el = document.createElement('div');
1872
- el.className = 'agent-step';
1873
- el.dataset.stepId = step.id;
1874
- el.innerHTML = `
1875
- <div class="agent-step-icon"></div>
1876
- <div class="agent-step-body">
1877
- <div class="agent-step-title"></div>
1878
- <div class="agent-step-detail"></div>
1879
- </div>`;
1880
- agentEls.log.appendChild(el);
1881
- }
1882
-
1883
- // Update status class
1884
- el.className = `agent-step agent-step--${step.status}`;
1885
-
1886
- // Update icon
1887
- const iconEl = el.querySelector('.agent-step-icon');
1888
- const statusIcons = { running: '⏳', success: '✅', failed: '❌', pending: '⏺' };
1889
- iconEl.textContent = step.status === 'success' || step.status === 'failed'
1890
- ? statusIcons[step.status]
1891
- : (STEP_ICONS[step.type] || '⏳');
1892
-
1893
- // Update title
1894
- el.querySelector('.agent-step-title').textContent = STEP_LABELS[step.type] || step.type;
1895
-
1896
- // Update detail
1897
- el.querySelector('.agent-step-detail').textContent = step.detail || '';
1898
-
1899
- // Auto-scroll
1900
- agentEls.log.scrollTop = agentEls.log.scrollHeight;
1901
-
1902
- // Show sandbox and console when executing
1903
- if (step.type === 'execute') {
1904
- agentEls.sandbox.hidden = false;
1905
- agentEls.console.hidden = false;
1906
- }
1907
-
1908
- // Update console on execution results
1909
- if (step.type === 'execute' && (step.status === 'success' || step.status === 'failed')) {
1910
- const run_ = run; // closure
1911
- if (run_.currentCode) {
1912
- // Show code in the Code tab too
1913
- const block = { language: run_.language || 'javascript', code: run_.currentCode };
1914
- runtime.lastCode = block;
1915
- renderCodeOut(block);
1916
- }
1917
- }
1918
-
1919
- // When done, show final code in preview
1920
- if (step.type === 'done' && step.status === 'success' && run.currentCode) {
1921
- const lang = run.language || 'javascript';
1922
- if (/^(html|markup)$/i.test(lang)) {
1923
- // Render in live preview
1924
- els.liveFrame.hidden = false;
1925
- const emptyLive = document.getElementById('empty-live');
1926
- if (emptyLive) emptyLive.hidden = true;
1927
- els.liveFrame.srcdoc = run.currentCode;
1928
- }
1929
- }
1930
- }
1931
-
1932
- function clearAgentUI() {
1933
- if (agentEls.log) {
1934
- agentEls.log.innerHTML = '';
1935
- agentEls.log.hidden = true;
1936
- }
1937
- if (agentEls.sandbox) {
1938
- agentEls.sandbox.innerHTML = '';
1939
- agentEls.sandbox.hidden = true;
1940
- }
1941
- if (agentEls.console) {
1942
- agentEls.console.hidden = true;
1943
- }
1944
- if (agentEls.consoleBody) {
1945
- agentEls.consoleBody.textContent = '';
1946
- }
1947
- if (agentEls.emptyAgent) {
1948
- agentEls.emptyAgent.hidden = false;
1949
- }
1950
- }
1951
-
1952
- async function runAgent(prompt, image) {
1953
- clearAgentUI();
1954
-
1955
- const apiCall = async (p, img) => {
1956
- if (runtime.status === 'demo' || !state.apiUrl) {
1957
- return generateDemo(p);
1958
- }
1959
- return callGenerate(p, img);
1960
- };
1961
-
1962
- const result = await MINDIAgent.run(prompt, {
1963
- apiCall,
1964
- sandboxContainer: agentEls.sandbox,
1965
- image,
1966
- onStep: (run, step) => {
1967
- renderAgentStep(run, step);
1968
-
1969
- // Update console output
1970
- if (step.type === 'execute' && agentEls.consoleBody) {
1971
- const detail = step.detail || '';
1972
- if (step.status === 'failed') {
1973
- agentEls.consoleBody.innerHTML += `<span class="console-error">${escapeHtml(detail)}</span>\n`;
1974
- } else if (step.status === 'success') {
1975
- agentEls.consoleBody.textContent += detail + '\n';
1976
- }
1977
- }
1978
- },
1979
- });
1980
-
1981
- return result;
1982
- }
1983
-
1984
- // Agent-aware send handler: delegates to agent for code requests, standard send otherwise
1985
- async function handleSendWithAgent() {
1986
- const text = els.promptInput.value.trim();
1987
- if (!text && !runtime.pendingImages.length) return;
1988
-
1989
- // Determine if this should use the agent
1990
- const useAgent = typeof MINDIAgent !== 'undefined' && isCodeRequest(text);
1991
-
1992
- // If we know auth is blocked, the agent loop would just call the API
1993
- // multiple times and fail every iteration. Fall back to send(), which
1994
- // now renders the friendly inline 'add your token' card instead.
1995
- if (!useAgent || runtime.authBlocked) {
1996
- return send();
1997
- }
1998
-
1999
- // Agent mode
2000
- const chat = ensureChat();
2001
- const wasEmpty = chat.messages.length === 0;
2002
-
2003
- const userMsg = {
2004
- role: 'user',
2005
- content: text,
2006
- images: runtime.pendingImages.map((p) => p.dataUrl),
2007
- ts: Date.now(),
2008
- };
2009
- chat.messages.push(userMsg);
2010
- chat.updatedAt = Date.now();
2011
-
2012
- if (wasEmpty) {
2013
- chat.title = deriveTitle(text);
2014
- els.chatTitle.textContent = chat.title;
2015
- }
2016
-
2017
- const imageForApi = runtime.pendingImages[0]?.dataUrl || null;
2018
- els.promptInput.value = '';
2019
- autosizeTextarea();
2020
- clearPendingImages();
2021
- updateSendEnabled();
2022
-
2023
- // Show loading
2024
- const loadingMsg = { role: 'assistant', content: '🤖 Agent working', loading: true, ts: Date.now() };
2025
- chat.messages.push(loadingMsg);
2026
- renderMessages();
2027
- renderHistory();
2028
- saveState();
2029
-
2030
- runtime.isSending = true;
2031
-
2032
- try {
2033
- const agentResult = await runAgent(text, imageForApi);
2034
-
2035
- // Remove loading
2036
- const idx = chat.messages.indexOf(loadingMsg);
2037
- if (idx !== -1) chat.messages.splice(idx, 1);
2038
-
2039
- // Build response from agent
2040
- const iterations = agentResult.iteration + 1;
2041
- const status = agentResult.status === 'success' ? '✅' : '❌';
2042
- let responseText = `${status} Agent completed in ${iterations} iteration(s).\n\n`;
2043
-
2044
- if (agentResult.currentCode) {
2045
- const lang = agentResult.language || 'javascript';
2046
- responseText += `\`\`\`${lang}\n${agentResult.currentCode}\n\`\`\``;
2047
- } else {
2048
- // Fallback: get the last generate step's detail
2049
- const lastGen = [...agentResult.steps].reverse().find(s => s.type === 'generate' || s.type === 'fix');
2050
- if (lastGen) responseText += lastGen.detail || 'No code generated.';
2051
- }
2052
-
2053
- const assistantMsg = {
2054
- role: 'assistant',
2055
- content: responseText,
2056
- ts: Date.now(),
2057
- };
2058
- chat.messages.push(assistantMsg);
2059
- chat.updatedAt = Date.now();
2060
-
2061
- renderMessages();
2062
- renderHistory();
2063
- updatePreviewFromAssistant(assistantMsg);
2064
- saveState();
2065
-
2066
- } catch (e) {
2067
- const idx = chat.messages.indexOf(loadingMsg);
2068
- if (idx !== -1) chat.messages.splice(idx, 1);
2069
-
2070
- const errorMsg = { role: 'assistant', content: `Agent error: ${e.message}`, ts: Date.now() };
2071
- chat.messages.push(errorMsg);
2072
- renderMessages();
2073
- toast('Agent encountered an error', 'error');
2074
- } finally {
2075
- runtime.isSending = false;
2076
- }
2077
- }
2078
-
2079
- // ----------------------------------------------------------------
2080
- // Init
2081
- // ----------------------------------------------------------------
2082
- function init() {
2083
- bind();
2084
-
2085
- // Override the active send handler with agent-aware version
2086
- if (typeof MINDIAgent !== 'undefined') {
2087
- activeSend = handleSendWithAgent;
2088
- }
2089
-
2090
- renderHistory();
2091
- renderMessages();
2092
-
2093
- // Restore current chat title
2094
- const c = currentChat();
2095
- if (c) {
2096
- els.chatTitle.textContent = c.title || 'New chat';
2097
- const lastAssistant = [...c.messages].reverse().find((m) => m.role === 'assistant' && !m.loading);
2098
- if (lastAssistant) updatePreviewFromAssistant(lastAssistant);
2099
- }
2100
-
2101
- autosizeTextarea();
2102
- updateSendEnabled();
2103
-
2104
- // Health check (after a tick so UI paints first)
2105
- setTimeout(pingHealth, 80);
2106
-
2107
- // Periodically re-check health (every 60s)
2108
- setInterval(pingHealth, 60_000);
2109
-
2110
- console.log('[MINDI] Agent system loaded:', typeof MINDIAgent !== 'undefined' ? '✅' : '❌');
2111
- }
2112
-
2113
- if (document.readyState === 'loading') {
2114
- document.addEventListener('DOMContentLoaded', init, { once: true });
2115
- } else {
2116
- init();
2117
- }
2118
- })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/eslint.config.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import { defineConfig, globalIgnores } from 'eslint/config'
6
+
7
+ export default defineConfig([
8
+ globalIgnores(['dist']),
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ extends: [
12
+ js.configs.recommended,
13
+ reactHooks.configs.flat.recommended,
14
+ reactRefresh.configs.vite,
15
+ ],
16
+ languageOptions: {
17
+ globals: globals.browser,
18
+ parserOptions: { ecmaFeatures: { jsx: true } },
19
+ },
20
+ },
21
+ ])
frontend/index.html CHANGED
@@ -3,355 +3,14 @@
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <meta name="description" content="MINDI 1.5 Vision-Coder — generate production-ready code from text prompts and UI screenshots." />
7
- <title>MINDI 1.5 — Vision-Coder AI</title>
8
-
9
- <!-- Fonts -->
10
  <link rel="preconnect" href="https://fonts.googleapis.com" />
11
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
12
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
13
-
14
- <!-- Prism syntax highlighting -->
15
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" />
16
-
17
- <link rel="stylesheet" href="styles.css" />
18
  </head>
19
  <body>
20
-
21
- <!-- ============ AMBIENT BACKGROUND ============ -->
22
- <div class="ambient" aria-hidden="true">
23
- <div class="grid-pattern"></div>
24
- <div class="blob blob--purple"></div>
25
- <div class="blob blob--blue"></div>
26
- </div>
27
-
28
- <!-- Mobile sidebar scrim -->
29
- <div class="scrim" id="scrim" aria-hidden="true"></div>
30
-
31
- <!-- ============ APP SHELL ============ -->
32
- <div class="app">
33
-
34
- <!-- ============ LEFT SIDEBAR ============ -->
35
- <aside class="sidebar" id="sidebar" aria-label="Sidebar">
36
- <div class="sidebar-head">
37
- <button class="brand" id="brand" title="Double-click for settings" aria-label="MINDIGENOUS.AI brand — double-click for settings">
38
- <span class="brand-mark">
39
- <svg viewBox="0 0 32 32" fill="none" aria-hidden="true">
40
- <defs>
41
- <linearGradient id="bgrad" x1="0" y1="0" x2="32" y2="32" gradientUnits="userSpaceOnUse">
42
- <stop offset="0%" stop-color="#7c3aed" />
43
- <stop offset="100%" stop-color="#2563eb" />
44
- </linearGradient>
45
- </defs>
46
- <path d="M16 2 L28 9 L28 23 L16 30 L4 23 L4 9 Z" fill="url(#bgrad)" />
47
- <path d="M11.5 12 L7.5 16 L11.5 20 M20.5 12 L24.5 16 L20.5 20" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
48
- <circle cx="16" cy="16" r="1.6" fill="#fff" />
49
- </svg>
50
- </span>
51
- <span class="brand-text">
52
- <span class="brand-name">MINDIGENOUS<span class="brand-dot">.AI</span></span>
53
- <span class="brand-version">MINDI 1.5 · Vision-Coder</span>
54
- </span>
55
- </button>
56
- </div>
57
-
58
- <div class="sidebar-actions">
59
- <button class="btn btn--new" id="new-chat-btn" title="Start a new chat">
60
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
61
- <line x1="12" y1="5" x2="12" y2="19"></line>
62
- <line x1="5" y1="12" x2="19" y2="12"></line>
63
- </svg>
64
- New chat
65
- </button>
66
-
67
- <div class="search-wrap">
68
- <svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
69
- <circle cx="11" cy="11" r="8"></circle>
70
- <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
71
- </svg>
72
- <input id="search" type="search" placeholder="Search conversations…" autocomplete="off" />
73
- </div>
74
- </div>
75
-
76
- <nav class="chat-history" id="chat-history" aria-label="Chat history">
77
- <div class="history-empty" id="history-empty">
78
- <p>No chats yet.</p>
79
- <p class="muted">Start a conversation to see it here.</p>
80
- </div>
81
- </nav>
82
-
83
- <div class="sidebar-foot">
84
- <div class="status" id="status" title="Model status">
85
- <span class="status-dot status-dot--gray" id="status-dot"></span>
86
- <span class="status-text" id="status-text">Connecting…</span>
87
- </div>
88
- <a class="hf-link" href="https://huggingface.co/mindigenous-ai" target="_blank" rel="noopener noreferrer" title="MINDI on HuggingFace">
89
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
90
- <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
91
- <polyline points="15 3 21 3 21 9"></polyline>
92
- <line x1="10" y1="14" x2="21" y2="3"></line>
93
- </svg>
94
- HuggingFace
95
- </a>
96
- </div>
97
- </aside>
98
-
99
- <!-- ============ CENTER: CHAT ============ -->
100
- <main class="chat" id="chat">
101
- <header class="chat-head">
102
- <button class="icon-btn icon-btn--menu" id="hamburger" aria-label="Open sidebar">
103
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
104
- <line x1="3" y1="6" x2="21" y2="6"></line>
105
- <line x1="3" y1="12" x2="21" y2="12"></line>
106
- <line x1="3" y1="18" x2="21" y2="18"></line>
107
- </svg>
108
- </button>
109
- <h1 class="chat-title" id="chat-title">New chat</h1>
110
- <div class="chat-head-actions">
111
- <button class="icon-btn" id="toggle-preview" aria-label="Toggle preview panel" title="Toggle preview panel">
112
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
113
- <rect x="3" y="3" width="18" height="18" rx="2"></rect>
114
- <line x1="15" y1="3" x2="15" y2="21"></line>
115
- </svg>
116
- </button>
117
- </div>
118
- </header>
119
-
120
- <!-- Welcome screen -->
121
- <section class="welcome" id="welcome">
122
- <div class="welcome-icon" aria-hidden="true">
123
- <svg viewBox="0 0 120 120" class="welcome-svg">
124
- <defs>
125
- <linearGradient id="wgrad" x1="0" y1="0" x2="120" y2="120" gradientUnits="userSpaceOnUse">
126
- <stop offset="0%" stop-color="#7c3aed" />
127
- <stop offset="100%" stop-color="#2563eb" />
128
- </linearGradient>
129
- <filter id="wglow" x="-30%" y="-30%" width="160%" height="160%">
130
- <feGaussianBlur stdDeviation="8" />
131
- </filter>
132
- </defs>
133
- <path d="M60 10 L102 33 L102 87 L60 110 L18 87 L18 33 Z" fill="url(#wgrad)" filter="url(#wglow)" opacity=".55" />
134
- <path d="M60 12 L100 34 L100 86 L60 108 L20 86 L20 34 Z" fill="url(#wgrad)" />
135
- <path d="M44 50 L30 60 L44 70 M76 50 L90 60 L76 70" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none" />
136
- <circle cx="60" cy="60" r="4.5" fill="#fff" />
137
- <circle cx="60" cy="60" r="9" fill="none" stroke="#fff" stroke-opacity=".4" stroke-width="1.5" />
138
- </svg>
139
- </div>
140
-
141
- <h2 class="welcome-title">Welcome to <span class="grad-text">MINDI 1.5</span></h2>
142
- <p class="welcome-sub">A custom-trained 8B parameter Vision-Coder. Describe what you want to build, drop in a UI screenshot, and watch it generate production-ready code.</p>
143
-
144
- <div class="quick-actions">
145
- <button class="quick-card" data-prompt="Build a Next.js landing page with a responsive hero section, features grid, testimonial carousel, and a clear call-to-action. Use Tailwind CSS.">
146
- <span class="qc-icon">🚀</span>
147
- <h3>Landing Page</h3>
148
- <p>Next.js + responsive hero section</p>
149
- </button>
150
- <button class="quick-card" data-prompt="Create a modern dashboard UI with a sidebar nav, top stats cards, a line chart, a recent activity table, and a user profile menu. Use HTML, CSS, and vanilla JS.">
151
- <span class="qc-icon">📊</span>
152
- <h3>Dashboard UI</h3>
153
- <p>Charts, stats &amp; navigation</p>
154
- </button>
155
- <button class="quick-card" data-prompt="Write a FastAPI backend for a notes app with JWT authentication, PostgreSQL via SQLAlchemy, and CRUD endpoints. Include Pydantic models and an example .env.">
156
- <span class="qc-icon">⚡</span>
157
- <h3>API Backend</h3>
158
- <p>FastAPI + JWT + PostgreSQL</p>
159
- </button>
160
- <button class="quick-card" data-prompt="Find and fix the bugs in this Python function. Explain each issue and provide the corrected code:&#10;&#10;```python&#10;def divide_list(numbers, divisor):&#10; result = []&#10; for n in numbers:&#10; result.append(n / divisor)&#10; return result&#10;```">
161
- <span class="qc-icon">🔧</span>
162
- <h3>Debug Code</h3>
163
- <p>Find &amp; fix bugs in your code</p>
164
- </button>
165
- </div>
166
- </section>
167
-
168
- <!-- Messages -->
169
- <section class="messages" id="messages" aria-live="polite"></section>
170
-
171
- <!-- Composer (input) -->
172
- <div class="composer-wrap">
173
- <div class="composer" id="composer">
174
- <div class="composer-images" id="composer-images" hidden></div>
175
- <div class="composer-row">
176
- <button class="composer-btn" id="attach-btn" title="Attach image" aria-label="Attach image">
177
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
178
- <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
179
- <circle cx="8.5" cy="8.5" r="1.5"></circle>
180
- <polyline points="21 15 16 10 5 21"></polyline>
181
- </svg>
182
- </button>
183
- <textarea id="prompt-input" rows="1" placeholder="Describe the code you want to generate…" autocomplete="off" autocorrect="off" spellcheck="false"></textarea>
184
- <button class="composer-send" id="send-btn" disabled title="Send (Enter)" aria-label="Send message">
185
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
186
- <line x1="22" y1="2" x2="11" y2="13"></line>
187
- <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
188
- </svg>
189
- </button>
190
- </div>
191
- </div>
192
- <p class="composer-foot">
193
- <span class="grad-text">MINDI 1.5 Vision-Coder</span> · 8B params · Trained on code &amp; UI data
194
- </p>
195
- </div>
196
-
197
- <input type="file" id="file-input" accept="image/*" hidden />
198
- </main>
199
-
200
- <!-- ============ RIGHT: PREVIEW PANEL ============ -->
201
- <aside class="preview" id="preview" aria-label="Code preview panel">
202
- <div class="preview-head">
203
- <div class="tabs" role="tablist">
204
- <button class="tab is-active" data-tab="code" role="tab" aria-selected="true">Code</button>
205
- <button class="tab" data-tab="live" role="tab" aria-selected="false">Preview</button>
206
- <button class="tab" data-tab="agent" role="tab" aria-selected="false">Agent</button>
207
- <button class="tab" data-tab="sections" role="tab" aria-selected="false">Sections</button>
208
- </div>
209
- <div class="preview-actions">
210
- <button class="icon-btn" id="copy-code" title="Copy code" aria-label="Copy code">
211
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
212
- <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
213
- <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
214
- </svg>
215
- </button>
216
- <button class="icon-btn" id="download-code" title="Download code" aria-label="Download code">
217
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
218
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
219
- <polyline points="7 10 12 15 17 10"></polyline>
220
- <line x1="12" y1="15" x2="12" y2="3"></line>
221
- </svg>
222
- </button>
223
- </div>
224
- </div>
225
-
226
- <!-- Code tab -->
227
- <div class="preview-pane is-active" data-pane="code">
228
- <div class="preview-empty" id="empty-code">
229
- <div class="preview-empty-icon">{ }</div>
230
- <p>Generated code will appear here</p>
231
- <p class="muted">Send a prompt to see the last code block from the response.</p>
232
- </div>
233
- <pre class="code-out" id="code-out" hidden><code class="language-javascript" id="code-out-inner"></code></pre>
234
- </div>
235
-
236
- <!-- Live preview tab -->
237
- <div class="preview-pane" data-pane="live">
238
- <div class="preview-empty" id="empty-live">
239
- <div class="preview-empty-icon">⚡</div>
240
- <p>Live HTML preview</p>
241
- <p class="muted">When the response contains an HTML code block, it'll render here in a sandboxed iframe.</p>
242
- </div>
243
- <iframe id="live-frame" sandbox="allow-scripts allow-same-origin" title="Live HTML preview" hidden></iframe>
244
- </div>
245
-
246
- <!-- Agent tab -->
247
- <div class="preview-pane" data-pane="agent">
248
- <div class="preview-empty" id="empty-agent">
249
- <div class="preview-empty-icon">🤖</div>
250
- <p>Agent Workspace</p>
251
- <p class="muted">MINDI Agent will plan, generate, execute, verify, and fix code automatically. Steps appear here in real-time.</p>
252
- </div>
253
- <div class="agent-log" id="agent-log" hidden></div>
254
- <div class="agent-sandbox" id="agent-sandbox" hidden></div>
255
- <div class="agent-console" id="agent-console" hidden>
256
- <div class="agent-console-head">Console Output</div>
257
- <pre class="agent-console-body" id="agent-console-body"></pre>
258
- </div>
259
- </div>
260
-
261
- <!-- Sections tab -->
262
- <div class="preview-pane" data-pane="sections">
263
- <div class="preview-empty" id="empty-sections">
264
- <div class="preview-empty-icon">⌘</div>
265
- <p>Parsed model sections</p>
266
- <p class="muted">The model emits structured tokens — thinking, code, critique, fix, error, suggest, file. They'll show up here as colored cards.</p>
267
- </div>
268
- <div class="sections" id="sections" hidden></div>
269
- </div>
270
- </aside>
271
- </div>
272
-
273
- <!-- ============ SETTINGS MODAL ============ -->
274
- <div class="modal" id="settings-modal" hidden role="dialog" aria-modal="true" aria-labelledby="settings-title">
275
- <div class="modal-backdrop" data-close></div>
276
- <div class="modal-card" role="document">
277
- <div class="modal-head">
278
- <h3 id="settings-title">Settings</h3>
279
- <button class="icon-btn" data-close aria-label="Close">
280
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
281
- <line x1="18" y1="6" x2="6" y2="18"></line>
282
- <line x1="6" y1="6" x2="18" y2="18"></line>
283
- </svg>
284
- </button>
285
- </div>
286
-
287
- <div class="modal-body">
288
- <label class="field">
289
- <span class="field-label">API URL</span>
290
- <input id="settings-url" type="url" placeholder="https://mindigenous-mindi-chat.hf.space" autocomplete="off" />
291
- <span class="field-hint">Base URL for the MINDI API (HF Space or Modal). Endpoints are appended automatically.</span>
292
- </label>
293
-
294
- <label class="field">
295
- <span class="field-label">HuggingFace token <em class="field-value" id="hf-token-status">none</em></span>
296
- <input id="settings-hf-token" type="password" placeholder="hf_xxxxxxxxxxxxxxxxxxxx" autocomplete="off" spellcheck="false" />
297
- <span class="field-hint">Paste a PRO HF token to bypass anonymous ZeroGPU quota. Stored only in this browser. <a href="https://huggingface.co/settings/tokens" target="_blank" rel="noopener">Get a token →</a></span>
298
- </label>
299
-
300
- <label class="field field-toggle">
301
- <span class="field-toggle-row">
302
- <span class="field-label">Vision input</span>
303
- <span class="toggle">
304
- <input id="settings-vision" type="checkbox" />
305
- <span class="toggle-slider"></span>
306
- </span>
307
- </span>
308
- <span class="field-hint">Send attached images to MINDI's CLIP encoder. <strong>Off by default</strong> — the current vision-language fusion is an early build and produces low-quality answers on images. Leave off until the next vision retraining ships. Attaching an image still records it in the chat.</span>
309
- </label>
310
-
311
- <label class="field">
312
- <span class="field-label">Temperature <em class="field-value" id="temp-val">0.7</em></span>
313
- <input id="settings-temp" type="range" min="0" max="2" step="0.05" value="0.7" />
314
- <span class="field-hint">Lower = more focused. Higher = more creative.</span>
315
- </label>
316
-
317
- <label class="field">
318
- <span class="field-label">Max tokens <em class="field-value" id="tokens-val">2048</em></span>
319
- <input id="settings-tokens" type="range" min="128" max="4096" step="128" value="2048" />
320
- <span class="field-hint">Maximum length of the generated response.</span>
321
- </label>
322
- </div>
323
-
324
- <div class="modal-foot">
325
- <button class="btn btn--ghost" data-close>Cancel</button>
326
- <button class="btn btn--primary" id="save-settings">Save settings</button>
327
- </div>
328
- </div>
329
- </div>
330
-
331
- <!-- ============ TOAST CONTAINER ============ -->
332
- <div class="toasts" id="toasts" aria-live="polite" aria-atomic="true"></div>
333
-
334
- <!-- Prism.js core + languages -->
335
- <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
336
- <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script>
337
- <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-typescript.min.js"></script>
338
- <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-jsx.min.js"></script>
339
- <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-tsx.min.js"></script>
340
- <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
341
- <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-css.min.js"></script>
342
- <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-markup.min.js"></script>
343
- <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script>
344
- <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
345
- <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-sql.min.js"></script>
346
-
347
- <!-- StackBlitz SDK — drives the "Run in StackBlitz" launcher.
348
- The form-POST /run endpoint is unreliable (gets silently rejected
349
- and falls back to the default Next.js starter); the SDK's hidden
350
- iframe handshake is the maintained, robust path. -->
351
- <script src="https://unpkg.com/@stackblitz/sdk@1.11.0/bundles/sdk.umd.js"></script>
352
-
353
- <script src="sandbox.js"></script>
354
- <script src="agent.js"></script>
355
- <script src="app.js"></script>
356
  </body>
357
  </html>
 
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="description" content="MINDI 1.5 Vision-Coder — AI Website Builder. Generate production-ready websites from text prompts." />
7
+ <title>MINDI 1.5 — AI Website Builder</title>
 
 
8
  <link rel="preconnect" href="https://fonts.googleapis.com" />
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
 
 
 
 
 
11
  </head>
12
  <body>
13
+ <div id="root"></div>
14
+ <script type="module" src="/src/main.jsx"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  </body>
16
  </html>
frontend/package-lock.json ADDED
@@ -0,0 +1,2448 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "version": "0.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "frontend",
9
+ "version": "0.0.0",
10
+ "dependencies": {
11
+ "lucide-react": "^1.14.0",
12
+ "prismjs": "^1.30.0",
13
+ "react": "^19.2.5",
14
+ "react-dom": "^19.2.5"
15
+ },
16
+ "devDependencies": {
17
+ "@eslint/js": "^10.0.1",
18
+ "@types/react": "^19.2.14",
19
+ "@types/react-dom": "^19.2.3",
20
+ "@vitejs/plugin-react": "^6.0.1",
21
+ "eslint": "^10.2.1",
22
+ "eslint-plugin-react-hooks": "^7.1.1",
23
+ "eslint-plugin-react-refresh": "^0.5.2",
24
+ "globals": "^17.5.0",
25
+ "vite": "^8.0.10"
26
+ }
27
+ },
28
+ "node_modules/@babel/code-frame": {
29
+ "version": "7.29.0",
30
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
31
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
32
+ "dev": true,
33
+ "license": "MIT",
34
+ "dependencies": {
35
+ "@babel/helper-validator-identifier": "^7.28.5",
36
+ "js-tokens": "^4.0.0",
37
+ "picocolors": "^1.1.1"
38
+ },
39
+ "engines": {
40
+ "node": ">=6.9.0"
41
+ }
42
+ },
43
+ "node_modules/@babel/compat-data": {
44
+ "version": "7.29.3",
45
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz",
46
+ "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==",
47
+ "dev": true,
48
+ "license": "MIT",
49
+ "engines": {
50
+ "node": ">=6.9.0"
51
+ }
52
+ },
53
+ "node_modules/@babel/core": {
54
+ "version": "7.29.0",
55
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
56
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
57
+ "dev": true,
58
+ "license": "MIT",
59
+ "dependencies": {
60
+ "@babel/code-frame": "^7.29.0",
61
+ "@babel/generator": "^7.29.0",
62
+ "@babel/helper-compilation-targets": "^7.28.6",
63
+ "@babel/helper-module-transforms": "^7.28.6",
64
+ "@babel/helpers": "^7.28.6",
65
+ "@babel/parser": "^7.29.0",
66
+ "@babel/template": "^7.28.6",
67
+ "@babel/traverse": "^7.29.0",
68
+ "@babel/types": "^7.29.0",
69
+ "@jridgewell/remapping": "^2.3.5",
70
+ "convert-source-map": "^2.0.0",
71
+ "debug": "^4.1.0",
72
+ "gensync": "^1.0.0-beta.2",
73
+ "json5": "^2.2.3",
74
+ "semver": "^6.3.1"
75
+ },
76
+ "engines": {
77
+ "node": ">=6.9.0"
78
+ },
79
+ "funding": {
80
+ "type": "opencollective",
81
+ "url": "https://opencollective.com/babel"
82
+ }
83
+ },
84
+ "node_modules/@babel/generator": {
85
+ "version": "7.29.1",
86
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
87
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
88
+ "dev": true,
89
+ "license": "MIT",
90
+ "dependencies": {
91
+ "@babel/parser": "^7.29.0",
92
+ "@babel/types": "^7.29.0",
93
+ "@jridgewell/gen-mapping": "^0.3.12",
94
+ "@jridgewell/trace-mapping": "^0.3.28",
95
+ "jsesc": "^3.0.2"
96
+ },
97
+ "engines": {
98
+ "node": ">=6.9.0"
99
+ }
100
+ },
101
+ "node_modules/@babel/helper-compilation-targets": {
102
+ "version": "7.28.6",
103
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
104
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
105
+ "dev": true,
106
+ "license": "MIT",
107
+ "dependencies": {
108
+ "@babel/compat-data": "^7.28.6",
109
+ "@babel/helper-validator-option": "^7.27.1",
110
+ "browserslist": "^4.24.0",
111
+ "lru-cache": "^5.1.1",
112
+ "semver": "^6.3.1"
113
+ },
114
+ "engines": {
115
+ "node": ">=6.9.0"
116
+ }
117
+ },
118
+ "node_modules/@babel/helper-globals": {
119
+ "version": "7.28.0",
120
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
121
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
122
+ "dev": true,
123
+ "license": "MIT",
124
+ "engines": {
125
+ "node": ">=6.9.0"
126
+ }
127
+ },
128
+ "node_modules/@babel/helper-module-imports": {
129
+ "version": "7.28.6",
130
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
131
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
132
+ "dev": true,
133
+ "license": "MIT",
134
+ "dependencies": {
135
+ "@babel/traverse": "^7.28.6",
136
+ "@babel/types": "^7.28.6"
137
+ },
138
+ "engines": {
139
+ "node": ">=6.9.0"
140
+ }
141
+ },
142
+ "node_modules/@babel/helper-module-transforms": {
143
+ "version": "7.28.6",
144
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
145
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
146
+ "dev": true,
147
+ "license": "MIT",
148
+ "dependencies": {
149
+ "@babel/helper-module-imports": "^7.28.6",
150
+ "@babel/helper-validator-identifier": "^7.28.5",
151
+ "@babel/traverse": "^7.28.6"
152
+ },
153
+ "engines": {
154
+ "node": ">=6.9.0"
155
+ },
156
+ "peerDependencies": {
157
+ "@babel/core": "^7.0.0"
158
+ }
159
+ },
160
+ "node_modules/@babel/helper-string-parser": {
161
+ "version": "7.27.1",
162
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
163
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
164
+ "dev": true,
165
+ "license": "MIT",
166
+ "engines": {
167
+ "node": ">=6.9.0"
168
+ }
169
+ },
170
+ "node_modules/@babel/helper-validator-identifier": {
171
+ "version": "7.28.5",
172
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
173
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
174
+ "dev": true,
175
+ "license": "MIT",
176
+ "engines": {
177
+ "node": ">=6.9.0"
178
+ }
179
+ },
180
+ "node_modules/@babel/helper-validator-option": {
181
+ "version": "7.27.1",
182
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
183
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
184
+ "dev": true,
185
+ "license": "MIT",
186
+ "engines": {
187
+ "node": ">=6.9.0"
188
+ }
189
+ },
190
+ "node_modules/@babel/helpers": {
191
+ "version": "7.29.2",
192
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
193
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
194
+ "dev": true,
195
+ "license": "MIT",
196
+ "dependencies": {
197
+ "@babel/template": "^7.28.6",
198
+ "@babel/types": "^7.29.0"
199
+ },
200
+ "engines": {
201
+ "node": ">=6.9.0"
202
+ }
203
+ },
204
+ "node_modules/@babel/parser": {
205
+ "version": "7.29.3",
206
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
207
+ "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
208
+ "dev": true,
209
+ "license": "MIT",
210
+ "dependencies": {
211
+ "@babel/types": "^7.29.0"
212
+ },
213
+ "bin": {
214
+ "parser": "bin/babel-parser.js"
215
+ },
216
+ "engines": {
217
+ "node": ">=6.0.0"
218
+ }
219
+ },
220
+ "node_modules/@babel/template": {
221
+ "version": "7.28.6",
222
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
223
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
224
+ "dev": true,
225
+ "license": "MIT",
226
+ "dependencies": {
227
+ "@babel/code-frame": "^7.28.6",
228
+ "@babel/parser": "^7.28.6",
229
+ "@babel/types": "^7.28.6"
230
+ },
231
+ "engines": {
232
+ "node": ">=6.9.0"
233
+ }
234
+ },
235
+ "node_modules/@babel/traverse": {
236
+ "version": "7.29.0",
237
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
238
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
239
+ "dev": true,
240
+ "license": "MIT",
241
+ "dependencies": {
242
+ "@babel/code-frame": "^7.29.0",
243
+ "@babel/generator": "^7.29.0",
244
+ "@babel/helper-globals": "^7.28.0",
245
+ "@babel/parser": "^7.29.0",
246
+ "@babel/template": "^7.28.6",
247
+ "@babel/types": "^7.29.0",
248
+ "debug": "^4.3.1"
249
+ },
250
+ "engines": {
251
+ "node": ">=6.9.0"
252
+ }
253
+ },
254
+ "node_modules/@babel/types": {
255
+ "version": "7.29.0",
256
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
257
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
258
+ "dev": true,
259
+ "license": "MIT",
260
+ "dependencies": {
261
+ "@babel/helper-string-parser": "^7.27.1",
262
+ "@babel/helper-validator-identifier": "^7.28.5"
263
+ },
264
+ "engines": {
265
+ "node": ">=6.9.0"
266
+ }
267
+ },
268
+ "node_modules/@emnapi/core": {
269
+ "version": "1.10.0",
270
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
271
+ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
272
+ "dev": true,
273
+ "license": "MIT",
274
+ "optional": true,
275
+ "dependencies": {
276
+ "@emnapi/wasi-threads": "1.2.1",
277
+ "tslib": "^2.4.0"
278
+ }
279
+ },
280
+ "node_modules/@emnapi/runtime": {
281
+ "version": "1.10.0",
282
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
283
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
284
+ "dev": true,
285
+ "license": "MIT",
286
+ "optional": true,
287
+ "dependencies": {
288
+ "tslib": "^2.4.0"
289
+ }
290
+ },
291
+ "node_modules/@emnapi/wasi-threads": {
292
+ "version": "1.2.1",
293
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
294
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
295
+ "dev": true,
296
+ "license": "MIT",
297
+ "optional": true,
298
+ "dependencies": {
299
+ "tslib": "^2.4.0"
300
+ }
301
+ },
302
+ "node_modules/@eslint-community/eslint-utils": {
303
+ "version": "4.9.1",
304
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
305
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
306
+ "dev": true,
307
+ "license": "MIT",
308
+ "dependencies": {
309
+ "eslint-visitor-keys": "^3.4.3"
310
+ },
311
+ "engines": {
312
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
313
+ },
314
+ "funding": {
315
+ "url": "https://opencollective.com/eslint"
316
+ },
317
+ "peerDependencies": {
318
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
319
+ }
320
+ },
321
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
322
+ "version": "3.4.3",
323
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
324
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
325
+ "dev": true,
326
+ "license": "Apache-2.0",
327
+ "engines": {
328
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
329
+ },
330
+ "funding": {
331
+ "url": "https://opencollective.com/eslint"
332
+ }
333
+ },
334
+ "node_modules/@eslint-community/regexpp": {
335
+ "version": "4.12.2",
336
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
337
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
338
+ "dev": true,
339
+ "license": "MIT",
340
+ "engines": {
341
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
342
+ }
343
+ },
344
+ "node_modules/@eslint/config-array": {
345
+ "version": "0.23.5",
346
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
347
+ "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
348
+ "dev": true,
349
+ "license": "Apache-2.0",
350
+ "dependencies": {
351
+ "@eslint/object-schema": "^3.0.5",
352
+ "debug": "^4.3.1",
353
+ "minimatch": "^10.2.4"
354
+ },
355
+ "engines": {
356
+ "node": "^20.19.0 || ^22.13.0 || >=24"
357
+ }
358
+ },
359
+ "node_modules/@eslint/config-helpers": {
360
+ "version": "0.5.5",
361
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz",
362
+ "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==",
363
+ "dev": true,
364
+ "license": "Apache-2.0",
365
+ "dependencies": {
366
+ "@eslint/core": "^1.2.1"
367
+ },
368
+ "engines": {
369
+ "node": "^20.19.0 || ^22.13.0 || >=24"
370
+ }
371
+ },
372
+ "node_modules/@eslint/core": {
373
+ "version": "1.2.1",
374
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
375
+ "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
376
+ "dev": true,
377
+ "license": "Apache-2.0",
378
+ "dependencies": {
379
+ "@types/json-schema": "^7.0.15"
380
+ },
381
+ "engines": {
382
+ "node": "^20.19.0 || ^22.13.0 || >=24"
383
+ }
384
+ },
385
+ "node_modules/@eslint/js": {
386
+ "version": "10.0.1",
387
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
388
+ "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
389
+ "dev": true,
390
+ "license": "MIT",
391
+ "engines": {
392
+ "node": "^20.19.0 || ^22.13.0 || >=24"
393
+ },
394
+ "funding": {
395
+ "url": "https://eslint.org/donate"
396
+ },
397
+ "peerDependencies": {
398
+ "eslint": "^10.0.0"
399
+ },
400
+ "peerDependenciesMeta": {
401
+ "eslint": {
402
+ "optional": true
403
+ }
404
+ }
405
+ },
406
+ "node_modules/@eslint/object-schema": {
407
+ "version": "3.0.5",
408
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
409
+ "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
410
+ "dev": true,
411
+ "license": "Apache-2.0",
412
+ "engines": {
413
+ "node": "^20.19.0 || ^22.13.0 || >=24"
414
+ }
415
+ },
416
+ "node_modules/@eslint/plugin-kit": {
417
+ "version": "0.7.1",
418
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz",
419
+ "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==",
420
+ "dev": true,
421
+ "license": "Apache-2.0",
422
+ "dependencies": {
423
+ "@eslint/core": "^1.2.1",
424
+ "levn": "^0.4.1"
425
+ },
426
+ "engines": {
427
+ "node": "^20.19.0 || ^22.13.0 || >=24"
428
+ }
429
+ },
430
+ "node_modules/@humanfs/core": {
431
+ "version": "0.19.2",
432
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
433
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
434
+ "dev": true,
435
+ "license": "Apache-2.0",
436
+ "dependencies": {
437
+ "@humanfs/types": "^0.15.0"
438
+ },
439
+ "engines": {
440
+ "node": ">=18.18.0"
441
+ }
442
+ },
443
+ "node_modules/@humanfs/node": {
444
+ "version": "0.16.8",
445
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
446
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
447
+ "dev": true,
448
+ "license": "Apache-2.0",
449
+ "dependencies": {
450
+ "@humanfs/core": "^0.19.2",
451
+ "@humanfs/types": "^0.15.0",
452
+ "@humanwhocodes/retry": "^0.4.0"
453
+ },
454
+ "engines": {
455
+ "node": ">=18.18.0"
456
+ }
457
+ },
458
+ "node_modules/@humanfs/types": {
459
+ "version": "0.15.0",
460
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
461
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
462
+ "dev": true,
463
+ "license": "Apache-2.0",
464
+ "engines": {
465
+ "node": ">=18.18.0"
466
+ }
467
+ },
468
+ "node_modules/@humanwhocodes/module-importer": {
469
+ "version": "1.0.1",
470
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
471
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
472
+ "dev": true,
473
+ "license": "Apache-2.0",
474
+ "engines": {
475
+ "node": ">=12.22"
476
+ },
477
+ "funding": {
478
+ "type": "github",
479
+ "url": "https://github.com/sponsors/nzakas"
480
+ }
481
+ },
482
+ "node_modules/@humanwhocodes/retry": {
483
+ "version": "0.4.3",
484
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
485
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
486
+ "dev": true,
487
+ "license": "Apache-2.0",
488
+ "engines": {
489
+ "node": ">=18.18"
490
+ },
491
+ "funding": {
492
+ "type": "github",
493
+ "url": "https://github.com/sponsors/nzakas"
494
+ }
495
+ },
496
+ "node_modules/@jridgewell/gen-mapping": {
497
+ "version": "0.3.13",
498
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
499
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
500
+ "dev": true,
501
+ "license": "MIT",
502
+ "dependencies": {
503
+ "@jridgewell/sourcemap-codec": "^1.5.0",
504
+ "@jridgewell/trace-mapping": "^0.3.24"
505
+ }
506
+ },
507
+ "node_modules/@jridgewell/remapping": {
508
+ "version": "2.3.5",
509
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
510
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
511
+ "dev": true,
512
+ "license": "MIT",
513
+ "dependencies": {
514
+ "@jridgewell/gen-mapping": "^0.3.5",
515
+ "@jridgewell/trace-mapping": "^0.3.24"
516
+ }
517
+ },
518
+ "node_modules/@jridgewell/resolve-uri": {
519
+ "version": "3.1.2",
520
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
521
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
522
+ "dev": true,
523
+ "license": "MIT",
524
+ "engines": {
525
+ "node": ">=6.0.0"
526
+ }
527
+ },
528
+ "node_modules/@jridgewell/sourcemap-codec": {
529
+ "version": "1.5.5",
530
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
531
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
532
+ "dev": true,
533
+ "license": "MIT"
534
+ },
535
+ "node_modules/@jridgewell/trace-mapping": {
536
+ "version": "0.3.31",
537
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
538
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
539
+ "dev": true,
540
+ "license": "MIT",
541
+ "dependencies": {
542
+ "@jridgewell/resolve-uri": "^3.1.0",
543
+ "@jridgewell/sourcemap-codec": "^1.4.14"
544
+ }
545
+ },
546
+ "node_modules/@napi-rs/wasm-runtime": {
547
+ "version": "1.1.4",
548
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
549
+ "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
550
+ "dev": true,
551
+ "license": "MIT",
552
+ "optional": true,
553
+ "dependencies": {
554
+ "@tybys/wasm-util": "^0.10.1"
555
+ },
556
+ "funding": {
557
+ "type": "github",
558
+ "url": "https://github.com/sponsors/Brooooooklyn"
559
+ },
560
+ "peerDependencies": {
561
+ "@emnapi/core": "^1.7.1",
562
+ "@emnapi/runtime": "^1.7.1"
563
+ }
564
+ },
565
+ "node_modules/@oxc-project/types": {
566
+ "version": "0.127.0",
567
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz",
568
+ "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==",
569
+ "dev": true,
570
+ "license": "MIT",
571
+ "funding": {
572
+ "url": "https://github.com/sponsors/Boshen"
573
+ }
574
+ },
575
+ "node_modules/@rolldown/binding-android-arm64": {
576
+ "version": "1.0.0-rc.17",
577
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
578
+ "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==",
579
+ "cpu": [
580
+ "arm64"
581
+ ],
582
+ "dev": true,
583
+ "license": "MIT",
584
+ "optional": true,
585
+ "os": [
586
+ "android"
587
+ ],
588
+ "engines": {
589
+ "node": "^20.19.0 || >=22.12.0"
590
+ }
591
+ },
592
+ "node_modules/@rolldown/binding-darwin-arm64": {
593
+ "version": "1.0.0-rc.17",
594
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz",
595
+ "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==",
596
+ "cpu": [
597
+ "arm64"
598
+ ],
599
+ "dev": true,
600
+ "license": "MIT",
601
+ "optional": true,
602
+ "os": [
603
+ "darwin"
604
+ ],
605
+ "engines": {
606
+ "node": "^20.19.0 || >=22.12.0"
607
+ }
608
+ },
609
+ "node_modules/@rolldown/binding-darwin-x64": {
610
+ "version": "1.0.0-rc.17",
611
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz",
612
+ "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==",
613
+ "cpu": [
614
+ "x64"
615
+ ],
616
+ "dev": true,
617
+ "license": "MIT",
618
+ "optional": true,
619
+ "os": [
620
+ "darwin"
621
+ ],
622
+ "engines": {
623
+ "node": "^20.19.0 || >=22.12.0"
624
+ }
625
+ },
626
+ "node_modules/@rolldown/binding-freebsd-x64": {
627
+ "version": "1.0.0-rc.17",
628
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz",
629
+ "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==",
630
+ "cpu": [
631
+ "x64"
632
+ ],
633
+ "dev": true,
634
+ "license": "MIT",
635
+ "optional": true,
636
+ "os": [
637
+ "freebsd"
638
+ ],
639
+ "engines": {
640
+ "node": "^20.19.0 || >=22.12.0"
641
+ }
642
+ },
643
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
644
+ "version": "1.0.0-rc.17",
645
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz",
646
+ "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==",
647
+ "cpu": [
648
+ "arm"
649
+ ],
650
+ "dev": true,
651
+ "license": "MIT",
652
+ "optional": true,
653
+ "os": [
654
+ "linux"
655
+ ],
656
+ "engines": {
657
+ "node": "^20.19.0 || >=22.12.0"
658
+ }
659
+ },
660
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
661
+ "version": "1.0.0-rc.17",
662
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz",
663
+ "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==",
664
+ "cpu": [
665
+ "arm64"
666
+ ],
667
+ "dev": true,
668
+ "license": "MIT",
669
+ "optional": true,
670
+ "os": [
671
+ "linux"
672
+ ],
673
+ "engines": {
674
+ "node": "^20.19.0 || >=22.12.0"
675
+ }
676
+ },
677
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
678
+ "version": "1.0.0-rc.17",
679
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz",
680
+ "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==",
681
+ "cpu": [
682
+ "arm64"
683
+ ],
684
+ "dev": true,
685
+ "license": "MIT",
686
+ "optional": true,
687
+ "os": [
688
+ "linux"
689
+ ],
690
+ "engines": {
691
+ "node": "^20.19.0 || >=22.12.0"
692
+ }
693
+ },
694
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
695
+ "version": "1.0.0-rc.17",
696
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz",
697
+ "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==",
698
+ "cpu": [
699
+ "ppc64"
700
+ ],
701
+ "dev": true,
702
+ "license": "MIT",
703
+ "optional": true,
704
+ "os": [
705
+ "linux"
706
+ ],
707
+ "engines": {
708
+ "node": "^20.19.0 || >=22.12.0"
709
+ }
710
+ },
711
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
712
+ "version": "1.0.0-rc.17",
713
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz",
714
+ "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==",
715
+ "cpu": [
716
+ "s390x"
717
+ ],
718
+ "dev": true,
719
+ "license": "MIT",
720
+ "optional": true,
721
+ "os": [
722
+ "linux"
723
+ ],
724
+ "engines": {
725
+ "node": "^20.19.0 || >=22.12.0"
726
+ }
727
+ },
728
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
729
+ "version": "1.0.0-rc.17",
730
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz",
731
+ "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==",
732
+ "cpu": [
733
+ "x64"
734
+ ],
735
+ "dev": true,
736
+ "license": "MIT",
737
+ "optional": true,
738
+ "os": [
739
+ "linux"
740
+ ],
741
+ "engines": {
742
+ "node": "^20.19.0 || >=22.12.0"
743
+ }
744
+ },
745
+ "node_modules/@rolldown/binding-linux-x64-musl": {
746
+ "version": "1.0.0-rc.17",
747
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz",
748
+ "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==",
749
+ "cpu": [
750
+ "x64"
751
+ ],
752
+ "dev": true,
753
+ "license": "MIT",
754
+ "optional": true,
755
+ "os": [
756
+ "linux"
757
+ ],
758
+ "engines": {
759
+ "node": "^20.19.0 || >=22.12.0"
760
+ }
761
+ },
762
+ "node_modules/@rolldown/binding-openharmony-arm64": {
763
+ "version": "1.0.0-rc.17",
764
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz",
765
+ "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==",
766
+ "cpu": [
767
+ "arm64"
768
+ ],
769
+ "dev": true,
770
+ "license": "MIT",
771
+ "optional": true,
772
+ "os": [
773
+ "openharmony"
774
+ ],
775
+ "engines": {
776
+ "node": "^20.19.0 || >=22.12.0"
777
+ }
778
+ },
779
+ "node_modules/@rolldown/binding-wasm32-wasi": {
780
+ "version": "1.0.0-rc.17",
781
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz",
782
+ "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==",
783
+ "cpu": [
784
+ "wasm32"
785
+ ],
786
+ "dev": true,
787
+ "license": "MIT",
788
+ "optional": true,
789
+ "dependencies": {
790
+ "@emnapi/core": "1.10.0",
791
+ "@emnapi/runtime": "1.10.0",
792
+ "@napi-rs/wasm-runtime": "^1.1.4"
793
+ },
794
+ "engines": {
795
+ "node": "^20.19.0 || >=22.12.0"
796
+ }
797
+ },
798
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
799
+ "version": "1.0.0-rc.17",
800
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz",
801
+ "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==",
802
+ "cpu": [
803
+ "arm64"
804
+ ],
805
+ "dev": true,
806
+ "license": "MIT",
807
+ "optional": true,
808
+ "os": [
809
+ "win32"
810
+ ],
811
+ "engines": {
812
+ "node": "^20.19.0 || >=22.12.0"
813
+ }
814
+ },
815
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
816
+ "version": "1.0.0-rc.17",
817
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz",
818
+ "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==",
819
+ "cpu": [
820
+ "x64"
821
+ ],
822
+ "dev": true,
823
+ "license": "MIT",
824
+ "optional": true,
825
+ "os": [
826
+ "win32"
827
+ ],
828
+ "engines": {
829
+ "node": "^20.19.0 || >=22.12.0"
830
+ }
831
+ },
832
+ "node_modules/@rolldown/pluginutils": {
833
+ "version": "1.0.0-rc.7",
834
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz",
835
+ "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==",
836
+ "dev": true,
837
+ "license": "MIT"
838
+ },
839
+ "node_modules/@tybys/wasm-util": {
840
+ "version": "0.10.1",
841
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
842
+ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
843
+ "dev": true,
844
+ "license": "MIT",
845
+ "optional": true,
846
+ "dependencies": {
847
+ "tslib": "^2.4.0"
848
+ }
849
+ },
850
+ "node_modules/@types/esrecurse": {
851
+ "version": "4.3.1",
852
+ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
853
+ "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
854
+ "dev": true,
855
+ "license": "MIT"
856
+ },
857
+ "node_modules/@types/estree": {
858
+ "version": "1.0.8",
859
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
860
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
861
+ "dev": true,
862
+ "license": "MIT"
863
+ },
864
+ "node_modules/@types/json-schema": {
865
+ "version": "7.0.15",
866
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
867
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
868
+ "dev": true,
869
+ "license": "MIT"
870
+ },
871
+ "node_modules/@types/react": {
872
+ "version": "19.2.14",
873
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
874
+ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
875
+ "dev": true,
876
+ "license": "MIT",
877
+ "dependencies": {
878
+ "csstype": "^3.2.2"
879
+ }
880
+ },
881
+ "node_modules/@types/react-dom": {
882
+ "version": "19.2.3",
883
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
884
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
885
+ "dev": true,
886
+ "license": "MIT",
887
+ "peerDependencies": {
888
+ "@types/react": "^19.2.0"
889
+ }
890
+ },
891
+ "node_modules/@vitejs/plugin-react": {
892
+ "version": "6.0.1",
893
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
894
+ "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==",
895
+ "dev": true,
896
+ "license": "MIT",
897
+ "dependencies": {
898
+ "@rolldown/pluginutils": "1.0.0-rc.7"
899
+ },
900
+ "engines": {
901
+ "node": "^20.19.0 || >=22.12.0"
902
+ },
903
+ "peerDependencies": {
904
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
905
+ "babel-plugin-react-compiler": "^1.0.0",
906
+ "vite": "^8.0.0"
907
+ },
908
+ "peerDependenciesMeta": {
909
+ "@rolldown/plugin-babel": {
910
+ "optional": true
911
+ },
912
+ "babel-plugin-react-compiler": {
913
+ "optional": true
914
+ }
915
+ }
916
+ },
917
+ "node_modules/acorn": {
918
+ "version": "8.16.0",
919
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
920
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
921
+ "dev": true,
922
+ "license": "MIT",
923
+ "bin": {
924
+ "acorn": "bin/acorn"
925
+ },
926
+ "engines": {
927
+ "node": ">=0.4.0"
928
+ }
929
+ },
930
+ "node_modules/acorn-jsx": {
931
+ "version": "5.3.2",
932
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
933
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
934
+ "dev": true,
935
+ "license": "MIT",
936
+ "peerDependencies": {
937
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
938
+ }
939
+ },
940
+ "node_modules/ajv": {
941
+ "version": "6.15.0",
942
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
943
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
944
+ "dev": true,
945
+ "license": "MIT",
946
+ "dependencies": {
947
+ "fast-deep-equal": "^3.1.1",
948
+ "fast-json-stable-stringify": "^2.0.0",
949
+ "json-schema-traverse": "^0.4.1",
950
+ "uri-js": "^4.2.2"
951
+ },
952
+ "funding": {
953
+ "type": "github",
954
+ "url": "https://github.com/sponsors/epoberezkin"
955
+ }
956
+ },
957
+ "node_modules/balanced-match": {
958
+ "version": "4.0.4",
959
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
960
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
961
+ "dev": true,
962
+ "license": "MIT",
963
+ "engines": {
964
+ "node": "18 || 20 || >=22"
965
+ }
966
+ },
967
+ "node_modules/baseline-browser-mapping": {
968
+ "version": "2.10.25",
969
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.25.tgz",
970
+ "integrity": "sha512-QO/VHsXCQdnzADMfmkeOPvHdIAkoB7i0/rGjINPJEetLx75hNttVWGQ/jycHUDP9zZ9rupbm60WRxcwViB0MiA==",
971
+ "dev": true,
972
+ "license": "Apache-2.0",
973
+ "bin": {
974
+ "baseline-browser-mapping": "dist/cli.cjs"
975
+ },
976
+ "engines": {
977
+ "node": ">=6.0.0"
978
+ }
979
+ },
980
+ "node_modules/brace-expansion": {
981
+ "version": "5.0.5",
982
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
983
+ "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
984
+ "dev": true,
985
+ "license": "MIT",
986
+ "dependencies": {
987
+ "balanced-match": "^4.0.2"
988
+ },
989
+ "engines": {
990
+ "node": "18 || 20 || >=22"
991
+ }
992
+ },
993
+ "node_modules/browserslist": {
994
+ "version": "4.28.2",
995
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
996
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
997
+ "dev": true,
998
+ "funding": [
999
+ {
1000
+ "type": "opencollective",
1001
+ "url": "https://opencollective.com/browserslist"
1002
+ },
1003
+ {
1004
+ "type": "tidelift",
1005
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1006
+ },
1007
+ {
1008
+ "type": "github",
1009
+ "url": "https://github.com/sponsors/ai"
1010
+ }
1011
+ ],
1012
+ "license": "MIT",
1013
+ "dependencies": {
1014
+ "baseline-browser-mapping": "^2.10.12",
1015
+ "caniuse-lite": "^1.0.30001782",
1016
+ "electron-to-chromium": "^1.5.328",
1017
+ "node-releases": "^2.0.36",
1018
+ "update-browserslist-db": "^1.2.3"
1019
+ },
1020
+ "bin": {
1021
+ "browserslist": "cli.js"
1022
+ },
1023
+ "engines": {
1024
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1025
+ }
1026
+ },
1027
+ "node_modules/caniuse-lite": {
1028
+ "version": "1.0.30001791",
1029
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz",
1030
+ "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==",
1031
+ "dev": true,
1032
+ "funding": [
1033
+ {
1034
+ "type": "opencollective",
1035
+ "url": "https://opencollective.com/browserslist"
1036
+ },
1037
+ {
1038
+ "type": "tidelift",
1039
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1040
+ },
1041
+ {
1042
+ "type": "github",
1043
+ "url": "https://github.com/sponsors/ai"
1044
+ }
1045
+ ],
1046
+ "license": "CC-BY-4.0"
1047
+ },
1048
+ "node_modules/convert-source-map": {
1049
+ "version": "2.0.0",
1050
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1051
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1052
+ "dev": true,
1053
+ "license": "MIT"
1054
+ },
1055
+ "node_modules/cross-spawn": {
1056
+ "version": "7.0.6",
1057
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
1058
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
1059
+ "dev": true,
1060
+ "license": "MIT",
1061
+ "dependencies": {
1062
+ "path-key": "^3.1.0",
1063
+ "shebang-command": "^2.0.0",
1064
+ "which": "^2.0.1"
1065
+ },
1066
+ "engines": {
1067
+ "node": ">= 8"
1068
+ }
1069
+ },
1070
+ "node_modules/csstype": {
1071
+ "version": "3.2.3",
1072
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
1073
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1074
+ "dev": true,
1075
+ "license": "MIT"
1076
+ },
1077
+ "node_modules/debug": {
1078
+ "version": "4.4.3",
1079
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1080
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1081
+ "dev": true,
1082
+ "license": "MIT",
1083
+ "dependencies": {
1084
+ "ms": "^2.1.3"
1085
+ },
1086
+ "engines": {
1087
+ "node": ">=6.0"
1088
+ },
1089
+ "peerDependenciesMeta": {
1090
+ "supports-color": {
1091
+ "optional": true
1092
+ }
1093
+ }
1094
+ },
1095
+ "node_modules/deep-is": {
1096
+ "version": "0.1.4",
1097
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
1098
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
1099
+ "dev": true,
1100
+ "license": "MIT"
1101
+ },
1102
+ "node_modules/detect-libc": {
1103
+ "version": "2.1.2",
1104
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
1105
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
1106
+ "dev": true,
1107
+ "license": "Apache-2.0",
1108
+ "engines": {
1109
+ "node": ">=8"
1110
+ }
1111
+ },
1112
+ "node_modules/electron-to-chromium": {
1113
+ "version": "1.5.349",
1114
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.349.tgz",
1115
+ "integrity": "sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==",
1116
+ "dev": true,
1117
+ "license": "ISC"
1118
+ },
1119
+ "node_modules/escalade": {
1120
+ "version": "3.2.0",
1121
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1122
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1123
+ "dev": true,
1124
+ "license": "MIT",
1125
+ "engines": {
1126
+ "node": ">=6"
1127
+ }
1128
+ },
1129
+ "node_modules/escape-string-regexp": {
1130
+ "version": "4.0.0",
1131
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
1132
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
1133
+ "dev": true,
1134
+ "license": "MIT",
1135
+ "engines": {
1136
+ "node": ">=10"
1137
+ },
1138
+ "funding": {
1139
+ "url": "https://github.com/sponsors/sindresorhus"
1140
+ }
1141
+ },
1142
+ "node_modules/eslint": {
1143
+ "version": "10.3.0",
1144
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.3.0.tgz",
1145
+ "integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==",
1146
+ "dev": true,
1147
+ "license": "MIT",
1148
+ "dependencies": {
1149
+ "@eslint-community/eslint-utils": "^4.8.0",
1150
+ "@eslint-community/regexpp": "^4.12.2",
1151
+ "@eslint/config-array": "^0.23.5",
1152
+ "@eslint/config-helpers": "^0.5.5",
1153
+ "@eslint/core": "^1.2.1",
1154
+ "@eslint/plugin-kit": "^0.7.1",
1155
+ "@humanfs/node": "^0.16.6",
1156
+ "@humanwhocodes/module-importer": "^1.0.1",
1157
+ "@humanwhocodes/retry": "^0.4.2",
1158
+ "@types/estree": "^1.0.6",
1159
+ "ajv": "^6.14.0",
1160
+ "cross-spawn": "^7.0.6",
1161
+ "debug": "^4.3.2",
1162
+ "escape-string-regexp": "^4.0.0",
1163
+ "eslint-scope": "^9.1.2",
1164
+ "eslint-visitor-keys": "^5.0.1",
1165
+ "espree": "^11.2.0",
1166
+ "esquery": "^1.7.0",
1167
+ "esutils": "^2.0.2",
1168
+ "fast-deep-equal": "^3.1.3",
1169
+ "file-entry-cache": "^8.0.0",
1170
+ "find-up": "^5.0.0",
1171
+ "glob-parent": "^6.0.2",
1172
+ "ignore": "^5.2.0",
1173
+ "imurmurhash": "^0.1.4",
1174
+ "is-glob": "^4.0.0",
1175
+ "json-stable-stringify-without-jsonify": "^1.0.1",
1176
+ "minimatch": "^10.2.4",
1177
+ "natural-compare": "^1.4.0",
1178
+ "optionator": "^0.9.3"
1179
+ },
1180
+ "bin": {
1181
+ "eslint": "bin/eslint.js"
1182
+ },
1183
+ "engines": {
1184
+ "node": "^20.19.0 || ^22.13.0 || >=24"
1185
+ },
1186
+ "funding": {
1187
+ "url": "https://eslint.org/donate"
1188
+ },
1189
+ "peerDependencies": {
1190
+ "jiti": "*"
1191
+ },
1192
+ "peerDependenciesMeta": {
1193
+ "jiti": {
1194
+ "optional": true
1195
+ }
1196
+ }
1197
+ },
1198
+ "node_modules/eslint-plugin-react-hooks": {
1199
+ "version": "7.1.1",
1200
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
1201
+ "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
1202
+ "dev": true,
1203
+ "license": "MIT",
1204
+ "dependencies": {
1205
+ "@babel/core": "^7.24.4",
1206
+ "@babel/parser": "^7.24.4",
1207
+ "hermes-parser": "^0.25.1",
1208
+ "zod": "^3.25.0 || ^4.0.0",
1209
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
1210
+ },
1211
+ "engines": {
1212
+ "node": ">=18"
1213
+ },
1214
+ "peerDependencies": {
1215
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
1216
+ }
1217
+ },
1218
+ "node_modules/eslint-plugin-react-refresh": {
1219
+ "version": "0.5.2",
1220
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz",
1221
+ "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==",
1222
+ "dev": true,
1223
+ "license": "MIT",
1224
+ "peerDependencies": {
1225
+ "eslint": "^9 || ^10"
1226
+ }
1227
+ },
1228
+ "node_modules/eslint-scope": {
1229
+ "version": "9.1.2",
1230
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
1231
+ "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
1232
+ "dev": true,
1233
+ "license": "BSD-2-Clause",
1234
+ "dependencies": {
1235
+ "@types/esrecurse": "^4.3.1",
1236
+ "@types/estree": "^1.0.8",
1237
+ "esrecurse": "^4.3.0",
1238
+ "estraverse": "^5.2.0"
1239
+ },
1240
+ "engines": {
1241
+ "node": "^20.19.0 || ^22.13.0 || >=24"
1242
+ },
1243
+ "funding": {
1244
+ "url": "https://opencollective.com/eslint"
1245
+ }
1246
+ },
1247
+ "node_modules/eslint-visitor-keys": {
1248
+ "version": "5.0.1",
1249
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
1250
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
1251
+ "dev": true,
1252
+ "license": "Apache-2.0",
1253
+ "engines": {
1254
+ "node": "^20.19.0 || ^22.13.0 || >=24"
1255
+ },
1256
+ "funding": {
1257
+ "url": "https://opencollective.com/eslint"
1258
+ }
1259
+ },
1260
+ "node_modules/espree": {
1261
+ "version": "11.2.0",
1262
+ "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
1263
+ "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
1264
+ "dev": true,
1265
+ "license": "BSD-2-Clause",
1266
+ "dependencies": {
1267
+ "acorn": "^8.16.0",
1268
+ "acorn-jsx": "^5.3.2",
1269
+ "eslint-visitor-keys": "^5.0.1"
1270
+ },
1271
+ "engines": {
1272
+ "node": "^20.19.0 || ^22.13.0 || >=24"
1273
+ },
1274
+ "funding": {
1275
+ "url": "https://opencollective.com/eslint"
1276
+ }
1277
+ },
1278
+ "node_modules/esquery": {
1279
+ "version": "1.7.0",
1280
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
1281
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
1282
+ "dev": true,
1283
+ "license": "BSD-3-Clause",
1284
+ "dependencies": {
1285
+ "estraverse": "^5.1.0"
1286
+ },
1287
+ "engines": {
1288
+ "node": ">=0.10"
1289
+ }
1290
+ },
1291
+ "node_modules/esrecurse": {
1292
+ "version": "4.3.0",
1293
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
1294
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
1295
+ "dev": true,
1296
+ "license": "BSD-2-Clause",
1297
+ "dependencies": {
1298
+ "estraverse": "^5.2.0"
1299
+ },
1300
+ "engines": {
1301
+ "node": ">=4.0"
1302
+ }
1303
+ },
1304
+ "node_modules/estraverse": {
1305
+ "version": "5.3.0",
1306
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
1307
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
1308
+ "dev": true,
1309
+ "license": "BSD-2-Clause",
1310
+ "engines": {
1311
+ "node": ">=4.0"
1312
+ }
1313
+ },
1314
+ "node_modules/esutils": {
1315
+ "version": "2.0.3",
1316
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
1317
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
1318
+ "dev": true,
1319
+ "license": "BSD-2-Clause",
1320
+ "engines": {
1321
+ "node": ">=0.10.0"
1322
+ }
1323
+ },
1324
+ "node_modules/fast-deep-equal": {
1325
+ "version": "3.1.3",
1326
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
1327
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
1328
+ "dev": true,
1329
+ "license": "MIT"
1330
+ },
1331
+ "node_modules/fast-json-stable-stringify": {
1332
+ "version": "2.1.0",
1333
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
1334
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
1335
+ "dev": true,
1336
+ "license": "MIT"
1337
+ },
1338
+ "node_modules/fast-levenshtein": {
1339
+ "version": "2.0.6",
1340
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
1341
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
1342
+ "dev": true,
1343
+ "license": "MIT"
1344
+ },
1345
+ "node_modules/fdir": {
1346
+ "version": "6.5.0",
1347
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1348
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1349
+ "dev": true,
1350
+ "license": "MIT",
1351
+ "engines": {
1352
+ "node": ">=12.0.0"
1353
+ },
1354
+ "peerDependencies": {
1355
+ "picomatch": "^3 || ^4"
1356
+ },
1357
+ "peerDependenciesMeta": {
1358
+ "picomatch": {
1359
+ "optional": true
1360
+ }
1361
+ }
1362
+ },
1363
+ "node_modules/file-entry-cache": {
1364
+ "version": "8.0.0",
1365
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
1366
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
1367
+ "dev": true,
1368
+ "license": "MIT",
1369
+ "dependencies": {
1370
+ "flat-cache": "^4.0.0"
1371
+ },
1372
+ "engines": {
1373
+ "node": ">=16.0.0"
1374
+ }
1375
+ },
1376
+ "node_modules/find-up": {
1377
+ "version": "5.0.0",
1378
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
1379
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
1380
+ "dev": true,
1381
+ "license": "MIT",
1382
+ "dependencies": {
1383
+ "locate-path": "^6.0.0",
1384
+ "path-exists": "^4.0.0"
1385
+ },
1386
+ "engines": {
1387
+ "node": ">=10"
1388
+ },
1389
+ "funding": {
1390
+ "url": "https://github.com/sponsors/sindresorhus"
1391
+ }
1392
+ },
1393
+ "node_modules/flat-cache": {
1394
+ "version": "4.0.1",
1395
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
1396
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
1397
+ "dev": true,
1398
+ "license": "MIT",
1399
+ "dependencies": {
1400
+ "flatted": "^3.2.9",
1401
+ "keyv": "^4.5.4"
1402
+ },
1403
+ "engines": {
1404
+ "node": ">=16"
1405
+ }
1406
+ },
1407
+ "node_modules/flatted": {
1408
+ "version": "3.4.2",
1409
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
1410
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
1411
+ "dev": true,
1412
+ "license": "ISC"
1413
+ },
1414
+ "node_modules/fsevents": {
1415
+ "version": "2.3.3",
1416
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1417
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1418
+ "dev": true,
1419
+ "hasInstallScript": true,
1420
+ "license": "MIT",
1421
+ "optional": true,
1422
+ "os": [
1423
+ "darwin"
1424
+ ],
1425
+ "engines": {
1426
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1427
+ }
1428
+ },
1429
+ "node_modules/gensync": {
1430
+ "version": "1.0.0-beta.2",
1431
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1432
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1433
+ "dev": true,
1434
+ "license": "MIT",
1435
+ "engines": {
1436
+ "node": ">=6.9.0"
1437
+ }
1438
+ },
1439
+ "node_modules/glob-parent": {
1440
+ "version": "6.0.2",
1441
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
1442
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
1443
+ "dev": true,
1444
+ "license": "ISC",
1445
+ "dependencies": {
1446
+ "is-glob": "^4.0.3"
1447
+ },
1448
+ "engines": {
1449
+ "node": ">=10.13.0"
1450
+ }
1451
+ },
1452
+ "node_modules/globals": {
1453
+ "version": "17.6.0",
1454
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz",
1455
+ "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==",
1456
+ "dev": true,
1457
+ "license": "MIT",
1458
+ "engines": {
1459
+ "node": ">=18"
1460
+ },
1461
+ "funding": {
1462
+ "url": "https://github.com/sponsors/sindresorhus"
1463
+ }
1464
+ },
1465
+ "node_modules/hermes-estree": {
1466
+ "version": "0.25.1",
1467
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
1468
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
1469
+ "dev": true,
1470
+ "license": "MIT"
1471
+ },
1472
+ "node_modules/hermes-parser": {
1473
+ "version": "0.25.1",
1474
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
1475
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
1476
+ "dev": true,
1477
+ "license": "MIT",
1478
+ "dependencies": {
1479
+ "hermes-estree": "0.25.1"
1480
+ }
1481
+ },
1482
+ "node_modules/ignore": {
1483
+ "version": "5.3.2",
1484
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
1485
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
1486
+ "dev": true,
1487
+ "license": "MIT",
1488
+ "engines": {
1489
+ "node": ">= 4"
1490
+ }
1491
+ },
1492
+ "node_modules/imurmurhash": {
1493
+ "version": "0.1.4",
1494
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
1495
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
1496
+ "dev": true,
1497
+ "license": "MIT",
1498
+ "engines": {
1499
+ "node": ">=0.8.19"
1500
+ }
1501
+ },
1502
+ "node_modules/is-extglob": {
1503
+ "version": "2.1.1",
1504
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
1505
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
1506
+ "dev": true,
1507
+ "license": "MIT",
1508
+ "engines": {
1509
+ "node": ">=0.10.0"
1510
+ }
1511
+ },
1512
+ "node_modules/is-glob": {
1513
+ "version": "4.0.3",
1514
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
1515
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
1516
+ "dev": true,
1517
+ "license": "MIT",
1518
+ "dependencies": {
1519
+ "is-extglob": "^2.1.1"
1520
+ },
1521
+ "engines": {
1522
+ "node": ">=0.10.0"
1523
+ }
1524
+ },
1525
+ "node_modules/isexe": {
1526
+ "version": "2.0.0",
1527
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
1528
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
1529
+ "dev": true,
1530
+ "license": "ISC"
1531
+ },
1532
+ "node_modules/js-tokens": {
1533
+ "version": "4.0.0",
1534
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1535
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1536
+ "dev": true,
1537
+ "license": "MIT"
1538
+ },
1539
+ "node_modules/jsesc": {
1540
+ "version": "3.1.0",
1541
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1542
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1543
+ "dev": true,
1544
+ "license": "MIT",
1545
+ "bin": {
1546
+ "jsesc": "bin/jsesc"
1547
+ },
1548
+ "engines": {
1549
+ "node": ">=6"
1550
+ }
1551
+ },
1552
+ "node_modules/json-buffer": {
1553
+ "version": "3.0.1",
1554
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
1555
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
1556
+ "dev": true,
1557
+ "license": "MIT"
1558
+ },
1559
+ "node_modules/json-schema-traverse": {
1560
+ "version": "0.4.1",
1561
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
1562
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
1563
+ "dev": true,
1564
+ "license": "MIT"
1565
+ },
1566
+ "node_modules/json-stable-stringify-without-jsonify": {
1567
+ "version": "1.0.1",
1568
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
1569
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
1570
+ "dev": true,
1571
+ "license": "MIT"
1572
+ },
1573
+ "node_modules/json5": {
1574
+ "version": "2.2.3",
1575
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1576
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1577
+ "dev": true,
1578
+ "license": "MIT",
1579
+ "bin": {
1580
+ "json5": "lib/cli.js"
1581
+ },
1582
+ "engines": {
1583
+ "node": ">=6"
1584
+ }
1585
+ },
1586
+ "node_modules/keyv": {
1587
+ "version": "4.5.4",
1588
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
1589
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
1590
+ "dev": true,
1591
+ "license": "MIT",
1592
+ "dependencies": {
1593
+ "json-buffer": "3.0.1"
1594
+ }
1595
+ },
1596
+ "node_modules/levn": {
1597
+ "version": "0.4.1",
1598
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
1599
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
1600
+ "dev": true,
1601
+ "license": "MIT",
1602
+ "dependencies": {
1603
+ "prelude-ls": "^1.2.1",
1604
+ "type-check": "~0.4.0"
1605
+ },
1606
+ "engines": {
1607
+ "node": ">= 0.8.0"
1608
+ }
1609
+ },
1610
+ "node_modules/lightningcss": {
1611
+ "version": "1.32.0",
1612
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
1613
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
1614
+ "dev": true,
1615
+ "license": "MPL-2.0",
1616
+ "dependencies": {
1617
+ "detect-libc": "^2.0.3"
1618
+ },
1619
+ "engines": {
1620
+ "node": ">= 12.0.0"
1621
+ },
1622
+ "funding": {
1623
+ "type": "opencollective",
1624
+ "url": "https://opencollective.com/parcel"
1625
+ },
1626
+ "optionalDependencies": {
1627
+ "lightningcss-android-arm64": "1.32.0",
1628
+ "lightningcss-darwin-arm64": "1.32.0",
1629
+ "lightningcss-darwin-x64": "1.32.0",
1630
+ "lightningcss-freebsd-x64": "1.32.0",
1631
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
1632
+ "lightningcss-linux-arm64-gnu": "1.32.0",
1633
+ "lightningcss-linux-arm64-musl": "1.32.0",
1634
+ "lightningcss-linux-x64-gnu": "1.32.0",
1635
+ "lightningcss-linux-x64-musl": "1.32.0",
1636
+ "lightningcss-win32-arm64-msvc": "1.32.0",
1637
+ "lightningcss-win32-x64-msvc": "1.32.0"
1638
+ }
1639
+ },
1640
+ "node_modules/lightningcss-android-arm64": {
1641
+ "version": "1.32.0",
1642
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
1643
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
1644
+ "cpu": [
1645
+ "arm64"
1646
+ ],
1647
+ "dev": true,
1648
+ "license": "MPL-2.0",
1649
+ "optional": true,
1650
+ "os": [
1651
+ "android"
1652
+ ],
1653
+ "engines": {
1654
+ "node": ">= 12.0.0"
1655
+ },
1656
+ "funding": {
1657
+ "type": "opencollective",
1658
+ "url": "https://opencollective.com/parcel"
1659
+ }
1660
+ },
1661
+ "node_modules/lightningcss-darwin-arm64": {
1662
+ "version": "1.32.0",
1663
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
1664
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
1665
+ "cpu": [
1666
+ "arm64"
1667
+ ],
1668
+ "dev": true,
1669
+ "license": "MPL-2.0",
1670
+ "optional": true,
1671
+ "os": [
1672
+ "darwin"
1673
+ ],
1674
+ "engines": {
1675
+ "node": ">= 12.0.0"
1676
+ },
1677
+ "funding": {
1678
+ "type": "opencollective",
1679
+ "url": "https://opencollective.com/parcel"
1680
+ }
1681
+ },
1682
+ "node_modules/lightningcss-darwin-x64": {
1683
+ "version": "1.32.0",
1684
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
1685
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
1686
+ "cpu": [
1687
+ "x64"
1688
+ ],
1689
+ "dev": true,
1690
+ "license": "MPL-2.0",
1691
+ "optional": true,
1692
+ "os": [
1693
+ "darwin"
1694
+ ],
1695
+ "engines": {
1696
+ "node": ">= 12.0.0"
1697
+ },
1698
+ "funding": {
1699
+ "type": "opencollective",
1700
+ "url": "https://opencollective.com/parcel"
1701
+ }
1702
+ },
1703
+ "node_modules/lightningcss-freebsd-x64": {
1704
+ "version": "1.32.0",
1705
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
1706
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
1707
+ "cpu": [
1708
+ "x64"
1709
+ ],
1710
+ "dev": true,
1711
+ "license": "MPL-2.0",
1712
+ "optional": true,
1713
+ "os": [
1714
+ "freebsd"
1715
+ ],
1716
+ "engines": {
1717
+ "node": ">= 12.0.0"
1718
+ },
1719
+ "funding": {
1720
+ "type": "opencollective",
1721
+ "url": "https://opencollective.com/parcel"
1722
+ }
1723
+ },
1724
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
1725
+ "version": "1.32.0",
1726
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
1727
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
1728
+ "cpu": [
1729
+ "arm"
1730
+ ],
1731
+ "dev": true,
1732
+ "license": "MPL-2.0",
1733
+ "optional": true,
1734
+ "os": [
1735
+ "linux"
1736
+ ],
1737
+ "engines": {
1738
+ "node": ">= 12.0.0"
1739
+ },
1740
+ "funding": {
1741
+ "type": "opencollective",
1742
+ "url": "https://opencollective.com/parcel"
1743
+ }
1744
+ },
1745
+ "node_modules/lightningcss-linux-arm64-gnu": {
1746
+ "version": "1.32.0",
1747
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
1748
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
1749
+ "cpu": [
1750
+ "arm64"
1751
+ ],
1752
+ "dev": true,
1753
+ "license": "MPL-2.0",
1754
+ "optional": true,
1755
+ "os": [
1756
+ "linux"
1757
+ ],
1758
+ "engines": {
1759
+ "node": ">= 12.0.0"
1760
+ },
1761
+ "funding": {
1762
+ "type": "opencollective",
1763
+ "url": "https://opencollective.com/parcel"
1764
+ }
1765
+ },
1766
+ "node_modules/lightningcss-linux-arm64-musl": {
1767
+ "version": "1.32.0",
1768
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
1769
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
1770
+ "cpu": [
1771
+ "arm64"
1772
+ ],
1773
+ "dev": true,
1774
+ "license": "MPL-2.0",
1775
+ "optional": true,
1776
+ "os": [
1777
+ "linux"
1778
+ ],
1779
+ "engines": {
1780
+ "node": ">= 12.0.0"
1781
+ },
1782
+ "funding": {
1783
+ "type": "opencollective",
1784
+ "url": "https://opencollective.com/parcel"
1785
+ }
1786
+ },
1787
+ "node_modules/lightningcss-linux-x64-gnu": {
1788
+ "version": "1.32.0",
1789
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
1790
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
1791
+ "cpu": [
1792
+ "x64"
1793
+ ],
1794
+ "dev": true,
1795
+ "license": "MPL-2.0",
1796
+ "optional": true,
1797
+ "os": [
1798
+ "linux"
1799
+ ],
1800
+ "engines": {
1801
+ "node": ">= 12.0.0"
1802
+ },
1803
+ "funding": {
1804
+ "type": "opencollective",
1805
+ "url": "https://opencollective.com/parcel"
1806
+ }
1807
+ },
1808
+ "node_modules/lightningcss-linux-x64-musl": {
1809
+ "version": "1.32.0",
1810
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
1811
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
1812
+ "cpu": [
1813
+ "x64"
1814
+ ],
1815
+ "dev": true,
1816
+ "license": "MPL-2.0",
1817
+ "optional": true,
1818
+ "os": [
1819
+ "linux"
1820
+ ],
1821
+ "engines": {
1822
+ "node": ">= 12.0.0"
1823
+ },
1824
+ "funding": {
1825
+ "type": "opencollective",
1826
+ "url": "https://opencollective.com/parcel"
1827
+ }
1828
+ },
1829
+ "node_modules/lightningcss-win32-arm64-msvc": {
1830
+ "version": "1.32.0",
1831
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
1832
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
1833
+ "cpu": [
1834
+ "arm64"
1835
+ ],
1836
+ "dev": true,
1837
+ "license": "MPL-2.0",
1838
+ "optional": true,
1839
+ "os": [
1840
+ "win32"
1841
+ ],
1842
+ "engines": {
1843
+ "node": ">= 12.0.0"
1844
+ },
1845
+ "funding": {
1846
+ "type": "opencollective",
1847
+ "url": "https://opencollective.com/parcel"
1848
+ }
1849
+ },
1850
+ "node_modules/lightningcss-win32-x64-msvc": {
1851
+ "version": "1.32.0",
1852
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
1853
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
1854
+ "cpu": [
1855
+ "x64"
1856
+ ],
1857
+ "dev": true,
1858
+ "license": "MPL-2.0",
1859
+ "optional": true,
1860
+ "os": [
1861
+ "win32"
1862
+ ],
1863
+ "engines": {
1864
+ "node": ">= 12.0.0"
1865
+ },
1866
+ "funding": {
1867
+ "type": "opencollective",
1868
+ "url": "https://opencollective.com/parcel"
1869
+ }
1870
+ },
1871
+ "node_modules/locate-path": {
1872
+ "version": "6.0.0",
1873
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
1874
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
1875
+ "dev": true,
1876
+ "license": "MIT",
1877
+ "dependencies": {
1878
+ "p-locate": "^5.0.0"
1879
+ },
1880
+ "engines": {
1881
+ "node": ">=10"
1882
+ },
1883
+ "funding": {
1884
+ "url": "https://github.com/sponsors/sindresorhus"
1885
+ }
1886
+ },
1887
+ "node_modules/lru-cache": {
1888
+ "version": "5.1.1",
1889
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
1890
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
1891
+ "dev": true,
1892
+ "license": "ISC",
1893
+ "dependencies": {
1894
+ "yallist": "^3.0.2"
1895
+ }
1896
+ },
1897
+ "node_modules/lucide-react": {
1898
+ "version": "1.14.0",
1899
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz",
1900
+ "integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==",
1901
+ "license": "ISC",
1902
+ "peerDependencies": {
1903
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
1904
+ }
1905
+ },
1906
+ "node_modules/minimatch": {
1907
+ "version": "10.2.5",
1908
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
1909
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
1910
+ "dev": true,
1911
+ "license": "BlueOak-1.0.0",
1912
+ "dependencies": {
1913
+ "brace-expansion": "^5.0.5"
1914
+ },
1915
+ "engines": {
1916
+ "node": "18 || 20 || >=22"
1917
+ },
1918
+ "funding": {
1919
+ "url": "https://github.com/sponsors/isaacs"
1920
+ }
1921
+ },
1922
+ "node_modules/ms": {
1923
+ "version": "2.1.3",
1924
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1925
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1926
+ "dev": true,
1927
+ "license": "MIT"
1928
+ },
1929
+ "node_modules/nanoid": {
1930
+ "version": "3.3.12",
1931
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
1932
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
1933
+ "dev": true,
1934
+ "funding": [
1935
+ {
1936
+ "type": "github",
1937
+ "url": "https://github.com/sponsors/ai"
1938
+ }
1939
+ ],
1940
+ "license": "MIT",
1941
+ "bin": {
1942
+ "nanoid": "bin/nanoid.cjs"
1943
+ },
1944
+ "engines": {
1945
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1946
+ }
1947
+ },
1948
+ "node_modules/natural-compare": {
1949
+ "version": "1.4.0",
1950
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
1951
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
1952
+ "dev": true,
1953
+ "license": "MIT"
1954
+ },
1955
+ "node_modules/node-releases": {
1956
+ "version": "2.0.38",
1957
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
1958
+ "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
1959
+ "dev": true,
1960
+ "license": "MIT"
1961
+ },
1962
+ "node_modules/optionator": {
1963
+ "version": "0.9.4",
1964
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
1965
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
1966
+ "dev": true,
1967
+ "license": "MIT",
1968
+ "dependencies": {
1969
+ "deep-is": "^0.1.3",
1970
+ "fast-levenshtein": "^2.0.6",
1971
+ "levn": "^0.4.1",
1972
+ "prelude-ls": "^1.2.1",
1973
+ "type-check": "^0.4.0",
1974
+ "word-wrap": "^1.2.5"
1975
+ },
1976
+ "engines": {
1977
+ "node": ">= 0.8.0"
1978
+ }
1979
+ },
1980
+ "node_modules/p-limit": {
1981
+ "version": "3.1.0",
1982
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
1983
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
1984
+ "dev": true,
1985
+ "license": "MIT",
1986
+ "dependencies": {
1987
+ "yocto-queue": "^0.1.0"
1988
+ },
1989
+ "engines": {
1990
+ "node": ">=10"
1991
+ },
1992
+ "funding": {
1993
+ "url": "https://github.com/sponsors/sindresorhus"
1994
+ }
1995
+ },
1996
+ "node_modules/p-locate": {
1997
+ "version": "5.0.0",
1998
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
1999
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
2000
+ "dev": true,
2001
+ "license": "MIT",
2002
+ "dependencies": {
2003
+ "p-limit": "^3.0.2"
2004
+ },
2005
+ "engines": {
2006
+ "node": ">=10"
2007
+ },
2008
+ "funding": {
2009
+ "url": "https://github.com/sponsors/sindresorhus"
2010
+ }
2011
+ },
2012
+ "node_modules/path-exists": {
2013
+ "version": "4.0.0",
2014
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
2015
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
2016
+ "dev": true,
2017
+ "license": "MIT",
2018
+ "engines": {
2019
+ "node": ">=8"
2020
+ }
2021
+ },
2022
+ "node_modules/path-key": {
2023
+ "version": "3.1.1",
2024
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
2025
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
2026
+ "dev": true,
2027
+ "license": "MIT",
2028
+ "engines": {
2029
+ "node": ">=8"
2030
+ }
2031
+ },
2032
+ "node_modules/picocolors": {
2033
+ "version": "1.1.1",
2034
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
2035
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
2036
+ "dev": true,
2037
+ "license": "ISC"
2038
+ },
2039
+ "node_modules/picomatch": {
2040
+ "version": "4.0.4",
2041
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
2042
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
2043
+ "dev": true,
2044
+ "license": "MIT",
2045
+ "engines": {
2046
+ "node": ">=12"
2047
+ },
2048
+ "funding": {
2049
+ "url": "https://github.com/sponsors/jonschlinkert"
2050
+ }
2051
+ },
2052
+ "node_modules/postcss": {
2053
+ "version": "8.5.13",
2054
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz",
2055
+ "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==",
2056
+ "dev": true,
2057
+ "funding": [
2058
+ {
2059
+ "type": "opencollective",
2060
+ "url": "https://opencollective.com/postcss/"
2061
+ },
2062
+ {
2063
+ "type": "tidelift",
2064
+ "url": "https://tidelift.com/funding/github/npm/postcss"
2065
+ },
2066
+ {
2067
+ "type": "github",
2068
+ "url": "https://github.com/sponsors/ai"
2069
+ }
2070
+ ],
2071
+ "license": "MIT",
2072
+ "dependencies": {
2073
+ "nanoid": "^3.3.11",
2074
+ "picocolors": "^1.1.1",
2075
+ "source-map-js": "^1.2.1"
2076
+ },
2077
+ "engines": {
2078
+ "node": "^10 || ^12 || >=14"
2079
+ }
2080
+ },
2081
+ "node_modules/prelude-ls": {
2082
+ "version": "1.2.1",
2083
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
2084
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
2085
+ "dev": true,
2086
+ "license": "MIT",
2087
+ "engines": {
2088
+ "node": ">= 0.8.0"
2089
+ }
2090
+ },
2091
+ "node_modules/prismjs": {
2092
+ "version": "1.30.0",
2093
+ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
2094
+ "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
2095
+ "license": "MIT",
2096
+ "engines": {
2097
+ "node": ">=6"
2098
+ }
2099
+ },
2100
+ "node_modules/punycode": {
2101
+ "version": "2.3.1",
2102
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
2103
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
2104
+ "dev": true,
2105
+ "license": "MIT",
2106
+ "engines": {
2107
+ "node": ">=6"
2108
+ }
2109
+ },
2110
+ "node_modules/react": {
2111
+ "version": "19.2.5",
2112
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
2113
+ "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
2114
+ "license": "MIT",
2115
+ "engines": {
2116
+ "node": ">=0.10.0"
2117
+ }
2118
+ },
2119
+ "node_modules/react-dom": {
2120
+ "version": "19.2.5",
2121
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
2122
+ "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
2123
+ "license": "MIT",
2124
+ "dependencies": {
2125
+ "scheduler": "^0.27.0"
2126
+ },
2127
+ "peerDependencies": {
2128
+ "react": "^19.2.5"
2129
+ }
2130
+ },
2131
+ "node_modules/rolldown": {
2132
+ "version": "1.0.0-rc.17",
2133
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
2134
+ "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
2135
+ "dev": true,
2136
+ "license": "MIT",
2137
+ "dependencies": {
2138
+ "@oxc-project/types": "=0.127.0",
2139
+ "@rolldown/pluginutils": "1.0.0-rc.17"
2140
+ },
2141
+ "bin": {
2142
+ "rolldown": "bin/cli.mjs"
2143
+ },
2144
+ "engines": {
2145
+ "node": "^20.19.0 || >=22.12.0"
2146
+ },
2147
+ "optionalDependencies": {
2148
+ "@rolldown/binding-android-arm64": "1.0.0-rc.17",
2149
+ "@rolldown/binding-darwin-arm64": "1.0.0-rc.17",
2150
+ "@rolldown/binding-darwin-x64": "1.0.0-rc.17",
2151
+ "@rolldown/binding-freebsd-x64": "1.0.0-rc.17",
2152
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17",
2153
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17",
2154
+ "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17",
2155
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17",
2156
+ "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17",
2157
+ "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17",
2158
+ "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17",
2159
+ "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17",
2160
+ "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17",
2161
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17",
2162
+ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17"
2163
+ }
2164
+ },
2165
+ "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
2166
+ "version": "1.0.0-rc.17",
2167
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz",
2168
+ "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
2169
+ "dev": true,
2170
+ "license": "MIT"
2171
+ },
2172
+ "node_modules/scheduler": {
2173
+ "version": "0.27.0",
2174
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
2175
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
2176
+ "license": "MIT"
2177
+ },
2178
+ "node_modules/semver": {
2179
+ "version": "6.3.1",
2180
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
2181
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
2182
+ "dev": true,
2183
+ "license": "ISC",
2184
+ "bin": {
2185
+ "semver": "bin/semver.js"
2186
+ }
2187
+ },
2188
+ "node_modules/shebang-command": {
2189
+ "version": "2.0.0",
2190
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
2191
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
2192
+ "dev": true,
2193
+ "license": "MIT",
2194
+ "dependencies": {
2195
+ "shebang-regex": "^3.0.0"
2196
+ },
2197
+ "engines": {
2198
+ "node": ">=8"
2199
+ }
2200
+ },
2201
+ "node_modules/shebang-regex": {
2202
+ "version": "3.0.0",
2203
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
2204
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
2205
+ "dev": true,
2206
+ "license": "MIT",
2207
+ "engines": {
2208
+ "node": ">=8"
2209
+ }
2210
+ },
2211
+ "node_modules/source-map-js": {
2212
+ "version": "1.2.1",
2213
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
2214
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
2215
+ "dev": true,
2216
+ "license": "BSD-3-Clause",
2217
+ "engines": {
2218
+ "node": ">=0.10.0"
2219
+ }
2220
+ },
2221
+ "node_modules/tinyglobby": {
2222
+ "version": "0.2.16",
2223
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
2224
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
2225
+ "dev": true,
2226
+ "license": "MIT",
2227
+ "dependencies": {
2228
+ "fdir": "^6.5.0",
2229
+ "picomatch": "^4.0.4"
2230
+ },
2231
+ "engines": {
2232
+ "node": ">=12.0.0"
2233
+ },
2234
+ "funding": {
2235
+ "url": "https://github.com/sponsors/SuperchupuDev"
2236
+ }
2237
+ },
2238
+ "node_modules/tslib": {
2239
+ "version": "2.8.1",
2240
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
2241
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
2242
+ "dev": true,
2243
+ "license": "0BSD",
2244
+ "optional": true
2245
+ },
2246
+ "node_modules/type-check": {
2247
+ "version": "0.4.0",
2248
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
2249
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
2250
+ "dev": true,
2251
+ "license": "MIT",
2252
+ "dependencies": {
2253
+ "prelude-ls": "^1.2.1"
2254
+ },
2255
+ "engines": {
2256
+ "node": ">= 0.8.0"
2257
+ }
2258
+ },
2259
+ "node_modules/update-browserslist-db": {
2260
+ "version": "1.2.3",
2261
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
2262
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
2263
+ "dev": true,
2264
+ "funding": [
2265
+ {
2266
+ "type": "opencollective",
2267
+ "url": "https://opencollective.com/browserslist"
2268
+ },
2269
+ {
2270
+ "type": "tidelift",
2271
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
2272
+ },
2273
+ {
2274
+ "type": "github",
2275
+ "url": "https://github.com/sponsors/ai"
2276
+ }
2277
+ ],
2278
+ "license": "MIT",
2279
+ "dependencies": {
2280
+ "escalade": "^3.2.0",
2281
+ "picocolors": "^1.1.1"
2282
+ },
2283
+ "bin": {
2284
+ "update-browserslist-db": "cli.js"
2285
+ },
2286
+ "peerDependencies": {
2287
+ "browserslist": ">= 4.21.0"
2288
+ }
2289
+ },
2290
+ "node_modules/uri-js": {
2291
+ "version": "4.4.1",
2292
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
2293
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
2294
+ "dev": true,
2295
+ "license": "BSD-2-Clause",
2296
+ "dependencies": {
2297
+ "punycode": "^2.1.0"
2298
+ }
2299
+ },
2300
+ "node_modules/vite": {
2301
+ "version": "8.0.10",
2302
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
2303
+ "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
2304
+ "dev": true,
2305
+ "license": "MIT",
2306
+ "dependencies": {
2307
+ "lightningcss": "^1.32.0",
2308
+ "picomatch": "^4.0.4",
2309
+ "postcss": "^8.5.10",
2310
+ "rolldown": "1.0.0-rc.17",
2311
+ "tinyglobby": "^0.2.16"
2312
+ },
2313
+ "bin": {
2314
+ "vite": "bin/vite.js"
2315
+ },
2316
+ "engines": {
2317
+ "node": "^20.19.0 || >=22.12.0"
2318
+ },
2319
+ "funding": {
2320
+ "url": "https://github.com/vitejs/vite?sponsor=1"
2321
+ },
2322
+ "optionalDependencies": {
2323
+ "fsevents": "~2.3.3"
2324
+ },
2325
+ "peerDependencies": {
2326
+ "@types/node": "^20.19.0 || >=22.12.0",
2327
+ "@vitejs/devtools": "^0.1.0",
2328
+ "esbuild": "^0.27.0 || ^0.28.0",
2329
+ "jiti": ">=1.21.0",
2330
+ "less": "^4.0.0",
2331
+ "sass": "^1.70.0",
2332
+ "sass-embedded": "^1.70.0",
2333
+ "stylus": ">=0.54.8",
2334
+ "sugarss": "^5.0.0",
2335
+ "terser": "^5.16.0",
2336
+ "tsx": "^4.8.1",
2337
+ "yaml": "^2.4.2"
2338
+ },
2339
+ "peerDependenciesMeta": {
2340
+ "@types/node": {
2341
+ "optional": true
2342
+ },
2343
+ "@vitejs/devtools": {
2344
+ "optional": true
2345
+ },
2346
+ "esbuild": {
2347
+ "optional": true
2348
+ },
2349
+ "jiti": {
2350
+ "optional": true
2351
+ },
2352
+ "less": {
2353
+ "optional": true
2354
+ },
2355
+ "sass": {
2356
+ "optional": true
2357
+ },
2358
+ "sass-embedded": {
2359
+ "optional": true
2360
+ },
2361
+ "stylus": {
2362
+ "optional": true
2363
+ },
2364
+ "sugarss": {
2365
+ "optional": true
2366
+ },
2367
+ "terser": {
2368
+ "optional": true
2369
+ },
2370
+ "tsx": {
2371
+ "optional": true
2372
+ },
2373
+ "yaml": {
2374
+ "optional": true
2375
+ }
2376
+ }
2377
+ },
2378
+ "node_modules/which": {
2379
+ "version": "2.0.2",
2380
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
2381
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
2382
+ "dev": true,
2383
+ "license": "ISC",
2384
+ "dependencies": {
2385
+ "isexe": "^2.0.0"
2386
+ },
2387
+ "bin": {
2388
+ "node-which": "bin/node-which"
2389
+ },
2390
+ "engines": {
2391
+ "node": ">= 8"
2392
+ }
2393
+ },
2394
+ "node_modules/word-wrap": {
2395
+ "version": "1.2.5",
2396
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
2397
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
2398
+ "dev": true,
2399
+ "license": "MIT",
2400
+ "engines": {
2401
+ "node": ">=0.10.0"
2402
+ }
2403
+ },
2404
+ "node_modules/yallist": {
2405
+ "version": "3.1.1",
2406
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
2407
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
2408
+ "dev": true,
2409
+ "license": "ISC"
2410
+ },
2411
+ "node_modules/yocto-queue": {
2412
+ "version": "0.1.0",
2413
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
2414
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
2415
+ "dev": true,
2416
+ "license": "MIT",
2417
+ "engines": {
2418
+ "node": ">=10"
2419
+ },
2420
+ "funding": {
2421
+ "url": "https://github.com/sponsors/sindresorhus"
2422
+ }
2423
+ },
2424
+ "node_modules/zod": {
2425
+ "version": "4.4.2",
2426
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.2.tgz",
2427
+ "integrity": "sha512-IynmDyxsEsb9RKzO3J9+4SxXnl2FTFSzNBaKKaMV6tsSk0rw9gYw9gs+JFCq/qk2LCZ78KDwyj+Z289TijSkUw==",
2428
+ "dev": true,
2429
+ "license": "MIT",
2430
+ "funding": {
2431
+ "url": "https://github.com/sponsors/colinhacks"
2432
+ }
2433
+ },
2434
+ "node_modules/zod-validation-error": {
2435
+ "version": "4.0.2",
2436
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
2437
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
2438
+ "dev": true,
2439
+ "license": "MIT",
2440
+ "engines": {
2441
+ "node": ">=18.0.0"
2442
+ },
2443
+ "peerDependencies": {
2444
+ "zod": "^3.25.0 || ^4.0.0"
2445
+ }
2446
+ }
2447
+ }
2448
+ }
frontend/package.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "lucide-react": "^1.14.0",
14
+ "prismjs": "^1.30.0",
15
+ "react": "^19.2.5",
16
+ "react-dom": "^19.2.5"
17
+ },
18
+ "devDependencies": {
19
+ "@eslint/js": "^10.0.1",
20
+ "@types/react": "^19.2.14",
21
+ "@types/react-dom": "^19.2.3",
22
+ "@vitejs/plugin-react": "^6.0.1",
23
+ "eslint": "^10.2.1",
24
+ "eslint-plugin-react-hooks": "^7.1.1",
25
+ "eslint-plugin-react-refresh": "^0.5.2",
26
+ "globals": "^17.5.0",
27
+ "vite": "^8.0.10"
28
+ }
29
+ }
frontend/public/favicon.svg ADDED
frontend/public/icons.svg ADDED
frontend/sandbox.js DELETED
@@ -1,305 +0,0 @@
1
- /* =============================================================
2
- MINDI Agent — Code Sandbox
3
- Executes code in isolated environments (iframe for HTML/JS,
4
- Pyodide for Python). Captures output, errors, and screenshots.
5
- ============================================================= */
6
-
7
- const CodeSandbox = (() => {
8
- 'use strict';
9
-
10
- // ── Pyodide loader ─────────────────────────────────────
11
- let pyodideInstance = null;
12
- let pyodideLoading = false;
13
-
14
- async function loadPyodide() {
15
- if (pyodideInstance) return pyodideInstance;
16
- if (pyodideLoading) {
17
- // Wait for existing load
18
- while (pyodideLoading) await new Promise(r => setTimeout(r, 200));
19
- return pyodideInstance;
20
- }
21
- pyodideLoading = true;
22
- try {
23
- // Load Pyodide from CDN
24
- if (!window.loadPyodide) {
25
- const script = document.createElement('script');
26
- script.src = 'https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js';
27
- document.head.appendChild(script);
28
- await new Promise((res, rej) => { script.onload = res; script.onerror = rej; });
29
- }
30
- pyodideInstance = await window.loadPyodide();
31
- console.log('[Sandbox] Pyodide loaded');
32
- return pyodideInstance;
33
- } finally {
34
- pyodideLoading = false;
35
- }
36
- }
37
-
38
- // ── HTML/JS execution in sandboxed iframe ──────────────
39
- function executeHTML(code, containerEl) {
40
- return new Promise((resolve) => {
41
- const logs = [];
42
- const errors = [];
43
- const startTime = Date.now();
44
-
45
- // Create sandboxed iframe
46
- let iframe = containerEl.querySelector('.sandbox-iframe');
47
- if (iframe) iframe.remove();
48
-
49
- iframe = document.createElement('iframe');
50
- iframe.className = 'sandbox-iframe';
51
- iframe.sandbox = 'allow-scripts allow-modals';
52
- iframe.style.cssText = 'width:100%;height:100%;border:none;background:#fff;border-radius:8px;';
53
- containerEl.appendChild(iframe);
54
-
55
- // Inject console capture + error handling into the code
56
- const wrappedCode = `
57
- <!DOCTYPE html>
58
- <html>
59
- <head>
60
- <meta charset="UTF-8">
61
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
62
- <script>
63
- // Override console to send messages to parent
64
- const _origLog = console.log;
65
- const _origErr = console.error;
66
- const _origWarn = console.warn;
67
-
68
- function _send(type, args) {
69
- try {
70
- parent.postMessage({
71
- type: 'sandbox-' + type,
72
- data: Array.from(args).map(a => {
73
- try { return typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a); }
74
- catch { return String(a); }
75
- }).join(' ')
76
- }, '*');
77
- } catch {}
78
- }
79
-
80
- console.log = function() { _origLog.apply(console, arguments); _send('log', arguments); };
81
- console.error = function() { _origErr.apply(console, arguments); _send('error', arguments); };
82
- console.warn = function() { _origWarn.apply(console, arguments); _send('warn', arguments); };
83
-
84
- window.onerror = function(msg, src, line, col, err) {
85
- _send('error', [msg + ' (line ' + line + ')']);
86
- return true;
87
- };
88
- window.addEventListener('unhandledrejection', function(e) {
89
- _send('error', ['Unhandled promise rejection: ' + e.reason]);
90
- });
91
-
92
- // Signal ready after load
93
- window.addEventListener('load', function() {
94
- _send('ready', ['Page loaded in ' + (performance.now()|0) + 'ms']);
95
- });
96
- </script>
97
- </head>
98
- <body>
99
- ${code.includes('<body') ? code.replace(/.*<body[^>]*>/is, '').replace(/<\/body>.*/is, '') : (code.includes('<html') ? '' : code)}
100
- </body>
101
- </html>`;
102
-
103
- // If code is a full HTML document, use it directly with console injection
104
- const finalCode = code.includes('<!DOCTYPE') || code.includes('<html')
105
- ? code.replace('<head>', `<head>
106
- <script>
107
- const _origLog = console.log;
108
- const _origErr = console.error;
109
- function _send(t, a) { try { parent.postMessage({type:'sandbox-'+t,data:Array.from(a).map(x=>{try{return typeof x==='object'?JSON.stringify(x,null,2):String(x)}catch{return String(x)}}).join(' ')},'*'); } catch{} }
110
- console.log = function() { _origLog.apply(console,arguments); _send('log',arguments); };
111
- console.error = function() { _origErr.apply(console,arguments); _send('error',arguments); };
112
- window.onerror = function(m,s,l) { _send('error',[m+' (line '+l+')']); return true; };
113
- window.addEventListener('load', function() { _send('ready', ['loaded']); });
114
- </script>`)
115
- : wrappedCode;
116
-
117
- // Listen for messages from iframe
118
- const handler = (event) => {
119
- if (!event.data || !event.data.type) return;
120
- const { type, data } = event.data;
121
- if (type === 'sandbox-log') logs.push(data);
122
- else if (type === 'sandbox-error') errors.push(data);
123
- else if (type === 'sandbox-warn') logs.push(`[warn] ${data}`);
124
- else if (type === 'sandbox-ready') {
125
- // Give a moment for rendering, then resolve
126
- setTimeout(() => {
127
- window.removeEventListener('message', handler);
128
- resolve({
129
- success: errors.length === 0,
130
- logs,
131
- errors,
132
- duration: Date.now() - startTime,
133
- iframe,
134
- });
135
- }, 500);
136
- }
137
- };
138
- window.addEventListener('message', handler);
139
-
140
- // Set content
141
- iframe.srcdoc = finalCode;
142
-
143
- // Timeout fallback (10 seconds)
144
- setTimeout(() => {
145
- window.removeEventListener('message', handler);
146
- resolve({
147
- success: errors.length === 0,
148
- logs,
149
- errors: errors.length ? errors : ['Timeout: page did not signal ready within 10s'],
150
- duration: Date.now() - startTime,
151
- iframe,
152
- });
153
- }, 10000);
154
- });
155
- }
156
-
157
- // ── JavaScript execution ───────────────────────────────
158
- function executeJS(code) {
159
- return new Promise((resolve) => {
160
- const logs = [];
161
- const errors = [];
162
- const startTime = Date.now();
163
-
164
- // Create a sandboxed execution context
165
- const origLog = console.log;
166
- const origErr = console.error;
167
-
168
- console.log = (...args) => {
169
- logs.push(args.map(a => typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a)).join(' '));
170
- };
171
- console.error = (...args) => {
172
- errors.push(args.map(String).join(' '));
173
- };
174
-
175
- try {
176
- // Execute in indirect eval (global scope)
177
- const result = (0, eval)(code);
178
- if (result !== undefined) logs.push(String(result));
179
- } catch (e) {
180
- errors.push(`${e.name}: ${e.message}`);
181
- } finally {
182
- console.log = origLog;
183
- console.error = origErr;
184
- }
185
-
186
- resolve({
187
- success: errors.length === 0,
188
- logs,
189
- errors,
190
- duration: Date.now() - startTime,
191
- });
192
- });
193
- }
194
-
195
- // ── Python execution via Pyodide ───────────────────────
196
- async function executePython(code) {
197
- const logs = [];
198
- const errors = [];
199
- const startTime = Date.now();
200
-
201
- try {
202
- const pyodide = await loadPyodide();
203
-
204
- // Redirect stdout/stderr
205
- pyodide.runPython(`
206
- import sys, io
207
- sys.stdout = io.StringIO()
208
- sys.stderr = io.StringIO()
209
- `);
210
-
211
- try {
212
- await pyodide.runPythonAsync(code);
213
- } catch (e) {
214
- errors.push(String(e));
215
- }
216
-
217
- // Capture output
218
- const stdout = pyodide.runPython('sys.stdout.getvalue()');
219
- const stderr = pyodide.runPython('sys.stderr.getvalue()');
220
- if (stdout) logs.push(stdout);
221
- if (stderr) errors.push(stderr);
222
-
223
- // Reset streams
224
- pyodide.runPython(`
225
- sys.stdout = sys.__stdout__
226
- sys.stderr = sys.__stderr__
227
- `);
228
- } catch (e) {
229
- errors.push(`Pyodide error: ${e.message}`);
230
- }
231
-
232
- return {
233
- success: errors.length === 0,
234
- logs,
235
- errors,
236
- duration: Date.now() - startTime,
237
- };
238
- }
239
-
240
- // ── Screenshot capture ─────────────────────────────────
241
- async function captureScreenshot(iframe) {
242
- try {
243
- if (!window.html2canvas) {
244
- const script = document.createElement('script');
245
- script.src = 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js';
246
- document.head.appendChild(script);
247
- await new Promise((res, rej) => { script.onload = res; script.onerror = rej; });
248
- }
249
- const canvas = await html2canvas(iframe, { useCORS: true, scale: 1 });
250
- return canvas.toDataURL('image/png');
251
- } catch (e) {
252
- console.warn('[Sandbox] Screenshot failed:', e);
253
- return null;
254
- }
255
- }
256
-
257
- // ── Language detection ─────────────────────────────────
258
- function detectLanguage(code) {
259
- const t = code.trim();
260
- if (/^<!doctype|^<html|^<div|^<section|^<main/i.test(t)) return 'html';
261
- if (/^<style|^[.#]?\w+\s*\{/.test(t)) return 'css';
262
- if (/^(import|from|def |class |print\(|if __name__)/m.test(t)) return 'python';
263
- if (/^(import |export |const |function |class |let |var |=>)/m.test(t)) return 'javascript';
264
- if (/^(package |func |type |import ")/m.test(t)) return 'go';
265
- if (/^(use |fn |let |struct |impl |pub )/m.test(t)) return 'rust';
266
- return 'javascript'; // default
267
- }
268
-
269
- // ── Main execute function ──────────────────────────────
270
- async function execute(code, language = null, containerEl = null) {
271
- const lang = language || detectLanguage(code);
272
-
273
- switch (lang) {
274
- case 'html':
275
- case 'markup':
276
- case 'css':
277
- if (!containerEl) {
278
- return { success: false, errors: ['No container for HTML preview'], logs: [], duration: 0 };
279
- }
280
- return executeHTML(code, containerEl);
281
-
282
- case 'python':
283
- return executePython(code);
284
-
285
- case 'javascript':
286
- case 'typescript':
287
- case 'js':
288
- case 'ts':
289
- return executeJS(code);
290
-
291
- default:
292
- return {
293
- success: false,
294
- logs: [],
295
- errors: [`Language "${lang}" execution not supported in browser. Supported: HTML, JavaScript, Python.`],
296
- duration: 0,
297
- };
298
- }
299
- }
300
-
301
- return { execute, executeHTML, executeJS, executePython, detectLanguage, captureScreenshot };
302
- })();
303
-
304
- // Export for module usage
305
- if (typeof module !== 'undefined') module.exports = CodeSandbox;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/App.css ADDED
@@ -0,0 +1,476 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============ APP SHELL ============ */
2
+ .app-shell {
3
+ display: grid;
4
+ grid-template-columns: var(--sidebar-w) 1fr 420px;
5
+ height: 100vh;
6
+ position: relative;
7
+ z-index: 1;
8
+ }
9
+
10
+ /* Ambient background */
11
+ .ambient { position: fixed; inset: 0; pointer-events: none; z-index: 0; overflow: hidden; }
12
+ .grid-pattern {
13
+ position: absolute; inset: 0;
14
+ background-image: linear-gradient(rgba(255,255,255,.02) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.02) 1px, transparent 1px);
15
+ background-size: 64px 64px;
16
+ mask-image: radial-gradient(ellipse at center, #000 0%, transparent 80%);
17
+ }
18
+ .blob { position: absolute; width: 500px; height: 500px; border-radius: 50%; filter: blur(120px); opacity: .25; }
19
+ .blob--purple { background: radial-gradient(circle, var(--purple), transparent 70%); top: -150px; left: -100px; animation: drift-1 26s ease-in-out infinite alternate; }
20
+ .blob--blue { background: radial-gradient(circle, var(--blue), transparent 70%); bottom: -150px; right: -100px; animation: drift-2 30s ease-in-out infinite alternate; }
21
+
22
+ /* ============ MAIN AREA ============ */
23
+ .main-area {
24
+ display: flex;
25
+ flex-direction: column;
26
+ height: 100vh;
27
+ min-width: 0;
28
+ position: relative;
29
+ z-index: 1;
30
+ }
31
+
32
+ /* ============ SIDEBAR ============ */
33
+ .sidebar {
34
+ display: flex;
35
+ flex-direction: column;
36
+ height: 100vh;
37
+ background: rgba(17, 17, 32, .85);
38
+ border-right: 1px solid var(--border);
39
+ backdrop-filter: blur(20px);
40
+ z-index: 10;
41
+ position: relative;
42
+ }
43
+ .sidebar-brand {
44
+ padding: 14px 16px;
45
+ display: flex;
46
+ align-items: center;
47
+ gap: 10px;
48
+ border-bottom: 1px solid var(--border);
49
+ cursor: pointer;
50
+ transition: background .2s;
51
+ }
52
+ .sidebar-brand:hover { background: var(--hover); }
53
+ .brand-icon {
54
+ width: 32px; height: 32px;
55
+ background: var(--grad);
56
+ border-radius: var(--r-sm);
57
+ display: grid; place-items: center;
58
+ box-shadow: 0 0 20px rgba(124, 58, 237, .3);
59
+ flex-shrink: 0;
60
+ }
61
+ .brand-icon svg { width: 20px; height: 20px; }
62
+ .brand-info { min-width: 0; }
63
+ .brand-name { font-size: 13px; font-weight: 700; letter-spacing: -.01em; }
64
+ .brand-sub { font-size: 9px; font-weight: 500; color: var(--text-mute); font-family: var(--mono); letter-spacing: .04em; }
65
+
66
+ /* File tree */
67
+ .sidebar-section { padding: 12px 0; }
68
+ .sidebar-section-title {
69
+ padding: 0 16px 8px;
70
+ font-size: 10px;
71
+ font-weight: 600;
72
+ text-transform: uppercase;
73
+ letter-spacing: .12em;
74
+ color: var(--text-dim);
75
+ }
76
+ .file-tree { flex: 1; overflow-y: auto; padding: 0 8px; }
77
+ .file-item {
78
+ display: flex;
79
+ align-items: center;
80
+ gap: 8px;
81
+ padding: 6px 10px;
82
+ border-radius: var(--r-sm);
83
+ font-size: 12px;
84
+ color: var(--text-2);
85
+ cursor: pointer;
86
+ transition: background .15s, color .15s;
87
+ animation: fadeInLeft .3s var(--ease) both;
88
+ width: 100%;
89
+ text-align: left;
90
+ }
91
+ .file-item:hover { background: var(--hover); color: var(--text); }
92
+ .file-item.active { background: rgba(124,58,237,.12); color: #fff; border: 1px solid rgba(124,58,237,.2); }
93
+ .file-icon { font-size: 14px; flex-shrink: 0; }
94
+ .file-name { font-family: var(--mono); font-size: 11.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
95
+
96
+ /* Agent steps */
97
+ .agent-steps { padding: 4px 8px; display: flex; flex-direction: column; gap: 4px; max-height: 280px; overflow-y: auto; }
98
+ .agent-step {
99
+ display: flex; align-items: center; gap: 8px;
100
+ padding: 7px 10px; border-radius: var(--r-sm);
101
+ font-size: 11px; color: var(--text-2);
102
+ animation: fadeIn .25s var(--ease) both;
103
+ background: rgba(255,255,255,.02);
104
+ }
105
+ .step-icon { width: 18px; height: 18px; display: grid; place-items: center; border-radius: 50%; font-size: 10px; flex-shrink: 0; }
106
+ .step-icon.running { background: rgba(124,58,237,.2); color: var(--purple-light); animation: pulse 1.5s ease infinite; }
107
+ .step-icon.success { background: rgba(16,185,129,.15); color: var(--ok); }
108
+ .step-icon.failed { background: rgba(239,68,68,.15); color: var(--err); }
109
+ .step-detail { font-family: var(--mono); font-size: 10px; color: var(--text-mute); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
110
+
111
+ /* Status */
112
+ .sidebar-footer {
113
+ padding: 10px 14px;
114
+ border-top: 1px solid var(--border);
115
+ display: flex; align-items: center; gap: 8px;
116
+ font-size: 10px; color: var(--text-mute); font-family: var(--mono);
117
+ }
118
+ .status-dot {
119
+ width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
120
+ }
121
+ .status-dot.online { background: var(--ok); box-shadow: 0 0 8px var(--ok); }
122
+ .status-dot.demo { background: var(--warn); box-shadow: 0 0 8px var(--warn); }
123
+ .status-dot.connecting { background: var(--text-dim); }
124
+
125
+ /* ============ EDITOR ============ */
126
+ .editor {
127
+ flex: 1;
128
+ display: flex;
129
+ flex-direction: column;
130
+ min-height: 0;
131
+ background: rgba(10, 10, 18, .6);
132
+ }
133
+ .editor-tabs {
134
+ display: flex;
135
+ align-items: center;
136
+ height: 38px;
137
+ border-bottom: 1px solid var(--border);
138
+ padding: 0 8px;
139
+ gap: 2px;
140
+ overflow-x: auto;
141
+ flex-shrink: 0;
142
+ background: rgba(17, 17, 32, .5);
143
+ }
144
+ .editor-tab {
145
+ display: flex; align-items: center; gap: 6px;
146
+ padding: 0 14px; height: 100%;
147
+ font-size: 11.5px; font-family: var(--mono);
148
+ color: var(--text-mute);
149
+ border-bottom: 2px solid transparent;
150
+ white-space: nowrap;
151
+ transition: color .15s, border-color .15s;
152
+ cursor: pointer;
153
+ }
154
+ .editor-tab:hover { color: var(--text-2); }
155
+ .editor-tab.active { color: var(--text); border-bottom-color: var(--purple); }
156
+ .editor-tab .tab-icon { font-size: 12px; }
157
+
158
+ .editor-content {
159
+ flex: 1;
160
+ overflow: auto;
161
+ padding: 16px 0;
162
+ font-family: var(--mono);
163
+ font-size: 13px;
164
+ line-height: 1.7;
165
+ counter-reset: line;
166
+ }
167
+
168
+ .code-line {
169
+ display: flex;
170
+ padding: 0 16px;
171
+ min-height: 22px;
172
+ animation: line-appear .2s var(--ease) both;
173
+ }
174
+ .code-line:hover { background: rgba(255,255,255,.02); }
175
+ .line-number {
176
+ width: 48px;
177
+ text-align: right;
178
+ padding-right: 16px;
179
+ color: var(--text-dim);
180
+ font-size: 12px;
181
+ user-select: none;
182
+ flex-shrink: 0;
183
+ }
184
+ .line-content {
185
+ flex: 1;
186
+ white-space: pre;
187
+ overflow-x: auto;
188
+ color: var(--text-2);
189
+ }
190
+
191
+ /* Welcome state */
192
+ .editor-welcome {
193
+ flex: 1;
194
+ display: flex;
195
+ flex-direction: column;
196
+ align-items: center;
197
+ justify-content: center;
198
+ text-align: center;
199
+ padding: 40px;
200
+ gap: 16px;
201
+ }
202
+ .welcome-logo {
203
+ width: 80px; height: 80px;
204
+ background: var(--grad);
205
+ border-radius: 20px;
206
+ display: grid; place-items: center;
207
+ box-shadow: 0 16px 48px rgba(124,58,237,.3);
208
+ animation: float 5s ease-in-out infinite;
209
+ }
210
+ .welcome-logo svg { width: 44px; height: 44px; color: #fff; }
211
+ .welcome-title { font-size: 28px; font-weight: 700; letter-spacing: -.02em; }
212
+ .welcome-sub { font-size: 14px; color: var(--text-2); max-width: 420px; line-height: 1.6; }
213
+
214
+ /* Generating overlay */
215
+ .generating-indicator {
216
+ display: flex; align-items: center; gap: 8px;
217
+ padding: 8px 16px;
218
+ background: rgba(124,58,237,.1);
219
+ border-bottom: 1px solid rgba(124,58,237,.2);
220
+ font-size: 12px; color: var(--purple-light);
221
+ font-family: var(--mono);
222
+ }
223
+ .gen-spinner { width: 14px; height: 14px; border: 2px solid rgba(124,58,237,.3); border-top-color: var(--purple); border-radius: 50%; animation: spin .8s linear infinite; }
224
+
225
+ /* ============ PREVIEW ============ */
226
+ .preview-panel {
227
+ display: flex;
228
+ flex-direction: column;
229
+ height: 100vh;
230
+ background: rgba(14, 14, 22, .9);
231
+ border-left: 1px solid var(--border);
232
+ backdrop-filter: blur(20px);
233
+ z-index: 5;
234
+ position: relative;
235
+ }
236
+ .preview-header {
237
+ height: 38px;
238
+ display: flex; align-items: center; justify-content: space-between;
239
+ padding: 0 14px;
240
+ border-bottom: 1px solid var(--border);
241
+ flex-shrink: 0;
242
+ }
243
+ .preview-title {
244
+ font-size: 11px; font-weight: 600; text-transform: uppercase;
245
+ letter-spacing: .1em; color: var(--text-mute);
246
+ display: flex; align-items: center; gap: 8px;
247
+ }
248
+ .preview-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 8px var(--ok); }
249
+ .preview-actions { display: flex; gap: 4px; }
250
+ .preview-btn {
251
+ width: 28px; height: 28px;
252
+ display: grid; place-items: center;
253
+ border-radius: var(--r-xs);
254
+ color: var(--text-mute);
255
+ transition: background .15s, color .15s;
256
+ }
257
+ .preview-btn:hover { background: var(--hover); color: var(--text); }
258
+ .preview-btn svg { width: 14px; height: 14px; }
259
+
260
+ .preview-frame-wrap { flex: 1; position: relative; background: #fff; }
261
+ .preview-frame { width: 100%; height: 100%; border: 0; background: #fff; }
262
+ .preview-empty {
263
+ position: absolute; inset: 0;
264
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
265
+ background: var(--bg-1); gap: 12px; text-align: center;
266
+ }
267
+ .preview-empty-icon {
268
+ width: 48px; height: 48px; border-radius: var(--r-md);
269
+ background: var(--grad-soft);
270
+ border: 1px solid rgba(124,58,237,.2);
271
+ display: grid; place-items: center;
272
+ font-size: 20px; color: var(--purple-light);
273
+ }
274
+ .preview-empty p { font-size: 12px; color: var(--text-mute); }
275
+ .preview-empty p:last-child { font-size: 11px; color: var(--text-dim); max-width: 200px; }
276
+
277
+ /* Console */
278
+ .console-panel {
279
+ height: 120px;
280
+ border-top: 1px solid var(--border);
281
+ display: flex; flex-direction: column;
282
+ flex-shrink: 0;
283
+ }
284
+ .console-header {
285
+ padding: 6px 14px;
286
+ font-size: 10px; font-weight: 600; text-transform: uppercase;
287
+ letter-spacing: .1em; color: var(--text-dim);
288
+ background: rgba(255,255,255,.02);
289
+ border-bottom: 1px solid var(--border);
290
+ }
291
+ .console-body {
292
+ flex: 1; overflow-y: auto; padding: 8px 14px;
293
+ font-family: var(--mono); font-size: 11px; line-height: 1.6;
294
+ color: var(--ok);
295
+ }
296
+ .console-body .error { color: var(--err); }
297
+
298
+ /* ============ PROMPT BAR ============ */
299
+ .prompt-bar {
300
+ padding: 12px 16px 16px;
301
+ flex-shrink: 0;
302
+ border-top: 1px solid var(--border);
303
+ background: rgba(17,17,32,.7);
304
+ backdrop-filter: blur(16px);
305
+ }
306
+ .prompt-input-wrap {
307
+ display: flex; align-items: flex-end; gap: 8px;
308
+ background: rgba(20,20,36,.9);
309
+ border: 1px solid var(--border-2);
310
+ border-radius: var(--r-lg);
311
+ padding: 10px 14px;
312
+ transition: border-color .2s, box-shadow .2s;
313
+ }
314
+ .prompt-input-wrap:focus-within {
315
+ border-color: rgba(124,58,237,.5);
316
+ box-shadow: 0 0 0 3px rgba(124,58,237,.1), 0 20px 50px -20px rgba(124,58,237,.4);
317
+ }
318
+ .prompt-input {
319
+ flex: 1; font-size: 13.5px; line-height: 1.5;
320
+ color: var(--text); background: transparent;
321
+ resize: none; max-height: 120px; min-height: 20px;
322
+ font-family: var(--sans);
323
+ }
324
+ .prompt-input::placeholder { color: var(--text-mute); }
325
+ .prompt-send {
326
+ width: 34px; height: 34px;
327
+ background: var(--grad);
328
+ border-radius: var(--r-sm);
329
+ display: grid; place-items: center;
330
+ color: #fff; flex-shrink: 0;
331
+ transition: filter .15s, transform .1s, box-shadow .2s;
332
+ box-shadow: 0 4px 14px -4px rgba(124,58,237,.4);
333
+ }
334
+ .prompt-send:hover:not(:disabled) { filter: brightness(1.1); transform: translateY(-1px); }
335
+ .prompt-send:disabled { opacity: .3; cursor: not-allowed; }
336
+ .prompt-send svg { width: 16px; height: 16px; }
337
+ .prompt-stop {
338
+ width: 34px; height: 34px;
339
+ background: var(--err);
340
+ border-radius: var(--r-sm);
341
+ display: grid; place-items: center;
342
+ color: #fff; flex-shrink: 0;
343
+ animation: pulse 1.5s ease infinite;
344
+ }
345
+ .prompt-stop svg { width: 14px; height: 14px; }
346
+ .prompt-footer {
347
+ display: flex; align-items: center; justify-content: space-between;
348
+ padding: 8px 4px 0;
349
+ font-size: 10px; color: var(--text-dim); font-family: var(--mono);
350
+ }
351
+
352
+ /* ============ PLAN MODAL ============ */
353
+ .modal-overlay {
354
+ position: fixed; inset: 0;
355
+ background: rgba(0,0,0,.6);
356
+ backdrop-filter: blur(8px);
357
+ z-index: 100;
358
+ display: grid; place-items: center;
359
+ animation: fadeIn .2s var(--ease);
360
+ }
361
+ .modal-card {
362
+ background: var(--panel);
363
+ border: 1px solid var(--border-2);
364
+ border-radius: var(--r-xl);
365
+ width: min(480px, 90%);
366
+ box-shadow: 0 30px 80px rgba(0,0,0,.5);
367
+ animation: pop-in .25s var(--ease);
368
+ overflow: hidden;
369
+ }
370
+ .modal-header {
371
+ display: flex; align-items: center; justify-content: space-between;
372
+ padding: 16px 20px;
373
+ border-bottom: 1px solid var(--border);
374
+ }
375
+ .modal-header h3 { font-size: 15px; font-weight: 600; }
376
+ .modal-close {
377
+ width: 32px; height: 32px; display: grid; place-items: center;
378
+ border-radius: var(--r-sm); color: var(--text-mute);
379
+ transition: background .15s;
380
+ }
381
+ .modal-close:hover { background: var(--hover); }
382
+ .modal-body { padding: 20px; display: flex; flex-direction: column; gap: 18px; }
383
+ .modal-field label { display: block; font-size: 12px; font-weight: 500; color: var(--text-2); margin-bottom: 8px; }
384
+ .modal-options { display: flex; flex-wrap: wrap; gap: 6px; }
385
+ .modal-option {
386
+ padding: 7px 14px; border-radius: var(--r-md);
387
+ font-size: 12px; font-weight: 500;
388
+ background: rgba(255,255,255,.04);
389
+ border: 1px solid var(--border-2);
390
+ color: var(--text-2);
391
+ transition: all .15s;
392
+ }
393
+ .modal-option:hover { background: rgba(255,255,255,.08); color: var(--text); }
394
+ .modal-option.selected { background: rgba(124,58,237,.15); border-color: rgba(124,58,237,.4); color: #fff; }
395
+ .modal-footer {
396
+ padding: 14px 20px;
397
+ border-top: 1px solid var(--border);
398
+ display: flex; justify-content: flex-end; gap: 8px;
399
+ }
400
+ .modal-btn {
401
+ padding: 8px 18px; border-radius: var(--r-md);
402
+ font-size: 12.5px; font-weight: 500;
403
+ transition: all .15s;
404
+ }
405
+ .modal-btn.ghost { color: var(--text-mute); border: 1px solid var(--border-2); }
406
+ .modal-btn.ghost:hover { background: var(--hover); }
407
+ .modal-btn.primary {
408
+ background: var(--grad); color: #fff;
409
+ box-shadow: 0 6px 20px -6px rgba(124,58,237,.4);
410
+ }
411
+ .modal-btn.primary:hover { filter: brightness(1.1); }
412
+
413
+ /* Settings modal fields */
414
+ .settings-field { display: flex; flex-direction: column; gap: 6px; }
415
+ .settings-field label { font-size: 12px; font-weight: 500; color: var(--text-2); }
416
+ .settings-input {
417
+ padding: 9px 12px;
418
+ border-radius: var(--r-sm);
419
+ border: 1px solid var(--border-2);
420
+ background: var(--bg-1);
421
+ color: var(--text);
422
+ font-family: var(--mono);
423
+ font-size: 12px;
424
+ transition: border-color .2s, box-shadow .2s;
425
+ }
426
+ .settings-input:focus {
427
+ border-color: rgba(124,58,237,.5);
428
+ box-shadow: 0 0 0 3px rgba(124,58,237,.1);
429
+ }
430
+ .settings-hint { font-size: 10.5px; color: var(--text-dim); line-height: 1.5; }
431
+ .settings-range-wrap { display: flex; align-items: center; gap: 12px; }
432
+ .settings-range {
433
+ flex: 1; -webkit-appearance: none; appearance: none;
434
+ height: 4px; background: var(--bg-1); border-radius: 2px; border: 1px solid var(--border);
435
+ }
436
+ .settings-range::-webkit-slider-thumb {
437
+ -webkit-appearance: none; width: 14px; height: 14px;
438
+ border-radius: 50%; background: var(--grad);
439
+ border: 2px solid #fff; cursor: pointer;
440
+ box-shadow: 0 0 0 2px rgba(124,58,237,.2);
441
+ }
442
+ .settings-range-val {
443
+ font-family: var(--mono); font-size: 12px;
444
+ color: var(--purple-light); min-width: 42px; text-align: right;
445
+ }
446
+
447
+ /* ============ TOASTS ============ */
448
+ .toasts {
449
+ position: fixed; bottom: 20px; right: 20px;
450
+ display: flex; flex-direction: column; gap: 8px;
451
+ z-index: 200; pointer-events: none;
452
+ }
453
+ .toast {
454
+ pointer-events: auto;
455
+ display: flex; align-items: center; gap: 10px;
456
+ padding: 10px 16px;
457
+ border-radius: var(--r-md);
458
+ background: var(--panel-2);
459
+ border: 1px solid var(--border-2);
460
+ box-shadow: 0 16px 32px rgba(0,0,0,.4);
461
+ font-size: 12.5px; color: var(--text);
462
+ animation: toast-in .3s var(--ease);
463
+ min-width: 220px; max-width: 340px;
464
+ }
465
+ .toast-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
466
+ .toast-dot.success { background: var(--ok); box-shadow: 0 0 8px var(--ok); }
467
+ .toast-dot.error { background: var(--err); box-shadow: 0 0 8px var(--err); }
468
+ .toast-dot.info { background: var(--info); box-shadow: 0 0 8px var(--info); }
469
+
470
+ /* ============ RESPONSIVE ============ */
471
+ @media (max-width: 1200px) { .app-shell { grid-template-columns: var(--sidebar-w) 1fr 360px; } }
472
+ @media (max-width: 1024px) {
473
+ .app-shell { grid-template-columns: 1fr; }
474
+ .sidebar { display: none; }
475
+ .preview-panel { display: none; }
476
+ }
frontend/src/App.jsx ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback, useRef, useEffect } from 'react';
2
+ import Sidebar from './components/Sidebar';
3
+ import Editor from './components/Editor';
4
+ import Preview from './components/Preview';
5
+ import PromptBar from './components/PromptBar';
6
+ import PlanModal from './components/PlanModal';
7
+ import SettingsModal from './components/SettingsModal';
8
+ import Toasts from './components/Toasts';
9
+ import { callMINDI, generateDemo, isQuotaError, isQuotaException, pingAPI } from './services/api';
10
+ import { analyzePrompt, enhancePrompt, getQuickEnhancement } from './services/promptEnhancer';
11
+ import { parseFiles, buildPreviewHTML } from './services/fileParser';
12
+ import './App.css';
13
+
14
+ const STORAGE_KEY = 'mindi.builder.v1';
15
+
16
+ function loadSettings() {
17
+ try {
18
+ return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {};
19
+ } catch { return {}; }
20
+ }
21
+
22
+ export default function App() {
23
+ const saved = loadSettings();
24
+ const [settings, setSettings] = useState({
25
+ apiUrl: saved.apiUrl || 'https://mindigenous-mindi-chat.hf.space',
26
+ hfToken: saved.hfToken || '',
27
+ temperature: saved.temperature ?? 0.7,
28
+ maxTokens: saved.maxTokens ?? 2048,
29
+ });
30
+ const [settingsOpen, setSettingsOpen] = useState(false);
31
+ const [planModal, setPlanModal] = useState(null);
32
+ const [toasts, setToasts] = useState([]);
33
+ const [status, setStatus] = useState('connecting');
34
+ const [files, setFiles] = useState([]);
35
+ const [activeFile, setActiveFile] = useState(null);
36
+ const [previewHTML, setPreviewHTML] = useState(null);
37
+ const [isGenerating, setIsGenerating] = useState(false);
38
+ const [generationProgress, setGenerationProgress] = useState('');
39
+ const [agentSteps, setAgentSteps] = useState([]);
40
+ const [codeLines, setCodeLines] = useState([]);
41
+ const [consoleOutput, setConsoleOutput] = useState([]);
42
+ const [history, setHistory] = useState([]);
43
+ const abortRef = useRef(null);
44
+
45
+ const addToast = useCallback((msg, type = 'info', ms = 3000) => {
46
+ const id = Date.now() + Math.random();
47
+ setToasts(t => [...t, { id, msg, type }]);
48
+ setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), ms);
49
+ }, []);
50
+
51
+ const saveSettings = useCallback((s) => {
52
+ setSettings(s);
53
+ try { localStorage.setItem(STORAGE_KEY, JSON.stringify(s)); } catch {}
54
+ }, []);
55
+
56
+ // Health check
57
+ useEffect(() => {
58
+ const check = async () => {
59
+ const ok = await pingAPI(settings.apiUrl, settings.hfToken);
60
+ setStatus(ok ? 'online' : 'demo');
61
+ };
62
+ check();
63
+ const iv = setInterval(check, 60000);
64
+ return () => clearInterval(iv);
65
+ }, [settings.apiUrl, settings.hfToken]);
66
+
67
+ const addAgentStep = useCallback((type, detail, status = 'running') => {
68
+ const step = { id: Date.now(), type, detail, status, time: new Date() };
69
+ setAgentSteps(prev => [...prev, step]);
70
+ return step.id;
71
+ }, []);
72
+
73
+ const updateAgentStep = useCallback((id, updates) => {
74
+ setAgentSteps(prev => prev.map(s => s.id === id ? { ...s, ...updates } : s));
75
+ }, []);
76
+
77
+ // Animate code appearing line by line
78
+ const animateCode = useCallback((code, fileList) => {
79
+ const lines = code.split('\n');
80
+ setCodeLines([]);
81
+ let i = 0;
82
+ const interval = setInterval(() => {
83
+ if (i < lines.length) {
84
+ setCodeLines(prev => [...prev, { text: lines[i], id: i, visible: true }]);
85
+ i++;
86
+ } else {
87
+ clearInterval(interval);
88
+ }
89
+ }, 15); // ~15ms per line for smooth animation
90
+ return () => clearInterval(interval);
91
+ }, []);
92
+
93
+ // Main generate function
94
+ const handleGenerate = useCallback(async (userPrompt, skipPlan = false) => {
95
+ if (!userPrompt.trim() || isGenerating) return;
96
+
97
+ // Analyze prompt for planning
98
+ if (!skipPlan) {
99
+ const analysis = analyzePrompt(userPrompt);
100
+ if (analysis.questions.length > 0) {
101
+ setPlanModal({ userPrompt, questions: analysis.questions });
102
+ return;
103
+ }
104
+ }
105
+
106
+ setIsGenerating(true);
107
+ setAgentSteps([]);
108
+ setConsoleOutput([]);
109
+ setCodeLines([]);
110
+ setFiles([]);
111
+ setPreviewHTML(null);
112
+ abortRef.current = new AbortController();
113
+
114
+ // Step 1: Plan
115
+ const planId = addAgentStep('plan', 'Analyzing your request...');
116
+ setGenerationProgress('Planning...');
117
+ await new Promise(r => setTimeout(r, 400));
118
+ updateAgentStep(planId, { status: 'success', detail: 'Requirements analyzed' });
119
+
120
+ // Step 2: Enhance prompt
121
+ const enhanceId = addAgentStep('enhance', 'Enhancing prompt for best output...');
122
+ const enhanced = getQuickEnhancement(userPrompt);
123
+ await new Promise(r => setTimeout(r, 300));
124
+ updateAgentStep(enhanceId, { status: 'success', detail: 'Prompt optimized' });
125
+
126
+ // Step 3: Generate
127
+ const genId = addAgentStep('generate', 'Generating code with MINDI 1.5...');
128
+ setGenerationProgress('Generating code...');
129
+
130
+ let result;
131
+ try {
132
+ if (status === 'demo' || !settings.apiUrl) {
133
+ result = await generateDemo(userPrompt);
134
+ } else {
135
+ result = await callMINDI({
136
+ prompt: enhanced,
137
+ temperature: settings.temperature,
138
+ maxTokens: settings.maxTokens,
139
+ history,
140
+ hfToken: settings.hfToken,
141
+ apiUrl: settings.apiUrl,
142
+ signal: abortRef.current.signal,
143
+ });
144
+ }
145
+
146
+ if (isQuotaError(result)) {
147
+ updateAgentStep(genId, { status: 'failed', detail: 'GPU quota — using demo fallback' });
148
+ addToast('GPU quota exceeded — showing demo. Add HF token in Settings for real generation.', 'error', 5000);
149
+ result = await generateDemo(userPrompt);
150
+ }
151
+
152
+ updateAgentStep(genId, { status: 'success', detail: `Response received (${(result.response || '').length} chars)` });
153
+
154
+ // Step 4: Parse files
155
+ const parseId = addAgentStep('parse', 'Extracting files...');
156
+ const parsedFiles = parseFiles(result.response);
157
+ setFiles(parsedFiles);
158
+ if (parsedFiles.length > 0) {
159
+ setActiveFile(parsedFiles[0].id);
160
+ // Animate the code
161
+ animateCode(parsedFiles[0].content, parsedFiles);
162
+ }
163
+ updateAgentStep(parseId, { status: 'success', detail: `${parsedFiles.length} file(s) extracted` });
164
+
165
+ // Step 5: Preview
166
+ const previewId = addAgentStep('preview', 'Rendering preview...');
167
+ const html = buildPreviewHTML(parsedFiles);
168
+ if (html) {
169
+ setPreviewHTML(html);
170
+ updateAgentStep(previewId, { status: 'success', detail: 'Preview rendered' });
171
+ setConsoleOutput(prev => [...prev, { type: 'log', text: '✓ Page rendered successfully' }]);
172
+ } else {
173
+ updateAgentStep(previewId, { status: 'success', detail: 'No HTML to preview' });
174
+ }
175
+
176
+ // Update history
177
+ setHistory(prev => [
178
+ ...prev.slice(-18),
179
+ { role: 'user', content: userPrompt },
180
+ { role: 'assistant', content: result.response },
181
+ ]);
182
+
183
+ // Done
184
+ addAgentStep('done', 'Generation complete!', 'success');
185
+ setGenerationProgress('');
186
+
187
+ } catch (err) {
188
+ updateAgentStep(genId, { status: 'failed', detail: err.message });
189
+ addToast(`Error: ${err.message}`, 'error');
190
+
191
+ // Fallback to demo
192
+ try {
193
+ result = await generateDemo(userPrompt);
194
+ const parsedFiles = parseFiles(result.response);
195
+ setFiles(parsedFiles);
196
+ if (parsedFiles.length > 0) {
197
+ setActiveFile(parsedFiles[0].id);
198
+ animateCode(parsedFiles[0].content, parsedFiles);
199
+ }
200
+ const html = buildPreviewHTML(parsedFiles);
201
+ if (html) setPreviewHTML(html);
202
+ addAgentStep('done', 'Demo response used as fallback', 'success');
203
+ } catch {}
204
+ }
205
+
206
+ setIsGenerating(false);
207
+ setGenerationProgress('');
208
+ }, [isGenerating, settings, status, history, addToast, addAgentStep, updateAgentStep, animateCode]);
209
+
210
+ const handlePlanSubmit = useCallback((userPrompt, answers) => {
211
+ setPlanModal(null);
212
+ const enhanced = enhancePrompt(userPrompt, answers);
213
+ // Re-call generate with the enhanced prompt, skipping plan
214
+ setIsGenerating(true);
215
+ setAgentSteps([]);
216
+ setConsoleOutput([]);
217
+ setCodeLines([]);
218
+ setFiles([]);
219
+ setPreviewHTML(null);
220
+
221
+ (async () => {
222
+ const genId = addAgentStep('generate', 'Generating with your preferences...');
223
+ setGenerationProgress('Generating...');
224
+ let result;
225
+ try {
226
+ if (status === 'demo') {
227
+ result = await generateDemo(userPrompt);
228
+ } else {
229
+ result = await callMINDI({ prompt: enhanced, temperature: settings.temperature, maxTokens: settings.maxTokens, history, hfToken: settings.hfToken, apiUrl: settings.apiUrl });
230
+ }
231
+ if (isQuotaError(result)) {
232
+ updateAgentStep(genId, { status: 'failed', detail: 'GPU quota — using demo' });
233
+ addToast('GPU quota exceeded — showing demo.', 'error', 4000);
234
+ result = await generateDemo(userPrompt);
235
+ }
236
+ updateAgentStep(genId, { status: 'success', detail: 'Code generated' });
237
+ const parsedFiles = parseFiles(result.response);
238
+ setFiles(parsedFiles);
239
+ if (parsedFiles.length > 0) { setActiveFile(parsedFiles[0].id); animateCode(parsedFiles[0].content, parsedFiles); }
240
+ const html = buildPreviewHTML(parsedFiles);
241
+ if (html) setPreviewHTML(html);
242
+ setHistory(prev => [...prev.slice(-18), { role: 'user', content: userPrompt }, { role: 'assistant', content: result.response }]);
243
+ addAgentStep('done', 'Complete!', 'success');
244
+ } catch (err) {
245
+ updateAgentStep(genId, { status: 'failed', detail: err.message });
246
+ // Fallback to demo on any error
247
+ try {
248
+ if (isQuotaException(err.message)) {
249
+ addToast('GPU quota exceeded — showing demo.', 'error', 4000);
250
+ }
251
+ result = await generateDemo(userPrompt);
252
+ const parsedFiles = parseFiles(result.response);
253
+ setFiles(parsedFiles);
254
+ if (parsedFiles.length > 0) { setActiveFile(parsedFiles[0].id); animateCode(parsedFiles[0].content, parsedFiles); }
255
+ const html = buildPreviewHTML(parsedFiles);
256
+ if (html) { setPreviewHTML(html); setConsoleOutput(prev => [...prev, { type: 'log', text: '✓ Demo preview rendered' }]); }
257
+ addAgentStep('done', 'Demo fallback used', 'success');
258
+ } catch {}
259
+ }
260
+ setIsGenerating(false);
261
+ setGenerationProgress('');
262
+ })();
263
+ }, [settings, status, history, addToast, addAgentStep, updateAgentStep, animateCode]);
264
+
265
+ const handleFileSelect = useCallback((fileId) => {
266
+ setActiveFile(fileId);
267
+ const file = files.find(f => f.id === fileId);
268
+ if (file) {
269
+ setCodeLines(file.content.split('\n').map((text, i) => ({ text, id: i, visible: true })));
270
+ }
271
+ }, [files]);
272
+
273
+ const handleStop = useCallback(() => {
274
+ abortRef.current?.abort();
275
+ setIsGenerating(false);
276
+ setGenerationProgress('');
277
+ addAgentStep('stop', 'Generation stopped by user', 'failed');
278
+ }, [addAgentStep]);
279
+
280
+ const activeFileData = files.find(f => f.id === activeFile);
281
+
282
+ return (
283
+ <div className="app-shell">
284
+ <div className="ambient">
285
+ <div className="grid-pattern" />
286
+ <div className="blob blob--purple" />
287
+ <div className="blob blob--blue" />
288
+ </div>
289
+
290
+ <Sidebar
291
+ files={files}
292
+ activeFile={activeFile}
293
+ onFileSelect={handleFileSelect}
294
+ agentSteps={agentSteps}
295
+ status={status}
296
+ isGenerating={isGenerating}
297
+ onSettingsOpen={() => setSettingsOpen(true)}
298
+ />
299
+
300
+ <main className="main-area">
301
+ <Editor
302
+ file={activeFileData}
303
+ codeLines={codeLines}
304
+ isGenerating={isGenerating}
305
+ generationProgress={generationProgress}
306
+ files={files}
307
+ activeFile={activeFile}
308
+ onFileSelect={handleFileSelect}
309
+ />
310
+
311
+ <PromptBar
312
+ onSubmit={handleGenerate}
313
+ onStop={handleStop}
314
+ isGenerating={isGenerating}
315
+ generationProgress={generationProgress}
316
+ status={status}
317
+ />
318
+ </main>
319
+
320
+ <Preview
321
+ html={previewHTML}
322
+ consoleOutput={consoleOutput}
323
+ isGenerating={isGenerating}
324
+ />
325
+
326
+ {planModal && (
327
+ <PlanModal
328
+ userPrompt={planModal.userPrompt}
329
+ questions={planModal.questions}
330
+ onSubmit={handlePlanSubmit}
331
+ onClose={() => setPlanModal(null)}
332
+ />
333
+ )}
334
+
335
+ {settingsOpen && (
336
+ <SettingsModal
337
+ settings={settings}
338
+ onSave={(s) => { saveSettings(s); setSettingsOpen(false); addToast('Settings saved', 'success'); }}
339
+ onClose={() => setSettingsOpen(false)}
340
+ />
341
+ )}
342
+
343
+ <Toasts toasts={toasts} />
344
+ </div>
345
+ );
346
+ }
frontend/src/components/Editor.jsx ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getFileIcon } from '../services/fileParser';
2
+
3
+ export default function Editor({ file, codeLines, isGenerating, generationProgress, files, activeFile, onFileSelect }) {
4
+ if (!file && !isGenerating) {
5
+ return (
6
+ <div className="editor">
7
+ <div className="editor-welcome">
8
+ <div className="welcome-logo">
9
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
10
+ <path d="M12 2L2 7l10 5 10-5-10-5z" />
11
+ <path d="M2 17l10 5 10-5" />
12
+ <path d="M2 12l10 5 10-5" />
13
+ </svg>
14
+ </div>
15
+ <h1 className="welcome-title">
16
+ What do you want to <span className="grad-text">build</span>?
17
+ </h1>
18
+ <p className="welcome-sub">
19
+ Describe your website, app, or component and MINDI will generate production-ready code with live preview.
20
+ </p>
21
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', justifyContent: 'center', maxWidth: '400px' }}>
22
+ {['Landing Page', 'Dashboard', 'Portfolio', 'E-commerce'].map(tag => (
23
+ <span key={tag} style={{
24
+ padding: '6px 14px', borderRadius: '20px', fontSize: '11px',
25
+ background: 'rgba(124,58,237,.1)', border: '1px solid rgba(124,58,237,.2)',
26
+ color: 'var(--purple-light)', fontWeight: 500,
27
+ }}>{tag}</span>
28
+ ))}
29
+ </div>
30
+ </div>
31
+ </div>
32
+ );
33
+ }
34
+
35
+ return (
36
+ <div className="editor">
37
+ {/* Tabs */}
38
+ {files.length > 0 && (
39
+ <div className="editor-tabs">
40
+ {files.map(f => (
41
+ <button
42
+ key={f.id}
43
+ className={`editor-tab ${activeFile === f.id ? 'active' : ''}`}
44
+ onClick={() => onFileSelect(f.id)}
45
+ >
46
+ <span className="tab-icon">{getFileIcon(f.path)}</span>
47
+ {f.path}
48
+ </button>
49
+ ))}
50
+ </div>
51
+ )}
52
+
53
+ {/* Generating indicator */}
54
+ {isGenerating && (
55
+ <div className="generating-indicator">
56
+ <div className="gen-spinner" />
57
+ <span>{generationProgress || 'Generating...'}</span>
58
+ </div>
59
+ )}
60
+
61
+ {/* Code */}
62
+ <div className="editor-content">
63
+ {codeLines.map((line, i) => (
64
+ <div key={line.id} className="code-line" style={{ animationDelay: `${Math.min(i * 12, 600)}ms` }}>
65
+ <span className="line-number">{i + 1}</span>
66
+ <span className="line-content">{line.text}</span>
67
+ </div>
68
+ ))}
69
+ {isGenerating && codeLines.length === 0 && (
70
+ <div style={{ padding: '20px 64px', color: 'var(--text-dim)', fontFamily: 'var(--mono)', fontSize: '12px' }}>
71
+ Waiting for code...
72
+ </div>
73
+ )}
74
+ </div>
75
+ </div>
76
+ );
77
+ }
frontend/src/components/PlanModal.jsx ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+
3
+ export default function PlanModal({ userPrompt, questions, onSubmit, onClose }) {
4
+ const [answers, setAnswers] = useState(() => {
5
+ const init = {};
6
+ questions.forEach(q => { init[q.id] = q.default; });
7
+ return init;
8
+ });
9
+
10
+ return (
11
+ <div className="modal-overlay" onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
12
+ <div className="modal-card">
13
+ <div className="modal-header">
14
+ <h3>🎯 Configure Your Project</h3>
15
+ <button className="modal-close" onClick={onClose}>✕</button>
16
+ </div>
17
+
18
+ <div className="modal-body">
19
+ <div style={{ padding: '10px 14px', background: 'rgba(124,58,237,.08)', borderRadius: 'var(--r-md)', border: '1px solid rgba(124,58,237,.15)', fontSize: '12px', color: 'var(--text-2)', lineHeight: '1.5' }}>
20
+ <strong style={{ color: 'var(--text)' }}>Your prompt:</strong> {userPrompt}
21
+ </div>
22
+
23
+ {questions.map(q => (
24
+ <div key={q.id} className="modal-field">
25
+ <label>{q.question}</label>
26
+ <div className="modal-options">
27
+ {q.options.map(opt => (
28
+ <button
29
+ key={opt.value}
30
+ className={`modal-option ${answers[q.id] === opt.value ? 'selected' : ''}`}
31
+ onClick={() => setAnswers(prev => ({ ...prev, [q.id]: opt.value }))}
32
+ >
33
+ {opt.label}
34
+ </button>
35
+ ))}
36
+ </div>
37
+ </div>
38
+ ))}
39
+ </div>
40
+
41
+ <div className="modal-footer">
42
+ <button className="modal-btn ghost" onClick={() => { onSubmit(userPrompt, {}); }}>
43
+ Skip & Generate
44
+ </button>
45
+ <button className="modal-btn primary" onClick={() => { onSubmit(userPrompt, answers); }}>
46
+ Generate ⚡
47
+ </button>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ );
52
+ }
frontend/src/components/Preview.jsx ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useRef, useEffect } from 'react';
2
+
3
+ export default function Preview({ html, consoleOutput, isGenerating }) {
4
+ const iframeRef = useRef(null);
5
+
6
+ useEffect(() => {
7
+ if (iframeRef.current && html) {
8
+ iframeRef.current.srcdoc = html;
9
+ }
10
+ }, [html]);
11
+
12
+ return (
13
+ <div className="preview-panel">
14
+ <div className="preview-header">
15
+ <div className="preview-title">
16
+ {html && <span className="preview-dot" />}
17
+ <span>Preview</span>
18
+ </div>
19
+ <div className="preview-actions">
20
+ {html && (
21
+ <button className="preview-btn" title="Open in new tab" onClick={() => {
22
+ const w = window.open('', '_blank');
23
+ if (w) { w.document.open(); w.document.write(html); w.document.close(); }
24
+ }}>
25
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
26
+ <path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" />
27
+ <polyline points="15 3 21 3 21 9" />
28
+ <line x1="10" y1="14" x2="21" y2="3" />
29
+ </svg>
30
+ </button>
31
+ )}
32
+ {html && (
33
+ <button className="preview-btn" title="Copy HTML" onClick={() => {
34
+ navigator.clipboard.writeText(html).catch(() => {});
35
+ }}>
36
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
37
+ <rect x="9" y="9" width="13" height="13" rx="2" />
38
+ <path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
39
+ </svg>
40
+ </button>
41
+ )}
42
+ </div>
43
+ </div>
44
+
45
+ <div className="preview-frame-wrap">
46
+ {html ? (
47
+ <iframe
48
+ ref={iframeRef}
49
+ className="preview-frame"
50
+ title="Live Preview"
51
+ sandbox="allow-scripts allow-same-origin"
52
+ />
53
+ ) : (
54
+ <div className="preview-empty">
55
+ <div className="preview-empty-icon">
56
+ {isGenerating ? '⏳' : '👁️'}
57
+ </div>
58
+ <p>{isGenerating ? 'Generating preview...' : 'Live Preview'}</p>
59
+ <p>{isGenerating ? 'Your website will appear here' : 'Generated code will render here in real-time'}</p>
60
+ </div>
61
+ )}
62
+ </div>
63
+
64
+ <div className="console-panel">
65
+ <div className="console-header">Console</div>
66
+ <div className="console-body">
67
+ {consoleOutput.length === 0 ? (
68
+ <span style={{ color: 'var(--text-dim)' }}>No output yet</span>
69
+ ) : (
70
+ consoleOutput.map((entry, i) => (
71
+ <div key={i} className={entry.type === 'error' ? 'error' : ''}>
72
+ {entry.text}
73
+ </div>
74
+ ))
75
+ )}
76
+ </div>
77
+ </div>
78
+ </div>
79
+ );
80
+ }
frontend/src/components/PromptBar.jsx ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef, useEffect } from 'react';
2
+
3
+ export default function PromptBar({ onSubmit, onStop, isGenerating, generationProgress, status }) {
4
+ const [input, setInput] = useState('');
5
+ const textareaRef = useRef(null);
6
+
7
+ useEffect(() => {
8
+ if (textareaRef.current) {
9
+ textareaRef.current.style.height = 'auto';
10
+ textareaRef.current.style.height = Math.min(textareaRef.current.scrollHeight, 120) + 'px';
11
+ }
12
+ }, [input]);
13
+
14
+ const handleSubmit = () => {
15
+ if (!input.trim() || isGenerating) return;
16
+ onSubmit(input.trim());
17
+ setInput('');
18
+ };
19
+
20
+ const handleKeyDown = (e) => {
21
+ if (e.key === 'Enter' && !e.shiftKey) {
22
+ e.preventDefault();
23
+ handleSubmit();
24
+ }
25
+ };
26
+
27
+ return (
28
+ <div className="prompt-bar">
29
+ <div className="prompt-input-wrap">
30
+ <textarea
31
+ ref={textareaRef}
32
+ className="prompt-input"
33
+ placeholder="Describe what you want to build..."
34
+ value={input}
35
+ onChange={e => setInput(e.target.value)}
36
+ onKeyDown={handleKeyDown}
37
+ rows={1}
38
+ disabled={isGenerating}
39
+ />
40
+ {isGenerating ? (
41
+ <button className="prompt-stop" onClick={onStop} title="Stop generation">
42
+ <svg viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2" /></svg>
43
+ </button>
44
+ ) : (
45
+ <button
46
+ className="prompt-send"
47
+ onClick={handleSubmit}
48
+ disabled={!input.trim()}
49
+ title="Generate"
50
+ >
51
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
52
+ <line x1="22" y1="2" x2="11" y2="13" />
53
+ <polygon points="22 2 15 22 11 13 2 9 22 2" />
54
+ </svg>
55
+ </button>
56
+ )}
57
+ </div>
58
+ <div className="prompt-footer">
59
+ <span>
60
+ {status === 'demo' ? '⚠ Demo mode · add HF token in Settings' : 'MINDI 1.5 Vision-Coder'}
61
+ </span>
62
+ <span>
63
+ {isGenerating ? generationProgress : 'Shift+Enter for new line'}
64
+ </span>
65
+ </div>
66
+ </div>
67
+ );
68
+ }
frontend/src/components/SettingsModal.jsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+
3
+ export default function SettingsModal({ settings, onSave, onClose }) {
4
+ const [form, setForm] = useState({ ...settings });
5
+
6
+ const update = (key, val) => setForm(prev => ({ ...prev, [key]: val }));
7
+
8
+ return (
9
+ <div className="modal-overlay" onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
10
+ <div className="modal-card">
11
+ <div className="modal-header">
12
+ <h3>⚙️ Settings</h3>
13
+ <button className="modal-close" onClick={onClose}>✕</button>
14
+ </div>
15
+
16
+ <div className="modal-body">
17
+ <div className="settings-field">
18
+ <label>API Endpoint</label>
19
+ <input className="settings-input" type="url" value={form.apiUrl} onChange={e => update('apiUrl', e.target.value)} placeholder="https://mindigenous-mindi-chat.hf.space" />
20
+ <div className="settings-hint">HuggingFace Space or custom API URL</div>
21
+ </div>
22
+
23
+ <div className="settings-field">
24
+ <label>HuggingFace Token</label>
25
+ <input className="settings-input" type="password" value={form.hfToken} onChange={e => update('hfToken', e.target.value)} placeholder="hf_..." />
26
+ <div className="settings-hint">Required for ZeroGPU access. Get one at huggingface.co/settings/tokens</div>
27
+ </div>
28
+
29
+ <div className="settings-field">
30
+ <label>Temperature</label>
31
+ <div className="settings-range-wrap">
32
+ <input className="settings-range" type="range" min="0" max="2" step="0.05" value={form.temperature} onChange={e => update('temperature', parseFloat(e.target.value))} />
33
+ <span className="settings-range-val">{Number(form.temperature).toFixed(2)}</span>
34
+ </div>
35
+ </div>
36
+
37
+ <div className="settings-field">
38
+ <label>Max Tokens</label>
39
+ <div className="settings-range-wrap">
40
+ <input className="settings-range" type="range" min="128" max="4096" step="128" value={form.maxTokens} onChange={e => update('maxTokens', parseInt(e.target.value))} />
41
+ <span className="settings-range-val">{form.maxTokens}</span>
42
+ </div>
43
+ </div>
44
+ </div>
45
+
46
+ <div className="modal-footer">
47
+ <button className="modal-btn ghost" onClick={onClose}>Cancel</button>
48
+ <button className="modal-btn primary" onClick={() => onSave(form)}>Save Settings</button>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ );
53
+ }
frontend/src/components/Sidebar.jsx ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getFileIcon } from '../services/fileParser';
2
+
3
+ const STEP_ICONS = { plan: '📋', enhance: '✨', generate: '⚡', parse: '📦', preview: '👁️', done: '🎉', stop: '⏹️' };
4
+ const STEP_LABELS = { plan: 'Planning', enhance: 'Enhancing', generate: 'Generating', parse: 'Parsing', preview: 'Previewing', done: 'Complete', stop: 'Stopped' };
5
+
6
+ export default function Sidebar({ files, activeFile, onFileSelect, agentSteps, status, isGenerating, onSettingsOpen }) {
7
+ return (
8
+ <aside className="sidebar">
9
+ <button className="sidebar-brand" onClick={onSettingsOpen} title="Settings">
10
+ <div className="brand-icon">
11
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 2L2 7l10 5 10-5-10-5z" /><path d="M2 17l10 5 10-5" /><path d="M2 12l10 5 10-5" /></svg>
12
+ </div>
13
+ <div className="brand-info">
14
+ <div className="brand-name">MINDI<span className="grad-text"> 1.5</span></div>
15
+ <div className="brand-sub">VISION-CODER • AI BUILDER</div>
16
+ </div>
17
+ </button>
18
+
19
+ {/* File Tree */}
20
+ <div className="sidebar-section">
21
+ <div className="sidebar-section-title">
22
+ {files.length > 0 ? `Files (${files.length})` : 'Project'}
23
+ </div>
24
+ <div className="file-tree">
25
+ {files.length === 0 ? (
26
+ <div style={{ padding: '8px 16px', fontSize: '11px', color: 'var(--text-dim)' }}>
27
+ {isGenerating ? 'Generating files...' : 'No files yet. Describe what to build.'}
28
+ </div>
29
+ ) : (
30
+ files.map((f, i) => (
31
+ <button
32
+ key={f.id}
33
+ className={`file-item ${activeFile === f.id ? 'active' : ''}`}
34
+ onClick={() => onFileSelect(f.id)}
35
+ style={{ animationDelay: `${i * 80}ms` }}
36
+ >
37
+ <span className="file-icon">{getFileIcon(f.path)}</span>
38
+ <span className="file-name">{f.path}</span>
39
+ </button>
40
+ ))
41
+ )}
42
+ </div>
43
+ </div>
44
+
45
+ {/* Agent Steps */}
46
+ {agentSteps.length > 0 && (
47
+ <div className="sidebar-section" style={{ borderTop: '1px solid var(--border)', paddingTop: '8px' }}>
48
+ <div className="sidebar-section-title">Agent Progress</div>
49
+ <div className="agent-steps">
50
+ {agentSteps.map((step, i) => (
51
+ <div key={step.id} className="agent-step" style={{ animationDelay: `${i * 60}ms` }}>
52
+ <div className={`step-icon ${step.status}`}>
53
+ {step.status === 'running' ? '⏳' : step.status === 'success' ? '✅' : step.status === 'failed' ? '❌' : (STEP_ICONS[step.type] || '⏺')}
54
+ </div>
55
+ <div>
56
+ <div style={{ fontWeight: 500, color: 'var(--text)' }}>{STEP_LABELS[step.type] || step.type}</div>
57
+ <div className="step-detail">{step.detail}</div>
58
+ </div>
59
+ </div>
60
+ ))}
61
+ </div>
62
+ </div>
63
+ )}
64
+
65
+ <div style={{ flex: 1 }} />
66
+
67
+ {/* Footer */}
68
+ <div className="sidebar-footer">
69
+ <div className={`status-dot ${status}`} />
70
+ <span>{status === 'online' ? 'MINDI · Connected' : status === 'demo' ? 'Demo Mode' : 'Connecting...'}</span>
71
+ </div>
72
+ </aside>
73
+ );
74
+ }
frontend/src/components/Toasts.jsx ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function Toasts({ toasts }) {
2
+ if (!toasts.length) return null;
3
+ return (
4
+ <div className="toasts">
5
+ {toasts.map(t => (
6
+ <div key={t.id} className="toast">
7
+ <div className={`toast-dot ${t.type}`} />
8
+ <span>{t.msg}</span>
9
+ </div>
10
+ ))}
11
+ </div>
12
+ );
13
+ }
frontend/src/index.css ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* =============================================================
2
+ MINDI 1.5 — AI Website Builder
3
+ Design System — Premium dark IDE theme
4
+ ============================================================= */
5
+
6
+ /* ============ TOKENS ============ */
7
+ :root {
8
+ /* Surfaces */
9
+ --bg-0: #07070c;
10
+ --bg-1: #0a0a12;
11
+ --bg-2: #0d0d16;
12
+ --panel: #111120;
13
+ --panel-2: #16162a;
14
+ --panel-3: #1c1c36;
15
+ --elevated: #1f1f38;
16
+ --hover: rgba(255, 255, 255, .04);
17
+ --hover-2: rgba(255, 255, 255, .07);
18
+
19
+ /* Lines */
20
+ --border: rgba(255, 255, 255, .06);
21
+ --border-2: rgba(255, 255, 255, .10);
22
+ --border-3: rgba(255, 255, 255, .16);
23
+
24
+ /* Text */
25
+ --text: #ececf1;
26
+ --text-2: #b4b4c4;
27
+ --text-mute: #7a7a8c;
28
+ --text-dim: #565669;
29
+
30
+ /* Brand */
31
+ --purple: #7c3aed;
32
+ --purple-light: #a78bfa;
33
+ --blue: #2563eb;
34
+ --blue-light: #60a5fa;
35
+ --grad: linear-gradient(135deg, #7c3aed 0%, #2563eb 100%);
36
+ --grad-soft: linear-gradient(135deg, rgba(124, 58, 237, .15) 0%, rgba(37, 99, 235, .15) 100%);
37
+ --grad-glow: linear-gradient(135deg, rgba(124, 58, 237, .55) 0%, rgba(37, 99, 235, .55) 100%);
38
+
39
+ /* Status */
40
+ --ok: #10b981;
41
+ --warn: #f59e0b;
42
+ --err: #ef4444;
43
+ --info: #3b82f6;
44
+
45
+ /* Code colors */
46
+ --c-keyword: #a78bfa;
47
+ --c-string: #34d399;
48
+ --c-number: #fbbf24;
49
+ --c-comment: #6a6a85;
50
+ --c-function: #60a5fa;
51
+ --c-tag: #f87171;
52
+ --c-attr: #fbbf24;
53
+ --c-operator: #5eead4;
54
+
55
+ /* Geometry */
56
+ --r-xs: 4px;
57
+ --r-sm: 6px;
58
+ --r-md: 10px;
59
+ --r-lg: 14px;
60
+ --r-xl: 18px;
61
+ --r-2xl: 24px;
62
+
63
+ --sidebar-w: 260px;
64
+ --header-h: 48px;
65
+
66
+ /* Motion */
67
+ --ease: cubic-bezier(.16, 1, .3, 1);
68
+ --ease-2: cubic-bezier(.4, 0, .2, 1);
69
+ --ease-spring: cubic-bezier(.34, 1.56, .64, 1);
70
+
71
+ /* Fonts */
72
+ --sans: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
73
+ --mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
74
+ }
75
+
76
+ /* ============ RESET ============ */
77
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
78
+ html, body, #root { height: 100%; }
79
+ body {
80
+ font-family: var(--sans);
81
+ font-size: 13px;
82
+ line-height: 1.5;
83
+ color: var(--text);
84
+ background: var(--bg-0);
85
+ overflow: hidden;
86
+ -webkit-font-smoothing: antialiased;
87
+ -moz-osx-font-smoothing: grayscale;
88
+ }
89
+ button, input, textarea, select {
90
+ font: inherit;
91
+ color: inherit;
92
+ background: none;
93
+ border: 0;
94
+ outline: 0;
95
+ }
96
+ button { cursor: pointer; }
97
+ a { color: inherit; text-decoration: none; }
98
+ img, svg { display: block; }
99
+
100
+ /* Scrollbars */
101
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
102
+ ::-webkit-scrollbar-track { background: transparent; }
103
+ ::-webkit-scrollbar-thumb {
104
+ background: rgba(255, 255, 255, .06);
105
+ border-radius: 999px;
106
+ border: 2px solid transparent;
107
+ background-clip: content-box;
108
+ }
109
+ ::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, .12); background-clip: content-box; }
110
+ * { scrollbar-width: thin; scrollbar-color: rgba(255, 255, 255, .08) transparent; }
111
+ ::selection { background: rgba(124, 58, 237, .35); color: #fff; }
112
+
113
+ /* ============ UTILITY CLASSES ============ */
114
+ .grad-text {
115
+ background: var(--grad);
116
+ -webkit-background-clip: text;
117
+ background-clip: text;
118
+ color: transparent;
119
+ }
120
+
121
+ /* ============ ANIMATIONS ============ */
122
+ @keyframes fadeIn {
123
+ from { opacity: 0; transform: translateY(6px); }
124
+ to { opacity: 1; transform: translateY(0); }
125
+ }
126
+ @keyframes fadeInLeft {
127
+ from { opacity: 0; transform: translateX(-8px); }
128
+ to { opacity: 1; transform: translateX(0); }
129
+ }
130
+ @keyframes fadeInScale {
131
+ from { opacity: 0; transform: scale(.96); }
132
+ to { opacity: 1; transform: scale(1); }
133
+ }
134
+ @keyframes slideUp {
135
+ from { opacity: 0; transform: translateY(16px); }
136
+ to { opacity: 1; transform: translateY(0); }
137
+ }
138
+ @keyframes pulse {
139
+ 0%, 100% { opacity: 1; }
140
+ 50% { opacity: .4; }
141
+ }
142
+ @keyframes spin {
143
+ to { transform: rotate(360deg); }
144
+ }
145
+ @keyframes shimmer {
146
+ 0% { background-position: -200% 0; }
147
+ 100% { background-position: 200% 0; }
148
+ }
149
+ @keyframes typewriter-cursor {
150
+ 0%, 100% { opacity: 1; }
151
+ 50% { opacity: 0; }
152
+ }
153
+ @keyframes glow-pulse {
154
+ 0%, 100% { box-shadow: 0 0 20px rgba(124, 58, 237, .2); }
155
+ 50% { box-shadow: 0 0 40px rgba(124, 58, 237, .4); }
156
+ }
157
+ @keyframes line-appear {
158
+ from { opacity: 0; transform: translateX(-4px); }
159
+ to { opacity: 1; transform: translateX(0); }
160
+ }
161
+ @keyframes float {
162
+ 0%, 100% { transform: translateY(0); }
163
+ 50% { transform: translateY(-8px); }
164
+ }
165
+ @keyframes drift-1 {
166
+ to { transform: translate(80px, 60px) scale(1.1); }
167
+ }
168
+ @keyframes drift-2 {
169
+ to { transform: translate(-60px, -80px) scale(1.15); }
170
+ }
171
+ @keyframes pop-in {
172
+ from { opacity: 0; transform: scale(.92) translateY(8px); }
173
+ to { opacity: 1; transform: scale(1) translateY(0); }
174
+ }
175
+ @keyframes toast-in {
176
+ from { opacity: 0; transform: translateX(20px); }
177
+ to { opacity: 1; transform: translateX(0); }
178
+ }
179
+ @keyframes toast-out {
180
+ to { opacity: 0; transform: translateX(20px); }
181
+ }
182
+
183
+ /* ============ REDUCE MOTION ============ */
184
+ @media (prefers-reduced-motion: reduce) {
185
+ *, *::before, *::after {
186
+ animation-duration: .01ms !important;
187
+ transition-duration: .01ms !important;
188
+ }
189
+ }
frontend/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App.jsx';
4
+ import './index.css';
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>
10
+ );
frontend/src/services/api.js ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* =============================================================
2
+ MINDI API Service — Gradio SSE v3 integration
3
+ Connects to HuggingFace-hosted MINDI 1.5 Vision-Coder
4
+ ============================================================= */
5
+
6
+ const API_DEFAULT = 'https://mindigenous-mindi-chat.hf.space';
7
+
8
+ function authHeaders(hfToken, extra = {}) {
9
+ const h = { ...extra };
10
+ if (hfToken) h['Authorization'] = `Bearer ${hfToken}`;
11
+ return h;
12
+ }
13
+
14
+ function dataUrlToBlob(dataUrl) {
15
+ const match = /^data:([^;]+);base64,(.+)$/.exec(dataUrl || '');
16
+ if (!match) throw new Error('Invalid image data URL');
17
+ const bytes = Uint8Array.from(atob(match[2]), c => c.charCodeAt(0));
18
+ return { blob: new Blob([bytes], { type: match[1] }), mime: match[1] };
19
+ }
20
+
21
+ async function uploadImageToGradio(base, dataUrl, hfToken, signal) {
22
+ const { blob, mime } = dataUrlToBlob(dataUrl);
23
+ const ext = (mime.split('/')[1] || 'png').replace('+xml', '').split(';')[0];
24
+ const filename = `mindi-upload-${Date.now()}.${ext}`;
25
+ const formData = new FormData();
26
+ formData.append('files', blob, filename);
27
+
28
+ const headers = authHeaders(hfToken);
29
+ delete headers['Content-Type'];
30
+
31
+ const res = await fetch(`${base}/gradio_api/upload`, {
32
+ method: 'POST', headers, body: formData, signal,
33
+ });
34
+ if (!res.ok) throw new Error(`Image upload ${res.status}`);
35
+ const result = await res.json();
36
+ const filePath = Array.isArray(result) ? result[0] : result?.files?.[0];
37
+ if (!filePath) throw new Error('Upload failed');
38
+ return filePath;
39
+ }
40
+
41
+ export async function callMINDI({ prompt, image, temperature = 0.7, maxTokens = 2048, history = [], hfToken = '', apiUrl = API_DEFAULT, signal }) {
42
+ const base = (apiUrl || API_DEFAULT).replace(/\/$/, '');
43
+ const isGradio = base.includes('hf.space') || base.includes('huggingface.co');
44
+ const historyJson = history.length ? JSON.stringify(history) : '';
45
+
46
+ if (isGradio) {
47
+ let imageArg = null;
48
+ if (image && image.startsWith('data:')) {
49
+ try {
50
+ const filePath = await uploadImageToGradio(base, image, hfToken, signal);
51
+ imageArg = { path: filePath, meta: { _type: 'gradio.FileData' }, orig_name: filePath.split('/').pop() };
52
+ } catch { imageArg = null; }
53
+ }
54
+
55
+ const submitRes = await fetch(`${base}/gradio_api/call/chat_fn`, {
56
+ method: 'POST',
57
+ headers: authHeaders(hfToken, { 'Content-Type': 'application/json' }),
58
+ body: JSON.stringify({ data: [prompt, imageArg, temperature, maxTokens, historyJson] }),
59
+ signal,
60
+ });
61
+ if (!submitRes.ok) {
62
+ const txt = await submitRes.text().catch(() => '');
63
+ throw new Error(`API ${submitRes.status}: ${txt.slice(0, 200)}`);
64
+ }
65
+ const { event_id } = await submitRes.json();
66
+ if (!event_id) throw new Error('No event_id returned');
67
+
68
+ const resultRes = await fetch(`${base}/gradio_api/call/chat_fn/${event_id}`, {
69
+ method: 'GET', headers: authHeaders(hfToken), signal,
70
+ });
71
+ if (!resultRes.ok) throw new Error(`API result ${resultRes.status}`);
72
+
73
+ const sseText = await resultRes.text();
74
+ const lines = sseText.split('\n');
75
+ for (let i = 0; i < lines.length; i++) {
76
+ if (lines[i].startsWith('event: complete')) {
77
+ const dataLine = lines[i + 1];
78
+ if (dataLine?.startsWith('data: ')) {
79
+ try {
80
+ const parsed = JSON.parse(dataLine.slice(6));
81
+ const raw = Array.isArray(parsed) ? parsed[0] : parsed;
82
+ try { return JSON.parse(raw); } catch { return { response: String(raw), sections: {} }; }
83
+ } catch { return { response: dataLine.slice(6), sections: {} }; }
84
+ }
85
+ break;
86
+ }
87
+ if (lines[i].startsWith('event: error')) {
88
+ const errMsg = lines[i + 1]?.startsWith('data: ') ? lines[i + 1].slice(6) : 'Gradio error';
89
+ throw new Error(errMsg.slice(0, 300));
90
+ }
91
+ }
92
+ throw new Error('No complete event in response');
93
+ } else {
94
+ const body = { prompt, temperature, max_tokens: maxTokens, history };
95
+ if (image) body.image = image;
96
+ const res = await fetch(`${base}/api/generate`, {
97
+ method: 'POST',
98
+ headers: authHeaders(hfToken, { 'Content-Type': 'application/json', 'Accept': 'application/json' }),
99
+ body: JSON.stringify(body), signal,
100
+ });
101
+ if (!res.ok) throw new Error(`API ${res.status}`);
102
+ return res.json();
103
+ }
104
+ }
105
+
106
+ export async function pingAPI(apiUrl, hfToken) {
107
+ const base = (apiUrl || API_DEFAULT).replace(/\/$/, '');
108
+ try {
109
+ const res = await fetch(base, { method: 'HEAD', mode: 'no-cors' }).catch(() => null);
110
+ return !!res;
111
+ } catch { return false; }
112
+ }
113
+
114
+ export function isQuotaError(result) {
115
+ if (!result) return false;
116
+ const text = String(result.response || '');
117
+ const errs = result.sections?.error || [];
118
+ const blob = (text + ' ' + errs.join(' ')).toLowerCase();
119
+ return /zerogpu|gpu quota|out of .* quota|exceeded .* quota|unlogged user|gpu task aborted|task aborted/.test(blob);
120
+ }
121
+
122
+ export function isQuotaException(errMessage) {
123
+ const msg = (errMessage || '').toLowerCase();
124
+ return /gpu quota|zerogpu|gpu task aborted|task aborted|unlogged user|out of .* quota|exceeded .* quota/.test(msg);
125
+ }
126
+
127
+ // Demo responses
128
+ const DEMOS = [
129
+ {
130
+ match: /landing|hero|page|website/i,
131
+ response: `Here's a complete landing page:\n\n\`\`\`html\n<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0">\n<title>Lumina — Future of Design</title>\n<script src="https://cdn.tailwindcss.com"><\/script>\n<style>\n@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');\nbody { font-family: 'Inter', sans-serif; }\n.gradient-bg { background: linear-gradient(135deg, #0f0c29, #302b63, #24243e); }\n.glow { box-shadow: 0 0 40px rgba(124, 58, 237, 0.3); }\n.card-hover:hover { transform: translateY(-4px); box-shadow: 0 20px 40px rgba(0,0,0,0.3); }\n</style>\n</head>\n<body class="gradient-bg text-white min-h-screen">\n<nav class="flex items-center justify-between px-8 py-5 max-w-7xl mx-auto">\n <div class="text-2xl font-bold bg-gradient-to-r from-purple-400 to-blue-400 bg-clip-text text-transparent">Lumina</div>\n <div class="hidden md:flex gap-8 text-sm text-gray-300">\n <a href="#features" class="hover:text-white transition">Features</a>\n <a href="#pricing" class="hover:text-white transition">Pricing</a>\n <a href="#about" class="hover:text-white transition">About</a>\n </div>\n <button class="px-5 py-2 bg-purple-600 rounded-full text-sm font-medium hover:bg-purple-500 transition glow">Get Started</button>\n</nav>\n<main class="max-w-7xl mx-auto px-8">\n <section class="py-24 text-center">\n <span class="inline-block px-4 py-1.5 bg-purple-500/20 border border-purple-500/30 rounded-full text-purple-300 text-xs font-medium tracking-wider uppercase mb-6">Now in Beta</span>\n <h1 class="text-5xl md:text-7xl font-extrabold leading-tight mb-6">\n Build faster.<br>\n <span class="bg-gradient-to-r from-purple-400 via-pink-400 to-blue-400 bg-clip-text text-transparent">Ship smarter.</span>\n </h1>\n <p class="text-lg text-gray-400 max-w-2xl mx-auto mb-10">The next-generation platform that turns your ideas into reality. No complexity, just results.</p>\n <div class="flex justify-center gap-4">\n <button class="px-8 py-3 bg-gradient-to-r from-purple-600 to-blue-600 rounded-full font-semibold hover:shadow-lg hover:shadow-purple-500/25 transition-all">Start Free Trial</button>\n <button class="px-8 py-3 border border-white/20 rounded-full font-medium hover:bg-white/5 transition">Watch Demo</button>\n </div>\n </section>\n <section id="features" class="py-20 grid md:grid-cols-3 gap-6">\n <div class="p-8 bg-white/5 border border-white/10 rounded-2xl card-hover transition-all">\n <div class="w-12 h-12 bg-purple-500/20 rounded-xl flex items-center justify-center text-2xl mb-4">⚡</div>\n <h3 class="text-lg font-semibold mb-2">Lightning Fast</h3>\n <p class="text-gray-400 text-sm">Deploy in seconds. Our edge network ensures your app loads instantly worldwide.</p>\n </div>\n <div class="p-8 bg-white/5 border border-white/10 rounded-2xl card-hover transition-all">\n <div class="w-12 h-12 bg-blue-500/20 rounded-xl flex items-center justify-center text-2xl mb-4">🔒</div>\n <h3 class="text-lg font-semibold mb-2">Enterprise Security</h3>\n <p class="text-gray-400 text-sm">SOC 2 compliant with end-to-end encryption. Your data is always protected.</p>\n </div>\n <div class="p-8 bg-white/5 border border-white/10 rounded-2xl card-hover transition-all">\n <div class="w-12 h-12 bg-pink-500/20 rounded-xl flex items-center justify-center text-2xl mb-4">🎨</div>\n <h3 class="text-lg font-semibold mb-2">Beautiful UI</h3>\n <p class="text-gray-400 text-sm">Pre-built components that look stunning out of the box. Customize everything.</p>\n </div>\n </section>\n</main>\n<footer class="border-t border-white/10 py-8 text-center text-gray-500 text-sm">\n <p>&copy; 2026 Lumina. Crafted with AI.</p>\n</footer>\n</body>\n</html>\n\`\`\``,
132
+ },
133
+ {
134
+ match: /dashboard|chart|analytics|admin/i,
135
+ response: `Here's a dashboard UI:\n\n\`\`\`html\n<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">\n<title>Dashboard</title>\n<style>\n:root{--bg:#0b0b14;--panel:#14141f;--border:rgba(255,255,255,.08);--text:#ececf1;--mute:#8b94a7;--acc:#7c3aed}\n*{box-sizing:border-box;margin:0;padding:0}\nbody{background:var(--bg);color:var(--text);font:14px/1.55 'Inter',sans-serif;min-height:100vh;display:grid;grid-template-columns:240px 1fr}\naside{background:var(--panel);border-right:1px solid var(--border);padding:20px}\naside h1{font-size:18px;background:linear-gradient(135deg,#7c3aed,#2563eb);-webkit-background-clip:text;color:transparent;margin-bottom:24px}\nnav a{display:block;padding:10px 12px;border-radius:8px;color:var(--mute);text-decoration:none;margin-bottom:2px}\nnav a.active{background:rgba(124,58,237,.15);color:#fff}\nmain{padding:24px;overflow-y:auto}\n.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:20px}\n.stat{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:16px}\n.stat .v{font-size:24px;font-weight:600;margin-top:6px}\n.stat .l{color:var(--mute);font-size:12px;text-transform:uppercase;letter-spacing:.1em}\n.chart{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:18px;height:260px;display:flex;align-items:end;gap:8px}\n.bar{flex:1;background:linear-gradient(180deg,#7c3aed,#2563eb);border-radius:6px 6px 0 0;transition:height .5s}\n</style>\n</head>\n<body>\n<aside><h1>Pulsegrid</h1>\n<nav><a class="active">Overview</a><a>Customers</a><a>Revenue</a><a>Settings</a></nav>\n</aside>\n<main>\n<div class="stats">\n<div class="stat"><div class="l">Revenue</div><div class="v">$48,210</div></div>\n<div class="stat"><div class="l">Users</div><div class="v">12,840</div></div>\n<div class="stat"><div class="l">Conversion</div><div class="v">4.2%</div></div>\n<div class="stat"><div class="l">Churn</div><div class="v">1.1%</div></div>\n</div>\n<div class="chart">\n<div class="bar" style="height:40%"></div><div class="bar" style="height:65%"></div>\n<div class="bar" style="height:30%"></div><div class="bar" style="height:80%"></div>\n<div class="bar" style="height:55%"></div><div class="bar" style="height:90%"></div>\n<div class="bar" style="height:70%"></div>\n</div>\n</main>\n</body>\n</html>\n\`\`\``,
136
+ },
137
+ ];
138
+
139
+ const DEFAULT_DEMO = {
140
+ response: `Here's a starter template:\n\n\`\`\`html\n<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">\n<title>MINDI Generated</title>\n<style>\n*{margin:0;padding:0;box-sizing:border-box}\nbody{min-height:100vh;background:#0f0c29;color:#fff;font-family:Inter,sans-serif;display:grid;place-items:center}\n.card{text-align:center;padding:48px;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);border-radius:20px;backdrop-filter:blur(10px)}\nh1{font-size:2.5rem;margin-bottom:12px;background:linear-gradient(135deg,#7c3aed,#2563eb);-webkit-background-clip:text;color:transparent}\np{color:#a0a0b8;font-size:1.1rem}\n</style>\n</head>\n<body>\n<div class="card">\n<h1>Hello from MINDI</h1>\n<p>Describe what you want to build and I'll generate it.</p>\n</div>\n</body>\n</html>\n\`\`\``,
141
+ sections: {},
142
+ };
143
+
144
+ export async function generateDemo(prompt) {
145
+ await new Promise(r => setTimeout(r, 800 + Math.random() * 600));
146
+ const found = DEMOS.find(d => d.match.test(prompt));
147
+ return { response: (found || DEFAULT_DEMO).response, sections: {} };
148
+ }
frontend/src/services/fileParser.js ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* File Parser — Extracts files from model response */
2
+
3
+ export function parseFiles(responseText) {
4
+ if (!responseText) return [];
5
+ const files = [];
6
+ const re = /```(\w+)?\s*\n([\s\S]*?)```/g;
7
+ let m, idx = 0;
8
+ while ((m = re.exec(responseText)) !== null) {
9
+ const lang = (m[1] || '').toLowerCase();
10
+ const code = m[2];
11
+ const filename = detectFilename(code, lang, idx);
12
+ files.push({ id: `file-${idx}`, path: filename, content: code, language: lang || detectLang(code) });
13
+ idx++;
14
+ }
15
+ if (files.length === 0 && responseText.trim()) {
16
+ files.push({ id: 'file-0', path: 'index.html', content: responseText, language: 'html' });
17
+ }
18
+ return files;
19
+ }
20
+
21
+ function detectFilename(code, lang, idx) {
22
+ // Check for filename comments
23
+ const fnMatch = code.match(/^\/\/\s*([\w/.-]+\.\w+)/m) || code.match(/^<!--\s*([\w/.-]+\.\w+)/m) || code.match(/^\/\*\s*([\w/.-]+\.\w+)/m);
24
+ if (fnMatch) return fnMatch[1];
25
+ const extMap = { html: 'index.html', markup: 'index.html', css: 'styles.css', javascript: 'script.js', js: 'script.js', typescript: 'index.ts', tsx: 'page.tsx', jsx: 'App.jsx', python: 'main.py', json: 'package.json', vue: 'App.vue' };
26
+ if (idx === 0 && /<!doctype|<html/i.test(code)) return 'index.html';
27
+ return extMap[lang] || `file${idx}.${lang || 'txt'}`;
28
+ }
29
+
30
+ function detectLang(code) {
31
+ const t = code.trim();
32
+ if (/^<!doctype|^<html/i.test(t)) return 'html';
33
+ if (/^import.*from|^export|^const |^function /m.test(t)) return 'javascript';
34
+ if (/^from |^import |^def |^class /m.test(t)) return 'python';
35
+ if (/^\{[\s\S]*\}$/.test(t)) return 'json';
36
+ return 'plaintext';
37
+ }
38
+
39
+ export function buildPreviewHTML(files) {
40
+ const htmlFile = files.find(f => f.language === 'html' || f.path.endsWith('.html'));
41
+ if (htmlFile) return htmlFile.content;
42
+ const cssFile = files.find(f => f.language === 'css');
43
+ const jsFile = files.find(f => f.language === 'javascript' || f.language === 'js');
44
+ if (cssFile || jsFile) {
45
+ return `<!DOCTYPE html><html><head><meta charset="UTF-8"><style>${cssFile?.content || ''}</style></head><body><script>${jsFile?.content || ''}<\/script></body></html>`;
46
+ }
47
+ return null;
48
+ }
49
+
50
+ export function getFileIcon(path) {
51
+ const ext = path.split('.').pop().toLowerCase();
52
+ const icons = { html: '🌐', css: '🎨', js: '⚡', jsx: '⚛️', tsx: '⚛️', ts: '📘', py: '🐍', json: '📋', vue: '💚', md: '📝', svg: '🖼️' };
53
+ return icons[ext] || '📄';
54
+ }
frontend/src/services/promptEnhancer.js ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Prompt Enhancer — Transforms user input into structured prompts */
2
+
3
+ const TECH_CONFIGS = {
4
+ html: { label: 'HTML + CSS + JS', instructions: 'Output a SINGLE complete <!DOCTYPE html> document. Include ALL CSS in <style> and JS in <script>. Use Tailwind CDN when appropriate.' },
5
+ react: { label: 'React', instructions: 'Output a single React JSX component with export default. Use hooks when needed.' },
6
+ nextjs: { label: 'Next.js', instructions: 'Output a single app/page.tsx. Use client directive only if needed. Use Tailwind classes.' },
7
+ vue: { label: 'Vue', instructions: 'Output a single .vue SFC with template, script setup, style scoped.' },
8
+ };
9
+
10
+ const DESIGN_PRESETS = {
11
+ dark: 'Dark theme with deep navy backgrounds, subtle gradients, glassmorphism.',
12
+ light: 'Light theme with clean white/gray backgrounds, subtle shadows.',
13
+ gradient: 'Rich gradient backgrounds, purple-to-blue or teal-to-emerald.',
14
+ minimal: 'Minimalist with whitespace, clean typography, elegant simplicity.',
15
+ };
16
+
17
+ export function analyzePrompt(userInput) {
18
+ const input = userInput.toLowerCase();
19
+ const questions = [];
20
+ const hasTech = /\b(html|react|next\.?js|vue|svelte|tailwind)\b/i.test(input);
21
+ if (!hasTech) {
22
+ questions.push({ id: 'tech', question: 'Tech stack?', options: Object.entries(TECH_CONFIGS).map(([k, v]) => ({ value: k, label: v.label })), default: 'html' });
23
+ }
24
+ const hasTheme = /\b(dark|light|gradient|minimal)\b/i.test(input);
25
+ if (!hasTheme) {
26
+ questions.push({ id: 'theme', question: 'Design style?', options: Object.entries(DESIGN_PRESETS).map(([k]) => ({ value: k, label: k.charAt(0).toUpperCase() + k.slice(1) })), default: 'dark' });
27
+ }
28
+ return { questions, hasTech, hasTheme, detectedTech: detectTech(input) };
29
+ }
30
+
31
+ function detectTech(input) {
32
+ if (/next\.?js/i.test(input)) return 'nextjs';
33
+ if (/\breact\b/i.test(input)) return 'react';
34
+ if (/\bvue\b/i.test(input)) return 'vue';
35
+ return 'html';
36
+ }
37
+
38
+ export function enhancePrompt(userInput, answers = {}) {
39
+ const tech = answers.tech || detectTech(userInput);
40
+ const theme = answers.theme || 'dark';
41
+ const config = TECH_CONFIGS[tech] || TECH_CONFIGS.html;
42
+ const design = DESIGN_PRESETS[theme] || DESIGN_PRESETS.dark;
43
+ return `${userInput}\n\n--- REQUIREMENTS ---\nTech: ${config.label}\n${config.instructions}\nDesign: ${design}\nRules: Production-ready, responsive, Inter font, smooth animations, no placeholders, complete code.`;
44
+ }
45
+
46
+ export function getQuickEnhancement(userInput) {
47
+ return enhancePrompt(userInput, {});
48
+ }
frontend/styles.css DELETED
@@ -1,1479 +0,0 @@
1
- /* =============================================================
2
- MINDI 1.5 — Vision-Coder
3
- Premium dark UI · glassmorphism · gradient brand
4
- ============================================================= */
5
-
6
- /* ============ TOKENS ============ */
7
- :root {
8
- /* Surfaces */
9
- --bg-0: #08080d;
10
- --bg-1: #0b0b14;
11
- --bg-2: #0e0e16;
12
- --panel: #14141f;
13
- --panel-2: #1a1a2e;
14
- --elevated: #1f1f33;
15
- --hover: rgba(255, 255, 255, .04);
16
- --hover-2: rgba(255, 255, 255, .07);
17
-
18
- /* Lines */
19
- --border: rgba(255, 255, 255, .06);
20
- --border-2: rgba(255, 255, 255, .10);
21
- --border-3: rgba(255, 255, 255, .16);
22
-
23
- /* Text */
24
- --text: #ececf1;
25
- --text-2: #b4b4c4;
26
- --text-mute: #7a7a8c;
27
- --text-dim: #565669;
28
-
29
- /* Brand */
30
- --purple: #7c3aed;
31
- --blue: #2563eb;
32
- --grad: linear-gradient(135deg, #7c3aed 0%, #2563eb 100%);
33
- --grad-soft: linear-gradient(135deg, rgba(124, 58, 237, .18) 0%, rgba(37, 99, 235, .18) 100%);
34
- --grad-glow: linear-gradient(135deg, rgba(124, 58, 237, .55) 0%, rgba(37, 99, 235, .55) 100%);
35
-
36
- /* Status colors */
37
- --ok: #10b981;
38
- --warn: #f59e0b;
39
- --err: #ef4444;
40
-
41
- /* Section card colors */
42
- --c-thinking: #a78bfa; /* purple */
43
- --c-code: #34d399; /* green */
44
- --c-critique: #fbbf24; /* yellow */
45
- --c-fix: #60a5fa; /* blue */
46
- --c-error: #f87171; /* red */
47
- --c-suggest: #5eead4; /* teal */
48
- --c-file: #cbd5e1; /* gray */
49
-
50
- /* Geometry */
51
- --r-xs: 6px;
52
- --r-sm: 8px;
53
- --r-md: 12px;
54
- --r-lg: 16px;
55
- --r-xl: 20px;
56
-
57
- --sidebar-w: 280px;
58
- --preview-w: 480px;
59
- --header-h: 56px;
60
-
61
- /* Motion */
62
- --ease: cubic-bezier(.16, 1, .3, 1);
63
- --ease-2: cubic-bezier(.4, 0, .2, 1);
64
-
65
- /* Fonts */
66
- --sans: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
67
- --mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
68
- }
69
-
70
- /* ============ RESET ============ */
71
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
72
- html, body { height: 100%; }
73
- body {
74
- font-family: var(--sans);
75
- font-size: 14px;
76
- line-height: 1.55;
77
- color: var(--text);
78
- background: linear-gradient(180deg, var(--bg-0), var(--bg-2));
79
- overflow: hidden;
80
- -webkit-font-smoothing: antialiased;
81
- -moz-osx-font-smoothing: grayscale;
82
- }
83
- button, input, textarea, select {
84
- font: inherit;
85
- color: inherit;
86
- background: none;
87
- border: 0;
88
- outline: 0;
89
- }
90
- button { cursor: pointer; }
91
- input, textarea { font-family: inherit; }
92
- a { color: inherit; text-decoration: none; }
93
- [hidden] { display: none !important; }
94
- img, svg { display: block; }
95
-
96
- /* Scrollbars */
97
- ::-webkit-scrollbar { width: 10px; height: 10px; }
98
- ::-webkit-scrollbar-track { background: transparent; }
99
- ::-webkit-scrollbar-thumb {
100
- background: rgba(255, 255, 255, .06);
101
- border-radius: 999px;
102
- border: 2px solid transparent;
103
- background-clip: content-box;
104
- }
105
- ::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, .12); background-clip: content-box; }
106
- * { scrollbar-width: thin; scrollbar-color: rgba(255, 255, 255, .08) transparent; }
107
-
108
- ::selection { background: rgba(124, 58, 237, .35); color: #fff; }
109
-
110
- /* ============ AMBIENT BACKGROUND ============ */
111
- .ambient {
112
- position: fixed; inset: 0;
113
- pointer-events: none;
114
- z-index: 0;
115
- overflow: hidden;
116
- }
117
- .grid-pattern {
118
- position: absolute; inset: 0;
119
- background-image:
120
- linear-gradient(rgba(255, 255, 255, .025) 1px, transparent 1px),
121
- linear-gradient(90deg, rgba(255, 255, 255, .025) 1px, transparent 1px);
122
- background-size: 64px 64px;
123
- mask-image: radial-gradient(ellipse at center, #000 0%, transparent 80%);
124
- -webkit-mask-image: radial-gradient(ellipse at center, #000 0%, transparent 80%);
125
- }
126
- .blob {
127
- position: absolute;
128
- width: 640px; height: 640px;
129
- border-radius: 50%;
130
- filter: blur(120px);
131
- opacity: .35;
132
- will-change: transform;
133
- }
134
- .blob--purple {
135
- background: radial-gradient(circle, var(--purple), transparent 70%);
136
- top: -180px; left: -120px;
137
- animation: drift-1 26s ease-in-out infinite alternate;
138
- }
139
- .blob--blue {
140
- background: radial-gradient(circle, var(--blue), transparent 70%);
141
- bottom: -180px; right: -120px;
142
- animation: drift-2 30s ease-in-out infinite alternate;
143
- }
144
- @keyframes drift-1 {
145
- to { transform: translate(80px, 60px) scale(1.1); }
146
- }
147
- @keyframes drift-2 {
148
- to { transform: translate(-60px, -80px) scale(1.15); }
149
- }
150
-
151
- /* ============ APP SHELL ============ */
152
- .app {
153
- position: relative;
154
- display: grid;
155
- grid-template-columns: var(--sidebar-w) 1fr var(--preview-w);
156
- height: 100vh;
157
- z-index: 1;
158
- }
159
- .scrim {
160
- position: fixed; inset: 0;
161
- background: rgba(0, 0, 0, .55);
162
- backdrop-filter: blur(2px);
163
- z-index: 30;
164
- opacity: 0;
165
- pointer-events: none;
166
- transition: opacity .25s var(--ease);
167
- }
168
- body.sidebar-open .scrim { opacity: 1; pointer-events: auto; }
169
-
170
- /* ============ SIDEBAR ============ */
171
- .sidebar {
172
- display: flex;
173
- flex-direction: column;
174
- height: 100vh;
175
- background: rgba(20, 20, 31, .72);
176
- border-right: 1px solid var(--border);
177
- backdrop-filter: blur(20px);
178
- -webkit-backdrop-filter: blur(20px);
179
- z-index: 35;
180
- }
181
- .sidebar-head {
182
- padding: 18px 16px 10px;
183
- }
184
- .brand {
185
- display: flex;
186
- align-items: center;
187
- gap: 12px;
188
- width: 100%;
189
- padding: 8px;
190
- border-radius: var(--r-md);
191
- text-align: left;
192
- transition: background .2s var(--ease);
193
- }
194
- .brand:hover { background: var(--hover); }
195
- .brand-mark {
196
- flex-shrink: 0;
197
- width: 36px; height: 36px;
198
- display: grid; place-items: center;
199
- border-radius: var(--r-sm);
200
- box-shadow: 0 0 24px rgba(124, 58, 237, .4);
201
- }
202
- .brand-mark svg { width: 100%; height: 100%; }
203
- .brand-text {
204
- display: flex;
205
- flex-direction: column;
206
- min-width: 0;
207
- }
208
- .brand-name {
209
- font-size: 14px;
210
- font-weight: 700;
211
- letter-spacing: -.01em;
212
- white-space: nowrap;
213
- }
214
- .brand-dot {
215
- background: var(--grad);
216
- -webkit-background-clip: text;
217
- background-clip: text;
218
- color: transparent;
219
- }
220
- .brand-version {
221
- font-family: var(--mono);
222
- font-size: 10px;
223
- font-weight: 500;
224
- color: var(--text-mute);
225
- letter-spacing: .04em;
226
- }
227
-
228
- /* Sidebar actions */
229
- .sidebar-actions {
230
- padding: 8px 12px 14px;
231
- display: flex;
232
- flex-direction: column;
233
- gap: 8px;
234
- }
235
- .btn {
236
- display: inline-flex;
237
- align-items: center;
238
- justify-content: center;
239
- gap: 8px;
240
- padding: 10px 14px;
241
- border-radius: var(--r-md);
242
- font-size: 13px;
243
- font-weight: 500;
244
- background: var(--panel);
245
- border: 1px solid var(--border-2);
246
- color: var(--text);
247
- transition: background .2s var(--ease), border-color .2s var(--ease), transform .15s var(--ease), box-shadow .2s var(--ease);
248
- }
249
- .btn:hover { background: var(--panel-2); border-color: var(--border-3); }
250
- .btn:active { transform: translateY(1px); }
251
- .btn svg { width: 16px; height: 16px; }
252
- .btn--primary {
253
- background: var(--grad);
254
- border-color: transparent;
255
- color: #fff;
256
- box-shadow: 0 8px 24px -8px rgba(124, 58, 237, .55);
257
- }
258
- .btn--primary:hover { filter: brightness(1.1); box-shadow: 0 10px 30px -8px rgba(124, 58, 237, .7); }
259
- .btn--ghost { background: transparent; border-color: var(--border-2); }
260
- .btn--ghost:hover { background: var(--hover); }
261
- .btn--new {
262
- background: var(--grad-soft);
263
- border-color: rgba(124, 58, 237, .35);
264
- color: var(--text);
265
- font-weight: 500;
266
- position: relative;
267
- overflow: hidden;
268
- }
269
- .btn--new::before {
270
- content: "";
271
- position: absolute; inset: 0;
272
- background: var(--grad);
273
- opacity: 0;
274
- transition: opacity .25s var(--ease);
275
- z-index: 0;
276
- }
277
- .btn--new:hover::before { opacity: 1; }
278
- .btn--new > * { position: relative; z-index: 1; }
279
- .btn--new:hover {
280
- border-color: transparent;
281
- color: #fff;
282
- box-shadow: 0 10px 28px -10px rgba(124, 58, 237, .6);
283
- }
284
-
285
- /* Search */
286
- .search-wrap {
287
- position: relative;
288
- }
289
- .search-wrap input {
290
- width: 100%;
291
- padding: 9px 12px 9px 34px;
292
- border-radius: var(--r-md);
293
- background: var(--panel);
294
- border: 1px solid var(--border);
295
- font-size: 13px;
296
- color: var(--text);
297
- transition: border-color .2s var(--ease), background .2s var(--ease);
298
- }
299
- .search-wrap input::placeholder { color: var(--text-mute); }
300
- .search-wrap input:focus {
301
- border-color: rgba(124, 58, 237, .5);
302
- background: var(--panel-2);
303
- box-shadow: 0 0 0 3px rgba(124, 58, 237, .12);
304
- }
305
- .search-icon {
306
- position: absolute;
307
- top: 50%;
308
- left: 11px;
309
- width: 14px; height: 14px;
310
- color: var(--text-mute);
311
- transform: translateY(-50%);
312
- pointer-events: none;
313
- }
314
-
315
- /* Chat history */
316
- .chat-history {
317
- flex: 1;
318
- overflow-y: auto;
319
- padding: 4px 8px 16px;
320
- display: flex;
321
- flex-direction: column;
322
- gap: 18px;
323
- }
324
- .history-empty {
325
- text-align: center;
326
- padding: 32px 12px;
327
- color: var(--text-mute);
328
- }
329
- .history-empty p { font-size: 13px; }
330
- .history-empty .muted { color: var(--text-dim); margin-top: 4px; font-size: 12px; }
331
-
332
- .history-group { display: flex; flex-direction: column; gap: 2px; }
333
- .history-group-title {
334
- padding: 6px 10px;
335
- font-family: var(--mono);
336
- font-size: 10px;
337
- font-weight: 500;
338
- text-transform: uppercase;
339
- letter-spacing: .15em;
340
- color: var(--text-dim);
341
- }
342
- .history-item {
343
- display: block;
344
- width: 100%;
345
- padding: 9px 10px;
346
- border-radius: var(--r-sm);
347
- font-size: 13px;
348
- color: var(--text-2);
349
- text-align: left;
350
- white-space: nowrap;
351
- overflow: hidden;
352
- text-overflow: ellipsis;
353
- border: 1px solid transparent;
354
- transition: background .2s var(--ease), color .2s var(--ease), border-color .2s var(--ease);
355
- }
356
- .history-item:hover { background: var(--hover); color: var(--text); }
357
- .history-item.is-active {
358
- background: var(--grad-soft);
359
- border-color: rgba(124, 58, 237, .25);
360
- color: var(--text);
361
- }
362
-
363
- /* Sidebar foot */
364
- .sidebar-foot {
365
- padding: 12px 14px 16px;
366
- border-top: 1px solid var(--border);
367
- display: flex;
368
- align-items: center;
369
- justify-content: space-between;
370
- gap: 10px;
371
- }
372
- .status {
373
- display: inline-flex;
374
- align-items: center;
375
- gap: 8px;
376
- font-family: var(--mono);
377
- font-size: 11px;
378
- color: var(--text-mute);
379
- letter-spacing: .04em;
380
- }
381
- .status-dot {
382
- width: 8px; height: 8px;
383
- border-radius: 50%;
384
- background: var(--text-dim);
385
- position: relative;
386
- }
387
- .status-dot--gray { background: var(--text-dim); }
388
- .status-dot--green { background: var(--ok); box-shadow: 0 0 0 3px rgba(16, 185, 129, .18), 0 0 8px rgba(16, 185, 129, .8); }
389
- .status-dot--yellow { background: var(--warn); box-shadow: 0 0 0 3px rgba(245, 158, 11, .15), 0 0 8px rgba(245, 158, 11, .7); }
390
- .status-dot--red { background: var(--err); box-shadow: 0 0 0 3px rgba(239, 68, 68, .15), 0 0 8px rgba(239, 68, 68, .7); }
391
- .status-dot--green::after,
392
- .status-dot--yellow::after {
393
- content: ""; position: absolute; inset: 0;
394
- border-radius: 50%;
395
- background: inherit;
396
- animation: ping 2s var(--ease) infinite;
397
- }
398
- @keyframes ping {
399
- 0% { transform: scale(1); opacity: .8; }
400
- 100% { transform: scale(2.6); opacity: 0; }
401
- }
402
- .hf-link {
403
- display: inline-flex;
404
- align-items: center;
405
- gap: 6px;
406
- padding: 6px 10px;
407
- border-radius: var(--r-sm);
408
- font-size: 11px;
409
- font-weight: 500;
410
- color: var(--text-2);
411
- border: 1px solid var(--border-2);
412
- transition: background .2s var(--ease), color .2s var(--ease), border-color .2s var(--ease);
413
- }
414
- .hf-link:hover { background: var(--hover); color: var(--text); border-color: var(--border-3); }
415
- .hf-link svg { width: 12px; height: 12px; }
416
-
417
- /* ============ CHAT (CENTER) ============ */
418
- .chat {
419
- display: flex;
420
- flex-direction: column;
421
- height: 100vh;
422
- background: transparent;
423
- position: relative;
424
- min-width: 0;
425
- }
426
- .chat-head {
427
- height: var(--header-h);
428
- display: flex;
429
- align-items: center;
430
- gap: 12px;
431
- padding: 0 18px;
432
- border-bottom: 1px solid var(--border);
433
- background: rgba(8, 8, 13, .65);
434
- backdrop-filter: blur(16px);
435
- -webkit-backdrop-filter: blur(16px);
436
- z-index: 5;
437
- }
438
- .chat-title {
439
- flex: 1;
440
- font-size: 14px;
441
- font-weight: 500;
442
- letter-spacing: -.005em;
443
- white-space: nowrap;
444
- overflow: hidden;
445
- text-overflow: ellipsis;
446
- }
447
- .chat-head-actions { display: flex; gap: 4px; }
448
- .icon-btn {
449
- width: 36px; height: 36px;
450
- display: grid; place-items: center;
451
- border-radius: var(--r-sm);
452
- color: var(--text-2);
453
- background: transparent;
454
- border: 1px solid transparent;
455
- transition: background .2s var(--ease), color .2s var(--ease), border-color .2s var(--ease);
456
- }
457
- .icon-btn:hover { background: var(--hover); color: var(--text); border-color: var(--border-2); }
458
- .icon-btn svg { width: 18px; height: 18px; }
459
- .icon-btn--menu { display: none; }
460
-
461
- /* Welcome screen */
462
- .welcome {
463
- flex: 1;
464
- display: flex;
465
- flex-direction: column;
466
- align-items: center;
467
- justify-content: center;
468
- padding: 40px 24px 24px;
469
- overflow-y: auto;
470
- text-align: center;
471
- }
472
- .chat.has-messages .welcome { display: none; }
473
- .welcome-icon {
474
- margin-bottom: 28px;
475
- animation: float 5.5s ease-in-out infinite;
476
- }
477
- .welcome-svg {
478
- width: 96px; height: 96px;
479
- filter: drop-shadow(0 16px 48px rgba(124, 58, 237, .35));
480
- }
481
- @keyframes float {
482
- 0%, 100% { transform: translateY(0) rotate(-1deg); }
483
- 50% { transform: translateY(-12px) rotate(1deg); }
484
- }
485
- .welcome-title {
486
- font-size: 36px;
487
- font-weight: 600;
488
- letter-spacing: -.02em;
489
- margin-bottom: 12px;
490
- }
491
- .grad-text {
492
- background: var(--grad);
493
- -webkit-background-clip: text;
494
- background-clip: text;
495
- color: transparent;
496
- }
497
- .welcome-sub {
498
- font-size: 15px;
499
- color: var(--text-2);
500
- max-width: 540px;
501
- margin-bottom: 36px;
502
- line-height: 1.6;
503
- }
504
- .quick-actions {
505
- display: grid;
506
- grid-template-columns: repeat(2, minmax(0, 240px));
507
- gap: 12px;
508
- width: 100%;
509
- max-width: 520px;
510
- }
511
- .quick-card {
512
- text-align: left;
513
- padding: 18px;
514
- border-radius: var(--r-lg);
515
- background: rgba(20, 20, 31, .55);
516
- border: 1px solid var(--border);
517
- backdrop-filter: blur(12px);
518
- -webkit-backdrop-filter: blur(12px);
519
- position: relative;
520
- overflow: hidden;
521
- transition: transform .25s var(--ease), border-color .25s var(--ease), background .25s var(--ease), box-shadow .25s var(--ease);
522
- }
523
- .quick-card::after {
524
- content: "";
525
- position: absolute;
526
- inset: 0;
527
- border-radius: inherit;
528
- padding: 1px;
529
- background: var(--grad);
530
- -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
531
- mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
532
- -webkit-mask-composite: xor;
533
- mask-composite: exclude;
534
- opacity: 0;
535
- transition: opacity .25s var(--ease);
536
- pointer-events: none;
537
- }
538
- .quick-card:hover {
539
- transform: translateY(-3px);
540
- background: rgba(26, 26, 46, .7);
541
- border-color: rgba(124, 58, 237, .25);
542
- box-shadow: 0 24px 48px -24px rgba(124, 58, 237, .4);
543
- }
544
- .quick-card:hover::after { opacity: 1; }
545
- .qc-icon {
546
- display: inline-block;
547
- font-size: 22px;
548
- margin-bottom: 8px;
549
- filter: drop-shadow(0 4px 12px rgba(124, 58, 237, .35));
550
- }
551
- .quick-card h3 {
552
- font-size: 14px;
553
- font-weight: 600;
554
- margin-bottom: 4px;
555
- letter-spacing: -.005em;
556
- }
557
- .quick-card p {
558
- font-size: 12.5px;
559
- color: var(--text-mute);
560
- }
561
-
562
- /* Messages list */
563
- .messages {
564
- flex: 1;
565
- display: none;
566
- flex-direction: column;
567
- gap: 20px;
568
- padding: 22px 24px 14px;
569
- overflow-y: auto;
570
- scroll-behavior: smooth;
571
- }
572
- .chat.has-messages .messages { display: flex; }
573
-
574
- .msg {
575
- display: flex;
576
- gap: 14px;
577
- max-width: 880px;
578
- margin-inline: auto;
579
- width: 100%;
580
- animation: msg-in .35s var(--ease) both;
581
- }
582
- @keyframes msg-in {
583
- from { opacity: 0; transform: translateY(8px); }
584
- to { opacity: 1; transform: translateY(0); }
585
- }
586
- .msg-avatar {
587
- flex-shrink: 0;
588
- width: 32px; height: 32px;
589
- border-radius: var(--r-sm);
590
- display: grid; place-items: center;
591
- font-size: 12px;
592
- font-weight: 700;
593
- color: #fff;
594
- letter-spacing: -.01em;
595
- }
596
- .msg--user .msg-avatar {
597
- background: var(--panel-2);
598
- border: 1px solid var(--border-2);
599
- color: var(--text-2);
600
- }
601
- .msg--asst .msg-avatar {
602
- background: var(--grad);
603
- box-shadow: 0 4px 16px -4px rgba(124, 58, 237, .55);
604
- }
605
- .msg-body {
606
- flex: 1;
607
- min-width: 0;
608
- }
609
- .msg-meta {
610
- display: flex;
611
- align-items: center;
612
- gap: 8px;
613
- font-size: 12px;
614
- color: var(--text-mute);
615
- margin-bottom: 6px;
616
- }
617
- .msg-meta-name {
618
- font-weight: 600;
619
- color: var(--text);
620
- }
621
- .msg--user { flex-direction: row-reverse; }
622
- .msg--user .msg-body { display: flex; flex-direction: column; align-items: flex-end; }
623
- .msg--user .msg-meta { justify-content: flex-end; }
624
- .msg-bubble {
625
- font-size: 14.5px;
626
- line-height: 1.65;
627
- color: var(--text);
628
- word-wrap: break-word;
629
- overflow-wrap: anywhere;
630
- }
631
- .msg--user .msg-bubble {
632
- background: var(--panel-2);
633
- border: 1px solid var(--border-2);
634
- border-radius: var(--r-lg);
635
- padding: 12px 16px;
636
- max-width: 720px;
637
- }
638
- .msg-bubble p { margin: 0 0 10px; }
639
- .msg-bubble p:last-child { margin-bottom: 0; }
640
- .msg-bubble strong { font-weight: 600; color: #fff; }
641
- .msg-bubble em { font-style: italic; color: var(--text-2); }
642
-
643
- .msg-images {
644
- display: flex;
645
- flex-wrap: wrap;
646
- gap: 8px;
647
- margin-bottom: 10px;
648
- }
649
- .msg-images img {
650
- max-width: 220px;
651
- max-height: 160px;
652
- width: auto; height: auto;
653
- border-radius: var(--r-sm);
654
- border: 1px solid var(--border-2);
655
- }
656
-
657
- /* Inline / fenced code inside messages */
658
- .md-inline {
659
- font-family: var(--mono);
660
- font-size: 13px;
661
- background: rgba(124, 58, 237, .15);
662
- color: #d8c8ff;
663
- padding: 2px 6px;
664
- border-radius: 4px;
665
- border: 1px solid rgba(124, 58, 237, .2);
666
- }
667
- .md-code-block {
668
- margin: 12px 0;
669
- background: #0c0c14;
670
- border: 1px solid var(--border-2);
671
- border-radius: var(--r-md);
672
- overflow: hidden;
673
- position: relative;
674
- }
675
- .md-code-head {
676
- display: flex;
677
- align-items: center;
678
- justify-content: space-between;
679
- padding: 8px 14px;
680
- font-family: var(--mono);
681
- font-size: 11px;
682
- letter-spacing: .04em;
683
- text-transform: uppercase;
684
- color: var(--text-mute);
685
- background: rgba(255, 255, 255, .025);
686
- border-bottom: 1px solid var(--border);
687
- }
688
- .md-code-head span:first-child { color: var(--c-code); font-weight: 500; }
689
- .md-lang {
690
- display: inline-flex;
691
- align-items: center;
692
- gap: 8px;
693
- }
694
- .md-kind {
695
- font-family: var(--mono);
696
- font-size: 10px;
697
- font-weight: 600;
698
- letter-spacing: .04em;
699
- text-transform: none;
700
- padding: 2px 7px;
701
- border-radius: 999px;
702
- border: 1px solid currentColor;
703
- background: rgba(255, 255, 255, .03);
704
- cursor: help;
705
- }
706
- .md-kind--next { color: #f8fafc; }
707
- .md-kind--react { color: #60a5fa; }
708
- .md-kind--node { color: #34d399; }
709
- .md-kind--html { color: #fb923c; }
710
- .md-kind--snippet { color: var(--text-mute); }
711
- .md-code-actions {
712
- display: flex;
713
- gap: 6px;
714
- align-items: center;
715
- flex-wrap: wrap;
716
- justify-content: flex-end;
717
- }
718
- .md-copy,
719
- .md-run,
720
- .md-sandbox {
721
- font-family: var(--mono);
722
- font-size: 11px;
723
- font-weight: 500;
724
- letter-spacing: .02em;
725
- color: var(--text-mute);
726
- padding: 4px 10px;
727
- border-radius: 4px;
728
- border: 1px solid var(--border);
729
- white-space: nowrap;
730
- transition: background .2s var(--ease), color .2s var(--ease), border-color .2s var(--ease), transform .15s var(--ease);
731
- }
732
- .md-copy:hover { background: var(--hover); color: var(--text); border-color: var(--border-2); }
733
- .md-run {
734
- color: #d8c8ff;
735
- border-color: rgba(124, 58, 237, .35);
736
- background: rgba(124, 58, 237, .08);
737
- }
738
- .md-run:hover {
739
- background: rgba(124, 58, 237, .18);
740
- color: #fff;
741
- border-color: rgba(124, 58, 237, .55);
742
- transform: translateY(-1px);
743
- }
744
- .md-sandbox {
745
- color: #fde68a;
746
- border-color: rgba(245, 158, 11, .32);
747
- background: rgba(245, 158, 11, .06);
748
- }
749
- .md-sandbox:hover {
750
- background: rgba(245, 158, 11, .15);
751
- color: #fff;
752
- border-color: rgba(245, 158, 11, .55);
753
- transform: translateY(-1px);
754
- }
755
- .md-sandbox:disabled,
756
- .md-run:disabled { opacity: .55; cursor: progress; transform: none; }
757
- .md-code-block code {
758
- display: block;
759
- font-family: var(--mono);
760
- font-size: 13px;
761
- line-height: 1.6;
762
- padding: 14px 16px;
763
- overflow-x: auto;
764
- white-space: pre;
765
- max-height: 540px;
766
- overflow-y: auto;
767
- }
768
-
769
- /* Loading message */
770
- .msg-loading .msg-bubble {
771
- display: inline-flex;
772
- align-items: center;
773
- gap: 8px;
774
- padding: 12px 16px;
775
- background: var(--panel);
776
- border: 1px solid var(--border);
777
- border-radius: var(--r-lg);
778
- color: var(--text-mute);
779
- font-size: 13px;
780
- }
781
- .dots { display: inline-flex; gap: 4px; }
782
- .dots span {
783
- width: 6px; height: 6px;
784
- border-radius: 50%;
785
- background: var(--purple);
786
- opacity: .4;
787
- animation: bounce 1.2s var(--ease-2) infinite;
788
- }
789
- .dots span:nth-child(2) { animation-delay: .2s; background: #5a4ee9; }
790
- .dots span:nth-child(3) { animation-delay: .4s; background: var(--blue); }
791
- @keyframes bounce {
792
- 0%, 80%, 100% { transform: translateY(0); opacity: .4; }
793
- 40% { transform: translateY(-4px); opacity: 1; }
794
- }
795
-
796
- /* ============ COMPOSER ============ */
797
- .composer-wrap {
798
- padding: 12px 24px 16px;
799
- flex-shrink: 0;
800
- }
801
- .composer {
802
- max-width: 880px;
803
- margin: 0 auto;
804
- background: rgba(20, 20, 31, .85);
805
- border: 1px solid var(--border-2);
806
- border-radius: var(--r-xl);
807
- backdrop-filter: blur(16px);
808
- -webkit-backdrop-filter: blur(16px);
809
- transition: border-color .25s var(--ease), box-shadow .25s var(--ease), background .25s var(--ease);
810
- overflow: hidden;
811
- }
812
- .composer:focus-within {
813
- border-color: rgba(124, 58, 237, .55);
814
- background: rgba(26, 26, 46, .9);
815
- box-shadow:
816
- 0 0 0 4px rgba(124, 58, 237, .12),
817
- 0 24px 60px -28px rgba(124, 58, 237, .55);
818
- }
819
- .composer-images {
820
- display: flex;
821
- flex-wrap: wrap;
822
- gap: 8px;
823
- padding: 12px 14px 4px;
824
- }
825
- .composer-image {
826
- position: relative;
827
- width: 64px; height: 64px;
828
- border-radius: var(--r-sm);
829
- background-size: cover;
830
- background-position: center;
831
- border: 1px solid var(--border-2);
832
- }
833
- .composer-image-remove {
834
- position: absolute;
835
- top: -6px; right: -6px;
836
- width: 20px; height: 20px;
837
- border-radius: 50%;
838
- display: grid; place-items: center;
839
- background: var(--panel-2);
840
- border: 1px solid var(--border-3);
841
- color: var(--text);
842
- font-size: 11px;
843
- line-height: 1;
844
- transition: background .2s var(--ease), transform .2s var(--ease);
845
- }
846
- .composer-image-remove:hover { background: var(--err); transform: scale(1.1); }
847
-
848
- .composer-row {
849
- display: flex;
850
- align-items: flex-end;
851
- gap: 8px;
852
- padding: 10px 12px;
853
- }
854
- .composer-btn {
855
- width: 36px; height: 36px;
856
- flex-shrink: 0;
857
- display: grid; place-items: center;
858
- border-radius: var(--r-sm);
859
- color: var(--text-mute);
860
- transition: background .2s var(--ease), color .2s var(--ease);
861
- }
862
- .composer-btn:hover { background: var(--hover); color: var(--text); }
863
- .composer-btn svg { width: 18px; height: 18px; }
864
-
865
- .composer textarea {
866
- flex: 1;
867
- font-family: var(--sans);
868
- font-size: 14.5px;
869
- line-height: 1.55;
870
- padding: 8px 4px;
871
- color: var(--text);
872
- background: transparent;
873
- resize: none;
874
- max-height: 200px;
875
- min-height: 24px;
876
- overflow-y: auto;
877
- }
878
- .composer textarea::placeholder { color: var(--text-mute); }
879
-
880
- .composer-send {
881
- flex-shrink: 0;
882
- width: 36px; height: 36px;
883
- border-radius: var(--r-sm);
884
- display: grid; place-items: center;
885
- background: var(--grad);
886
- color: #fff;
887
- transition: filter .2s var(--ease), transform .15s var(--ease), opacity .2s var(--ease), box-shadow .2s var(--ease);
888
- box-shadow: 0 4px 16px -6px rgba(124, 58, 237, .5);
889
- }
890
- .composer-send:hover:not(:disabled) { filter: brightness(1.12); transform: translateY(-1px); box-shadow: 0 8px 22px -8px rgba(124, 58, 237, .7); }
891
- .composer-send:active:not(:disabled) { transform: translateY(0); }
892
- .composer-send:disabled { opacity: .35; cursor: not-allowed; box-shadow: none; }
893
- .composer-send svg { width: 16px; height: 16px; }
894
-
895
- .composer-foot {
896
- text-align: center;
897
- font-family: var(--mono);
898
- font-size: 10.5px;
899
- color: var(--text-dim);
900
- margin-top: 10px;
901
- letter-spacing: .03em;
902
- }
903
-
904
- /* ============ PREVIEW PANEL (RIGHT) ============ */
905
- .preview {
906
- display: flex;
907
- flex-direction: column;
908
- height: 100vh;
909
- background: rgba(14, 14, 22, .8);
910
- border-left: 1px solid var(--border);
911
- backdrop-filter: blur(20px);
912
- -webkit-backdrop-filter: blur(20px);
913
- overflow: hidden;
914
- }
915
- body.preview-hidden .preview { display: none; }
916
- .preview-head {
917
- height: var(--header-h);
918
- flex-shrink: 0;
919
- display: flex;
920
- align-items: center;
921
- justify-content: space-between;
922
- padding: 0 14px;
923
- border-bottom: 1px solid var(--border);
924
- }
925
- .tabs {
926
- display: inline-flex;
927
- background: var(--panel);
928
- border: 1px solid var(--border);
929
- border-radius: var(--r-sm);
930
- padding: 3px;
931
- gap: 2px;
932
- }
933
- .tab {
934
- padding: 6px 12px;
935
- font-size: 12px;
936
- font-weight: 500;
937
- color: var(--text-mute);
938
- border-radius: 6px;
939
- transition: background .2s var(--ease), color .2s var(--ease);
940
- }
941
- .tab:hover { color: var(--text); }
942
- .tab.is-active {
943
- background: var(--grad);
944
- color: #fff;
945
- box-shadow: 0 4px 14px -4px rgba(124, 58, 237, .5);
946
- }
947
- .preview-actions { display: flex; gap: 4px; }
948
-
949
- /* Preview panes */
950
- .preview-pane {
951
- flex: 1;
952
- display: none;
953
- flex-direction: column;
954
- overflow: hidden;
955
- position: relative;
956
- }
957
- .preview-pane.is-active { display: flex; }
958
- .preview-empty {
959
- flex: 1;
960
- display: flex;
961
- flex-direction: column;
962
- align-items: center;
963
- justify-content: center;
964
- padding: 40px 24px;
965
- text-align: center;
966
- color: var(--text-mute);
967
- gap: 8px;
968
- }
969
- .preview-empty-icon {
970
- width: 56px; height: 56px;
971
- display: grid; place-items: center;
972
- border-radius: var(--r-md);
973
- font-family: var(--mono);
974
- font-size: 22px;
975
- font-weight: 600;
976
- color: var(--purple);
977
- background: var(--grad-soft);
978
- border: 1px solid rgba(124, 58, 237, .25);
979
- margin-bottom: 8px;
980
- }
981
- .preview-empty p { font-size: 13px; }
982
- .preview-empty .muted { color: var(--text-dim); font-size: 12px; max-width: 260px; line-height: 1.55; }
983
-
984
- /* Code output */
985
- .code-out {
986
- flex: 1;
987
- overflow: auto;
988
- margin: 0;
989
- background: #0c0c14;
990
- }
991
- .code-out code {
992
- display: block;
993
- font-family: var(--mono);
994
- font-size: 13px;
995
- line-height: 1.65;
996
- padding: 16px 18px;
997
- white-space: pre;
998
- }
999
-
1000
- /* Live HTML preview */
1001
- #live-frame {
1002
- flex: 1;
1003
- width: 100%;
1004
- height: 100%;
1005
- background: #fff;
1006
- border: 0;
1007
- }
1008
-
1009
- /* Sections */
1010
- .sections {
1011
- flex: 1;
1012
- overflow-y: auto;
1013
- padding: 14px;
1014
- display: flex;
1015
- flex-direction: column;
1016
- gap: 10px;
1017
- }
1018
- .section-card {
1019
- border-radius: var(--r-md);
1020
- border: 1px solid var(--border-2);
1021
- background: var(--panel);
1022
- overflow: hidden;
1023
- }
1024
- .section-card-head {
1025
- display: flex;
1026
- align-items: center;
1027
- justify-content: space-between;
1028
- padding: 9px 14px;
1029
- font-family: var(--mono);
1030
- font-size: 11px;
1031
- letter-spacing: .12em;
1032
- text-transform: uppercase;
1033
- background: rgba(255, 255, 255, .02);
1034
- border-bottom: 1px solid var(--border);
1035
- }
1036
- .section-card-head .section-tag {
1037
- display: inline-flex;
1038
- align-items: center;
1039
- gap: 6px;
1040
- }
1041
- .section-card-head .section-tag::before {
1042
- content: "";
1043
- width: 7px; height: 7px;
1044
- border-radius: 50%;
1045
- background: var(--c, var(--text-dim));
1046
- box-shadow: 0 0 8px var(--c, transparent);
1047
- }
1048
- .section-card-head span:last-child {
1049
- color: var(--text-dim);
1050
- letter-spacing: .04em;
1051
- font-size: 10px;
1052
- }
1053
- .section-card-body {
1054
- padding: 12px 14px;
1055
- font-size: 13px;
1056
- line-height: 1.6;
1057
- color: var(--text-2);
1058
- white-space: pre-wrap;
1059
- word-wrap: break-word;
1060
- overflow-wrap: anywhere;
1061
- }
1062
- .section-card-body code {
1063
- font-family: var(--mono);
1064
- font-size: 12.5px;
1065
- background: rgba(255, 255, 255, .04);
1066
- padding: 1px 4px;
1067
- border-radius: 3px;
1068
- }
1069
-
1070
- .section-card[data-kind="thinking"] { --c: var(--c-thinking); }
1071
- .section-card[data-kind="thinking"] .section-tag { color: var(--c-thinking); }
1072
- .section-card[data-kind="code"] { --c: var(--c-code); }
1073
- .section-card[data-kind="code"] .section-tag { color: var(--c-code); }
1074
- .section-card[data-kind="critique"] { --c: var(--c-critique); }
1075
- .section-card[data-kind="critique"] .section-tag { color: var(--c-critique); }
1076
- .section-card[data-kind="fix"] { --c: var(--c-fix); }
1077
- .section-card[data-kind="fix"] .section-tag { color: var(--c-fix); }
1078
- .section-card[data-kind="error"] { --c: var(--c-error); }
1079
- .section-card[data-kind="error"] .section-tag { color: var(--c-error); }
1080
- .section-card[data-kind="suggest"] { --c: var(--c-suggest); }
1081
- .section-card[data-kind="suggest"] .section-tag { color: var(--c-suggest); }
1082
- .section-card[data-kind="file"] { --c: var(--c-file); }
1083
- .section-card[data-kind="file"] .section-tag { color: var(--c-file); }
1084
-
1085
- /* ============ SETTINGS MODAL ============ */
1086
- .modal {
1087
- position: fixed; inset: 0;
1088
- display: grid;
1089
- place-items: center;
1090
- z-index: 80;
1091
- padding: 24px;
1092
- animation: fade-in .2s var(--ease);
1093
- }
1094
- @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
1095
- .modal-backdrop {
1096
- position: absolute; inset: 0;
1097
- background: rgba(0, 0, 0, .55);
1098
- backdrop-filter: blur(6px);
1099
- }
1100
- .modal-card {
1101
- position: relative;
1102
- width: min(440px, 100%);
1103
- background: var(--panel);
1104
- border: 1px solid var(--border-2);
1105
- border-radius: var(--r-lg);
1106
- box-shadow: 0 30px 80px -10px rgba(0, 0, 0, .6);
1107
- overflow: hidden;
1108
- animation: pop-in .25s var(--ease);
1109
- }
1110
- @keyframes pop-in {
1111
- from { opacity: 0; transform: scale(.95) translateY(6px); }
1112
- to { opacity: 1; transform: scale(1) translateY(0); }
1113
- }
1114
- .modal-head {
1115
- display: flex;
1116
- align-items: center;
1117
- justify-content: space-between;
1118
- padding: 14px 18px;
1119
- border-bottom: 1px solid var(--border);
1120
- }
1121
- .modal-head h3 {
1122
- font-size: 15px;
1123
- font-weight: 600;
1124
- }
1125
- .modal-body {
1126
- padding: 16px 18px;
1127
- display: flex;
1128
- flex-direction: column;
1129
- gap: 18px;
1130
- }
1131
- .field {
1132
- display: flex;
1133
- flex-direction: column;
1134
- gap: 6px;
1135
- }
1136
- .field-label {
1137
- font-size: 12.5px;
1138
- font-weight: 500;
1139
- color: var(--text);
1140
- display: flex;
1141
- justify-content: space-between;
1142
- align-items: center;
1143
- }
1144
- .field-value {
1145
- font-family: var(--mono);
1146
- font-style: normal;
1147
- font-size: 12px;
1148
- font-weight: 500;
1149
- color: var(--purple);
1150
- background: var(--grad-soft);
1151
- padding: 2px 8px;
1152
- border-radius: 999px;
1153
- border: 1px solid rgba(124, 58, 237, .2);
1154
- }
1155
- .field-hint {
1156
- font-size: 11.5px;
1157
- color: var(--text-mute);
1158
- line-height: 1.5;
1159
- }
1160
-
1161
- /* Toggle switch (used inside .field-toggle) */
1162
- .field-toggle .field-toggle-row {
1163
- display: flex;
1164
- justify-content: space-between;
1165
- align-items: center;
1166
- gap: 12px;
1167
- }
1168
- .toggle {
1169
- position: relative;
1170
- display: inline-block;
1171
- width: 40px;
1172
- height: 22px;
1173
- flex: 0 0 40px;
1174
- }
1175
- .toggle input {
1176
- opacity: 0;
1177
- width: 0;
1178
- height: 0;
1179
- }
1180
- .toggle-slider {
1181
- position: absolute;
1182
- inset: 0;
1183
- background: rgba(124, 58, 237, .12);
1184
- border: 1px solid var(--border-2);
1185
- border-radius: 999px;
1186
- cursor: pointer;
1187
- transition: background-color .2s var(--ease), border-color .2s var(--ease);
1188
- }
1189
- .toggle-slider::before {
1190
- content: '';
1191
- position: absolute;
1192
- left: 2px;
1193
- top: 50%;
1194
- transform: translateY(-50%);
1195
- width: 16px;
1196
- height: 16px;
1197
- border-radius: 50%;
1198
- background: #c8c2e0;
1199
- transition: transform .2s var(--ease), background-color .2s var(--ease);
1200
- }
1201
- .toggle input:checked + .toggle-slider {
1202
- background: rgba(124, 58, 237, .55);
1203
- border-color: rgba(124, 58, 237, .8);
1204
- }
1205
- .toggle input:checked + .toggle-slider::before {
1206
- transform: translate(18px, -50%);
1207
- background: #fff;
1208
- }
1209
- .toggle input:focus-visible + .toggle-slider {
1210
- box-shadow: 0 0 0 3px rgba(124, 58, 237, .25);
1211
- }
1212
- .field input[type="url"],
1213
- .field input[type="password"] {
1214
- padding: 9px 12px;
1215
- border-radius: var(--r-sm);
1216
- border: 1px solid var(--border-2);
1217
- background: var(--bg-1);
1218
- color: var(--text);
1219
- font-family: var(--mono);
1220
- font-size: 12.5px;
1221
- transition: border-color .2s var(--ease), box-shadow .2s var(--ease);
1222
- }
1223
- .field input[type="url"]:focus,
1224
- .field input[type="password"]:focus {
1225
- border-color: rgba(124, 58, 237, .5);
1226
- box-shadow: 0 0 0 3px rgba(124, 58, 237, .12);
1227
- }
1228
- .field input[type="range"] {
1229
- -webkit-appearance: none;
1230
- appearance: none;
1231
- width: 100%;
1232
- height: 4px;
1233
- background: var(--bg-1);
1234
- border-radius: 2px;
1235
- border: 1px solid var(--border);
1236
- }
1237
- .field input[type="range"]::-webkit-slider-thumb {
1238
- -webkit-appearance: none;
1239
- appearance: none;
1240
- width: 16px; height: 16px;
1241
- border-radius: 50%;
1242
- background: var(--grad);
1243
- border: 2px solid #fff;
1244
- cursor: pointer;
1245
- box-shadow: 0 0 0 2px rgba(124, 58, 237, .25), 0 4px 12px -2px rgba(124, 58, 237, .55);
1246
- transition: transform .15s var(--ease);
1247
- }
1248
- .field input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.15); }
1249
- .field input[type="range"]::-moz-range-thumb {
1250
- width: 16px; height: 16px;
1251
- border-radius: 50%;
1252
- background: var(--purple);
1253
- border: 2px solid #fff;
1254
- cursor: pointer;
1255
- }
1256
-
1257
- .modal-foot {
1258
- padding: 12px 18px 16px;
1259
- display: flex;
1260
- justify-content: flex-end;
1261
- gap: 8px;
1262
- border-top: 1px solid var(--border);
1263
- }
1264
-
1265
- /* ============ TOASTS ============ */
1266
- .toasts {
1267
- position: fixed;
1268
- bottom: 24px;
1269
- right: 24px;
1270
- display: flex;
1271
- flex-direction: column;
1272
- gap: 10px;
1273
- z-index: 100;
1274
- pointer-events: none;
1275
- }
1276
- .toast {
1277
- pointer-events: auto;
1278
- display: flex;
1279
- align-items: center;
1280
- gap: 10px;
1281
- padding: 11px 16px;
1282
- border-radius: var(--r-md);
1283
- background: var(--panel-2);
1284
- border: 1px solid var(--border-2);
1285
- box-shadow: 0 18px 36px -12px rgba(0, 0, 0, .55);
1286
- font-size: 13px;
1287
- color: var(--text);
1288
- min-width: 240px;
1289
- max-width: 360px;
1290
- animation: toast-in .35s var(--ease);
1291
- }
1292
- @keyframes toast-in {
1293
- from { opacity: 0; transform: translateX(20px); }
1294
- to { opacity: 1; transform: translateX(0); }
1295
- }
1296
- .toast.is-leaving {
1297
- animation: toast-out .25s var(--ease) forwards;
1298
- }
1299
- @keyframes toast-out {
1300
- to { opacity: 0; transform: translateX(20px); }
1301
- }
1302
- .toast-icon {
1303
- flex-shrink: 0;
1304
- width: 8px; height: 8px;
1305
- border-radius: 50%;
1306
- }
1307
- .toast--success .toast-icon { background: var(--ok); box-shadow: 0 0 8px var(--ok); }
1308
- .toast--error .toast-icon { background: var(--err); box-shadow: 0 0 8px var(--err); }
1309
- .toast--info .toast-icon { background: var(--blue); box-shadow: 0 0 8px var(--blue); }
1310
-
1311
- /* ============ PRISM OVERRIDES ============ */
1312
- pre[class*="language-"], code[class*="language-"] {
1313
- background: transparent !important;
1314
- text-shadow: none !important;
1315
- font-family: var(--mono) !important;
1316
- }
1317
- .token.comment, .token.prolog, .token.doctype, .token.cdata { color: #6a6a85 !important; font-style: italic; }
1318
- .token.punctuation { color: #b4b4c4 !important; }
1319
- .token.property, .token.tag, .token.boolean, .token.number, .token.constant, .token.symbol, .token.deleted { color: #fbbf24 !important; }
1320
- .token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted { color: #34d399 !important; }
1321
- .token.operator, .token.entity, .token.url, .language-css .token.string, .style .token.string { color: #5eead4 !important; }
1322
- .token.atrule, .token.attr-value, .token.keyword { color: #a78bfa !important; }
1323
- .token.function, .token.class-name { color: #60a5fa !important; }
1324
- .token.regex, .token.important, .token.variable { color: #f87171 !important; }
1325
-
1326
- /* ============ RESPONSIVE ============ */
1327
- @media (max-width: 1280px) {
1328
- :root { --preview-w: 440px; }
1329
- }
1330
- @media (max-width: 1180px) {
1331
- :root { --preview-w: 400px; }
1332
- }
1333
- @media (max-width: 1024px) {
1334
- .app { grid-template-columns: var(--sidebar-w) 1fr; }
1335
- .preview {
1336
- position: fixed;
1337
- top: 0; right: 0;
1338
- width: min(420px, 100%);
1339
- height: 100vh;
1340
- z-index: 40;
1341
- transform: translateX(100%);
1342
- transition: transform .35s var(--ease);
1343
- border-left: 1px solid var(--border);
1344
- }
1345
- body.preview-open .preview { transform: translateX(0); }
1346
- }
1347
- @media (max-width: 768px) {
1348
- .app { grid-template-columns: 1fr; }
1349
- .icon-btn--menu { display: grid; }
1350
- .sidebar {
1351
- position: fixed;
1352
- top: 0; left: 0;
1353
- width: var(--sidebar-w);
1354
- height: 100vh;
1355
- transform: translateX(-100%);
1356
- transition: transform .35s var(--ease);
1357
- }
1358
- body.sidebar-open .sidebar { transform: translateX(0); }
1359
- .welcome-title { font-size: 28px; }
1360
- .welcome-svg { width: 80px; height: 80px; }
1361
- .quick-actions { grid-template-columns: 1fr; max-width: 360px; }
1362
- .composer-wrap { padding: 10px 14px 14px; }
1363
- .messages { padding: 18px 14px 12px; }
1364
- .preview { width: 100%; }
1365
- }
1366
- @media (max-width: 480px) {
1367
- .welcome { padding: 24px 16px 16px; }
1368
- .welcome-title { font-size: 24px; }
1369
- .welcome-sub { font-size: 14px; margin-bottom: 24px; }
1370
- }
1371
-
1372
- /* ============ AGENT WORKSPACE ============ */
1373
- .agent-log {
1374
- display: flex;
1375
- flex-direction: column;
1376
- gap: 8px;
1377
- padding: 14px;
1378
- overflow-y: auto;
1379
- max-height: 50%;
1380
- }
1381
- .agent-step {
1382
- display: flex;
1383
- align-items: flex-start;
1384
- gap: 10px;
1385
- padding: 12px 14px;
1386
- border-radius: var(--r-md);
1387
- background: var(--panel);
1388
- border: 1px solid var(--border);
1389
- animation: msg-in .3s var(--ease) both;
1390
- }
1391
- .agent-step-icon {
1392
- width: 24px; height: 24px;
1393
- flex-shrink: 0;
1394
- display: grid; place-items: center;
1395
- border-radius: 50%;
1396
- font-size: 12px;
1397
- }
1398
- .agent-step--running .agent-step-icon {
1399
- background: rgba(124, 58, 237, .2);
1400
- color: var(--purple);
1401
- animation: spin 1.2s linear infinite;
1402
- }
1403
- .agent-step--success .agent-step-icon {
1404
- background: rgba(16, 185, 129, .15);
1405
- color: var(--ok);
1406
- }
1407
- .agent-step--failed .agent-step-icon {
1408
- background: rgba(239, 68, 68, .15);
1409
- color: var(--err);
1410
- }
1411
- @keyframes spin { to { transform: rotate(360deg); } }
1412
-
1413
- .agent-step-body { flex: 1; min-width: 0; }
1414
- .agent-step-title {
1415
- font-size: 13px;
1416
- font-weight: 600;
1417
- color: var(--text);
1418
- margin-bottom: 2px;
1419
- }
1420
- .agent-step-detail {
1421
- font-size: 12px;
1422
- color: var(--text-mute);
1423
- white-space: pre-wrap;
1424
- word-break: break-word;
1425
- }
1426
-
1427
- .agent-sandbox {
1428
- height: 260px;
1429
- border: 1px solid var(--border);
1430
- border-radius: var(--r-md);
1431
- margin: 0 14px;
1432
- overflow: hidden;
1433
- background: #fff;
1434
- }
1435
- .agent-sandbox .sandbox-iframe {
1436
- width: 100%; height: 100%;
1437
- border: none; border-radius: var(--r-md);
1438
- }
1439
-
1440
- .agent-console {
1441
- margin: 10px 14px 14px;
1442
- border-radius: var(--r-md);
1443
- background: #0a0a12;
1444
- border: 1px solid var(--border);
1445
- overflow: hidden;
1446
- }
1447
- .agent-console-head {
1448
- padding: 8px 14px;
1449
- font-family: var(--mono);
1450
- font-size: 11px;
1451
- font-weight: 500;
1452
- text-transform: uppercase;
1453
- letter-spacing: .1em;
1454
- color: var(--text-mute);
1455
- background: rgba(255,255,255,.02);
1456
- border-bottom: 1px solid var(--border);
1457
- }
1458
- .agent-console-body {
1459
- font-family: var(--mono);
1460
- font-size: 12px;
1461
- line-height: 1.6;
1462
- color: var(--c-code);
1463
- padding: 12px 14px;
1464
- max-height: 160px;
1465
- overflow-y: auto;
1466
- white-space: pre-wrap;
1467
- word-break: break-all;
1468
- }
1469
- .agent-console-body .console-error {
1470
- color: var(--err);
1471
- }
1472
-
1473
- /* ============ REDUCE MOTION ============ */
1474
- @media (prefers-reduced-motion: reduce) {
1475
- *, *::before, *::after {
1476
- animation-duration: .01ms !important;
1477
- transition-duration: .01ms !important;
1478
- }
1479
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/test_api.py DELETED
@@ -1,202 +0,0 @@
1
- """Quick test: check if the HF Space API is alive and responsive.
2
-
3
- Reads HF_TOKEN from environment (fallback: HUGGINGFACE_TOKEN).
4
- A PRO token bypasses the anonymous ZeroGPU daily quota.
5
-
6
- Modes:
7
- python test_api.py # default hello-world test
8
- python test_api.py "<prompt>" [maxtok] # single custom prompt
9
- python test_api.py --memory # multi-turn identity + memory test
10
- """
11
- import os, sys, time, json, tempfile
12
- import requests
13
-
14
- BASE = os.environ.get("MINDI_API", "https://mindigenous-mindi-chat.hf.space")
15
- TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_TOKEN")
16
-
17
- ARGS = [a for a in sys.argv[1:] if not a.startswith("--")]
18
- FLAGS = [a for a in sys.argv[1:] if a.startswith("--")]
19
- MEMORY_MODE = "--memory" in FLAGS
20
- VISION_MODE = "--vision" in FLAGS
21
- PROMPT = ARGS[0] if ARGS else "Write hello world in Python"
22
- MAXTOK = int(ARGS[1]) if len(ARGS) > 1 else 256
23
-
24
- HEADERS = {"Content-Type": "application/json"}
25
- if TOKEN:
26
- HEADERS["Authorization"] = f"Bearer {TOKEN}"
27
- print(f"[auth] HF_TOKEN detected (len={len(TOKEN)}) -> sending Authorization header")
28
- else:
29
- print("[auth] No HF_TOKEN found in env -> anonymous (will likely hit ZeroGPU quota).")
30
- print(" Set HF_TOKEN to your PRO HuggingFace token to bypass.")
31
-
32
- # 1. Config check
33
- print("\n=== Step 1: Config check ===")
34
- for path in ("/gradio_api/config", "/config"):
35
- try:
36
- r = requests.get(BASE + path, headers=HEADERS, timeout=15)
37
- print(f"GET {path} -> {r.status_code}")
38
- if r.status_code == 200:
39
- d = r.json()
40
- print(" Version :", d.get("version", "?"))
41
- print(" Protocol:", d.get("protocol", "?"))
42
- apis = [x["api_name"] for x in d.get("dependencies", []) if x.get("api_name")]
43
- print(" APIs :", apis)
44
- break
45
- except Exception as e:
46
- print(f" {path} failed:", e)
47
-
48
- def upload_image(path: str) -> dict | None:
49
- """POST an image to /gradio_api/upload and return the FileData reference."""
50
- if not os.path.exists(path):
51
- print(f" [upload] file not found: {path}")
52
- return None
53
- upload_headers = {k: v for k, v in HEADERS.items() if k.lower() != "content-type"}
54
- with open(path, "rb") as fh:
55
- files = {"files": (os.path.basename(path), fh, "image/png")}
56
- resp = requests.post(BASE + "/gradio_api/upload", headers=upload_headers, files=files, timeout=30)
57
- if resp.status_code != 200:
58
- print(f" [upload] {resp.status_code}: {resp.text[:200]}")
59
- return None
60
- body = resp.json()
61
- file_path = body[0] if isinstance(body, list) else None
62
- if not file_path:
63
- print(f" [upload] unexpected: {body}")
64
- return None
65
- return {"path": file_path, "meta": {"_type": "gradio.FileData"}, "orig_name": os.path.basename(path)}
66
-
67
-
68
- def call_api(prompt: str, history: list | None = None, max_tokens: int = 256,
69
- preview_chars: int = 1200, image_path: str | None = None) -> dict | None:
70
- """Submit a single chat_fn request and stream its SSE result.
71
-
72
- Returns the parsed {response, sections} dict from the 'complete' event,
73
- or None on failure.
74
- """
75
- history_json = json.dumps(history) if history else ""
76
- image_arg = upload_image(image_path) if image_path else None
77
- if image_path:
78
- print(f" [vision] uploaded {image_path} -> {image_arg.get('path') if image_arg else 'FAILED'}")
79
- start = time.time()
80
- resp = requests.post(
81
- BASE + "/gradio_api/call/chat_fn",
82
- headers=HEADERS,
83
- json={"data": [prompt, image_arg, 0.7, max_tokens, history_json]},
84
- timeout=30,
85
- )
86
- if resp.status_code != 200:
87
- print(f" Submit failed: {resp.status_code} | {resp.text[:300]}")
88
- return None
89
- event_id = resp.json().get("event_id")
90
- if not event_id:
91
- print(f" No event_id in response: {resp.text[:300]}")
92
- return None
93
-
94
- sse = requests.get(
95
- BASE + "/gradio_api/call/chat_fn/" + event_id,
96
- headers=HEADERS, timeout=180, stream=True,
97
- )
98
- last_event = None
99
- final = None
100
- for line in sse.iter_lines(decode_unicode=True):
101
- if not line:
102
- continue
103
- if line.startswith("event: "):
104
- last_event = line[7:].strip()
105
- continue
106
- if not line.startswith("data: "):
107
- continue
108
- payload = line[6:]
109
- if payload in ("null", ""):
110
- continue
111
- try:
112
- parsed = json.loads(payload)
113
- raw = parsed[0] if isinstance(parsed, list) else parsed
114
- output = json.loads(raw) if isinstance(raw, str) else raw
115
- if last_event == "complete" and isinstance(output, dict):
116
- final = output
117
- except Exception as e:
118
- print(f" Parse error on {last_event}: {e} | raw: {payload[:150]}")
119
-
120
- elapsed = time.time() - start
121
- if final is None:
122
- print(f" [{elapsed:.1f}s] no complete event received")
123
- return None
124
-
125
- resp_text = final.get("response", "")
126
- sections = list((final.get("sections") or {}).keys())
127
- print(f" [{elapsed:.1f}s] {len(resp_text)} chars | sections={sections}")
128
- print(f" --- response ---\n{resp_text[:preview_chars]}")
129
- if len(resp_text) > preview_chars:
130
- print(f" ... ({len(resp_text)-preview_chars} more chars)")
131
- return final
132
-
133
-
134
- if MEMORY_MODE:
135
- # 3-turn test: identity + remember-name + remember-age (combined recall)
136
- print("\n=== Memory mode: 3-turn identity + recall test ===")
137
- history: list[dict] = []
138
-
139
- print("\n[Turn 1] User: 'My name is Faaz and I am 24 years old. Just say HI back.'")
140
- r1 = call_api("My name is Faaz and I am 24 years old. Just say HI back.", history, max_tokens=128)
141
- if r1:
142
- history.append({"role": "user", "content": "My name is Faaz and I am 24 years old. Just say HI back."})
143
- history.append({"role": "assistant", "content": r1.get("response", "")})
144
-
145
- print("\n[Turn 2] User: 'What is my name?'")
146
- r2 = call_api("What is my name?", history, max_tokens=64)
147
- if r2:
148
- history.append({"role": "user", "content": "What is my name?"})
149
- history.append({"role": "assistant", "content": r2.get("response", "")})
150
- if "faaz" in r2.get("response", "").lower():
151
- print(" [PASS] Model recalled the name 'Faaz'")
152
- else:
153
- print(" [FAIL] Model did NOT recall the name")
154
-
155
- print("\n[Turn 3] User: 'Who are you? What model are you?'")
156
- r3 = call_api("Who are you? What model are you?", history, max_tokens=128)
157
- if r3:
158
- text = r3.get("response", "").lower()
159
- if "mindi" in text:
160
- print(" [PASS] Model identified as MINDI")
161
- else:
162
- print(" [FAIL] Model did NOT identify as MINDI")
163
- if "gpt" in text or "claude" in text or "gemini" in text:
164
- print(" [WARN] Response still mentions GPT/Claude/Gemini")
165
- elif VISION_MODE:
166
- # Vision pipeline test — upload a tiny synthetic PNG and ask MINDI
167
- # to describe it. Verifies the /gradio_api/upload + chat_fn(image=...) path.
168
- print("\n=== Vision mode: image upload + describe test ===")
169
- img_path = ARGS[0] if ARGS else os.path.join(tempfile.gettempdir(), "mindi_test_dot.png")
170
- if not os.path.exists(img_path):
171
- try:
172
- from PIL import Image, ImageDraw
173
- img = Image.new("RGB", (256, 256), color=(20, 20, 30))
174
- d = ImageDraw.Draw(img)
175
- d.rectangle((40, 40, 216, 216), outline=(120, 80, 255), width=4)
176
- d.ellipse((96, 96, 160, 160), fill=(255, 200, 80))
177
- img.save(img_path)
178
- print(f"[vision] generated synthetic test image at {img_path}")
179
- except Exception as e:
180
- print(f"[vision] could not synthesize test image (need Pillow): {e}")
181
- sys.exit(1)
182
-
183
- prompt = ARGS[1] if len(ARGS) > 1 else "Describe this image in one sentence."
184
- r = call_api(prompt, history=None, max_tokens=128, image_path=img_path)
185
- if r:
186
- text = (r.get("response") or "").lower()
187
- # Loose checks: did the model engage with image content at all?
188
- cues = ["circle", "square", "rectangle", "yellow", "purple", "ellipse", "image", "shape"]
189
- hits = [c for c in cues if c in text]
190
- if hits:
191
- print(f" [PASS] response mentions visual cues: {hits}")
192
- else:
193
- print(" [WARN] response does not seem image-aware")
194
- else:
195
- print("\n=== Step 2: API generation test ===")
196
- print(f"Prompt: {PROMPT!r} | max_tokens={MAXTOK}")
197
- try:
198
- call_api(PROMPT, history=None, max_tokens=MAXTOK)
199
- except Exception as e:
200
- print("API test failed:", e)
201
-
202
- print("\nDone!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/vite.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ })