/* ============================================================= 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\n\n\n\n\nLumina — Future of Design\n