Add HF Space backend and frontend with HF token auth
Browse filesFrontend (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 +266 -0
- frontend/app.js +1645 -0
- frontend/index.html +340 -0
- frontend/sandbox.js +305 -0
- frontend/styles.css +1364 -0
- frontend/test_api.py +103 -0
- hf_space/README.md +11 -0
- hf_space/app.py +283 -0
- hf_space/requirements.txt +11 -0
|
@@ -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;
|
|
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>')
|
| 154 |
+
.replace(/"/g, '"').replace(/'/g, ''');
|
| 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 |
+
})();
|
|
@@ -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 & 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: ```python def divide_list(numbers, divisor): result = [] for n in numbers: result.append(n / divisor) return result ```">
|
| 161 |
+
<span class="qc-icon">π§</span>
|
| 162 |
+
<h3>Debug Code</h3>
|
| 163 |
+
<p>Find & 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 & 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>
|
|
@@ -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;
|
|
@@ -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 |
+
}
|
|
@@ -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!")
|
|
@@ -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 |
+
---
|
|
@@ -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()
|
|
@@ -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
|