feat: Built the Oracle Tab1 (#14)
#13 Built the complete Oracle Tab with all the functionalities. Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local> Reviewed-on: #14
This commit was merged in pull request #14.
This commit is contained in:
199
app/src/oracle/lib/oracleApiClient.ts
Normal file
199
app/src/oracle/lib/oracleApiClient.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
const BASE_URL = (import.meta.env.VITE_ORACLE_API_URL as string | undefined) ?? '';
|
||||
const WS_URL = (import.meta.env.VITE_ORACLE_WS_URL as string | undefined) ?? '';
|
||||
|
||||
function apiUrl(path: string): string {
|
||||
return `${BASE_URL}/api/oracle/v1${path}`;
|
||||
}
|
||||
|
||||
async function apiFetch<T>(
|
||||
path: string,
|
||||
options?: RequestInit & { idempotencyKey?: string },
|
||||
): Promise<T> {
|
||||
if (!BASE_URL) {
|
||||
throw new Error('Oracle API is not configured. Set VITE_ORACLE_API_URL to a live backend.');
|
||||
}
|
||||
|
||||
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');
|
||||
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;
|
||||
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.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();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user