// API client — talks to the FastAPI backend. Every request carries the // Supabase bearer token so the FastAPI auth gate can identify the user. const _authHeader = () => { const s = window.palette_auth && window.palette_auth.getSession(); const tok = s && s.access_token; return tok ? { 'authorization': 'Bearer ' + tok } : {}; }; const _authQuery = () => { const s = window.palette_auth && window.palette_auth.getSession(); const tok = s && s.access_token; return tok ? ('access_token=' + encodeURIComponent(tok)) : ''; }; const _fetch = async (url, init = {}) => { const headers = { ...(init.headers || {}), ..._authHeader() }; const res = await fetch(url, { ...init, headers }); if (res.status === 401) { // Token expired mid-session — push the user back to login. if (window.palette_auth) { try { await window.palette_auth.signOut(); } catch(e) {} } window.dispatchEvent(new CustomEvent('palette:unauth')); } return res; }; const API = { async me() { const r = await _fetch('/api/auth/me'); if (!r.ok) throw new Error('me ' + r.status); return r.json(); }, async createInvite({ email, role } = {}) { const r = await _fetch('/api/auth/invites', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email, role: role || 'member' }), }); if (!r.ok) throw new Error('invite ' + r.status + ' ' + await r.text()); return r.json(); }, async checkInvite(token) { const r = await fetch('/api/auth/invites/' + encodeURIComponent(token)); if (!r.ok) throw new Error('invite ' + r.status + ' ' + await r.text()); return r.json(); }, async state() { const r = await _fetch('/api/state'); if (!r.ok) throw new Error('state ' + r.status); return r.json(); }, async createProject({client, name, brief}) { const r = await _fetch('/api/projects', { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({client, name, brief}), }); if (!r.ok) throw new Error('createProject ' + r.status); return r.json(); }, async deleteProject(pid) { const r = await _fetch('/api/projects/' + encodeURIComponent(pid), {method:'DELETE'}); if (!r.ok) throw new Error('deleteProject ' + r.status); return r.json(); }, async createFolder(pid, name) { const r = await _fetch(`/api/projects/${encodeURIComponent(pid)}/folders`, { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({name}), }); if (!r.ok) throw new Error('createFolder ' + r.status); return r.json(); }, async uploadFile(pid, fid, file, onProgress) { // Use XHR to get progress; fetch doesn't expose upload progress. return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', `/api/projects/${encodeURIComponent(pid)}/folders/${encodeURIComponent(fid)}/upload`); const h = _authHeader(); if (h.authorization) xhr.setRequestHeader('Authorization', h.authorization); xhr.upload.onprogress = (e) => { if (e.lengthComputable && onProgress) onProgress(e.loaded / e.total); }; xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { try { resolve(JSON.parse(xhr.responseText)); } catch(e){ reject(e); } } else reject(new Error('upload ' + xhr.status + ': ' + xhr.responseText)); }; xhr.onerror = () => reject(new Error('upload network')); const fd = new FormData(); fd.append('file', file); xhr.send(fd); }); }, async deleteFile(file_id) { const r = await _fetch('/api/files/' + encodeURIComponent(file_id), {method:'DELETE'}); if (!r.ok) throw new Error('deleteFile ' + r.status); return r.json(); }, async analyzeFile(file_id) { const r = await _fetch(`/api/files/${encodeURIComponent(file_id)}/analyze`, {method:'POST'}); if (!r.ok) throw new Error('analyze ' + r.status + ' ' + await r.text()); return r.json(); }, streamAnalyze(file_id, { onParsed, onBlock, onDone, onError } = {}) { // Live per-block progress via SSE. Returns a close() handle. // EventSource can't set headers — pass token as ?access_token= const q = _authQuery(); const url = `/api/files/${encodeURIComponent(file_id)}/analyze/stream` + (q ? '?' + q : ''); const es = new EventSource(url); const safe = (fn) => (e) => { try { fn && fn(JSON.parse(e.data)); } catch (err) { console.warn('sse parse', err); } }; es.addEventListener('parsed', safe(onParsed)); es.addEventListener('block', safe(onBlock)); es.addEventListener('done', (e) => { safe(onDone)(e); es.close(); }); es.addEventListener('error', (e) => { // e.data is only present for server-sent 'error' events; native EventSource // errors (network) fire with no data. try { onError && onError(e.data ? JSON.parse(e.data) : { error: '연결 끊김' }); } catch (err) { onError && onError({ error: '스트림 파싱 실패' }); } es.close(); }); return () => es.close(); }, async analyzeProject(pid) { const r = await _fetch(`/api/projects/${encodeURIComponent(pid)}/analyze`, {method:'POST'}); if (!r.ok) throw new Error('analyzeProject ' + r.status + ' ' + await r.text()); return r.json(); }, async saveBoard(pid, board) { const r = await _fetch(`/api/projects/${encodeURIComponent(pid)}/board`, { method:'PUT', headers:{'content-type':'application/json'}, body: JSON.stringify(board), }); if (!r.ok) throw new Error('saveBoard ' + r.status); return r.json(); }, async brainstorm(project_id, model, messages) { const r = await _fetch('/api/brainstorm', { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({project_id, model, messages}), }); if (!r.ok) throw new Error('brainstorm ' + r.status + ' ' + await r.text()); return r.json(); }, async saveBrainstorm(pid, messages) { const r = await _fetch(`/api/projects/${encodeURIComponent(pid)}/brainstorm`, { method:'PUT', headers:{'content-type':'application/json'}, body: JSON.stringify({messages}), }); if (!r.ok) throw new Error('saveBrainstorm ' + r.status); return r.json(); }, async distill(pid) { const r = await _fetch(`/api/projects/${encodeURIComponent(pid)}/distill`, {method:'POST'}); if (!r.ok) throw new Error('distill ' + r.status); return r.json(); }, async exportProject(pid) { const r = await _fetch(`/api/projects/${encodeURIComponent(pid)}/export`, {method:'POST'}); if (!r.ok) throw new Error('export ' + r.status); return r.json(); }, async getJob(job_id) { const r = await _fetch('/api/jobs/' + encodeURIComponent(job_id)); if (!r.ok) throw new Error('job ' + r.status); return r.json(); }, async patchBlock(file_id, block_id, patch) { const r = await _fetch(`/api/files/${encodeURIComponent(file_id)}/blocks/${encodeURIComponent(block_id)}`, { method:'PATCH', headers:{'content-type':'application/json'}, body: JSON.stringify(patch), }); if (!r.ok) throw new Error('patchBlock ' + r.status + ' ' + await r.text()); return r.json(); }, async mutateKeyword(body) { const r = await _fetch('/api/keywords/mutate', { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify(body), }); if (!r.ok) throw new Error('mutate ' + r.status + ' ' + await r.text()); return r.json(); }, async searchReferences(pid, body) { const r = await _fetch(`/api/projects/${encodeURIComponent(pid)}/references/search`, { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify(body), }); if (!r.ok) throw new Error('refSearch ' + r.status + ' ' + await r.text()); return r.json(); }, async archiveReference(pid, candidate) { const r = await _fetch(`/api/projects/${encodeURIComponent(pid)}/references/archive`, { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({candidate}), }); if (!r.ok) throw new Error('refArchive ' + r.status); return r.json(); }, async downloadReference(pid, body) { // body = { ref_id? | candidate?, folder_id? } const r = await _fetch(`/api/projects/${encodeURIComponent(pid)}/references/download`, { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify(body), }); if (!r.ok) throw new Error('refDownload ' + r.status + ' ' + await r.text()); return r.json(); }, async removeReference(pid, rid) { const r = await _fetch(`/api/projects/${encodeURIComponent(pid)}/references/${encodeURIComponent(rid)}`, {method:'DELETE'}); if (!r.ok) throw new Error('refRemove ' + r.status); return r.json(); }, async keywordFeedback(pid, term, ko, verdict) { const r = await _fetch(`/api/projects/${encodeURIComponent(pid)}/feedback`, { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({term, ko, verdict}), }); if (!r.ok) throw new Error('feedback ' + r.status); return r.json(); }, }; window.API = API;