File size: 12,796 Bytes
4386567 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | /* =============================================================
MINDI API Service — Gradio SSE v3 integration
Connects to HuggingFace-hosted MINDI 1.5 Vision-Coder
============================================================= */
const API_DEFAULT = 'https://mindigenous-mindi-chat.hf.space';
function authHeaders(hfToken, extra = {}) {
const h = { ...extra };
if (hfToken) h['Authorization'] = `Bearer ${hfToken}`;
return h;
}
function dataUrlToBlob(dataUrl) {
const match = /^data:([^;]+);base64,(.+)$/.exec(dataUrl || '');
if (!match) throw new Error('Invalid image data URL');
const bytes = Uint8Array.from(atob(match[2]), c => c.charCodeAt(0));
return { blob: new Blob([bytes], { type: match[1] }), mime: match[1] };
}
async function uploadImageToGradio(base, dataUrl, hfToken, signal) {
const { blob, mime } = dataUrlToBlob(dataUrl);
const ext = (mime.split('/')[1] || 'png').replace('+xml', '').split(';')[0];
const filename = `mindi-upload-${Date.now()}.${ext}`;
const formData = new FormData();
formData.append('files', blob, filename);
const headers = authHeaders(hfToken);
delete headers['Content-Type'];
const res = await fetch(`${base}/gradio_api/upload`, {
method: 'POST', headers, body: formData, signal,
});
if (!res.ok) throw new Error(`Image upload ${res.status}`);
const result = await res.json();
const filePath = Array.isArray(result) ? result[0] : result?.files?.[0];
if (!filePath) throw new Error('Upload failed');
return filePath;
}
export async function callMINDI({ prompt, image, temperature = 0.7, maxTokens = 2048, history = [], hfToken = '', apiUrl = API_DEFAULT, signal }) {
const base = (apiUrl || API_DEFAULT).replace(/\/$/, '');
const isGradio = base.includes('hf.space') || base.includes('huggingface.co');
const historyJson = history.length ? JSON.stringify(history) : '';
if (isGradio) {
let imageArg = null;
if (image && image.startsWith('data:')) {
try {
const filePath = await uploadImageToGradio(base, image, hfToken, signal);
imageArg = { path: filePath, meta: { _type: 'gradio.FileData' }, orig_name: filePath.split('/').pop() };
} catch { imageArg = null; }
}
const submitRes = await fetch(`${base}/gradio_api/call/chat_fn`, {
method: 'POST',
headers: authHeaders(hfToken, { 'Content-Type': 'application/json' }),
body: JSON.stringify({ data: [prompt, imageArg, temperature, maxTokens, historyJson] }),
signal,
});
if (!submitRes.ok) {
const txt = await submitRes.text().catch(() => '');
throw new Error(`API ${submitRes.status}: ${txt.slice(0, 200)}`);
}
const { event_id } = await submitRes.json();
if (!event_id) throw new Error('No event_id returned');
const resultRes = await fetch(`${base}/gradio_api/call/chat_fn/${event_id}`, {
method: 'GET', headers: authHeaders(hfToken), signal,
});
if (!resultRes.ok) throw new Error(`API result ${resultRes.status}`);
const sseText = await resultRes.text();
const lines = sseText.split('\n');
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('event: complete')) {
const dataLine = lines[i + 1];
if (dataLine?.startsWith('data: ')) {
try {
const parsed = JSON.parse(dataLine.slice(6));
const raw = Array.isArray(parsed) ? parsed[0] : parsed;
try { return JSON.parse(raw); } catch { return { response: String(raw), sections: {} }; }
} catch { return { response: dataLine.slice(6), sections: {} }; }
}
break;
}
if (lines[i].startsWith('event: error')) {
const errMsg = lines[i + 1]?.startsWith('data: ') ? lines[i + 1].slice(6) : 'Gradio error';
throw new Error(errMsg.slice(0, 300));
}
}
throw new Error('No complete event in response');
} else {
const body = { prompt, temperature, max_tokens: maxTokens, history };
if (image) body.image = image;
const res = await fetch(`${base}/api/generate`, {
method: 'POST',
headers: authHeaders(hfToken, { 'Content-Type': 'application/json', 'Accept': 'application/json' }),
body: JSON.stringify(body), signal,
});
if (!res.ok) throw new Error(`API ${res.status}`);
return res.json();
}
}
export async function pingAPI(apiUrl, hfToken) {
const base = (apiUrl || API_DEFAULT).replace(/\/$/, '');
try {
const res = await fetch(base, { method: 'HEAD', mode: 'no-cors' }).catch(() => null);
return !!res;
} catch { return false; }
}
export function isQuotaError(result) {
if (!result) return false;
const text = String(result.response || '');
const errs = result.sections?.error || [];
const blob = (text + ' ' + errs.join(' ')).toLowerCase();
return /zerogpu|gpu quota|out of .* quota|exceeded .* quota|unlogged user|gpu task aborted|task aborted/.test(blob);
}
export function isQuotaException(errMessage) {
const msg = (errMessage || '').toLowerCase();
return /gpu quota|zerogpu|gpu task aborted|task aborted|unlogged user|out of .* quota|exceeded .* quota/.test(msg);
}
// Demo responses
const DEMOS = [
{
match: /landing|hero|page|website/i,
response: `Here's a complete landing page:\n\n\`\`\`html\n<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0">\n<title>Lumina — Future of Design</title>\n<script src="https://cdn.tailwindcss.com"><\/script>\n<style>\n@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');\nbody { font-family: 'Inter', sans-serif; }\n.gradient-bg { background: linear-gradient(135deg, #0f0c29, #302b63, #24243e); }\n.glow { box-shadow: 0 0 40px rgba(124, 58, 237, 0.3); }\n.card-hover:hover { transform: translateY(-4px); box-shadow: 0 20px 40px rgba(0,0,0,0.3); }\n</style>\n</head>\n<body class="gradient-bg text-white min-h-screen">\n<nav class="flex items-center justify-between px-8 py-5 max-w-7xl mx-auto">\n <div class="text-2xl font-bold bg-gradient-to-r from-purple-400 to-blue-400 bg-clip-text text-transparent">Lumina</div>\n <div class="hidden md:flex gap-8 text-sm text-gray-300">\n <a href="#features" class="hover:text-white transition">Features</a>\n <a href="#pricing" class="hover:text-white transition">Pricing</a>\n <a href="#about" class="hover:text-white transition">About</a>\n </div>\n <button class="px-5 py-2 bg-purple-600 rounded-full text-sm font-medium hover:bg-purple-500 transition glow">Get Started</button>\n</nav>\n<main class="max-w-7xl mx-auto px-8">\n <section class="py-24 text-center">\n <span class="inline-block px-4 py-1.5 bg-purple-500/20 border border-purple-500/30 rounded-full text-purple-300 text-xs font-medium tracking-wider uppercase mb-6">Now in Beta</span>\n <h1 class="text-5xl md:text-7xl font-extrabold leading-tight mb-6">\n Build faster.<br>\n <span class="bg-gradient-to-r from-purple-400 via-pink-400 to-blue-400 bg-clip-text text-transparent">Ship smarter.</span>\n </h1>\n <p class="text-lg text-gray-400 max-w-2xl mx-auto mb-10">The next-generation platform that turns your ideas into reality. No complexity, just results.</p>\n <div class="flex justify-center gap-4">\n <button class="px-8 py-3 bg-gradient-to-r from-purple-600 to-blue-600 rounded-full font-semibold hover:shadow-lg hover:shadow-purple-500/25 transition-all">Start Free Trial</button>\n <button class="px-8 py-3 border border-white/20 rounded-full font-medium hover:bg-white/5 transition">Watch Demo</button>\n </div>\n </section>\n <section id="features" class="py-20 grid md:grid-cols-3 gap-6">\n <div class="p-8 bg-white/5 border border-white/10 rounded-2xl card-hover transition-all">\n <div class="w-12 h-12 bg-purple-500/20 rounded-xl flex items-center justify-center text-2xl mb-4">⚡</div>\n <h3 class="text-lg font-semibold mb-2">Lightning Fast</h3>\n <p class="text-gray-400 text-sm">Deploy in seconds. Our edge network ensures your app loads instantly worldwide.</p>\n </div>\n <div class="p-8 bg-white/5 border border-white/10 rounded-2xl card-hover transition-all">\n <div class="w-12 h-12 bg-blue-500/20 rounded-xl flex items-center justify-center text-2xl mb-4">🔒</div>\n <h3 class="text-lg font-semibold mb-2">Enterprise Security</h3>\n <p class="text-gray-400 text-sm">SOC 2 compliant with end-to-end encryption. Your data is always protected.</p>\n </div>\n <div class="p-8 bg-white/5 border border-white/10 rounded-2xl card-hover transition-all">\n <div class="w-12 h-12 bg-pink-500/20 rounded-xl flex items-center justify-center text-2xl mb-4">🎨</div>\n <h3 class="text-lg font-semibold mb-2">Beautiful UI</h3>\n <p class="text-gray-400 text-sm">Pre-built components that look stunning out of the box. Customize everything.</p>\n </div>\n </section>\n</main>\n<footer class="border-t border-white/10 py-8 text-center text-gray-500 text-sm">\n <p>© 2026 Lumina. Crafted with AI.</p>\n</footer>\n</body>\n</html>\n\`\`\``,
},
{
match: /dashboard|chart|analytics|admin/i,
response: `Here's a dashboard UI:\n\n\`\`\`html\n<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">\n<title>Dashboard</title>\n<style>\n:root{--bg:#0b0b14;--panel:#14141f;--border:rgba(255,255,255,.08);--text:#ececf1;--mute:#8b94a7;--acc:#7c3aed}\n*{box-sizing:border-box;margin:0;padding:0}\nbody{background:var(--bg);color:var(--text);font:14px/1.55 'Inter',sans-serif;min-height:100vh;display:grid;grid-template-columns:240px 1fr}\naside{background:var(--panel);border-right:1px solid var(--border);padding:20px}\naside h1{font-size:18px;background:linear-gradient(135deg,#7c3aed,#2563eb);-webkit-background-clip:text;color:transparent;margin-bottom:24px}\nnav a{display:block;padding:10px 12px;border-radius:8px;color:var(--mute);text-decoration:none;margin-bottom:2px}\nnav a.active{background:rgba(124,58,237,.15);color:#fff}\nmain{padding:24px;overflow-y:auto}\n.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:20px}\n.stat{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:16px}\n.stat .v{font-size:24px;font-weight:600;margin-top:6px}\n.stat .l{color:var(--mute);font-size:12px;text-transform:uppercase;letter-spacing:.1em}\n.chart{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:18px;height:260px;display:flex;align-items:end;gap:8px}\n.bar{flex:1;background:linear-gradient(180deg,#7c3aed,#2563eb);border-radius:6px 6px 0 0;transition:height .5s}\n</style>\n</head>\n<body>\n<aside><h1>Pulsegrid</h1>\n<nav><a class="active">Overview</a><a>Customers</a><a>Revenue</a><a>Settings</a></nav>\n</aside>\n<main>\n<div class="stats">\n<div class="stat"><div class="l">Revenue</div><div class="v">$48,210</div></div>\n<div class="stat"><div class="l">Users</div><div class="v">12,840</div></div>\n<div class="stat"><div class="l">Conversion</div><div class="v">4.2%</div></div>\n<div class="stat"><div class="l">Churn</div><div class="v">1.1%</div></div>\n</div>\n<div class="chart">\n<div class="bar" style="height:40%"></div><div class="bar" style="height:65%"></div>\n<div class="bar" style="height:30%"></div><div class="bar" style="height:80%"></div>\n<div class="bar" style="height:55%"></div><div class="bar" style="height:90%"></div>\n<div class="bar" style="height:70%"></div>\n</div>\n</main>\n</body>\n</html>\n\`\`\``,
},
];
const DEFAULT_DEMO = {
response: `Here's a starter template:\n\n\`\`\`html\n<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">\n<title>MINDI Generated</title>\n<style>\n*{margin:0;padding:0;box-sizing:border-box}\nbody{min-height:100vh;background:#0f0c29;color:#fff;font-family:Inter,sans-serif;display:grid;place-items:center}\n.card{text-align:center;padding:48px;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);border-radius:20px;backdrop-filter:blur(10px)}\nh1{font-size:2.5rem;margin-bottom:12px;background:linear-gradient(135deg,#7c3aed,#2563eb);-webkit-background-clip:text;color:transparent}\np{color:#a0a0b8;font-size:1.1rem}\n</style>\n</head>\n<body>\n<div class="card">\n<h1>Hello from MINDI</h1>\n<p>Describe what you want to build and I'll generate it.</p>\n</div>\n</body>\n</html>\n\`\`\``,
sections: {},
};
export async function generateDemo(prompt) {
await new Promise(r => setTimeout(r, 800 + Math.random() * 600));
const found = DEMOS.find(d => d.match.test(prompt));
return { response: (found || DEFAULT_DEMO).response, sections: {} };
}
|