forked from sagnik/Project_Velocity
223 lines
5.9 KiB
TypeScript
223 lines
5.9 KiB
TypeScript
/**
|
|
* Oracle API Client — production-only client for the Oracle v1 backend.
|
|
*/
|
|
|
|
import type {
|
|
CanvasPage,
|
|
PromptSubmitRequest,
|
|
PromptSubmitResponse,
|
|
ForkCreateRequest,
|
|
ForkCreateResponse,
|
|
MergeRequestCreateRequest,
|
|
MergeRequest,
|
|
MergeReviewRequest,
|
|
ComponentTemplate,
|
|
UserProfile,
|
|
OracleWSMessage,
|
|
OracleEnvelope,
|
|
CanvasPageRevision,
|
|
} from '../types/canvas';
|
|
import { VELOCITY_TOKEN_KEY } from '@/lib/velocityPlatformClient';
|
|
|
|
function getBrowserOrigin(): string {
|
|
return typeof window !== 'undefined' ? window.location.origin : '';
|
|
}
|
|
|
|
function resolveBaseUrl(): string {
|
|
const configured = (import.meta.env.VITE_ORACLE_API_URL as string | undefined)?.trim();
|
|
if (configured) {
|
|
return configured.replace(/\/$/, '');
|
|
}
|
|
return getBrowserOrigin();
|
|
}
|
|
|
|
function resolveWsUrl(): string {
|
|
const configured = (import.meta.env.VITE_ORACLE_WS_URL as string | undefined)?.trim();
|
|
if (configured) {
|
|
return configured.replace(/\/$/, '');
|
|
}
|
|
const origin = getBrowserOrigin();
|
|
return origin ? origin.replace(/^http/, 'ws') : '';
|
|
}
|
|
|
|
const BASE_URL = resolveBaseUrl();
|
|
const WS_URL = resolveWsUrl();
|
|
|
|
function apiUrl(path: string): string {
|
|
return `${BASE_URL}/api/oracle/v1${path}`;
|
|
}
|
|
|
|
async function apiFetch<T>(
|
|
path: string,
|
|
options?: RequestInit & { idempotencyKey?: string },
|
|
): Promise<T> {
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
'X-Oracle-Contract-Version': 'v1',
|
|
...(options?.idempotencyKey ? { 'Idempotency-Key': options.idempotencyKey } : {}),
|
|
};
|
|
|
|
const token = localStorage.getItem('oracle_jwt') ?? localStorage.getItem(VELOCITY_TOKEN_KEY);
|
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
|
|
const res = await fetch(apiUrl(path), { ...options, headers });
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => ({}));
|
|
throw Object.assign(new Error(body?.error?.message ?? body?.detail?.errors?.join?.(', ') ?? `HTTP ${res.status}`), { apiError: body });
|
|
}
|
|
|
|
const body = await res.json() as OracleEnvelope<T> | T;
|
|
if (typeof body === 'object' && body !== null && 'data' in body) {
|
|
return (body as OracleEnvelope<T>).data;
|
|
}
|
|
return body as T;
|
|
}
|
|
|
|
export async function fetchMe(): Promise<UserProfile> {
|
|
return apiFetch<UserProfile>('/me');
|
|
}
|
|
|
|
export async function fetchCanvasPage(pageId: string): Promise<CanvasPage> {
|
|
return apiFetch<CanvasPage>(`/canvas-pages/${pageId}`);
|
|
}
|
|
|
|
export async function submitPrompt(
|
|
pageId: string,
|
|
payload: PromptSubmitRequest,
|
|
): Promise<PromptSubmitResponse> {
|
|
return apiFetch<PromptSubmitResponse>(`/canvas-pages/${pageId}/prompts`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
idempotencyKey: payload.clientRequestId,
|
|
});
|
|
}
|
|
|
|
export async function rollbackPage(
|
|
pageId: string,
|
|
targetRevision: number,
|
|
clientRequestId: string,
|
|
): Promise<{ headRevision: number; pageId: string; components: CanvasPage['components'] }> {
|
|
return apiFetch<{ headRevision: number; pageId: string; components: CanvasPage['components'] }>(
|
|
`/canvas-pages/${pageId}/rollback`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify({ targetRevision, clientRequestId }),
|
|
idempotencyKey: clientRequestId,
|
|
},
|
|
);
|
|
}
|
|
|
|
export async function listRevisions(pageId: string): Promise<CanvasPageRevision[]> {
|
|
return apiFetch<CanvasPageRevision[]>(`/canvas-pages/${pageId}/revisions`);
|
|
}
|
|
|
|
export async function createFork(
|
|
pageId: string,
|
|
payload: ForkCreateRequest,
|
|
): Promise<ForkCreateResponse> {
|
|
return apiFetch<ForkCreateResponse>(`/canvas-pages/${pageId}/forks`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function openMergeRequest(
|
|
payload: MergeRequestCreateRequest,
|
|
): Promise<MergeRequest> {
|
|
return apiFetch<MergeRequest>('/merge-requests', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function reviewMergeRequest(
|
|
mrId: string,
|
|
payload: MergeReviewRequest,
|
|
): Promise<MergeRequest> {
|
|
return apiFetch<MergeRequest>(`/merge-requests/${mrId}/review`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function listMergeRequests(pageId: string): Promise<MergeRequest[]> {
|
|
return apiFetch<MergeRequest[]>(`/merge-requests?targetPageId=${pageId}`);
|
|
}
|
|
|
|
export async function listComponentTemplates(filters?: {
|
|
category?: string;
|
|
status?: string;
|
|
}): Promise<ComponentTemplate[]> {
|
|
const qs = new URLSearchParams(filters as Record<string, string>).toString();
|
|
return apiFetch<ComponentTemplate[]>(`/component-templates${qs ? `?${qs}` : ''}`);
|
|
}
|
|
|
|
export async function synthesizeTemplate(params: {
|
|
prompt: string;
|
|
dataShape: string[];
|
|
styleSignatureRef?: string;
|
|
}): Promise<ComponentTemplate> {
|
|
return apiFetch<ComponentTemplate>('/component-templates/synthesize', {
|
|
method: 'POST',
|
|
body: JSON.stringify(params),
|
|
});
|
|
}
|
|
|
|
export function connectPageSocket(
|
|
pageId: string,
|
|
handlers: {
|
|
onMessage: (msg: OracleWSMessage) => void;
|
|
onReconnect?: () => void;
|
|
onOpen?: () => void;
|
|
onClose: () => void;
|
|
},
|
|
): () => void {
|
|
if (!WS_URL && !BASE_URL) {
|
|
handlers.onClose();
|
|
return () => undefined;
|
|
}
|
|
|
|
const wsBase = WS_URL || BASE_URL.replace(/^http/, 'ws');
|
|
let ws: WebSocket;
|
|
let stopped = false;
|
|
let retryTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
|
|
function connect() {
|
|
ws = new WebSocket(`${wsBase}/ws/oracle/canvas/${pageId}`);
|
|
|
|
ws.onopen = () => {
|
|
handlers.onOpen?.();
|
|
};
|
|
|
|
ws.onmessage = (event) => {
|
|
try {
|
|
handlers.onMessage(JSON.parse(event.data as string) as OracleWSMessage);
|
|
} catch {
|
|
// Ignore malformed messages from the transport.
|
|
}
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
handlers.onClose();
|
|
if (!stopped) {
|
|
retryTimeout = setTimeout(() => {
|
|
handlers.onReconnect?.();
|
|
connect();
|
|
}, 3000);
|
|
}
|
|
};
|
|
|
|
ws.onerror = () => {
|
|
ws.close();
|
|
};
|
|
}
|
|
|
|
connect();
|
|
|
|
return () => {
|
|
stopped = true;
|
|
if (retryTimeout) clearTimeout(retryTimeout);
|
|
ws?.close();
|
|
};
|
|
}
|