| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import { InteractiveApiError, } from "./types"; |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async function _consumeGenerationStream(url, authHeaders, opts) { |
| const resp = await fetch(url, { |
| method: "GET", |
| credentials: "include", |
| headers: { Accept: "text/event-stream", ...authHeaders }, |
| signal: opts.signal, |
| }); |
| if (!resp.ok || !resp.body) { |
| const detail = await resp.text().catch(() => ""); |
| throw new InteractiveApiError(detail || `generation stream failed (${resp.status})`, resp.status); |
| } |
| const reader = resp.body.getReader(); |
| const decoder = new TextDecoder("utf-8"); |
| let buffer = ""; |
| let finalResult = null; |
| let errorMessage = null; |
| |
| while (true) { |
| const { value, done } = await reader.read(); |
| if (done) |
| break; |
| buffer += decoder.decode(value, { stream: true }); |
| let idx; |
| while ((idx = buffer.indexOf("\n\n")) >= 0) { |
| const raw = buffer.slice(0, idx).trim(); |
| buffer = buffer.slice(idx + 2); |
| if (!raw.startsWith("data:")) |
| continue; |
| const body = raw.slice("data:".length).trim(); |
| if (!body) |
| continue; |
| let frame; |
| try { |
| frame = JSON.parse(body); |
| } |
| catch { |
| continue; |
| } |
| if (opts.onEvent) { |
| try { |
| opts.onEvent(frame); |
| } |
| catch { } |
| } |
| if (frame.type === "result") { |
| finalResult = frame.payload || null; |
| } |
| if (frame.type === "error") { |
| const payload = (frame.payload || {}); |
| errorMessage = typeof payload.reason === "string" |
| ? payload.reason |
| : "generation failed"; |
| } |
| if (frame.type === "done") |
| break; |
| } |
| } |
| if (errorMessage) |
| throw new InteractiveApiError(errorMessage, 0); |
| if (!finalResult) |
| throw new InteractiveApiError("stream ended without a result frame", 0); |
| return finalResult; |
| } |
| export function createInteractiveApi(backendUrl, apiKey) { |
| const base = backendUrl.replace(/\/+$/, "") + "/v1/interactive"; |
| const authHeaders = apiKey |
| ? { "x-api-key": apiKey.trim() } |
| : {}; |
| async function call(path, init = {}, signal) { |
| const headers = { |
| "Content-Type": "application/json", |
| ...authHeaders, |
| ...init.headers, |
| }; |
| let res; |
| try { |
| |
| |
| |
| |
| |
| res = await fetch(`${base}${path}`, { |
| ...init, headers, signal, credentials: "include", |
| }); |
| } |
| catch (err) { |
| if (err.name === "AbortError") |
| throw err; |
| throw new InteractiveApiError(`Network error contacting ${path}: ${err.message}`, 0, "network_error"); |
| } |
| const contentType = res.headers.get("content-type") || ""; |
| const isJson = contentType.includes("application/json"); |
| const body = isJson ? await res.json().catch(() => ({})) : await res.text(); |
| if (!res.ok) { |
| const detail = (isJson && body.detail) || body; |
| if (detail && typeof detail === "object") { |
| const d = detail; |
| throw new InteractiveApiError(d.error || `HTTP ${res.status}`, res.status, d.code || "http_error", d.data || {}); |
| } |
| throw new InteractiveApiError(typeof detail === "string" ? detail : `HTTP ${res.status}`, res.status, "http_error"); |
| } |
| return body; |
| } |
| return { |
| health: (signal) => call("/health", { method: "GET" }, signal), |
| listPresets: (signal) => call("/presets", { method: "GET" }, signal) |
| .then((r) => r.items), |
| listExperiences: (signal) => call("/experiences", { method: "GET" }, signal) |
| .then((r) => r.items), |
| getExperience: (id, signal) => call(`/experiences/${encodeURIComponent(id)}`, { method: "GET" }, signal) |
| .then((r) => r.experience), |
| createExperience: (input) => call("/experiences", { |
| method: "POST", |
| body: JSON.stringify(input), |
| }).then((r) => r.experience), |
| patchExperience: (id, patch) => call(`/experiences/${encodeURIComponent(id)}`, { |
| method: "PATCH", |
| body: JSON.stringify(patch), |
| }).then((r) => r.experience), |
| deleteExperience: (id) => call(`/experiences/${encodeURIComponent(id)}`, { method: "DELETE" }) |
| .then(() => undefined), |
| plan: (req) => call("/plan", { |
| method: "POST", |
| body: JSON.stringify(req), |
| }).then((r) => r.intent), |
| planAuto: (req) => call("/plan-auto", { |
| method: "POST", |
| body: JSON.stringify(req), |
| }), |
| autoGenerate: (id) => call(`/experiences/${encodeURIComponent(id)}/auto-generate`, { method: "POST", body: JSON.stringify({}) }), |
| autoGenerateStream: (id, opts) => _consumeGenerationStream(`${base}/experiences/${encodeURIComponent(id)}/auto-generate/stream`, authHeaders, opts), |
| generateAllStream: (id, opts) => _consumeGenerationStream(`${base}/experiences/${encodeURIComponent(id)}/generate-all/stream`, authHeaders, opts), |
| renderSingleScene: (experienceId, nodeId) => call(`/experiences/${encodeURIComponent(experienceId)}/nodes/${encodeURIComponent(nodeId)}/render`, { method: "POST", body: JSON.stringify({}) }), |
| patchNode: (nodeId, patch) => call(`/nodes/${encodeURIComponent(nodeId)}`, { method: "PATCH", body: JSON.stringify(patch) }).then((r) => r.node), |
| resolveAssetUrl: (assetId, signal) => call(`/assets/${encodeURIComponent(assetId)}/url`, { method: "GET" }, signal).then((r) => r.url), |
| seedGraph: (id, req) => call(`/experiences/${encodeURIComponent(id)}/seed-graph`, { method: "POST", body: JSON.stringify(req) }), |
| listNodes: (id, signal) => call(`/experiences/${encodeURIComponent(id)}/nodes`, { method: "GET" }, signal).then((r) => r.items), |
| listEdges: (id, signal) => call(`/experiences/${encodeURIComponent(id)}/edges`, { method: "GET" }, signal).then((r) => r.items), |
| listActions: (id, signal) => call(`/experiences/${encodeURIComponent(id)}/actions`, { method: "GET" }, signal).then((r) => r.items), |
| createAction: (id, payload) => call(`/experiences/${encodeURIComponent(id)}/actions`, { method: "POST", body: JSON.stringify(payload) }).then((r) => r.action), |
| deleteAction: (actionId) => call(`/actions/${encodeURIComponent(actionId)}`, { method: "DELETE" }) |
| .then(() => undefined), |
| listRules: (id, signal) => call(`/experiences/${encodeURIComponent(id)}/rules`, { method: "GET" }, signal).then((r) => r.items), |
| createRule: (id, payload) => call(`/experiences/${encodeURIComponent(id)}/rules`, { method: "POST", body: JSON.stringify(payload) }).then((r) => r.rule), |
| deleteRule: (ruleId) => call(`/rules/${encodeURIComponent(ruleId)}`, { method: "DELETE" }) |
| .then(() => undefined), |
| runQa: (id) => call(`/experiences/${encodeURIComponent(id)}/qa-run`, { method: "POST" }), |
| latestReport: (id, signal) => call(`/experiences/${encodeURIComponent(id)}/qa-reports`, { method: "GET" }, signal), |
| publish: (id, channel) => call(`/experiences/${encodeURIComponent(id)}/publish`, { method: "POST", body: JSON.stringify({ channel }) }), |
| listPublications: (id, signal) => call(`/experiences/${encodeURIComponent(id)}/publications`, { method: "GET" }, signal).then((r) => r.items), |
| experienceAnalytics: (id, signal) => call(`/experiences/${encodeURIComponent(id)}/analytics`, { method: "GET" }, signal), |
| startSession: (req) => call("/play/sessions", { method: "POST", body: JSON.stringify(req) }).then((r) => ({ |
| ...r.session, |
| initial_scene: r.initial_scene || null, |
| opening_turn: r.opening_turn, |
| persona_portrait_url: r.persona_portrait_url, |
| render_media_type: r.render_media_type, |
| })), |
| chat: (sessionId, req) => call(`/play/sessions/${encodeURIComponent(sessionId)}/chat`, { method: "POST", body: JSON.stringify(req) }), |
| pending: (sessionId, opts, signal) => { |
| const params = new URLSearchParams(); |
| if (opts?.since_id) |
| params.set("since_id", opts.since_id); |
| if (opts?.limit !== undefined) |
| params.set("limit", String(opts.limit)); |
| const suffix = params.toString() ? `?${params.toString()}` : ""; |
| return call(`/play/sessions/${encodeURIComponent(sessionId)}/pending${suffix}`, { method: "GET" }, signal); |
| }, |
| getCatalog: (sessionId, signal) => call(`/play/sessions/${encodeURIComponent(sessionId)}/catalog`, { method: "GET" }, signal).then((r) => r.items), |
| getProgress: (sessionId, signal) => call(`/play/sessions/${encodeURIComponent(sessionId)}/progress`, { method: "GET" }, signal), |
| resolveTurn: (sessionId, req) => call(`/play/sessions/${encodeURIComponent(sessionId)}/resolve`, { method: "POST", body: JSON.stringify(req) }).then((r) => r.resolved), |
| personaLiveGenerate: (req) => call("/persona-live/generate", { method: "POST", body: JSON.stringify(req) }), |
| personaLiveStart: (req) => call("/persona-live/start", { method: "POST", body: JSON.stringify(req) }).then((r) => r.session), |
| personaLiveSession: (sessionId) => call(`/persona-live/session/${encodeURIComponent(sessionId)}`, { method: "GET" }), |
| personaLiveAction: (sessionId, req) => call(`/persona-live/session/${encodeURIComponent(sessionId)}/action`, { method: "POST", body: JSON.stringify(req) }), |
| personaLiveJob: (jobId) => call(`/persona-live/jobs/${encodeURIComponent(jobId)}`, { method: "GET" }), |
| personaLiveRestore: (sessionId, versionId) => call(`/persona-live/session/${encodeURIComponent(sessionId)}/restore`, { method: "POST", body: JSON.stringify({ version_id: versionId }) }), |
| personaLiveChat: (sessionId, message) => call(`/persona-live/session/${encodeURIComponent(sessionId)}/chat`, { method: "POST", body: JSON.stringify({ message }) }).then((r) => ({ |
| dialogue: r.dialogue, |
| scene_context: r.scene_context, |
| scene_memory: r.scene_memory, |
| emotional_state: r.emotional_state, |
| optional_action_suggestion: r.optional_action_suggestion, |
| })), |
| }; |
| } |
|
|