Spaces:
Running
Running
| // Default to '' (relative URLs) so any production-style build - whether | |
| // done inside Docker (Dockerfile sets REACT_APP_API_URL=) or on the host | |
| // without that env var - hits the same origin that served the page. This | |
| // avoids the cross-origin trap where a `npm run build` on the host | |
| // without REACT_APP_API_URL would silently bake `http://localhost:8000` | |
| // into the bundle and break every API call from a Docker deployment. | |
| // | |
| // If you want the CRA dev server (`npm start` on :3000) to talk to a | |
| // FastAPI backend on :8000, set REACT_APP_API_URL=http://localhost:8000 | |
| // in `frontend/.env.development` or your shell. | |
| const API_BASE = process.env.REACT_APP_API_URL !== undefined | |
| ? process.env.REACT_APP_API_URL | |
| : ''; | |
| export async function fetchModels() { | |
| const resp = await fetch(`${API_BASE}/api/models`, { cache: 'no-store' }); | |
| if (!resp.ok) throw new Error(`Failed to fetch models: ${resp.status}`); | |
| return resp.json(); | |
| } | |
| export async function fetchPersonas() { | |
| const resp = await fetch(`${API_BASE}/api/personas`, { cache: 'no-store' }); | |
| if (!resp.ok) throw new Error(`Failed to fetch personas: ${resp.status}`); | |
| return resp.json(); | |
| } | |
| export async function fetchDemoQuestions() { | |
| const resp = await fetch(`${API_BASE}/api/demo-questions`, { cache: 'no-store' }); | |
| if (!resp.ok) throw new Error(`Failed to fetch demo questions: ${resp.status}`); | |
| return resp.json(); | |
| } | |
| export async function generateRole({ | |
| name, | |
| profile, | |
| identity, | |
| samples, | |
| role_style, | |
| orchestrator_model_id, | |
| }) { | |
| const resp = await fetch(`${API_BASE}/api/chat/generate-role`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| name, | |
| profile, | |
| identity, | |
| samples, | |
| role_style, | |
| orchestrator_model_id, | |
| }), | |
| }); | |
| if (!resp.ok) { | |
| const err = await resp.json().catch(() => ({ detail: resp.statusText })); | |
| throw new Error(err.detail || 'Role generation failed'); | |
| } | |
| return resp.json(); | |
| } | |
| export async function generateRoleFreeform({ | |
| name, | |
| text, | |
| role_style, | |
| orchestrator_model_id, | |
| }) { | |
| const resp = await fetch(`${API_BASE}/api/chat/generate-role-freeform`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| name, | |
| text, | |
| role_style, | |
| orchestrator_model_id, | |
| }), | |
| }); | |
| if (!resp.ok) { | |
| const err = await resp.json().catch(() => ({ detail: resp.statusText })); | |
| throw new Error(err.detail || 'Role generation failed'); | |
| } | |
| return resp.json(); | |
| } | |
| /** | |
| * Suggest an LLM for an Expert Persona from the builder's live model list. | |
| * | |
| * Body: { persona_name, source_text, role_prompt, available_models, | |
| * panel_context, orchestrator_model_id } | |
| * | |
| * Returns: { recommended_model_id, rationale } | |
| */ | |
| export async function suggestModel({ | |
| persona_name, | |
| source_text, | |
| role_prompt, | |
| available_models, | |
| panel_context, | |
| orchestrator_model_id, | |
| }) { | |
| const resp = await fetch(`${API_BASE}/api/chat/suggest-model`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| persona_name, | |
| source_text, | |
| role_prompt, | |
| available_models, | |
| panel_context, | |
| orchestrator_model_id, | |
| }), | |
| }); | |
| if (!resp.ok) { | |
| const err = await resp.json().catch(() => ({ detail: resp.statusText })); | |
| const detail = err.detail; | |
| const message = typeof detail === 'string' | |
| ? detail | |
| : Array.isArray(detail) | |
| ? detail.map(d => d.msg || JSON.stringify(d)).join('; ') | |
| : 'Model suggestion failed'; | |
| throw new Error(message); | |
| } | |
| return resp.json(); | |
| } | |
| /** | |
| * Start a CCAI conversation and consume the SSE stream. | |
| * | |
| * Body: { question, participants[], expert_personas[], model_assignments, | |
| * orchestrator_model_id, summarizer_model_id, max_participants } | |
| */ | |
| export async function startChat(body, handlers, abortSignal) { | |
| const resp = await fetch(`${API_BASE}/api/chat/start`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(body), | |
| signal: abortSignal, | |
| }); | |
| if (!resp.ok) { | |
| const err = await resp.json().catch(() => ({ detail: resp.statusText })); | |
| throw new Error(err.detail || 'Chat start failed'); | |
| } | |
| const reader = resp.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buffer = ''; | |
| try { | |
| while (true) { | |
| if (abortSignal?.aborted) break; | |
| const { value, done } = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, { stream: true }); | |
| const parts = buffer.split('\n\n'); | |
| buffer = parts.pop(); | |
| for (const part of parts) { | |
| const lines = part.trim().split('\n'); | |
| let eventType = 'message'; | |
| let data = ''; | |
| for (const line of lines) { | |
| if (line.startsWith('event: ')) eventType = line.slice(7).trim(); | |
| else if (line.startsWith('data: ')) data = line.slice(6); | |
| } | |
| if (!data) continue; | |
| try { | |
| const parsed = JSON.parse(data); | |
| const handler = handlers[eventHandlerKey(eventType)]; | |
| if (handler) handler(parsed); | |
| } catch (e) { | |
| console.warn('SSE parse error', e, data); | |
| } | |
| } | |
| } | |
| } finally { | |
| reader.releaseLock(); | |
| } | |
| handlers.onDone?.(); | |
| } | |
| function eventHandlerKey(eventType) { | |
| switch (eventType) { | |
| case 'session': return 'onSession'; | |
| case 'message': return 'onMessage'; | |
| case 'message_stream_start': return 'onMessageStreamStart'; | |
| case 'message_delta': return 'onMessageDelta'; | |
| case 'orchestrator': return 'onOrchestrator'; | |
| case 'system': return 'onSystem'; | |
| case 'status': return 'onStatus'; | |
| case 'error': return 'onError'; | |
| case 'done': return 'onDone'; | |
| case 'failsafe_pause': return 'onFailsafePause'; | |
| case 'orchestrator_cap_pause': return 'onOrchestratorCapPause'; | |
| case 'participant_error': return 'onParticipantError'; | |
| case 'participant_substituted': return 'onParticipantSubstituted'; | |
| case 'participant_replaced': return 'onParticipantReplaced'; | |
| case 'vote_cast': return 'onVoteCast'; | |
| case 'vote_tally': return 'onVoteTally'; | |
| case 'credentials_updated': return 'onCredentialsUpdated'; | |
| case 'human_turn_needed': return 'onHumanTurnNeeded'; | |
| case 'human_turn_cleared': return 'onHumanTurnCleared'; | |
| default: return null; | |
| } | |
| } | |
| export async function continueChat(sessionId, reason) { | |
| const resp = await fetch( | |
| `${API_BASE}/api/chat/${sessionId}/continue?reason=${encodeURIComponent(reason)}`, | |
| { method: 'POST' }, | |
| ); | |
| if (!resp.ok) throw new Error('Continue failed'); | |
| return resp.json(); | |
| } | |
| export async function getOrchestrator() { | |
| const resp = await fetch(`${API_BASE}/api/chat/orchestrator`); | |
| if (!resp.ok) throw new Error('Failed to get orchestrator'); | |
| return resp.json(); | |
| } | |
| export async function setOrchestrator(modelId) { | |
| const resp = await fetch(`${API_BASE}/api/chat/orchestrator`, { | |
| method: 'PUT', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ model_id: modelId }), | |
| }); | |
| if (!resp.ok) throw new Error('Failed to set orchestrator'); | |
| return resp.json(); | |
| } | |
| export async function getSpeedPriority() { | |
| const resp = await fetch(`${API_BASE}/api/chat/speed-priority`); | |
| if (!resp.ok) throw new Error('Failed to get speed priority'); | |
| return resp.json(); | |
| } | |
| export async function setSpeedPriority(enabled) { | |
| const resp = await fetch(`${API_BASE}/api/chat/speed-priority`, { | |
| method: 'PUT', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ enabled }), | |
| }); | |
| if (!resp.ok) throw new Error('Failed to set speed priority'); | |
| return resp.json(); | |
| } | |
| /** | |
| * Fetch the catalog of available conversation structures and | |
| * decision-making methods. The Settings menu's "Conversation format" | |
| * accordion populates from this so adding a new plugin server-side | |
| * doesn't require frontend code changes. | |
| * | |
| * Returns: { structures: [{id, name, description}, ...], | |
| * decisions: [{id, name, description}, ...], | |
| * default_structure_id, default_decision_id } | |
| */ | |
| export async function fetchConversationFormats() { | |
| const resp = await fetch(`${API_BASE}/api/chat/conversation-formats`, { cache: 'no-store' }); | |
| if (!resp.ok) throw new Error('Failed to fetch conversation formats'); | |
| return resp.json(); | |
| } | |
| export async function exportChat(sessionId, fmt = 'txt') { | |
| const resp = await fetch(`${API_BASE}/api/chat/${sessionId}/export?fmt=${fmt}`); | |
| if (!resp.ok) throw new Error('Export failed'); | |
| return resp.json(); | |
| } | |
| export async function exportApiLog(sessionId) { | |
| const resp = await fetch(`${API_BASE}/api/chat/${sessionId}/api-log`); | |
| if (!resp.ok) throw new Error('API log export failed'); | |
| return resp.json(); | |
| } | |
| export async function fetchTableView(sessionId) { | |
| const resp = await fetch(`${API_BASE}/api/chat/${sessionId}/table`); | |
| if (!resp.ok) throw new Error('Table view fetch failed'); | |
| return resp.json(); | |
| } | |
| /** | |
| * Fetch the catalog of every prompt template the orchestrator and | |
| * participants use, grouped by phase and annotated with purpose and | |
| * runtime variables. Backs the "View current chat prompts" modal. | |
| */ | |
| export async function fetchPromptCatalog() { | |
| const resp = await fetch(`${API_BASE}/api/chat/prompts/catalog`, { cache: 'no-store' }); | |
| if (!resp.ok) throw new Error('Failed to fetch prompt catalog'); | |
| return resp.json(); | |
| } | |
| /** | |
| * Ask the backend to pick the top `count` participants from the | |
| * candidate pool by relevance to the question. Used by the | |
| * "Select N Automatically" toggle in the participants dropdown. | |
| * | |
| * Returns: { selected: [participant_id, ...], rationale: "..." } | |
| */ | |
| export async function autoSelectParticipants({ question, count, candidates, orchestrator_model_id }) { | |
| const resp = await fetch(`${API_BASE}/api/chat/auto-select-participants`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ question, count, candidates, orchestrator_model_id }), | |
| }); | |
| if (!resp.ok) { | |
| const err = await resp.json().catch(() => ({ detail: resp.statusText })); | |
| throw new Error(err.detail || 'Auto-select failed'); | |
| } | |
| return resp.json(); | |
| } | |
| /** | |
| * Fetch the user-tunable conversation-limit defaults, bounds, and | |
| * descriptions. The frontend uses this to render the "Conversation | |
| * limits" settings modal entirely from the server schema, so adding | |
| * a new knob in the backend doesn't require a frontend change. | |
| * | |
| * Shape: { defaults: {field: int}, bounds: {field: {min, max}}, | |
| * descriptions: {field: {group, label, help}} } | |
| */ | |
| export async function fetchConversationLimitsDefaults() { | |
| const resp = await fetch(`${API_BASE}/api/chat/limits/defaults`, { cache: 'no-store' }); | |
| if (!resp.ok) throw new Error('Failed to fetch conversation-limit defaults'); | |
| return resp.json(); | |
| } | |
| export async function fetchCredentials(sessionId) { | |
| const resp = await fetch( | |
| `${API_BASE}/api/chat/${sessionId}/credentials`, | |
| { cache: 'no-store' }, | |
| ); | |
| if (!resp.ok) throw new Error('Credentials fetch failed'); | |
| return resp.json(); | |
| } | |
| /** | |
| * Submit the human participant's response to the orchestrator for the | |
| * currently pending turn. `skip=true` flips the turn into a "declined | |
| * to comment" note rather than a message. | |
| */ | |
| export async function submitHumanResponse(sessionId, { text, skip = false } = {}) { | |
| const resp = await fetch(`${API_BASE}/api/chat/${sessionId}/human-response`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ text: text || '', skip: !!skip }), | |
| }); | |
| if (!resp.ok) { | |
| const err = await resp.json().catch(() => ({ detail: resp.statusText })); | |
| throw new Error(err.detail || 'Submit failed'); | |
| } | |
| return resp.json(); | |
| } | |
| /** | |
| * Patch the in-the-loop human's credential summary. Used by the | |
| * CredentialSummaryModal's edit affordance on the human's row. The | |
| * backend rejects fields it doesn't know about; we send only the | |
| * fields the user actually changed (sparse patch). | |
| */ | |
| export async function patchHumanCredential(sessionId, patch) { | |
| const resp = await fetch(`${API_BASE}/api/chat/${sessionId}/credentials/human`, { | |
| method: 'PATCH', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(patch), | |
| }); | |
| if (!resp.ok) { | |
| const err = await resp.json().catch(() => ({ detail: resp.statusText })); | |
| throw new Error(err.detail || 'Edit failed'); | |
| } | |
| return resp.json(); | |
| } | |
| /** | |
| * Start the AI-assisted credential intake Q&A flow. Returns either a | |
| * first question or (rarely) a final summary if the LLM bails. The | |
| * draft_id is needed for subsequent /answer calls. | |
| */ | |
| /** | |
| * Generate a structured credential summary from a human's freeform | |
| * profile text (experience, personality, etc.). Uses the orchestrator | |
| * the same way it assesses an LLM participant's role prompt. | |
| */ | |
| export async function generateHumanCredentialFromProfile({ | |
| name, question, profile_text, participant_id, orchestrator_model_id, | |
| }) { | |
| const resp = await fetch(`${API_BASE}/api/chat/credentials/from-profile`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| name, | |
| question, | |
| profile_text, | |
| participant_id: participant_id || '', | |
| orchestrator_model_id: orchestrator_model_id || null, | |
| }), | |
| }); | |
| if (!resp.ok) { | |
| const err = await resp.json().catch(() => ({ detail: resp.statusText })); | |
| throw new Error(err.detail || 'Credential generation failed'); | |
| } | |
| return resp.json(); | |
| } | |
| export async function startCredentialDraft({ | |
| name, question, max_questions = 6, orchestrator_model_id = null, | |
| }) { | |
| const resp = await fetch(`${API_BASE}/api/chat/credentials/draft`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ name, question, max_questions, orchestrator_model_id }), | |
| }); | |
| if (!resp.ok) { | |
| const err = await resp.json().catch(() => ({ detail: resp.statusText })); | |
| throw new Error(err.detail || 'Credential draft start failed'); | |
| } | |
| return resp.json(); | |
| } | |
| export async function answerCredentialDraft(draftId, answer) { | |
| const resp = await fetch(`${API_BASE}/api/chat/credentials/draft/${draftId}/answer`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ answer: answer || '' }), | |
| }); | |
| if (!resp.ok) { | |
| const err = await resp.json().catch(() => ({ detail: resp.statusText })); | |
| throw new Error(err.detail || 'Credential draft answer failed'); | |
| } | |
| return resp.json(); | |
| } | |
| export async function cancelCredentialDraft(draftId) { | |
| try { | |
| await fetch(`${API_BASE}/api/chat/credentials/draft/${draftId}`, { | |
| method: 'DELETE', | |
| }); | |
| } catch (_) { /* fire-and-forget cleanup; ignore */ } | |
| } | |
| export async function getAuthStatus() { | |
| const resp = await fetch(`${API_BASE}/api/auth/status`, { credentials: 'include' }); | |
| if (!resp.ok) return { logged_in: false, remaining_conversations: -1 }; | |
| return resp.json(); | |
| } | |
| export async function getRateLimitStatus() { | |
| const resp = await fetch(`${API_BASE}/api/rate-limit/status`, { credentials: 'include' }); | |
| if (!resp.ok) return { remaining: -1, daily_limit: 30 }; | |
| return resp.json(); | |
| } | |