Faaz commited on
Commit
6e8fa2a
Β·
1 Parent(s): 38afdff

Add HF Space backend and frontend with HF token auth

Browse files

Frontend (frontend/):
- index.html: three-panel layout (sidebar, chat, code preview) with
settings modal
- app.js: chat logic, Gradio 5.x SSE v3 protocol (POST + GET event_id),
HF token Authorization header, agent integration, demo fallback,
cold-start hint
- agent.js, sandbox.js: Plan->Generate->Execute->Verify->Fix agent loop
with iframe sandbox for code preview
- styles.css: dark theme with purple/blue gradients
- test_api.py: CLI test that reads HF_TOKEN from env, supports custom
prompt + max_tokens, parses SSE events, falls back to /config when
/gradio_api/config returns 404

Backend (hf_space/):
- app.py: ZeroGPU Space loading full bf16 MINDI-1.5 (Qwen2.5-Coder-7B
+ LoRA + CLIP + fusion). Model load is inside @spaces.GPU function
per ZeroGPU requirements. chat_fn returns clean JSON with quota error
handling.
- requirements.txt: gradio>=5.0, transformers, peft, torch, spaces
- README.md: SDK metadata for HF Space

Fixes Session 4 ZeroGPU quota issue: anonymous requests immediately hit
the per-IP daily bucket. Sending Authorization: Bearer <hf_token> routes
through the user's PRO quota (8x larger). Frontend Settings modal now
has a password field for the token; test_api.py reads HF_TOKEN env var.
detectAuthError() in app.js surfaces a clear actionable toast when
quota is hit.

frontend/agent.js ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,1645 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ temperature: 0.7,
44
+ maxTokens: 2048,
45
+ chats: [], // [{id, title, createdAt, updatedAt, messages: [{role, content, images?}]}]
46
+ currentId: null,
47
+ });
48
+
49
+ const state = loadState();
50
+
51
+ function loadState() {
52
+ try {
53
+ const raw = localStorage.getItem(STORAGE_KEY);
54
+ if (!raw) return defaultState();
55
+ const parsed = JSON.parse(raw);
56
+ return Object.assign(defaultState(), parsed);
57
+ } catch {
58
+ return defaultState();
59
+ }
60
+ }
61
+
62
+ function saveState() {
63
+ try {
64
+ localStorage.setItem(STORAGE_KEY, JSON.stringify({
65
+ apiUrl: state.apiUrl,
66
+ hfToken: state.hfToken,
67
+ temperature: state.temperature,
68
+ maxTokens: state.maxTokens,
69
+ chats: state.chats,
70
+ currentId: state.currentId,
71
+ }));
72
+ } catch (e) {
73
+ console.warn('[mindi] failed to save state', e);
74
+ }
75
+ }
76
+
77
+ // Runtime-only state (not persisted)
78
+ const runtime = {
79
+ status: 'connecting', // connecting | online | demo | offline
80
+ pendingImages: [], // [{name, dataUrl}]
81
+ isSending: false,
82
+ lastCode: null, // {language, code}
83
+ lastSections: null, // {thinking: [], ...}
84
+ };
85
+
86
+ // ----------------------------------------------------------------
87
+ // DOM
88
+ // ----------------------------------------------------------------
89
+ const $ = (s) => document.querySelector(s);
90
+ const $$ = (s) => Array.from(document.querySelectorAll(s));
91
+
92
+ const els = {
93
+ body: document.body,
94
+ sidebar: $('#sidebar'),
95
+ scrim: $('#scrim'),
96
+ brand: $('#brand'),
97
+ newChatBtn: $('#new-chat-btn'),
98
+ search: $('#search'),
99
+ history: $('#chat-history'),
100
+ historyEmpty: $('#history-empty'),
101
+ statusDot: $('#status-dot'),
102
+ statusText: $('#status-text'),
103
+
104
+ chat: $('#chat'),
105
+ hamburger: $('#hamburger'),
106
+ chatTitle: $('#chat-title'),
107
+ togglePreview: $('#toggle-preview'),
108
+
109
+ welcome: $('#welcome'),
110
+ quickCards: $$('.quick-card'),
111
+ messages: $('#messages'),
112
+
113
+ composer: $('#composer'),
114
+ composerImages: $('#composer-images'),
115
+ attachBtn: $('#attach-btn'),
116
+ fileInput: $('#file-input'),
117
+ promptInput: $('#prompt-input'),
118
+ sendBtn: $('#send-btn'),
119
+
120
+ preview: $('#preview'),
121
+ tabs: $$('.tab'),
122
+ panes: $$('.preview-pane'),
123
+ copyCode: $('#copy-code'),
124
+ downloadCode: $('#download-code'),
125
+ codeOut: $('#code-out'),
126
+ codeOutInner: $('#code-out-inner'),
127
+ emptyCode: $('#empty-code'),
128
+ liveFrame: $('#live-frame'),
129
+ emptyLive: $('#empty-live'),
130
+ sections: $('#sections'),
131
+ emptySections: $('#empty-sections'),
132
+
133
+ settingsModal: $('#settings-modal'),
134
+ settingsUrl: $('#settings-url'),
135
+ settingsHfToken:$('#settings-hf-token'),
136
+ hfTokenStatus: $('#hf-token-status'),
137
+ settingsTemp: $('#settings-temp'),
138
+ settingsTokens: $('#settings-tokens'),
139
+ tempVal: $('#temp-val'),
140
+ tokensVal: $('#tokens-val'),
141
+ saveSettings: $('#save-settings'),
142
+
143
+ toasts: $('#toasts'),
144
+ };
145
+
146
+ // ----------------------------------------------------------------
147
+ // Utilities
148
+ // ----------------------------------------------------------------
149
+ function uid() {
150
+ return 'c-' + Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
151
+ }
152
+ function escapeHtml(s) {
153
+ return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
154
+ .replace(/"/g, '&quot;').replace(/'/g, '&#039;');
155
+ }
156
+ function escapeAttr(s) {
157
+ return escapeHtml(s);
158
+ }
159
+ function fileToDataUrl(file) {
160
+ return new Promise((resolve, reject) => {
161
+ const reader = new FileReader();
162
+ reader.onload = () => resolve(reader.result);
163
+ reader.onerror = reject;
164
+ reader.readAsDataURL(file);
165
+ });
166
+ }
167
+ function debounce(fn, ms) {
168
+ let t = null;
169
+ return (...args) => {
170
+ clearTimeout(t);
171
+ t = setTimeout(() => fn(...args), ms);
172
+ };
173
+ }
174
+ function downloadFile(filename, content) {
175
+ const blob = new Blob([content], { type: 'text/plain' });
176
+ const url = URL.createObjectURL(blob);
177
+ const a = document.createElement('a');
178
+ a.href = url;
179
+ a.download = filename;
180
+ document.body.appendChild(a);
181
+ a.click();
182
+ document.body.removeChild(a);
183
+ URL.revokeObjectURL(url);
184
+ }
185
+ function relativeDateGroup(ts) {
186
+ const d = new Date(ts);
187
+ const now = new Date();
188
+ const start = (x) => { const z = new Date(x); z.setHours(0,0,0,0); return z; };
189
+ const today = start(now);
190
+ const yesterday = new Date(today); yesterday.setDate(today.getDate() - 1);
191
+ const week = new Date(today); week.setDate(today.getDate() - 7);
192
+
193
+ if (d >= today) return 'Today';
194
+ if (d >= yesterday) return 'Yesterday';
195
+ if (d >= week) return 'This Week';
196
+ return 'Earlier';
197
+ }
198
+ function languageFromCode(code) {
199
+ const trimmed = code.trim();
200
+ if (/^<!doctype|^<html|^<\w+[\s>]/i.test(trimmed)) return 'markup';
201
+ if (/^(import|from|def|class|print|if __name__)/m.test(trimmed)) return 'python';
202
+ if (/^(import|export|const|function|class|let|var|=>)/m.test(trimmed)) return 'javascript';
203
+ if (/^[\s\S]*\{[\s\S]*\}\s*$/.test(trimmed) && /^\s*"\w+"\s*:/m.test(trimmed)) return 'json';
204
+ if (/(SELECT|INSERT|UPDATE|DELETE|CREATE TABLE)/i.test(trimmed)) return 'sql';
205
+ if (/^[\.#]?[\w-]+\s*\{[^}]*:\s*[^;]+;/.test(trimmed)) return 'css';
206
+ return 'plaintext';
207
+ }
208
+
209
+ // ----------------------------------------------------------------
210
+ // Output cleaning + section parsing
211
+ // ----------------------------------------------------------------
212
+ // Strip all special tokens for chat display
213
+ function cleanForDisplay(raw) {
214
+ if (!raw) return '';
215
+ let t = String(raw);
216
+
217
+ // Section start/end tokens
218
+ Object.keys(TOKEN_TO_KEY).forEach((tok) => {
219
+ t = t.replace(new RegExp(`<\\|${tok}_start\\|>`, 'g'), '');
220
+ t = t.replace(new RegExp(`<\\|${tok}_end\\|>`, 'g'), '');
221
+ });
222
+
223
+ // Conversation tokens
224
+ t = t.replace(/<\|im_start\|>/g, '');
225
+ t = t.replace(/<\|im_end\|>/g, '');
226
+ t = t.replace(/<\|endoftext\|>/g, '');
227
+
228
+ // Role prefixes at line starts
229
+ t = t.replace(/^(system|user|assistant)\s*\n/gim, '');
230
+
231
+ return t.trim();
232
+ }
233
+
234
+ // Parse the special token sections out of the raw response
235
+ function parseSections(raw) {
236
+ const sections = {};
237
+ SECTION_ORDER.forEach((k) => { sections[k] = []; });
238
+ if (!raw) return sections;
239
+
240
+ const text = String(raw);
241
+ Object.entries(TOKEN_TO_KEY).forEach(([tok, key]) => {
242
+ const re = new RegExp(`<\\|${tok}_start\\|>([\\s\\S]*?)<\\|${tok}_end\\|>`, 'g');
243
+ let m;
244
+ while ((m = re.exec(text)) !== null) {
245
+ const body = m[1].trim();
246
+ if (body) sections[key].push(body);
247
+ }
248
+ });
249
+ return sections;
250
+ }
251
+
252
+ // Merge API-provided sections with parsed ones (API wins, parsed fills gaps)
253
+ function mergeSections(api, parsed) {
254
+ const merged = {};
255
+ SECTION_ORDER.forEach((k) => {
256
+ const a = (api && Array.isArray(api[k])) ? api[k] : [];
257
+ const p = (parsed && Array.isArray(parsed[k])) ? parsed[k] : [];
258
+ merged[k] = a.length ? a : p;
259
+ });
260
+ return merged;
261
+ }
262
+
263
+ // Extract last fenced code block from the response text
264
+ function extractLastCodeBlock(text) {
265
+ if (!text) return null;
266
+ const re = /```(\w+)?\s*\n([\s\S]*?)```/g;
267
+ let last = null, m;
268
+ while ((m = re.exec(text)) !== null) {
269
+ last = { language: (m[1] || '').toLowerCase() || null, code: m[2] };
270
+ }
271
+ if (last) {
272
+ if (!last.language) last.language = languageFromCode(last.code);
273
+ return last;
274
+ }
275
+ return null;
276
+ }
277
+
278
+ // ----------------------------------------------------------------
279
+ // Markdown renderer (limited: paragraphs, fenced code, inline code, bold/italic)
280
+ // ----------------------------------------------------------------
281
+ function renderMarkdown(text) {
282
+ if (!text) return '';
283
+
284
+ // Tokenize: split into fenced-code parts and text parts
285
+ const segments = [];
286
+ const re = /```(\w+)?\s*\n([\s\S]*?)```/g;
287
+ let lastIdx = 0, m;
288
+ while ((m = re.exec(text)) !== null) {
289
+ if (m.index > lastIdx) {
290
+ segments.push({ type: 'text', value: text.slice(lastIdx, m.index) });
291
+ }
292
+ segments.push({ type: 'code', lang: (m[1] || '').toLowerCase() || null, value: m[2] });
293
+ lastIdx = re.lastIndex;
294
+ }
295
+ if (lastIdx < text.length) {
296
+ segments.push({ type: 'text', value: text.slice(lastIdx) });
297
+ }
298
+
299
+ return segments.map((seg) => {
300
+ if (seg.type === 'code') {
301
+ const lang = seg.lang || languageFromCode(seg.value);
302
+ const safe = escapeHtml(seg.value);
303
+ const dataCode = escapeAttr(seg.value);
304
+ return (
305
+ `<pre class="md-code-block">` +
306
+ `<div class="md-code-head">` +
307
+ `<span>${escapeHtml(lang)}</span>` +
308
+ `<button class="md-copy" data-code="${dataCode}" type="button">Copy</button>` +
309
+ `</div>` +
310
+ `<code class="language-${escapeHtml(lang)}">${safe}</code>` +
311
+ `</pre>`
312
+ );
313
+ }
314
+ // text segment
315
+ let h = seg.value.trim();
316
+ if (!h) return '';
317
+ h = escapeHtml(h);
318
+ h = h.replace(/`([^`\n]+)`/g, '<code class="md-inline">$1</code>');
319
+ h = h.replace(/\*\*([^*\n]+)\*\*/g, '<strong>$1</strong>');
320
+ h = h.replace(/(^|[\s(])\*([^*\n]+)\*(?=[\s).,!?:;]|$)/g, '$1<em>$2</em>');
321
+ return h.split(/\n{2,}/)
322
+ .map((p) => '<p>' + p.replace(/\n/g, '<br>') + '</p>')
323
+ .join('');
324
+ }).join('');
325
+ }
326
+
327
+ // ----------------------------------------------------------------
328
+ // API client
329
+ // ----------------------------------------------------------------
330
+ function authHeaders(extra) {
331
+ const h = Object.assign({}, extra || {});
332
+ if (state.hfToken) h['Authorization'] = `Bearer ${state.hfToken}`;
333
+ return h;
334
+ }
335
+
336
+ // Detect responses that came back as a quota / auth error from the
337
+ // backend's chat_fn try/except, so we can show actionable UX.
338
+ function detectAuthError(result) {
339
+ if (!result) return null;
340
+ const text = String(result.response || '');
341
+ const errs = (result.sections && result.sections.error) || [];
342
+ const blob = (text + ' ' + errs.join(' ')).toLowerCase();
343
+ if (/zerogpu|gpu quota|out of .* quota|exceeded .* quota|unlogged user/.test(blob)) {
344
+ return state.hfToken
345
+ ? 'Your HF token hit its ZeroGPU quota. Wait for the daily reset or use a PRO token.'
346
+ : 'Anonymous ZeroGPU quota exhausted. Open Settings (double-click the MINDI logo) and paste your HF token.';
347
+ }
348
+ return null;
349
+ }
350
+
351
+ async function pingHealth() {
352
+ if (!state.apiUrl) {
353
+ setStatus('demo', 'Demo Mode');
354
+ return;
355
+ }
356
+ try {
357
+ const base = state.apiUrl.replace(/\/$/, '');
358
+ const isGradio = base.includes('hf.space') || base.includes('huggingface.co');
359
+
360
+ if (isGradio) {
361
+ // For Gradio/HF Spaces: check the root URL which returns the Gradio page
362
+ const res = await fetch(base, { method: 'HEAD', mode: 'no-cors' }).catch(() => null);
363
+ // no-cors always returns opaque response, so we check for network errors
364
+ if (res) {
365
+ setStatus('online', 'MINDI Β· HF Space');
366
+ } else {
367
+ setStatus('demo', 'Demo Mode (Space unreachable)');
368
+ }
369
+ } else {
370
+ // Direct REST API health check
371
+ try {
372
+ const res = await fetch(`${base}/api/health`, { method: 'GET', headers: { 'Accept': 'application/json' } });
373
+ if (res.ok) {
374
+ const d = await res.json().catch(() => ({}));
375
+ setStatus('online', `${d.model || 'MINDI'} Β· online`);
376
+ } else {
377
+ setStatus('demo', 'Demo Mode');
378
+ }
379
+ } catch {
380
+ setStatus('demo', 'Demo Mode');
381
+ }
382
+ }
383
+ } catch {
384
+ setStatus('demo', 'Demo Mode');
385
+ }
386
+ }
387
+
388
+ async function callGenerate(prompt, image, signal) {
389
+ const base = state.apiUrl.replace(/\/$/, '');
390
+
391
+ // Detect if this is a Gradio HF Space
392
+ const isGradio = base.includes('hf.space') || base.includes('huggingface.co/spaces');
393
+
394
+ if (isGradio) {
395
+ // Gradio 5.x SSE v3 protocol β€” two-step:
396
+ // 1. POST /gradio_api/call/{api_name} β†’ get event_id
397
+ // 2. GET /gradio_api/call/{api_name}/{event_id} β†’ stream result
398
+
399
+ // Step 1: Submit the request
400
+ const submitRes = await fetch(`${base}/gradio_api/call/chat_fn`, {
401
+ method: 'POST',
402
+ headers: authHeaders({ 'Content-Type': 'application/json' }),
403
+ body: JSON.stringify({
404
+ data: [prompt, image || null, state.temperature, state.maxTokens],
405
+ }),
406
+ signal,
407
+ });
408
+ if (!submitRes.ok) {
409
+ const txt = await submitRes.text().catch(() => '');
410
+ throw new Error(`API submit ${submitRes.status}: ${txt.slice(0, 200) || 'request failed'}`);
411
+ }
412
+ const { event_id } = await submitRes.json();
413
+ if (!event_id) {
414
+ throw new Error('No event_id returned from Gradio API');
415
+ }
416
+
417
+ // Step 2: Get the result via SSE stream
418
+ const resultRes = await fetch(`${base}/gradio_api/call/chat_fn/${event_id}`, {
419
+ method: 'GET',
420
+ headers: authHeaders(),
421
+ signal,
422
+ });
423
+ if (!resultRes.ok) {
424
+ const txt = await resultRes.text().catch(() => '');
425
+ throw new Error(`API result ${resultRes.status}: ${txt.slice(0, 200) || 'request failed'}`);
426
+ }
427
+
428
+ // Parse SSE response β€” look for the "complete" event with data
429
+ const sseText = await resultRes.text();
430
+ const lines = sseText.split('\n');
431
+ let raw = null;
432
+ for (let i = 0; i < lines.length; i++) {
433
+ if (lines[i].startsWith('event: complete')) {
434
+ // Next line(s) starting with "data: " contain the result
435
+ const dataLine = lines[i + 1];
436
+ if (dataLine && dataLine.startsWith('data: ')) {
437
+ try {
438
+ const parsed = JSON.parse(dataLine.slice(6));
439
+ // Gradio wraps in array
440
+ raw = Array.isArray(parsed) ? parsed[0] : parsed;
441
+ } catch {
442
+ raw = dataLine.slice(6);
443
+ }
444
+ }
445
+ break;
446
+ }
447
+ if (lines[i].startsWith('event: error')) {
448
+ const dataLine = lines[i + 1];
449
+ const errMsg = dataLine?.startsWith('data: ') ? dataLine.slice(6) : 'Unknown Gradio error';
450
+ throw new Error(`Gradio error: ${errMsg.slice(0, 300)}`);
451
+ }
452
+ }
453
+
454
+ if (raw === null) {
455
+ throw new Error('No complete event found in Gradio SSE response');
456
+ }
457
+
458
+ // raw is a JSON string from our chat_fn
459
+ try {
460
+ return JSON.parse(raw);
461
+ } catch {
462
+ return { response: String(raw), sections: {} };
463
+ }
464
+
465
+ } else {
466
+ // Direct REST API (Modal or custom)
467
+ const body = { prompt, temperature: state.temperature, max_tokens: state.maxTokens };
468
+ if (image) body.image = image;
469
+ const res = await fetch(`${base}/api/generate`, {
470
+ method: 'POST',
471
+ headers: authHeaders({ 'Content-Type': 'application/json', 'Accept': 'application/json' }),
472
+ body: JSON.stringify(body),
473
+ signal,
474
+ });
475
+ if (!res.ok) {
476
+ const txt = await res.text().catch(() => '');
477
+ throw new Error(`API ${res.status}: ${txt.slice(0, 200) || 'request failed'}`);
478
+ }
479
+ return res.json();
480
+ }
481
+ }
482
+
483
+ // ----------------------------------------------------------------
484
+ // Demo / Fallback responses
485
+ // ----------------------------------------------------------------
486
+ const DEMO_RESPONSES = [
487
+ {
488
+ match: /landing|hero|next\.?js/i,
489
+ response:
490
+ `Here's a clean Next.js landing page using Tailwind CSS:
491
+
492
+ \`\`\`tsx
493
+ // app/page.tsx
494
+ export default function Home() {
495
+ return (
496
+ <main className="min-h-screen bg-gradient-to-b from-slate-950 to-slate-900 text-white">
497
+ <section className="max-w-6xl mx-auto px-6 py-24 text-center">
498
+ <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">
499
+ Now in beta
500
+ </span>
501
+ <h1 className="text-5xl md:text-7xl font-semibold tracking-tight">
502
+ Build faster.<br/>
503
+ <span className="bg-gradient-to-r from-violet-400 to-blue-400 bg-clip-text text-transparent">
504
+ Ship sooner.
505
+ </span>
506
+ </h1>
507
+ <p className="mt-6 text-lg text-slate-300 max-w-2xl mx-auto">
508
+ The frontend you'd build if you had unlimited time, in a single prompt.
509
+ </p>
510
+ <div className="mt-10 flex justify-center gap-3">
511
+ <a className="px-6 py-3 rounded-full bg-gradient-to-r from-violet-600 to-blue-600 font-medium" href="#cta">
512
+ Get started
513
+ </a>
514
+ <a className="px-6 py-3 rounded-full border border-white/10 hover:bg-white/5" href="#features">
515
+ See features
516
+ </a>
517
+ </div>
518
+ </section>
519
+ </main>
520
+ );
521
+ }
522
+ \`\`\`
523
+
524
+ 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.`,
525
+ sections: {
526
+ 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.'],
527
+ code: ['app/page.tsx generated with hero section + gradient headline.'],
528
+ critique: [],
529
+ fix: [],
530
+ },
531
+ },
532
+ {
533
+ match: /dashboard|chart|analytics/i,
534
+ response:
535
+ `Here's a self-contained dashboard UI in vanilla HTML/CSS:
536
+
537
+ \`\`\`html
538
+ <!DOCTYPE html>
539
+ <html lang="en">
540
+ <head>
541
+ <meta charset="UTF-8" />
542
+ <title>Pulsegrid Β· Dashboard</title>
543
+ <style>
544
+ :root { --bg:#0b0b14; --panel:#14141f; --border:rgba(255,255,255,.08); --text:#ececf1; --mute:#8b94a7; --acc:#7c3aed; }
545
+ * { box-sizing:border-box; margin:0; padding:0; }
546
+ body { background:var(--bg); color:var(--text); font:14px/1.55 'Inter', sans-serif; min-height:100vh; display:grid; grid-template-columns:240px 1fr; }
547
+ aside { background:var(--panel); border-right:1px solid var(--border); padding:20px; }
548
+ aside h1 { font-size:18px; background:linear-gradient(135deg,#7c3aed,#2563eb); -webkit-background-clip:text; color:transparent; margin-bottom:24px; }
549
+ nav a { display:block; padding:10px 12px; border-radius:8px; color:var(--mute); text-decoration:none; margin-bottom:2px; }
550
+ nav a.active { background:rgba(124,58,237,.15); color:#fff; }
551
+ main { padding:24px; overflow-y:auto; }
552
+ .stats { display:grid; grid-template-columns:repeat(4,1fr); gap:14px; margin-bottom:20px; }
553
+ .stat { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:16px; }
554
+ .stat .v { font-size:24px; font-weight:600; margin-top:6px; }
555
+ .stat .l { color:var(--mute); font-size:12px; text-transform:uppercase; letter-spacing:.1em; }
556
+ .chart { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:18px; height:260px; display:flex; align-items:end; gap:8px; }
557
+ .bar { flex:1; background:linear-gradient(180deg,#7c3aed,#2563eb); border-radius:6px 6px 0 0; }
558
+ </style>
559
+ </head>
560
+ <body>
561
+ <aside>
562
+ <h1>Pulsegrid</h1>
563
+ <nav>
564
+ <a class="active">Overview</a>
565
+ <a>Customers</a>
566
+ <a>Revenue</a>
567
+ <a>Settings</a>
568
+ </nav>
569
+ </aside>
570
+ <main>
571
+ <div class="stats">
572
+ <div class="stat"><div class="l">Revenue</div><div class="v">$48,210</div></div>
573
+ <div class="stat"><div class="l">Active users</div><div class="v">12,840</div></div>
574
+ <div class="stat"><div class="l">Conversion</div><div class="v">4.2%</div></div>
575
+ <div class="stat"><div class="l">Churn</div><div class="v">1.1%</div></div>
576
+ </div>
577
+ <div class="chart">
578
+ <div class="bar" style="height:40%"></div>
579
+ <div class="bar" style="height:65%"></div>
580
+ <div class="bar" style="height:30%"></div>
581
+ <div class="bar" style="height:80%"></div>
582
+ <div class="bar" style="height:55%"></div>
583
+ <div class="bar" style="height:90%"></div>
584
+ <div class="bar" style="height:70%"></div>
585
+ </div>
586
+ </main>
587
+ </body>
588
+ </html>
589
+ \`\`\`
590
+
591
+ Hit the **Preview** tab to see it rendered live.`,
592
+ sections: {
593
+ 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.'],
594
+ code: ['Single-file dashboard.html with grid layout, stats, sidebar.'],
595
+ critique: ['No real charting library β€” bars are static. For production, swap in Recharts/Chart.js.'],
596
+ fix: [],
597
+ },
598
+ },
599
+ {
600
+ match: /api|fastapi|backend|jwt|postgres/i,
601
+ response:
602
+ `Here's a minimal but production-shaped FastAPI service:
603
+
604
+ \`\`\`python
605
+ # main.py
606
+ from datetime import datetime, timedelta
607
+ from fastapi import FastAPI, Depends, HTTPException, status
608
+ from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
609
+ from sqlalchemy.orm import Session
610
+ from passlib.context import CryptContext
611
+ from jose import jwt
612
+ from pydantic import BaseModel
613
+
614
+ from .database import SessionLocal, engine
615
+ from . import models, schemas
616
+
617
+ models.Base.metadata.create_all(bind=engine)
618
+
619
+ app = FastAPI(title="Notes API")
620
+ SECRET_KEY = "change-me"
621
+ ALGORITHM = "HS256"
622
+ EXPIRES = timedelta(hours=24)
623
+ pwd = CryptContext(schemes=["bcrypt"])
624
+ oauth2 = OAuth2PasswordBearer(tokenUrl="/auth/login")
625
+
626
+ def get_db():
627
+ db = SessionLocal()
628
+ try: yield db
629
+ finally: db.close()
630
+
631
+ def make_token(sub: str) -> str:
632
+ return jwt.encode({"sub": sub, "exp": datetime.utcnow() + EXPIRES}, SECRET_KEY, ALGORITHM)
633
+
634
+ @app.post("/auth/login")
635
+ def login(form: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
636
+ user = db.query(models.User).filter_by(email=form.username).first()
637
+ if not user or not pwd.verify(form.password, user.hashed_password):
638
+ raise HTTPException(401, "Invalid credentials")
639
+ return {"access_token": make_token(user.email), "token_type": "bearer"}
640
+
641
+ @app.get("/notes", response_model=list[schemas.Note])
642
+ def list_notes(db: Session = Depends(get_db), token: str = Depends(oauth2)):
643
+ payload = jwt.decode(token, SECRET_KEY, [ALGORITHM])
644
+ return db.query(models.Note).filter_by(owner_email=payload["sub"]).all()
645
+ \`\`\`
646
+
647
+ Pair this with \`models.py\`, \`schemas.py\`, and a \`.env\` containing \`DATABASE_URL=postgresql://…\`.`,
648
+ sections: {
649
+ thinking: ['User asked for FastAPI + JWT + Postgres. Sketching the service entrypoint with auth flow and a protected GET endpoint.'],
650
+ code: ['main.py with /auth/login and /notes endpoints.'],
651
+ critique: ['SECRET_KEY hardcoded β€” move to env. bcrypt rounds are default β€” increase for production.'],
652
+ suggest: ['Add Alembic migrations and pytest fixtures next.'],
653
+ },
654
+ },
655
+ {
656
+ match: /bug|fix|debug|divide/i,
657
+ response:
658
+ `Two bugs in that snippet:
659
+
660
+ 1. **No zero-divisor guard** β€” \`divide_list([1,2], 0)\` throws \`ZeroDivisionError\`.
661
+ 2. **No type validation** β€” non-numeric items raise \`TypeError\` deep in the loop.
662
+
663
+ Here's the fix:
664
+
665
+ \`\`\`python
666
+ def divide_list(numbers, divisor):
667
+ if divisor == 0:
668
+ raise ValueError("divisor must be non-zero")
669
+ result = []
670
+ for n in numbers:
671
+ if not isinstance(n, (int, float)):
672
+ raise TypeError(f"non-numeric item: {n!r}")
673
+ result.append(n / divisor)
674
+ return result
675
+ \`\`\`
676
+
677
+ For very large lists, switch to a generator (\`yield\` instead of \`append\`) to keep memory flat.`,
678
+ sections: {
679
+ thinking: ['Two issues: zero-divisor crash, non-numeric crash. Adding explicit guards and a clearer error message.'],
680
+ critique: ['Function silently coerces booleans because bool βŠ‚ int in Python β€” may want to exclude them explicitly.'],
681
+ fix: ['Added divisor==0 guard, added isinstance validation, kept the original signature.'],
682
+ code: ['Patched divide_list with safe input checking.'],
683
+ },
684
+ },
685
+ ];
686
+
687
+ const DEFAULT_DEMO = {
688
+ response:
689
+ `I'm running in **Demo Mode** because the live API isn't reachable.
690
+
691
+ 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.
692
+
693
+ \`\`\`javascript
694
+ // You sent a prompt and I'm a placeholder.
695
+ // Connect the real API to see actual generations.
696
+ console.log("MINDI 1.5 β€” awaiting connection");
697
+ \`\`\``,
698
+ sections: {
699
+ thinking: ['No API URL configured or the endpoint is unreachable. Returning a demo response.'],
700
+ code: ['Stub response.'],
701
+ },
702
+ };
703
+
704
+ function pickDemo(prompt) {
705
+ const found = DEMO_RESPONSES.find((d) => d.match.test(prompt));
706
+ return found || DEFAULT_DEMO;
707
+ }
708
+
709
+ async function generateDemo(prompt) {
710
+ await new Promise((r) => setTimeout(r, 1200 + Math.random() * 700));
711
+ const demo = pickDemo(prompt);
712
+ return { response: demo.response, sections: demo.sections || {} };
713
+ }
714
+
715
+ // ----------------------------------------------------------------
716
+ // Status
717
+ // ----------------------------------------------------------------
718
+ function setStatus(status, label) {
719
+ runtime.status = status;
720
+ els.statusDot.classList.remove('status-dot--gray', 'status-dot--green', 'status-dot--yellow', 'status-dot--red');
721
+ const map = { connecting: 'gray', online: 'green', demo: 'yellow', offline: 'red' };
722
+ els.statusDot.classList.add(`status-dot--${map[status] || 'gray'}`);
723
+ els.statusText.textContent = label;
724
+ }
725
+
726
+ // ----------------------------------------------------------------
727
+ // Toasts
728
+ // ----------------------------------------------------------------
729
+ function toast(msg, kind = 'info', ms = 2400) {
730
+ const el = document.createElement('div');
731
+ el.className = `toast toast--${kind}`;
732
+ el.innerHTML = `<span class="toast-icon"></span><span>${escapeHtml(msg)}</span>`;
733
+ els.toasts.appendChild(el);
734
+ setTimeout(() => {
735
+ el.classList.add('is-leaving');
736
+ setTimeout(() => el.remove(), 260);
737
+ }, ms);
738
+ }
739
+
740
+ // ----------------------------------------------------------------
741
+ // Chat data helpers
742
+ // ----------------------------------------------------------------
743
+ function currentChat() {
744
+ return state.chats.find((c) => c.id === state.currentId) || null;
745
+ }
746
+ function ensureChat() {
747
+ let chat = currentChat();
748
+ if (!chat) {
749
+ chat = { id: uid(), title: 'New chat', createdAt: Date.now(), updatedAt: Date.now(), messages: [] };
750
+ state.chats.unshift(chat);
751
+ state.currentId = chat.id;
752
+ }
753
+ return chat;
754
+ }
755
+ function deriveTitle(text) {
756
+ const t = (text || '').replace(/\s+/g, ' ').trim();
757
+ if (!t) return 'New chat';
758
+ return t.length > 42 ? t.slice(0, 42).trim() + '…' : t;
759
+ }
760
+
761
+ // ----------------------------------------------------------------
762
+ // Render: messages
763
+ // ----------------------------------------------------------------
764
+ function renderMessages() {
765
+ const chat = currentChat();
766
+ const hasMessages = !!(chat && chat.messages.length);
767
+ els.chat.classList.toggle('has-messages', hasMessages);
768
+ els.messages.innerHTML = '';
769
+ if (!hasMessages) return;
770
+
771
+ chat.messages.forEach((m) => els.messages.appendChild(renderMessageEl(m)));
772
+
773
+ // Highlight after insertion
774
+ if (window.Prism) {
775
+ try { Prism.highlightAllUnder(els.messages); } catch { /* noop */ }
776
+ }
777
+ scrollMessagesToBottom();
778
+ }
779
+
780
+ function renderMessageEl(m) {
781
+ const wrap = document.createElement('div');
782
+ wrap.className = `msg msg--${m.role === 'user' ? 'user' : 'asst'}`;
783
+
784
+ const avatar = document.createElement('div');
785
+ avatar.className = 'msg-avatar';
786
+ avatar.textContent = m.role === 'user' ? 'You'.slice(0,1) : 'M';
787
+
788
+ const body = document.createElement('div');
789
+ body.className = 'msg-body';
790
+
791
+ const meta = document.createElement('div');
792
+ meta.className = 'msg-meta';
793
+ meta.innerHTML = `<span class="msg-meta-name">${m.role === 'user' ? 'You' : 'MINDI 1.5'}</span>`;
794
+ body.appendChild(meta);
795
+
796
+ if (Array.isArray(m.images) && m.images.length) {
797
+ const imgsWrap = document.createElement('div');
798
+ imgsWrap.className = 'msg-images';
799
+ m.images.forEach((src) => {
800
+ const img = document.createElement('img');
801
+ img.src = src;
802
+ img.alt = 'Attached image';
803
+ imgsWrap.appendChild(img);
804
+ });
805
+ body.appendChild(imgsWrap);
806
+ }
807
+
808
+ const bubble = document.createElement('div');
809
+ bubble.className = 'msg-bubble';
810
+ if (m.loading) {
811
+ bubble.innerHTML = `<span>${escapeHtml(m.content || 'Thinking')}</span><span class="dots"><span></span><span></span><span></span></span>`;
812
+ wrap.classList.add('msg-loading');
813
+ } else {
814
+ bubble.innerHTML = renderMarkdown(cleanForDisplay(m.content));
815
+ }
816
+ body.appendChild(bubble);
817
+
818
+ wrap.appendChild(avatar);
819
+ wrap.appendChild(body);
820
+ return wrap;
821
+ }
822
+
823
+ function scrollMessagesToBottom() {
824
+ requestAnimationFrame(() => {
825
+ els.messages.scrollTop = els.messages.scrollHeight;
826
+ });
827
+ }
828
+
829
+ // ----------------------------------------------------------------
830
+ // Render: history sidebar
831
+ // ----------------------------------------------------------------
832
+ function renderHistory() {
833
+ const q = (els.search.value || '').toLowerCase().trim();
834
+ const filtered = q
835
+ ? state.chats.filter((c) => c.title.toLowerCase().includes(q) ||
836
+ c.messages.some((m) => (m.content || '').toLowerCase().includes(q)))
837
+ : state.chats;
838
+
839
+ els.history.innerHTML = '';
840
+
841
+ if (!filtered.length) {
842
+ els.history.innerHTML = `
843
+ <div class="history-empty">
844
+ <p>${q ? 'No matches.' : 'No chats yet.'}</p>
845
+ <p class="muted">${q ? 'Try a different search.' : 'Start a conversation to see it here.'}</p>
846
+ </div>`;
847
+ return;
848
+ }
849
+
850
+ // Group by date
851
+ const groupOrder = ['Today', 'Yesterday', 'This Week', 'Earlier'];
852
+ const groups = {};
853
+ filtered.forEach((c) => {
854
+ const g = relativeDateGroup(c.updatedAt || c.createdAt);
855
+ (groups[g] ||= []).push(c);
856
+ });
857
+
858
+ groupOrder.forEach((g) => {
859
+ if (!groups[g]) return;
860
+ const wrap = document.createElement('div');
861
+ wrap.className = 'history-group';
862
+ wrap.innerHTML = `<div class="history-group-title">${g}</div>`;
863
+ groups[g].forEach((c) => {
864
+ const btn = document.createElement('button');
865
+ btn.className = 'history-item';
866
+ if (c.id === state.currentId) btn.classList.add('is-active');
867
+ btn.textContent = c.title || 'New chat';
868
+ btn.title = c.title;
869
+ btn.addEventListener('click', () => loadChat(c.id));
870
+ wrap.appendChild(btn);
871
+ });
872
+ els.history.appendChild(wrap);
873
+ });
874
+ }
875
+
876
+ function loadChat(id) {
877
+ state.currentId = id;
878
+ const chat = currentChat();
879
+ if (chat) {
880
+ els.chatTitle.textContent = chat.title || 'New chat';
881
+ // recompute preview panels from last assistant message
882
+ const lastAssistant = [...chat.messages].reverse().find((m) => m.role === 'assistant' && !m.loading);
883
+ if (lastAssistant) updatePreviewFromAssistant(lastAssistant);
884
+ else clearPreview();
885
+ }
886
+ renderMessages();
887
+ renderHistory();
888
+ saveState();
889
+ closeMobileSidebar();
890
+ }
891
+
892
+ // ----------------------------------------------------------------
893
+ // Preview panel updates
894
+ // ----------------------------------------------------------------
895
+ function clearPreview() {
896
+ runtime.lastCode = null;
897
+ runtime.lastSections = null;
898
+ els.codeOut.hidden = true;
899
+ els.emptyCode.hidden = false;
900
+ els.liveFrame.hidden = true;
901
+ els.emptyLive.hidden = false;
902
+ els.sections.hidden = true;
903
+ els.emptySections.hidden = false;
904
+ els.sections.innerHTML = '';
905
+ els.codeOutInner.textContent = '';
906
+ }
907
+
908
+ function updatePreviewFromAssistant(msg) {
909
+ const cleaned = cleanForDisplay(msg.content);
910
+ const block = extractLastCodeBlock(cleaned);
911
+ runtime.lastCode = block;
912
+ if (block) renderCodeOut(block);
913
+ else { els.codeOut.hidden = true; els.emptyCode.hidden = false; }
914
+
915
+ // Live HTML preview
916
+ if (block && /^(markup|html)$/i.test(block.language || '')) {
917
+ renderLivePreview(block.code);
918
+ } else {
919
+ els.liveFrame.hidden = true;
920
+ els.emptyLive.hidden = false;
921
+ }
922
+
923
+ // Sections
924
+ const apiSections = msg.sections || {};
925
+ const parsedSections = parseSections(msg.content);
926
+ runtime.lastSections = mergeSections(apiSections, parsedSections);
927
+ renderSections(runtime.lastSections);
928
+ }
929
+
930
+ function renderCodeOut(block) {
931
+ const lang = block.language || 'plaintext';
932
+ els.codeOutInner.className = `language-${lang}`;
933
+ els.codeOutInner.textContent = block.code;
934
+ els.emptyCode.hidden = true;
935
+ els.codeOut.hidden = false;
936
+ if (window.Prism) {
937
+ try { Prism.highlightElement(els.codeOutInner); } catch { /* noop */ }
938
+ }
939
+ }
940
+
941
+ function renderLivePreview(html) {
942
+ els.emptyLive.hidden = true;
943
+ els.liveFrame.hidden = false;
944
+ const doc = els.liveFrame.contentDocument || els.liveFrame.contentWindow.document;
945
+ doc.open();
946
+ doc.write(html);
947
+ doc.close();
948
+ }
949
+
950
+ function renderSections(sections) {
951
+ const hasAny = SECTION_ORDER.some((k) => (sections[k] || []).length);
952
+ if (!hasAny) {
953
+ els.sections.hidden = true;
954
+ els.emptySections.hidden = false;
955
+ els.sections.innerHTML = '';
956
+ return;
957
+ }
958
+ els.emptySections.hidden = true;
959
+ els.sections.hidden = false;
960
+ els.sections.innerHTML = '';
961
+
962
+ SECTION_ORDER.forEach((kind) => {
963
+ const items = sections[kind] || [];
964
+ items.forEach((body, i) => {
965
+ const card = document.createElement('div');
966
+ card.className = 'section-card';
967
+ card.dataset.kind = kind;
968
+ card.innerHTML = `
969
+ <div class="section-card-head">
970
+ <span class="section-tag">${SECTION_LABELS[kind]}</span>
971
+ <span>${items.length > 1 ? `${i + 1} / ${items.length}` : ''}</span>
972
+ </div>
973
+ <div class="section-card-body">${escapeHtml(body)}</div>
974
+ `;
975
+ els.sections.appendChild(card);
976
+ });
977
+ });
978
+ }
979
+
980
+ // ----------------------------------------------------------------
981
+ // Send flow
982
+ // ----------------------------------------------------------------
983
+ async function send() {
984
+ if (runtime.isSending) return;
985
+ const text = els.promptInput.value.trim();
986
+ if (!text && !runtime.pendingImages.length) return;
987
+
988
+ const chat = ensureChat();
989
+ const wasEmpty = chat.messages.length === 0;
990
+
991
+ const userMsg = {
992
+ role: 'user',
993
+ content: text,
994
+ images: runtime.pendingImages.map((p) => p.dataUrl),
995
+ ts: Date.now(),
996
+ };
997
+ chat.messages.push(userMsg);
998
+ chat.updatedAt = Date.now();
999
+
1000
+ if (wasEmpty) {
1001
+ chat.title = deriveTitle(text);
1002
+ els.chatTitle.textContent = chat.title;
1003
+ }
1004
+
1005
+ // Reset input
1006
+ const imageForApi = runtime.pendingImages[0]?.dataUrl || null;
1007
+ els.promptInput.value = '';
1008
+ autosizeTextarea();
1009
+ clearPendingImages();
1010
+ updateSendEnabled();
1011
+
1012
+ // Loading message
1013
+ const loadingMsg = { role: 'assistant', content: 'Thinking', loading: true, ts: Date.now() };
1014
+ chat.messages.push(loadingMsg);
1015
+ renderMessages();
1016
+ renderHistory();
1017
+ saveState();
1018
+
1019
+ runtime.isSending = true;
1020
+ let coldStartTimer = setTimeout(() => {
1021
+ loadingMsg.content = 'Cold start β€” booting the GPU. First request can take ~4 minutes';
1022
+ renderMessages();
1023
+ }, COLD_START_HINT_MS);
1024
+
1025
+ let result, errored = null;
1026
+ try {
1027
+ if (runtime.status === 'demo' || !state.apiUrl) {
1028
+ result = await generateDemo(text);
1029
+ } else {
1030
+ result = await callGenerate(text, imageForApi);
1031
+ }
1032
+ } catch (e) {
1033
+ errored = e;
1034
+ // Auto-fallback to demo so the UI never feels dead
1035
+ result = await generateDemo(text).catch(() => ({ response: 'Generation failed.' }));
1036
+ if (!/cold|abort|signal/i.test(String(e?.message || ''))) {
1037
+ toast('API error β€” falling back to demo', 'error', 3500);
1038
+ }
1039
+ } finally {
1040
+ clearTimeout(coldStartTimer);
1041
+ runtime.isSending = false;
1042
+ }
1043
+
1044
+ // If the API returned a quota / auth error, surface it clearly
1045
+ const authMsg = detectAuthError(result);
1046
+ if (authMsg) {
1047
+ toast(authMsg, 'error', 7000);
1048
+ setStatus('demo', state.hfToken ? 'Quota exhausted' : 'Auth required');
1049
+ }
1050
+
1051
+ // Remove loading, push assistant
1052
+ const idx = chat.messages.indexOf(loadingMsg);
1053
+ if (idx !== -1) chat.messages.splice(idx, 1);
1054
+
1055
+ const assistantMsg = {
1056
+ role: 'assistant',
1057
+ content: result?.response || '(no response)',
1058
+ sections: result?.sections || null,
1059
+ ts: Date.now(),
1060
+ };
1061
+ chat.messages.push(assistantMsg);
1062
+ chat.updatedAt = Date.now();
1063
+
1064
+ renderMessages();
1065
+ renderHistory();
1066
+ updatePreviewFromAssistant(assistantMsg);
1067
+ saveState();
1068
+
1069
+ if (errored) console.warn('[mindi] generate error:', errored);
1070
+ }
1071
+
1072
+ // ----------------------------------------------------------------
1073
+ // Composer interactions
1074
+ // ----------------------------------------------------------------
1075
+ function autosizeTextarea() {
1076
+ const ta = els.promptInput;
1077
+ ta.style.height = 'auto';
1078
+ const next = Math.min(ta.scrollHeight, MAX_TEXTAREA);
1079
+ ta.style.height = next + 'px';
1080
+ }
1081
+ function updateSendEnabled() {
1082
+ const has = els.promptInput.value.trim().length > 0 || runtime.pendingImages.length > 0;
1083
+ els.sendBtn.disabled = !has || runtime.isSending;
1084
+ }
1085
+ function clearPendingImages() {
1086
+ runtime.pendingImages = [];
1087
+ renderPendingImages();
1088
+ }
1089
+ function renderPendingImages() {
1090
+ if (!runtime.pendingImages.length) {
1091
+ els.composerImages.hidden = true;
1092
+ els.composerImages.innerHTML = '';
1093
+ return;
1094
+ }
1095
+ els.composerImages.hidden = false;
1096
+ els.composerImages.innerHTML = '';
1097
+ runtime.pendingImages.forEach((p, i) => {
1098
+ const tile = document.createElement('div');
1099
+ tile.className = 'composer-image';
1100
+ tile.style.backgroundImage = `url("${p.dataUrl}")`;
1101
+ tile.title = p.name;
1102
+ const rm = document.createElement('button');
1103
+ rm.className = 'composer-image-remove';
1104
+ rm.setAttribute('aria-label', 'Remove image');
1105
+ rm.textContent = 'Γ—';
1106
+ rm.addEventListener('click', () => {
1107
+ runtime.pendingImages.splice(i, 1);
1108
+ renderPendingImages();
1109
+ updateSendEnabled();
1110
+ });
1111
+ tile.appendChild(rm);
1112
+ els.composerImages.appendChild(tile);
1113
+ });
1114
+ }
1115
+
1116
+ async function handleFileChosen(file) {
1117
+ if (!file || !file.type.startsWith('image/')) {
1118
+ toast('Only image files are supported.', 'error');
1119
+ return;
1120
+ }
1121
+ if (file.size > 6 * 1024 * 1024) {
1122
+ toast('Image too large (max 6 MB).', 'error');
1123
+ return;
1124
+ }
1125
+ try {
1126
+ const dataUrl = await fileToDataUrl(file);
1127
+ runtime.pendingImages = [{ name: file.name, dataUrl }]; // single image per request
1128
+ renderPendingImages();
1129
+ updateSendEnabled();
1130
+ } catch {
1131
+ toast('Could not read that image.', 'error');
1132
+ }
1133
+ }
1134
+
1135
+ // ----------------------------------------------------------------
1136
+ // Tabs
1137
+ // ----------------------------------------------------------------
1138
+ function activateTab(tabName) {
1139
+ els.tabs.forEach((t) => {
1140
+ const on = t.dataset.tab === tabName;
1141
+ t.classList.toggle('is-active', on);
1142
+ t.setAttribute('aria-selected', on ? 'true' : 'false');
1143
+ });
1144
+ els.panes.forEach((p) => {
1145
+ p.classList.toggle('is-active', p.dataset.pane === tabName);
1146
+ });
1147
+ }
1148
+
1149
+ // ----------------------------------------------------------------
1150
+ // Settings modal
1151
+ // ----------------------------------------------------------------
1152
+ function maskToken(t) {
1153
+ if (!t) return 'none';
1154
+ if (t.length <= 8) return 'set';
1155
+ return `${t.slice(0, 4)}…${t.slice(-4)}`;
1156
+ }
1157
+ function refreshTokenStatus() {
1158
+ if (els.hfTokenStatus) els.hfTokenStatus.textContent = maskToken(state.hfToken);
1159
+ }
1160
+ function openSettings() {
1161
+ els.settingsUrl.value = state.apiUrl || '';
1162
+ if (els.settingsHfToken) els.settingsHfToken.value = state.hfToken || '';
1163
+ els.settingsTemp.value = state.temperature;
1164
+ els.settingsTokens.value = state.maxTokens;
1165
+ els.tempVal.textContent = Number(state.temperature).toFixed(2);
1166
+ els.tokensVal.textContent = state.maxTokens;
1167
+ refreshTokenStatus();
1168
+ els.settingsModal.hidden = false;
1169
+ setTimeout(() => els.settingsUrl.focus(), 50);
1170
+ }
1171
+ function closeSettings() {
1172
+ els.settingsModal.hidden = true;
1173
+ }
1174
+ function applySettings() {
1175
+ const url = els.settingsUrl.value.trim();
1176
+ const token = els.settingsHfToken ? els.settingsHfToken.value.trim() : '';
1177
+ const temp = parseFloat(els.settingsTemp.value);
1178
+ const tokens = parseInt(els.settingsTokens.value, 10);
1179
+ state.apiUrl = url || API_DEFAULT;
1180
+ state.hfToken = token;
1181
+ state.temperature = isFinite(temp) ? temp : 0.7;
1182
+ state.maxTokens = isFinite(tokens) ? tokens : 2048;
1183
+ saveState();
1184
+ refreshTokenStatus();
1185
+ closeSettings();
1186
+ toast('Settings saved', 'success');
1187
+ pingHealth();
1188
+ }
1189
+
1190
+ // ----------------------------------------------------------------
1191
+ // Mobile sidebar / preview toggling
1192
+ // ----------------------------------------------------------------
1193
+ function openMobileSidebar() { els.body.classList.add('sidebar-open'); }
1194
+ function closeMobileSidebar() { els.body.classList.remove('sidebar-open'); }
1195
+ function togglePreview() {
1196
+ if (window.matchMedia('(max-width: 1024px)').matches) {
1197
+ els.body.classList.toggle('preview-open');
1198
+ } else {
1199
+ els.body.classList.toggle('preview-hidden');
1200
+ }
1201
+ }
1202
+
1203
+ // ----------------------------------------------------------------
1204
+ // Copy / download from preview panel
1205
+ // ----------------------------------------------------------------
1206
+ async function copyLastCode() {
1207
+ if (!runtime.lastCode) {
1208
+ toast('No code to copy yet', 'info');
1209
+ return;
1210
+ }
1211
+ try {
1212
+ await navigator.clipboard.writeText(runtime.lastCode.code);
1213
+ toast('Copied to clipboard', 'success', 1600);
1214
+ } catch {
1215
+ toast('Clipboard unavailable', 'error');
1216
+ }
1217
+ }
1218
+ function downloadLastCode() {
1219
+ if (!runtime.lastCode) {
1220
+ toast('No code to download yet', 'info');
1221
+ return;
1222
+ }
1223
+ const ext = (() => {
1224
+ const m = { javascript: 'js', typescript: 'ts', tsx: 'tsx', jsx: 'jsx',
1225
+ python: 'py', markup: 'html', html: 'html', css: 'css',
1226
+ json: 'json', sql: 'sql', bash: 'sh' };
1227
+ return m[runtime.lastCode.language] || 'txt';
1228
+ })();
1229
+ downloadFile(`mindi-output.${ext}`, runtime.lastCode.code);
1230
+ }
1231
+
1232
+ // ----------------------------------------------------------------
1233
+ // Bind events
1234
+ // ----------------------------------------------------------------
1235
+ // The active send handler β€” overridden by agent init if available
1236
+ let activeSend = send;
1237
+
1238
+ function bind() {
1239
+ // Composer
1240
+ els.promptInput.addEventListener('input', () => { autosizeTextarea(); updateSendEnabled(); });
1241
+ els.promptInput.addEventListener('keydown', (e) => {
1242
+ if (e.key === 'Enter' && !e.shiftKey) {
1243
+ e.preventDefault();
1244
+ activeSend();
1245
+ }
1246
+ });
1247
+ els.sendBtn.addEventListener('click', () => activeSend());
1248
+
1249
+ // Attach
1250
+ els.attachBtn.addEventListener('click', () => els.fileInput.click());
1251
+ els.fileInput.addEventListener('change', (e) => {
1252
+ const f = e.target.files?.[0];
1253
+ if (f) handleFileChosen(f);
1254
+ e.target.value = '';
1255
+ });
1256
+
1257
+ // Drag & drop on composer
1258
+ ['dragenter', 'dragover'].forEach((ev) => {
1259
+ els.composer.addEventListener(ev, (e) => { e.preventDefault(); els.composer.style.borderColor = 'rgba(124, 58, 237, .6)'; });
1260
+ });
1261
+ ['dragleave', 'drop'].forEach((ev) => {
1262
+ els.composer.addEventListener(ev, (e) => { e.preventDefault(); els.composer.style.borderColor = ''; });
1263
+ });
1264
+ els.composer.addEventListener('drop', (e) => {
1265
+ const file = e.dataTransfer?.files?.[0];
1266
+ if (file) handleFileChosen(file);
1267
+ });
1268
+
1269
+ // Quick action cards
1270
+ els.quickCards.forEach((card) => {
1271
+ card.addEventListener('click', () => {
1272
+ els.promptInput.value = card.dataset.prompt || '';
1273
+ autosizeTextarea();
1274
+ updateSendEnabled();
1275
+ els.promptInput.focus();
1276
+ });
1277
+ });
1278
+
1279
+ // Sidebar
1280
+ els.newChatBtn.addEventListener('click', () => {
1281
+ state.currentId = null;
1282
+ ensureChat();
1283
+ els.chatTitle.textContent = 'New chat';
1284
+ clearPreview();
1285
+ renderMessages();
1286
+ renderHistory();
1287
+ saveState();
1288
+ els.promptInput.focus();
1289
+ closeMobileSidebar();
1290
+ });
1291
+ els.search.addEventListener('input', debounce(renderHistory, 120));
1292
+ els.hamburger.addEventListener('click', openMobileSidebar);
1293
+ els.scrim.addEventListener('click', () => { closeMobileSidebar(); els.body.classList.remove('preview-open'); });
1294
+ els.togglePreview.addEventListener('click', togglePreview);
1295
+
1296
+ // Tabs
1297
+ els.tabs.forEach((t) => t.addEventListener('click', () => activateTab(t.dataset.tab)));
1298
+
1299
+ // Code copy / download
1300
+ els.copyCode.addEventListener('click', copyLastCode);
1301
+ els.downloadCode.addEventListener('click', downloadLastCode);
1302
+
1303
+ // Click handler for inline copy buttons inside messages (delegated)
1304
+ els.messages.addEventListener('click', async (e) => {
1305
+ const btn = e.target.closest('.md-copy');
1306
+ if (!btn) return;
1307
+ try {
1308
+ await navigator.clipboard.writeText(btn.dataset.code || '');
1309
+ const prev = btn.textContent;
1310
+ btn.textContent = 'Copied!';
1311
+ setTimeout(() => { btn.textContent = prev; }, 1400);
1312
+ } catch {
1313
+ toast('Clipboard unavailable', 'error');
1314
+ }
1315
+ });
1316
+
1317
+ // Brand β†’ settings (double-click)
1318
+ els.brand.addEventListener('dblclick', openSettings);
1319
+
1320
+ // Settings modal
1321
+ els.settingsModal.addEventListener('click', (e) => {
1322
+ if (e.target.matches('[data-close]') || e.target.closest('[data-close]')) closeSettings();
1323
+ });
1324
+ els.settingsTemp.addEventListener('input', () => {
1325
+ els.tempVal.textContent = Number(els.settingsTemp.value).toFixed(2);
1326
+ });
1327
+ els.settingsTokens.addEventListener('input', () => {
1328
+ els.tokensVal.textContent = els.settingsTokens.value;
1329
+ });
1330
+ els.saveSettings.addEventListener('click', applySettings);
1331
+
1332
+ // Esc to close modal / mobile drawers
1333
+ document.addEventListener('keydown', (e) => {
1334
+ if (e.key === 'Escape') {
1335
+ if (!els.settingsModal.hidden) closeSettings();
1336
+ closeMobileSidebar();
1337
+ els.body.classList.remove('preview-open');
1338
+ }
1339
+ // Cmd/Ctrl + K β†’ focus search
1340
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
1341
+ e.preventDefault();
1342
+ els.search.focus();
1343
+ }
1344
+ });
1345
+
1346
+ // Keep textarea sized after window resize (font reflow)
1347
+ window.addEventListener('resize', autosizeTextarea);
1348
+ }
1349
+
1350
+ // ----------------------------------------------------------------
1351
+ // Agent integration
1352
+ // ----------------------------------------------------------------
1353
+ const agentEls = {
1354
+ log: document.getElementById('agent-log'),
1355
+ sandbox: document.getElementById('agent-sandbox'),
1356
+ console: document.getElementById('agent-console'),
1357
+ consoleBody: document.getElementById('agent-console-body'),
1358
+ emptyAgent: document.getElementById('empty-agent'),
1359
+ };
1360
+
1361
+ const STEP_ICONS = {
1362
+ plan: 'πŸ“‹',
1363
+ generate: '⚑',
1364
+ execute: '▢️',
1365
+ verify: 'βœ…',
1366
+ fix: 'πŸ”§',
1367
+ done: 'πŸŽ‰',
1368
+ error: '❌',
1369
+ };
1370
+
1371
+ const STEP_LABELS = {
1372
+ plan: 'Planning',
1373
+ generate: 'Generating Code',
1374
+ execute: 'Executing',
1375
+ verify: 'Verifying Output',
1376
+ fix: 'Fixing Error',
1377
+ done: 'Complete',
1378
+ error: 'Error',
1379
+ };
1380
+
1381
+ function isCodeRequest(text) {
1382
+ 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);
1383
+ }
1384
+
1385
+ function renderAgentStep(run, step) {
1386
+ if (!agentEls.log) return;
1387
+
1388
+ // Show agent panel, hide empty state
1389
+ agentEls.emptyAgent && (agentEls.emptyAgent.hidden = true);
1390
+ agentEls.log.hidden = false;
1391
+
1392
+ // Switch to agent tab
1393
+ const agentTab = document.querySelector('.tab[data-tab="agent"]');
1394
+ if (agentTab && !agentTab.classList.contains('is-active')) {
1395
+ agentTab.click();
1396
+ }
1397
+
1398
+ // Find or create step element
1399
+ let el = agentEls.log.querySelector(`[data-step-id="${step.id}"]`);
1400
+ if (!el) {
1401
+ el = document.createElement('div');
1402
+ el.className = 'agent-step';
1403
+ el.dataset.stepId = step.id;
1404
+ el.innerHTML = `
1405
+ <div class="agent-step-icon"></div>
1406
+ <div class="agent-step-body">
1407
+ <div class="agent-step-title"></div>
1408
+ <div class="agent-step-detail"></div>
1409
+ </div>`;
1410
+ agentEls.log.appendChild(el);
1411
+ }
1412
+
1413
+ // Update status class
1414
+ el.className = `agent-step agent-step--${step.status}`;
1415
+
1416
+ // Update icon
1417
+ const iconEl = el.querySelector('.agent-step-icon');
1418
+ const statusIcons = { running: '⏳', success: 'βœ…', failed: '❌', pending: '⏺' };
1419
+ iconEl.textContent = step.status === 'success' || step.status === 'failed'
1420
+ ? statusIcons[step.status]
1421
+ : (STEP_ICONS[step.type] || '⏳');
1422
+
1423
+ // Update title
1424
+ el.querySelector('.agent-step-title').textContent = STEP_LABELS[step.type] || step.type;
1425
+
1426
+ // Update detail
1427
+ el.querySelector('.agent-step-detail').textContent = step.detail || '';
1428
+
1429
+ // Auto-scroll
1430
+ agentEls.log.scrollTop = agentEls.log.scrollHeight;
1431
+
1432
+ // Show sandbox and console when executing
1433
+ if (step.type === 'execute') {
1434
+ agentEls.sandbox.hidden = false;
1435
+ agentEls.console.hidden = false;
1436
+ }
1437
+
1438
+ // Update console on execution results
1439
+ if (step.type === 'execute' && (step.status === 'success' || step.status === 'failed')) {
1440
+ const run_ = run; // closure
1441
+ if (run_.currentCode) {
1442
+ // Show code in the Code tab too
1443
+ const block = { language: run_.language || 'javascript', code: run_.currentCode };
1444
+ runtime.lastCode = block;
1445
+ renderCodeOut(block);
1446
+ }
1447
+ }
1448
+
1449
+ // When done, show final code in preview
1450
+ if (step.type === 'done' && step.status === 'success' && run.currentCode) {
1451
+ const lang = run.language || 'javascript';
1452
+ if (/^(html|markup)$/i.test(lang)) {
1453
+ // Render in live preview
1454
+ els.liveFrame.hidden = false;
1455
+ const emptyLive = document.getElementById('empty-live');
1456
+ if (emptyLive) emptyLive.hidden = true;
1457
+ els.liveFrame.srcdoc = run.currentCode;
1458
+ }
1459
+ }
1460
+ }
1461
+
1462
+ function clearAgentUI() {
1463
+ if (agentEls.log) {
1464
+ agentEls.log.innerHTML = '';
1465
+ agentEls.log.hidden = true;
1466
+ }
1467
+ if (agentEls.sandbox) {
1468
+ agentEls.sandbox.innerHTML = '';
1469
+ agentEls.sandbox.hidden = true;
1470
+ }
1471
+ if (agentEls.console) {
1472
+ agentEls.console.hidden = true;
1473
+ }
1474
+ if (agentEls.consoleBody) {
1475
+ agentEls.consoleBody.textContent = '';
1476
+ }
1477
+ if (agentEls.emptyAgent) {
1478
+ agentEls.emptyAgent.hidden = false;
1479
+ }
1480
+ }
1481
+
1482
+ async function runAgent(prompt, image) {
1483
+ clearAgentUI();
1484
+
1485
+ const apiCall = async (p, img) => {
1486
+ if (runtime.status === 'demo' || !state.apiUrl) {
1487
+ return generateDemo(p);
1488
+ }
1489
+ return callGenerate(p, img);
1490
+ };
1491
+
1492
+ const result = await MINDIAgent.run(prompt, {
1493
+ apiCall,
1494
+ sandboxContainer: agentEls.sandbox,
1495
+ image,
1496
+ onStep: (run, step) => {
1497
+ renderAgentStep(run, step);
1498
+
1499
+ // Update console output
1500
+ if (step.type === 'execute' && agentEls.consoleBody) {
1501
+ const detail = step.detail || '';
1502
+ if (step.status === 'failed') {
1503
+ agentEls.consoleBody.innerHTML += `<span class="console-error">${escapeHtml(detail)}</span>\n`;
1504
+ } else if (step.status === 'success') {
1505
+ agentEls.consoleBody.textContent += detail + '\n';
1506
+ }
1507
+ }
1508
+ },
1509
+ });
1510
+
1511
+ return result;
1512
+ }
1513
+
1514
+ // Agent-aware send handler: delegates to agent for code requests, standard send otherwise
1515
+ async function handleSendWithAgent() {
1516
+ const text = els.promptInput.value.trim();
1517
+ if (!text && !runtime.pendingImages.length) return;
1518
+
1519
+ // Determine if this should use the agent
1520
+ const useAgent = typeof MINDIAgent !== 'undefined' && isCodeRequest(text);
1521
+
1522
+ if (!useAgent) {
1523
+ return send();
1524
+ }
1525
+
1526
+ // Agent mode
1527
+ const chat = ensureChat();
1528
+ const wasEmpty = chat.messages.length === 0;
1529
+
1530
+ const userMsg = {
1531
+ role: 'user',
1532
+ content: text,
1533
+ images: runtime.pendingImages.map((p) => p.dataUrl),
1534
+ ts: Date.now(),
1535
+ };
1536
+ chat.messages.push(userMsg);
1537
+ chat.updatedAt = Date.now();
1538
+
1539
+ if (wasEmpty) {
1540
+ chat.title = deriveTitle(text);
1541
+ els.chatTitle.textContent = chat.title;
1542
+ }
1543
+
1544
+ const imageForApi = runtime.pendingImages[0]?.dataUrl || null;
1545
+ els.promptInput.value = '';
1546
+ autosizeTextarea();
1547
+ clearPendingImages();
1548
+ updateSendEnabled();
1549
+
1550
+ // Show loading
1551
+ const loadingMsg = { role: 'assistant', content: 'πŸ€– Agent working', loading: true, ts: Date.now() };
1552
+ chat.messages.push(loadingMsg);
1553
+ renderMessages();
1554
+ renderHistory();
1555
+ saveState();
1556
+
1557
+ runtime.isSending = true;
1558
+
1559
+ try {
1560
+ const agentResult = await runAgent(text, imageForApi);
1561
+
1562
+ // Remove loading
1563
+ const idx = chat.messages.indexOf(loadingMsg);
1564
+ if (idx !== -1) chat.messages.splice(idx, 1);
1565
+
1566
+ // Build response from agent
1567
+ const iterations = agentResult.iteration + 1;
1568
+ const status = agentResult.status === 'success' ? 'βœ…' : '❌';
1569
+ let responseText = `${status} Agent completed in ${iterations} iteration(s).\n\n`;
1570
+
1571
+ if (agentResult.currentCode) {
1572
+ const lang = agentResult.language || 'javascript';
1573
+ responseText += `\`\`\`${lang}\n${agentResult.currentCode}\n\`\`\``;
1574
+ } else {
1575
+ // Fallback: get the last generate step's detail
1576
+ const lastGen = [...agentResult.steps].reverse().find(s => s.type === 'generate' || s.type === 'fix');
1577
+ if (lastGen) responseText += lastGen.detail || 'No code generated.';
1578
+ }
1579
+
1580
+ const assistantMsg = {
1581
+ role: 'assistant',
1582
+ content: responseText,
1583
+ ts: Date.now(),
1584
+ };
1585
+ chat.messages.push(assistantMsg);
1586
+ chat.updatedAt = Date.now();
1587
+
1588
+ renderMessages();
1589
+ renderHistory();
1590
+ updatePreviewFromAssistant(assistantMsg);
1591
+ saveState();
1592
+
1593
+ } catch (e) {
1594
+ const idx = chat.messages.indexOf(loadingMsg);
1595
+ if (idx !== -1) chat.messages.splice(idx, 1);
1596
+
1597
+ const errorMsg = { role: 'assistant', content: `Agent error: ${e.message}`, ts: Date.now() };
1598
+ chat.messages.push(errorMsg);
1599
+ renderMessages();
1600
+ toast('Agent encountered an error', 'error');
1601
+ } finally {
1602
+ runtime.isSending = false;
1603
+ }
1604
+ }
1605
+
1606
+ // ----------------------------------------------------------------
1607
+ // Init
1608
+ // ----------------------------------------------------------------
1609
+ function init() {
1610
+ bind();
1611
+
1612
+ // Override the active send handler with agent-aware version
1613
+ if (typeof MINDIAgent !== 'undefined') {
1614
+ activeSend = handleSendWithAgent;
1615
+ }
1616
+
1617
+ renderHistory();
1618
+ renderMessages();
1619
+
1620
+ // Restore current chat title
1621
+ const c = currentChat();
1622
+ if (c) {
1623
+ els.chatTitle.textContent = c.title || 'New chat';
1624
+ const lastAssistant = [...c.messages].reverse().find((m) => m.role === 'assistant' && !m.loading);
1625
+ if (lastAssistant) updatePreviewFromAssistant(lastAssistant);
1626
+ }
1627
+
1628
+ autosizeTextarea();
1629
+ updateSendEnabled();
1630
+
1631
+ // Health check (after a tick so UI paints first)
1632
+ setTimeout(pingHealth, 80);
1633
+
1634
+ // Periodically re-check health (every 60s)
1635
+ setInterval(pingHealth, 60_000);
1636
+
1637
+ console.log('[MINDI] Agent system loaded:', typeof MINDIAgent !== 'undefined' ? 'βœ…' : '❌');
1638
+ }
1639
+
1640
+ if (document.readyState === 'loading') {
1641
+ document.addEventListener('DOMContentLoaded', init, { once: true });
1642
+ } else {
1643
+ init();
1644
+ }
1645
+ })();
frontend/index.html ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <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">
301
+ <span class="field-label">Temperature <em class="field-value" id="temp-val">0.7</em></span>
302
+ <input id="settings-temp" type="range" min="0" max="2" step="0.05" value="0.7" />
303
+ <span class="field-hint">Lower = more focused. Higher = more creative.</span>
304
+ </label>
305
+
306
+ <label class="field">
307
+ <span class="field-label">Max tokens <em class="field-value" id="tokens-val">2048</em></span>
308
+ <input id="settings-tokens" type="range" min="128" max="4096" step="128" value="2048" />
309
+ <span class="field-hint">Maximum length of the generated response.</span>
310
+ </label>
311
+ </div>
312
+
313
+ <div class="modal-foot">
314
+ <button class="btn btn--ghost" data-close>Cancel</button>
315
+ <button class="btn btn--primary" id="save-settings">Save settings</button>
316
+ </div>
317
+ </div>
318
+ </div>
319
+
320
+ <!-- ============ TOAST CONTAINER ============ -->
321
+ <div class="toasts" id="toasts" aria-live="polite" aria-atomic="true"></div>
322
+
323
+ <!-- Prism.js core + languages -->
324
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
325
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script>
326
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-typescript.min.js"></script>
327
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-jsx.min.js"></script>
328
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-tsx.min.js"></script>
329
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
330
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-css.min.js"></script>
331
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-markup.min.js"></script>
332
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script>
333
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
334
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-sql.min.js"></script>
335
+
336
+ <script src="sandbox.js"></script>
337
+ <script src="agent.js"></script>
338
+ <script src="app.js"></script>
339
+ </body>
340
+ </html>
frontend/sandbox.js ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/styles.css ADDED
@@ -0,0 +1,1364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: 420px;
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: 24px;
568
+ padding: 28px 24px 16px;
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-copy {
690
+ font-family: var(--mono);
691
+ font-size: 11px;
692
+ color: var(--text-mute);
693
+ padding: 4px 10px;
694
+ border-radius: 4px;
695
+ border: 1px solid var(--border);
696
+ transition: background .2s var(--ease), color .2s var(--ease), border-color .2s var(--ease);
697
+ }
698
+ .md-copy:hover { background: var(--hover); color: var(--text); border-color: var(--border-2); }
699
+ .md-code-block code {
700
+ display: block;
701
+ font-family: var(--mono);
702
+ font-size: 13px;
703
+ line-height: 1.6;
704
+ padding: 14px 16px;
705
+ overflow-x: auto;
706
+ white-space: pre;
707
+ }
708
+
709
+ /* Loading message */
710
+ .msg-loading .msg-bubble {
711
+ display: inline-flex;
712
+ align-items: center;
713
+ gap: 8px;
714
+ padding: 12px 16px;
715
+ background: var(--panel);
716
+ border: 1px solid var(--border);
717
+ border-radius: var(--r-lg);
718
+ color: var(--text-mute);
719
+ font-size: 13px;
720
+ }
721
+ .dots { display: inline-flex; gap: 4px; }
722
+ .dots span {
723
+ width: 6px; height: 6px;
724
+ border-radius: 50%;
725
+ background: var(--purple);
726
+ opacity: .4;
727
+ animation: bounce 1.2s var(--ease-2) infinite;
728
+ }
729
+ .dots span:nth-child(2) { animation-delay: .2s; background: #5a4ee9; }
730
+ .dots span:nth-child(3) { animation-delay: .4s; background: var(--blue); }
731
+ @keyframes bounce {
732
+ 0%, 80%, 100% { transform: translateY(0); opacity: .4; }
733
+ 40% { transform: translateY(-4px); opacity: 1; }
734
+ }
735
+
736
+ /* ============ COMPOSER ============ */
737
+ .composer-wrap {
738
+ padding: 12px 24px 16px;
739
+ flex-shrink: 0;
740
+ }
741
+ .composer {
742
+ max-width: 880px;
743
+ margin: 0 auto;
744
+ background: rgba(20, 20, 31, .85);
745
+ border: 1px solid var(--border-2);
746
+ border-radius: var(--r-xl);
747
+ backdrop-filter: blur(16px);
748
+ -webkit-backdrop-filter: blur(16px);
749
+ transition: border-color .25s var(--ease), box-shadow .25s var(--ease), background .25s var(--ease);
750
+ overflow: hidden;
751
+ }
752
+ .composer:focus-within {
753
+ border-color: rgba(124, 58, 237, .55);
754
+ background: rgba(26, 26, 46, .9);
755
+ box-shadow:
756
+ 0 0 0 4px rgba(124, 58, 237, .12),
757
+ 0 24px 60px -28px rgba(124, 58, 237, .55);
758
+ }
759
+ .composer-images {
760
+ display: flex;
761
+ flex-wrap: wrap;
762
+ gap: 8px;
763
+ padding: 12px 14px 4px;
764
+ }
765
+ .composer-image {
766
+ position: relative;
767
+ width: 64px; height: 64px;
768
+ border-radius: var(--r-sm);
769
+ background-size: cover;
770
+ background-position: center;
771
+ border: 1px solid var(--border-2);
772
+ }
773
+ .composer-image-remove {
774
+ position: absolute;
775
+ top: -6px; right: -6px;
776
+ width: 20px; height: 20px;
777
+ border-radius: 50%;
778
+ display: grid; place-items: center;
779
+ background: var(--panel-2);
780
+ border: 1px solid var(--border-3);
781
+ color: var(--text);
782
+ font-size: 11px;
783
+ line-height: 1;
784
+ transition: background .2s var(--ease), transform .2s var(--ease);
785
+ }
786
+ .composer-image-remove:hover { background: var(--err); transform: scale(1.1); }
787
+
788
+ .composer-row {
789
+ display: flex;
790
+ align-items: flex-end;
791
+ gap: 8px;
792
+ padding: 10px 12px;
793
+ }
794
+ .composer-btn {
795
+ width: 36px; height: 36px;
796
+ flex-shrink: 0;
797
+ display: grid; place-items: center;
798
+ border-radius: var(--r-sm);
799
+ color: var(--text-mute);
800
+ transition: background .2s var(--ease), color .2s var(--ease);
801
+ }
802
+ .composer-btn:hover { background: var(--hover); color: var(--text); }
803
+ .composer-btn svg { width: 18px; height: 18px; }
804
+
805
+ .composer textarea {
806
+ flex: 1;
807
+ font-family: var(--sans);
808
+ font-size: 14.5px;
809
+ line-height: 1.55;
810
+ padding: 8px 4px;
811
+ color: var(--text);
812
+ background: transparent;
813
+ resize: none;
814
+ max-height: 200px;
815
+ min-height: 24px;
816
+ overflow-y: auto;
817
+ }
818
+ .composer textarea::placeholder { color: var(--text-mute); }
819
+
820
+ .composer-send {
821
+ flex-shrink: 0;
822
+ width: 36px; height: 36px;
823
+ border-radius: var(--r-sm);
824
+ display: grid; place-items: center;
825
+ background: var(--grad);
826
+ color: #fff;
827
+ transition: filter .2s var(--ease), transform .15s var(--ease), opacity .2s var(--ease), box-shadow .2s var(--ease);
828
+ box-shadow: 0 4px 16px -6px rgba(124, 58, 237, .5);
829
+ }
830
+ .composer-send:hover:not(:disabled) { filter: brightness(1.12); transform: translateY(-1px); box-shadow: 0 8px 22px -8px rgba(124, 58, 237, .7); }
831
+ .composer-send:active:not(:disabled) { transform: translateY(0); }
832
+ .composer-send:disabled { opacity: .35; cursor: not-allowed; box-shadow: none; }
833
+ .composer-send svg { width: 16px; height: 16px; }
834
+
835
+ .composer-foot {
836
+ text-align: center;
837
+ font-family: var(--mono);
838
+ font-size: 10.5px;
839
+ color: var(--text-dim);
840
+ margin-top: 10px;
841
+ letter-spacing: .03em;
842
+ }
843
+
844
+ /* ============ PREVIEW PANEL (RIGHT) ============ */
845
+ .preview {
846
+ display: flex;
847
+ flex-direction: column;
848
+ height: 100vh;
849
+ background: rgba(14, 14, 22, .8);
850
+ border-left: 1px solid var(--border);
851
+ backdrop-filter: blur(20px);
852
+ -webkit-backdrop-filter: blur(20px);
853
+ overflow: hidden;
854
+ }
855
+ body.preview-hidden .preview { display: none; }
856
+ .preview-head {
857
+ height: var(--header-h);
858
+ flex-shrink: 0;
859
+ display: flex;
860
+ align-items: center;
861
+ justify-content: space-between;
862
+ padding: 0 14px;
863
+ border-bottom: 1px solid var(--border);
864
+ }
865
+ .tabs {
866
+ display: inline-flex;
867
+ background: var(--panel);
868
+ border: 1px solid var(--border);
869
+ border-radius: var(--r-sm);
870
+ padding: 3px;
871
+ gap: 2px;
872
+ }
873
+ .tab {
874
+ padding: 6px 12px;
875
+ font-size: 12px;
876
+ font-weight: 500;
877
+ color: var(--text-mute);
878
+ border-radius: 6px;
879
+ transition: background .2s var(--ease), color .2s var(--ease);
880
+ }
881
+ .tab:hover { color: var(--text); }
882
+ .tab.is-active {
883
+ background: var(--grad);
884
+ color: #fff;
885
+ box-shadow: 0 4px 14px -4px rgba(124, 58, 237, .5);
886
+ }
887
+ .preview-actions { display: flex; gap: 4px; }
888
+
889
+ /* Preview panes */
890
+ .preview-pane {
891
+ flex: 1;
892
+ display: none;
893
+ flex-direction: column;
894
+ overflow: hidden;
895
+ position: relative;
896
+ }
897
+ .preview-pane.is-active { display: flex; }
898
+ .preview-empty {
899
+ flex: 1;
900
+ display: flex;
901
+ flex-direction: column;
902
+ align-items: center;
903
+ justify-content: center;
904
+ padding: 40px 24px;
905
+ text-align: center;
906
+ color: var(--text-mute);
907
+ gap: 8px;
908
+ }
909
+ .preview-empty-icon {
910
+ width: 56px; height: 56px;
911
+ display: grid; place-items: center;
912
+ border-radius: var(--r-md);
913
+ font-family: var(--mono);
914
+ font-size: 22px;
915
+ font-weight: 600;
916
+ color: var(--purple);
917
+ background: var(--grad-soft);
918
+ border: 1px solid rgba(124, 58, 237, .25);
919
+ margin-bottom: 8px;
920
+ }
921
+ .preview-empty p { font-size: 13px; }
922
+ .preview-empty .muted { color: var(--text-dim); font-size: 12px; max-width: 260px; line-height: 1.55; }
923
+
924
+ /* Code output */
925
+ .code-out {
926
+ flex: 1;
927
+ overflow: auto;
928
+ margin: 0;
929
+ background: #0c0c14;
930
+ }
931
+ .code-out code {
932
+ display: block;
933
+ font-family: var(--mono);
934
+ font-size: 13px;
935
+ line-height: 1.65;
936
+ padding: 16px 18px;
937
+ white-space: pre;
938
+ }
939
+
940
+ /* Live HTML preview */
941
+ #live-frame {
942
+ flex: 1;
943
+ width: 100%;
944
+ height: 100%;
945
+ background: #fff;
946
+ border: 0;
947
+ }
948
+
949
+ /* Sections */
950
+ .sections {
951
+ flex: 1;
952
+ overflow-y: auto;
953
+ padding: 14px;
954
+ display: flex;
955
+ flex-direction: column;
956
+ gap: 10px;
957
+ }
958
+ .section-card {
959
+ border-radius: var(--r-md);
960
+ border: 1px solid var(--border-2);
961
+ background: var(--panel);
962
+ overflow: hidden;
963
+ }
964
+ .section-card-head {
965
+ display: flex;
966
+ align-items: center;
967
+ justify-content: space-between;
968
+ padding: 9px 14px;
969
+ font-family: var(--mono);
970
+ font-size: 11px;
971
+ letter-spacing: .12em;
972
+ text-transform: uppercase;
973
+ background: rgba(255, 255, 255, .02);
974
+ border-bottom: 1px solid var(--border);
975
+ }
976
+ .section-card-head .section-tag {
977
+ display: inline-flex;
978
+ align-items: center;
979
+ gap: 6px;
980
+ }
981
+ .section-card-head .section-tag::before {
982
+ content: "";
983
+ width: 7px; height: 7px;
984
+ border-radius: 50%;
985
+ background: var(--c, var(--text-dim));
986
+ box-shadow: 0 0 8px var(--c, transparent);
987
+ }
988
+ .section-card-head span:last-child {
989
+ color: var(--text-dim);
990
+ letter-spacing: .04em;
991
+ font-size: 10px;
992
+ }
993
+ .section-card-body {
994
+ padding: 12px 14px;
995
+ font-size: 13px;
996
+ line-height: 1.6;
997
+ color: var(--text-2);
998
+ white-space: pre-wrap;
999
+ word-wrap: break-word;
1000
+ overflow-wrap: anywhere;
1001
+ }
1002
+ .section-card-body code {
1003
+ font-family: var(--mono);
1004
+ font-size: 12.5px;
1005
+ background: rgba(255, 255, 255, .04);
1006
+ padding: 1px 4px;
1007
+ border-radius: 3px;
1008
+ }
1009
+
1010
+ .section-card[data-kind="thinking"] { --c: var(--c-thinking); }
1011
+ .section-card[data-kind="thinking"] .section-tag { color: var(--c-thinking); }
1012
+ .section-card[data-kind="code"] { --c: var(--c-code); }
1013
+ .section-card[data-kind="code"] .section-tag { color: var(--c-code); }
1014
+ .section-card[data-kind="critique"] { --c: var(--c-critique); }
1015
+ .section-card[data-kind="critique"] .section-tag { color: var(--c-critique); }
1016
+ .section-card[data-kind="fix"] { --c: var(--c-fix); }
1017
+ .section-card[data-kind="fix"] .section-tag { color: var(--c-fix); }
1018
+ .section-card[data-kind="error"] { --c: var(--c-error); }
1019
+ .section-card[data-kind="error"] .section-tag { color: var(--c-error); }
1020
+ .section-card[data-kind="suggest"] { --c: var(--c-suggest); }
1021
+ .section-card[data-kind="suggest"] .section-tag { color: var(--c-suggest); }
1022
+ .section-card[data-kind="file"] { --c: var(--c-file); }
1023
+ .section-card[data-kind="file"] .section-tag { color: var(--c-file); }
1024
+
1025
+ /* ============ SETTINGS MODAL ============ */
1026
+ .modal {
1027
+ position: fixed; inset: 0;
1028
+ display: grid;
1029
+ place-items: center;
1030
+ z-index: 80;
1031
+ padding: 24px;
1032
+ animation: fade-in .2s var(--ease);
1033
+ }
1034
+ @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
1035
+ .modal-backdrop {
1036
+ position: absolute; inset: 0;
1037
+ background: rgba(0, 0, 0, .55);
1038
+ backdrop-filter: blur(6px);
1039
+ }
1040
+ .modal-card {
1041
+ position: relative;
1042
+ width: min(440px, 100%);
1043
+ background: var(--panel);
1044
+ border: 1px solid var(--border-2);
1045
+ border-radius: var(--r-lg);
1046
+ box-shadow: 0 30px 80px -10px rgba(0, 0, 0, .6);
1047
+ overflow: hidden;
1048
+ animation: pop-in .25s var(--ease);
1049
+ }
1050
+ @keyframes pop-in {
1051
+ from { opacity: 0; transform: scale(.95) translateY(6px); }
1052
+ to { opacity: 1; transform: scale(1) translateY(0); }
1053
+ }
1054
+ .modal-head {
1055
+ display: flex;
1056
+ align-items: center;
1057
+ justify-content: space-between;
1058
+ padding: 14px 18px;
1059
+ border-bottom: 1px solid var(--border);
1060
+ }
1061
+ .modal-head h3 {
1062
+ font-size: 15px;
1063
+ font-weight: 600;
1064
+ }
1065
+ .modal-body {
1066
+ padding: 16px 18px;
1067
+ display: flex;
1068
+ flex-direction: column;
1069
+ gap: 18px;
1070
+ }
1071
+ .field {
1072
+ display: flex;
1073
+ flex-direction: column;
1074
+ gap: 6px;
1075
+ }
1076
+ .field-label {
1077
+ font-size: 12.5px;
1078
+ font-weight: 500;
1079
+ color: var(--text);
1080
+ display: flex;
1081
+ justify-content: space-between;
1082
+ align-items: center;
1083
+ }
1084
+ .field-value {
1085
+ font-family: var(--mono);
1086
+ font-style: normal;
1087
+ font-size: 12px;
1088
+ font-weight: 500;
1089
+ color: var(--purple);
1090
+ background: var(--grad-soft);
1091
+ padding: 2px 8px;
1092
+ border-radius: 999px;
1093
+ border: 1px solid rgba(124, 58, 237, .2);
1094
+ }
1095
+ .field-hint {
1096
+ font-size: 11.5px;
1097
+ color: var(--text-mute);
1098
+ line-height: 1.5;
1099
+ }
1100
+ .field input[type="url"],
1101
+ .field input[type="password"] {
1102
+ padding: 9px 12px;
1103
+ border-radius: var(--r-sm);
1104
+ border: 1px solid var(--border-2);
1105
+ background: var(--bg-1);
1106
+ color: var(--text);
1107
+ font-family: var(--mono);
1108
+ font-size: 12.5px;
1109
+ transition: border-color .2s var(--ease), box-shadow .2s var(--ease);
1110
+ }
1111
+ .field input[type="url"]:focus,
1112
+ .field input[type="password"]:focus {
1113
+ border-color: rgba(124, 58, 237, .5);
1114
+ box-shadow: 0 0 0 3px rgba(124, 58, 237, .12);
1115
+ }
1116
+ .field input[type="range"] {
1117
+ -webkit-appearance: none;
1118
+ appearance: none;
1119
+ width: 100%;
1120
+ height: 4px;
1121
+ background: var(--bg-1);
1122
+ border-radius: 2px;
1123
+ border: 1px solid var(--border);
1124
+ }
1125
+ .field input[type="range"]::-webkit-slider-thumb {
1126
+ -webkit-appearance: none;
1127
+ appearance: none;
1128
+ width: 16px; height: 16px;
1129
+ border-radius: 50%;
1130
+ background: var(--grad);
1131
+ border: 2px solid #fff;
1132
+ cursor: pointer;
1133
+ box-shadow: 0 0 0 2px rgba(124, 58, 237, .25), 0 4px 12px -2px rgba(124, 58, 237, .55);
1134
+ transition: transform .15s var(--ease);
1135
+ }
1136
+ .field input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.15); }
1137
+ .field input[type="range"]::-moz-range-thumb {
1138
+ width: 16px; height: 16px;
1139
+ border-radius: 50%;
1140
+ background: var(--purple);
1141
+ border: 2px solid #fff;
1142
+ cursor: pointer;
1143
+ }
1144
+
1145
+ .modal-foot {
1146
+ padding: 12px 18px 16px;
1147
+ display: flex;
1148
+ justify-content: flex-end;
1149
+ gap: 8px;
1150
+ border-top: 1px solid var(--border);
1151
+ }
1152
+
1153
+ /* ============ TOASTS ============ */
1154
+ .toasts {
1155
+ position: fixed;
1156
+ bottom: 24px;
1157
+ right: 24px;
1158
+ display: flex;
1159
+ flex-direction: column;
1160
+ gap: 10px;
1161
+ z-index: 100;
1162
+ pointer-events: none;
1163
+ }
1164
+ .toast {
1165
+ pointer-events: auto;
1166
+ display: flex;
1167
+ align-items: center;
1168
+ gap: 10px;
1169
+ padding: 11px 16px;
1170
+ border-radius: var(--r-md);
1171
+ background: var(--panel-2);
1172
+ border: 1px solid var(--border-2);
1173
+ box-shadow: 0 18px 36px -12px rgba(0, 0, 0, .55);
1174
+ font-size: 13px;
1175
+ color: var(--text);
1176
+ min-width: 240px;
1177
+ max-width: 360px;
1178
+ animation: toast-in .35s var(--ease);
1179
+ }
1180
+ @keyframes toast-in {
1181
+ from { opacity: 0; transform: translateX(20px); }
1182
+ to { opacity: 1; transform: translateX(0); }
1183
+ }
1184
+ .toast.is-leaving {
1185
+ animation: toast-out .25s var(--ease) forwards;
1186
+ }
1187
+ @keyframes toast-out {
1188
+ to { opacity: 0; transform: translateX(20px); }
1189
+ }
1190
+ .toast-icon {
1191
+ flex-shrink: 0;
1192
+ width: 8px; height: 8px;
1193
+ border-radius: 50%;
1194
+ }
1195
+ .toast--success .toast-icon { background: var(--ok); box-shadow: 0 0 8px var(--ok); }
1196
+ .toast--error .toast-icon { background: var(--err); box-shadow: 0 0 8px var(--err); }
1197
+ .toast--info .toast-icon { background: var(--blue); box-shadow: 0 0 8px var(--blue); }
1198
+
1199
+ /* ============ PRISM OVERRIDES ============ */
1200
+ pre[class*="language-"], code[class*="language-"] {
1201
+ background: transparent !important;
1202
+ text-shadow: none !important;
1203
+ font-family: var(--mono) !important;
1204
+ }
1205
+ .token.comment, .token.prolog, .token.doctype, .token.cdata { color: #6a6a85 !important; font-style: italic; }
1206
+ .token.punctuation { color: #b4b4c4 !important; }
1207
+ .token.property, .token.tag, .token.boolean, .token.number, .token.constant, .token.symbol, .token.deleted { color: #fbbf24 !important; }
1208
+ .token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted { color: #34d399 !important; }
1209
+ .token.operator, .token.entity, .token.url, .language-css .token.string, .style .token.string { color: #5eead4 !important; }
1210
+ .token.atrule, .token.attr-value, .token.keyword { color: #a78bfa !important; }
1211
+ .token.function, .token.class-name { color: #60a5fa !important; }
1212
+ .token.regex, .token.important, .token.variable { color: #f87171 !important; }
1213
+
1214
+ /* ============ RESPONSIVE ============ */
1215
+ @media (max-width: 1180px) {
1216
+ :root { --preview-w: 380px; }
1217
+ }
1218
+ @media (max-width: 1024px) {
1219
+ .app { grid-template-columns: var(--sidebar-w) 1fr; }
1220
+ .preview {
1221
+ position: fixed;
1222
+ top: 0; right: 0;
1223
+ width: min(420px, 100%);
1224
+ height: 100vh;
1225
+ z-index: 40;
1226
+ transform: translateX(100%);
1227
+ transition: transform .35s var(--ease);
1228
+ border-left: 1px solid var(--border);
1229
+ }
1230
+ body.preview-open .preview { transform: translateX(0); }
1231
+ }
1232
+ @media (max-width: 768px) {
1233
+ .app { grid-template-columns: 1fr; }
1234
+ .icon-btn--menu { display: grid; }
1235
+ .sidebar {
1236
+ position: fixed;
1237
+ top: 0; left: 0;
1238
+ width: var(--sidebar-w);
1239
+ height: 100vh;
1240
+ transform: translateX(-100%);
1241
+ transition: transform .35s var(--ease);
1242
+ }
1243
+ body.sidebar-open .sidebar { transform: translateX(0); }
1244
+ .welcome-title { font-size: 28px; }
1245
+ .welcome-svg { width: 80px; height: 80px; }
1246
+ .quick-actions { grid-template-columns: 1fr; max-width: 360px; }
1247
+ .composer-wrap { padding: 10px 14px 14px; }
1248
+ .messages { padding: 18px 14px 12px; }
1249
+ .preview { width: 100%; }
1250
+ }
1251
+ @media (max-width: 480px) {
1252
+ .welcome { padding: 24px 16px 16px; }
1253
+ .welcome-title { font-size: 24px; }
1254
+ .welcome-sub { font-size: 14px; margin-bottom: 24px; }
1255
+ }
1256
+
1257
+ /* ============ AGENT WORKSPACE ============ */
1258
+ .agent-log {
1259
+ display: flex;
1260
+ flex-direction: column;
1261
+ gap: 8px;
1262
+ padding: 14px;
1263
+ overflow-y: auto;
1264
+ max-height: 50%;
1265
+ }
1266
+ .agent-step {
1267
+ display: flex;
1268
+ align-items: flex-start;
1269
+ gap: 10px;
1270
+ padding: 12px 14px;
1271
+ border-radius: var(--r-md);
1272
+ background: var(--panel);
1273
+ border: 1px solid var(--border);
1274
+ animation: msg-in .3s var(--ease) both;
1275
+ }
1276
+ .agent-step-icon {
1277
+ width: 24px; height: 24px;
1278
+ flex-shrink: 0;
1279
+ display: grid; place-items: center;
1280
+ border-radius: 50%;
1281
+ font-size: 12px;
1282
+ }
1283
+ .agent-step--running .agent-step-icon {
1284
+ background: rgba(124, 58, 237, .2);
1285
+ color: var(--purple);
1286
+ animation: spin 1.2s linear infinite;
1287
+ }
1288
+ .agent-step--success .agent-step-icon {
1289
+ background: rgba(16, 185, 129, .15);
1290
+ color: var(--ok);
1291
+ }
1292
+ .agent-step--failed .agent-step-icon {
1293
+ background: rgba(239, 68, 68, .15);
1294
+ color: var(--err);
1295
+ }
1296
+ @keyframes spin { to { transform: rotate(360deg); } }
1297
+
1298
+ .agent-step-body { flex: 1; min-width: 0; }
1299
+ .agent-step-title {
1300
+ font-size: 13px;
1301
+ font-weight: 600;
1302
+ color: var(--text);
1303
+ margin-bottom: 2px;
1304
+ }
1305
+ .agent-step-detail {
1306
+ font-size: 12px;
1307
+ color: var(--text-mute);
1308
+ white-space: pre-wrap;
1309
+ word-break: break-word;
1310
+ }
1311
+
1312
+ .agent-sandbox {
1313
+ height: 260px;
1314
+ border: 1px solid var(--border);
1315
+ border-radius: var(--r-md);
1316
+ margin: 0 14px;
1317
+ overflow: hidden;
1318
+ background: #fff;
1319
+ }
1320
+ .agent-sandbox .sandbox-iframe {
1321
+ width: 100%; height: 100%;
1322
+ border: none; border-radius: var(--r-md);
1323
+ }
1324
+
1325
+ .agent-console {
1326
+ margin: 10px 14px 14px;
1327
+ border-radius: var(--r-md);
1328
+ background: #0a0a12;
1329
+ border: 1px solid var(--border);
1330
+ overflow: hidden;
1331
+ }
1332
+ .agent-console-head {
1333
+ padding: 8px 14px;
1334
+ font-family: var(--mono);
1335
+ font-size: 11px;
1336
+ font-weight: 500;
1337
+ text-transform: uppercase;
1338
+ letter-spacing: .1em;
1339
+ color: var(--text-mute);
1340
+ background: rgba(255,255,255,.02);
1341
+ border-bottom: 1px solid var(--border);
1342
+ }
1343
+ .agent-console-body {
1344
+ font-family: var(--mono);
1345
+ font-size: 12px;
1346
+ line-height: 1.6;
1347
+ color: var(--c-code);
1348
+ padding: 12px 14px;
1349
+ max-height: 160px;
1350
+ overflow-y: auto;
1351
+ white-space: pre-wrap;
1352
+ word-break: break-all;
1353
+ }
1354
+ .agent-console-body .console-error {
1355
+ color: var(--err);
1356
+ }
1357
+
1358
+ /* ============ REDUCE MOTION ============ */
1359
+ @media (prefers-reduced-motion: reduce) {
1360
+ *, *::before, *::after {
1361
+ animation-duration: .01ms !important;
1362
+ transition-duration: .01ms !important;
1363
+ }
1364
+ }
frontend/test_api.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import os, sys, time, json
7
+ import requests
8
+
9
+ BASE = os.environ.get("MINDI_API", "https://mindigenous-mindi-chat.hf.space")
10
+ TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_TOKEN")
11
+ PROMPT = sys.argv[1] if len(sys.argv) > 1 else "Write hello world in Python"
12
+ MAXTOK = int(sys.argv[2]) if len(sys.argv) > 2 else 256
13
+
14
+ HEADERS = {"Content-Type": "application/json"}
15
+ if TOKEN:
16
+ HEADERS["Authorization"] = f"Bearer {TOKEN}"
17
+ print(f"[auth] HF_TOKEN detected (len={len(TOKEN)}) -> sending Authorization header")
18
+ else:
19
+ print("[auth] No HF_TOKEN found in env -> anonymous (will likely hit ZeroGPU quota).")
20
+ print(" Set HF_TOKEN to your PRO HuggingFace token to bypass.")
21
+
22
+ # 1. Config check
23
+ print("\n=== Step 1: Config check ===")
24
+ for path in ("/gradio_api/config", "/config"):
25
+ try:
26
+ r = requests.get(BASE + path, headers=HEADERS, timeout=15)
27
+ print(f"GET {path} -> {r.status_code}")
28
+ if r.status_code == 200:
29
+ d = r.json()
30
+ print(" Version :", d.get("version", "?"))
31
+ print(" Protocol:", d.get("protocol", "?"))
32
+ apis = [x["api_name"] for x in d.get("dependencies", []) if x.get("api_name")]
33
+ print(" APIs :", apis)
34
+ break
35
+ except Exception as e:
36
+ print(f" {path} failed:", e)
37
+
38
+ # 2. Quick generation test
39
+ print("\n=== Step 2: API generation test ===")
40
+ print(f"Prompt: {PROMPT!r} | max_tokens={MAXTOK}")
41
+ try:
42
+ start = time.time()
43
+ resp = requests.post(
44
+ BASE + "/gradio_api/call/chat_fn",
45
+ headers=HEADERS,
46
+ json={"data": [PROMPT, None, 0.7, MAXTOK]},
47
+ timeout=30,
48
+ )
49
+ print("Submit status:", resp.status_code)
50
+ if resp.status_code != 200:
51
+ print("Error body:", resp.text[:400])
52
+ sys.exit(1)
53
+
54
+ event_id = resp.json().get("event_id")
55
+ print("Event ID:", event_id)
56
+ if not event_id:
57
+ print("No event_id returned:", resp.text[:300])
58
+ sys.exit(1)
59
+
60
+ sse = requests.get(
61
+ BASE + "/gradio_api/call/chat_fn/" + event_id,
62
+ headers=HEADERS, timeout=180, stream=True,
63
+ )
64
+ print("SSE status :", sse.status_code)
65
+
66
+ last_event = None
67
+ got_complete = False
68
+ for line in sse.iter_lines(decode_unicode=True):
69
+ if not line:
70
+ continue
71
+ if line.startswith("event: "):
72
+ last_event = line[7:].strip()
73
+ continue
74
+ if not line.startswith("data: "):
75
+ continue
76
+ payload = line[6:]
77
+ if payload in ("null", ""):
78
+ continue
79
+ try:
80
+ parsed = json.loads(payload)
81
+ raw = parsed[0] if isinstance(parsed, list) else parsed
82
+ output = json.loads(raw) if isinstance(raw, str) else raw
83
+ resp_text = output.get("response", "") if isinstance(output, dict) else str(output)
84
+ sections = list(output.get("sections", {}).keys()) if isinstance(output, dict) else []
85
+ elapsed = time.time() - start
86
+ print(f"\n--- {last_event or 'data'} ({elapsed:.1f}s) ---")
87
+ print("Length :", len(resp_text), "chars")
88
+ print("Sections:", sections)
89
+ print("Preview :")
90
+ print(resp_text[:1200])
91
+ if len(resp_text) > 1200:
92
+ print(f"... ({len(resp_text)-1200} more chars)")
93
+ if last_event == "complete":
94
+ got_complete = True
95
+ except Exception as e:
96
+ print(f"Parse error on {last_event}: {e} | raw: {payload[:200]}")
97
+
98
+ if not got_complete:
99
+ print("\n[!] No 'complete' event received.")
100
+ except Exception as e:
101
+ print("API test failed:", e)
102
+
103
+ print("\nDone!")
hf_space/README.md ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: MINDI 1.5 Vision-Coder API
3
+ emoji: 🧠
4
+ colorFrom: purple
5
+ colorTo: blue
6
+ sdk: gradio
7
+ sdk_version: 5.23.0
8
+ app_file: app.py
9
+ pinned: true
10
+ license: apache-2.0
11
+ ---
hf_space/app.py ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MINDI 1.5 Vision-Coder β€” HuggingFace Space (ZeroGPU)
3
+
4
+ Uses ZeroGPU for free A100 access (40GB VRAM).
5
+ Full bf16 model β€” NO quantization.
6
+
7
+ IMPORTANT: All .to("cuda") calls MUST be inside @spaces.GPU decorated functions.
8
+ ZeroGPU only provides GPU access inside those functions.
9
+ """
10
+
11
+ import os
12
+ import re
13
+ import gc
14
+ import json
15
+ import torch
16
+ import spaces
17
+ import gradio as gr
18
+ from pathlib import Path
19
+ from PIL import Image
20
+ from huggingface_hub import snapshot_download
21
+
22
+ # ── Global model reference ──────────────────────────────
23
+ MODEL = None
24
+ TOKENIZER = None
25
+ IS_LOADED = False
26
+
27
+ # ── Special token definitions ───────────────────────────
28
+ SECTION_TOKENS = {
29
+ "thinking": ("<|think_start|>", "<|think_end|>"),
30
+ "file": ("<|file_start|>", "<|file_end|>"),
31
+ "code": ("<|code_start|>", "<|code_end|>"),
32
+ "critique": ("<|critique_start|>", "<|critique_end|>"),
33
+ "suggest": ("<|suggest_start|>", "<|suggest_end|>"),
34
+ "search": ("<|search_start|>", "<|search_end|>"),
35
+ "error": ("<|error_start|>", "<|error_end|>"),
36
+ "fix": ("<|fix_start|>", "<|fix_end|>"),
37
+ }
38
+
39
+
40
+ def parse_output(text: str) -> dict:
41
+ result = {}
42
+ for section, (start_tok, end_tok) in SECTION_TOKENS.items():
43
+ pattern = re.escape(start_tok) + r"(.*?)" + re.escape(end_tok)
44
+ matches = re.findall(pattern, text, flags=re.DOTALL)
45
+ if matches:
46
+ result[section] = [m.strip() for m in matches]
47
+ return result
48
+
49
+
50
+ def clean_output(text: str) -> str:
51
+ text = text.replace("<|im_end|>", "").replace("<|im_start|>", "")
52
+ text = re.sub(r"^(system|user|assistant)\n?", "", text).strip()
53
+ return text
54
+
55
+
56
+ def download_checkpoint():
57
+ """Download checkpoint (CPU-safe, no CUDA needed)."""
58
+ ckpt_dir = Path("/tmp/mindi_ckpt")
59
+ if not ckpt_dir.exists():
60
+ print("[MINDI] Downloading checkpoint from HuggingFace...")
61
+ snapshot_download(
62
+ "Mindigenous/MINDI-1.5-Vision-Coder",
63
+ local_dir=str(ckpt_dir),
64
+ allow_patterns=[
65
+ "checkpoints/phase3_final/**",
66
+ "data/tokenizer/**",
67
+ ],
68
+ )
69
+ print("[MINDI] Download complete")
70
+ return ckpt_dir
71
+
72
+
73
+ def load_tokenizer(ckpt_dir):
74
+ """Load tokenizer (CPU-safe)."""
75
+ global TOKENIZER
76
+ if TOKENIZER is not None:
77
+ return
78
+
79
+ from transformers import AutoTokenizer
80
+ tok_path = ckpt_dir / "data" / "tokenizer" / "mindi_tokenizer"
81
+ if not tok_path.exists():
82
+ tok_path = "Qwen/Qwen2.5-Coder-7B-Instruct"
83
+ TOKENIZER = AutoTokenizer.from_pretrained(str(tok_path), trust_remote_code=True)
84
+ print(f"[MINDI] Tokenizer loaded: {len(TOKENIZER)} tokens")
85
+
86
+
87
+ def load_model_to_gpu(ckpt_dir):
88
+ """Load model TO GPU β€” MUST be called inside @spaces.GPU function."""
89
+ global MODEL, IS_LOADED
90
+
91
+ if IS_LOADED:
92
+ return
93
+
94
+ from transformers import (
95
+ AutoModelForCausalLM,
96
+ CLIPVisionModel, CLIPImageProcessor,
97
+ )
98
+ from peft import PeftModel
99
+ import torch.nn as nn
100
+
101
+ print("[MINDI] Loading full bf16 model to GPU...")
102
+
103
+ # Base LLM
104
+ base_model = AutoModelForCausalLM.from_pretrained(
105
+ "Qwen/Qwen2.5-Coder-7B-Instruct",
106
+ torch_dtype=torch.bfloat16,
107
+ device_map="auto",
108
+ trust_remote_code=True,
109
+ )
110
+ base_model.resize_token_embeddings(len(TOKENIZER))
111
+ print("[MINDI] Base model loaded (bf16)")
112
+
113
+ # LoRA
114
+ lora_path = ckpt_dir / "checkpoints" / "phase3_final" / "lora"
115
+ if lora_path.exists():
116
+ base_model = PeftModel.from_pretrained(base_model, str(lora_path))
117
+ print("[MINDI] LoRA loaded")
118
+
119
+ # CLIP
120
+ clip_model = CLIPVisionModel.from_pretrained(
121
+ "openai/clip-vit-large-patch14",
122
+ torch_dtype=torch.bfloat16,
123
+ ).to("cuda").eval()
124
+ clip_processor = CLIPImageProcessor.from_pretrained("openai/clip-vit-large-patch14")
125
+ print("[MINDI] CLIP loaded")
126
+
127
+ # Vision projection
128
+ class VisionProjection(nn.Module):
129
+ def __init__(self, clip_dim=1024, llm_dim=3584):
130
+ super().__init__()
131
+ self.projection = nn.Linear(clip_dim, llm_dim)
132
+ self.layer_norm = nn.LayerNorm(llm_dim)
133
+ def forward(self, x):
134
+ return self.layer_norm(self.projection(x))
135
+
136
+ vision_proj = VisionProjection().to("cuda").to(torch.bfloat16)
137
+ vision_ckpt = ckpt_dir / "checkpoints" / "phase3_final" / "vision"
138
+ if vision_ckpt.exists():
139
+ for f in vision_ckpt.iterdir():
140
+ if f.suffix in (".pt", ".bin", ".safetensors"):
141
+ state = torch.load(f, map_location="cuda", weights_only=True)
142
+ vision_proj.load_state_dict(state, strict=False)
143
+ print("[MINDI] Vision projection loaded")
144
+ break
145
+
146
+ # Fusion
147
+ class SimpleFusion(nn.Module):
148
+ def __init__(self, hidden_size=3584):
149
+ super().__init__()
150
+ self.visual_gate = nn.Linear(hidden_size, hidden_size)
151
+ self.text_gate_param = nn.Parameter(torch.zeros(1))
152
+ self.layer_norm = nn.LayerNorm(hidden_size)
153
+ def forward(self, text_embeds, visual_embeds):
154
+ gated_visual = torch.sigmoid(self.visual_gate(visual_embeds)) * visual_embeds
155
+ combined = torch.cat([gated_visual, text_embeds], dim=1)
156
+ return self.layer_norm(combined)
157
+
158
+ fusion = SimpleFusion().to("cuda").to(torch.bfloat16)
159
+ fusion_file = ckpt_dir / "checkpoints" / "phase3_final" / "fusion" / "fusion.pt"
160
+ if fusion_file.exists():
161
+ state = torch.load(fusion_file, map_location="cuda", weights_only=True)
162
+ fusion.load_state_dict(state, strict=False)
163
+ print("[MINDI] Fusion loaded")
164
+
165
+ MODEL = {
166
+ "llm": base_model,
167
+ "clip": clip_model,
168
+ "clip_processor": clip_processor,
169
+ "vision_proj": vision_proj,
170
+ "fusion": fusion,
171
+ }
172
+ IS_LOADED = True
173
+
174
+ vram = torch.cuda.memory_allocated() / 1e9
175
+ print(f"[MINDI] Ready! VRAM: {vram:.1f} GB")
176
+
177
+
178
+ # Pre-download checkpoint and tokenizer at startup (CPU-safe)
179
+ _ckpt_dir = download_checkpoint()
180
+ load_tokenizer(_ckpt_dir)
181
+
182
+
183
+ @spaces.GPU(duration=60)
184
+ def generate(prompt: str, image: Image.Image = None,
185
+ temperature: float = 0.7, max_tokens: int = 2048) -> str:
186
+ """Generate with full bf16 model. GPU allocated by ZeroGPU."""
187
+
188
+ # Load model INSIDE the GPU function
189
+ load_model_to_gpu(_ckpt_dir)
190
+
191
+ system_msg = (
192
+ "You are MINDI 1.5 Vision-Coder, an expert AI coding assistant. "
193
+ "When asked for code, respond with complete, working implementations. "
194
+ "Be concise and provide actual code, not descriptions."
195
+ )
196
+ formatted = (
197
+ f"<|im_start|>system\n{system_msg}<|im_end|>\n"
198
+ f"<|im_start|>user\n{prompt}<|im_end|>\n"
199
+ f"<|im_start|>assistant\n"
200
+ )
201
+
202
+ inputs = TOKENIZER(formatted, return_tensors="pt").to("cuda")
203
+
204
+ # Vision path
205
+ if image is not None and MODEL["clip"] is not None:
206
+ try:
207
+ pixel_values = MODEL["clip_processor"](
208
+ images=image, return_tensors="pt"
209
+ ).pixel_values.to("cuda", dtype=torch.bfloat16)
210
+
211
+ with torch.no_grad():
212
+ clip_out = MODEL["clip"](pixel_values).last_hidden_state
213
+ visual_tokens = MODEL["vision_proj"](clip_out)
214
+ text_embeds = MODEL["llm"].get_input_embeddings()(inputs["input_ids"])
215
+ fused = MODEL["fusion"](text_embeds, visual_tokens)
216
+
217
+ outputs = MODEL["llm"].generate(
218
+ inputs_embeds=fused,
219
+ attention_mask=torch.ones(fused.shape[:2], device="cuda"),
220
+ max_new_tokens=int(max_tokens),
221
+ temperature=max(float(temperature), 0.01),
222
+ do_sample=float(temperature) > 0,
223
+ pad_token_id=TOKENIZER.pad_token_id or TOKENIZER.eos_token_id,
224
+ )
225
+ return clean_output(TOKENIZER.decode(outputs[0], skip_special_tokens=False))
226
+ except Exception as e:
227
+ print(f"[WARN] Vision failed: {e}")
228
+
229
+ # Text-only path
230
+ with torch.no_grad():
231
+ outputs = MODEL["llm"].generate(
232
+ **inputs,
233
+ max_new_tokens=int(max_tokens),
234
+ temperature=max(float(temperature), 0.01),
235
+ do_sample=float(temperature) > 0,
236
+ pad_token_id=TOKENIZER.pad_token_id or TOKENIZER.eos_token_id,
237
+ )
238
+ generated = outputs[:, inputs["input_ids"].shape[1]:]
239
+ return clean_output(TOKENIZER.decode(generated[0], skip_special_tokens=False))
240
+
241
+
242
+ # ── Gradio endpoint ─────────────────────────────────────
243
+
244
+ def chat_fn(message: str, image: Image.Image = None,
245
+ temperature: float = 0.7, max_tokens: int = 2048) -> str:
246
+ """Exposed via Gradio API β€” wraps generate."""
247
+ try:
248
+ response = generate(message, image, temperature, max_tokens)
249
+ sections = parse_output(response)
250
+ return json.dumps({"response": response, "sections": sections})
251
+ except Exception as e:
252
+ error_msg = str(e)
253
+ if "quota" in error_msg.lower() or "gpu" in error_msg.lower():
254
+ return json.dumps({"response": f"⚠️ GPU quota exceeded. Please try again later or reduce Max Tokens. Error: {error_msg}", "sections": {"error": [error_msg]}})
255
+ return json.dumps({"response": f"Error during generation: {error_msg}", "sections": {"error": [error_msg]}})
256
+
257
+
258
+ # ── Gradio App ──────────────────────────────────────────
259
+
260
+ with gr.Blocks(title="MINDI 1.5 API", theme=gr.themes.Soft(
261
+ primary_hue="purple", secondary_hue="blue",
262
+ )) as demo:
263
+ gr.Markdown("# 🧠 MINDI 1.5 Vision-Coder API\nFull bf16 on ZeroGPU A100 · No quantization")
264
+
265
+ with gr.Row():
266
+ with gr.Column(scale=3):
267
+ prompt = gr.Textbox(label="Prompt", placeholder="Write code...", lines=4)
268
+ image = gr.Image(label="Image (optional)", type="pil")
269
+ with gr.Column(scale=1):
270
+ temperature = gr.Slider(0, 2, value=0.7, step=0.1, label="Temperature")
271
+ max_tokens = gr.Slider(128, 4096, value=2048, step=128, label="Max Tokens")
272
+ submit_btn = gr.Button("Generate", variant="primary")
273
+
274
+ output = gr.Textbox(label="Response (JSON)", lines=20)
275
+
276
+ submit_btn.click(
277
+ fn=chat_fn,
278
+ inputs=[prompt, image, temperature, max_tokens],
279
+ outputs=output,
280
+ )
281
+
282
+ demo.queue()
283
+ demo.launch()
hf_space/requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch
2
+ transformers>=4.40.0
3
+ peft>=0.10.0
4
+ accelerate>=0.30.0
5
+ safetensors
6
+ sentencepiece
7
+ protobuf
8
+ pillow
9
+ gradio>=5.0.0
10
+ huggingface_hub
11
+ spaces