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:
130
app/src/oracle/hooks/useOracleExecution.ts
Normal file
130
app/src/oracle/hooks/useOracleExecution.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* useOracleExecution — manages prompt submission and durable execution history.
|
||||
*/
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import type { PromptExecution, CanvasComponent, PlacementMode } from '../types/canvas';
|
||||
import { submitPrompt } from '../lib/oracleApiClient';
|
||||
|
||||
export interface ExecutionEntry {
|
||||
execution: PromptExecution;
|
||||
componentsCreated: string[];
|
||||
}
|
||||
|
||||
export interface OracleExecutionState {
|
||||
history: ExecutionEntry[];
|
||||
inFlight: PromptExecution | null;
|
||||
lastError: string | null;
|
||||
submit: (params: {
|
||||
pageId: string;
|
||||
branchId: string;
|
||||
prompt: string;
|
||||
tenantId: string;
|
||||
actorId: string;
|
||||
placementMode?: PlacementMode;
|
||||
conversationContext?: Array<{ role: 'user' | 'assistant'; content: string }>;
|
||||
onExecutionCommitted?: (commit: {
|
||||
headRevision: number;
|
||||
components: CanvasComponent[];
|
||||
execution: PromptExecution;
|
||||
}) => void;
|
||||
}) => Promise<void>;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
export function useOracleExecution(): OracleExecutionState {
|
||||
const [history, setHistory] = useState<ExecutionEntry[]>([]);
|
||||
const [inFlight, setInFlight] = useState<PromptExecution | null>(null);
|
||||
const [lastError, setLastError] = useState<string | null>(null);
|
||||
const requestIdRef = useRef(0);
|
||||
|
||||
const submit = useCallback(
|
||||
async ({
|
||||
pageId,
|
||||
branchId,
|
||||
prompt,
|
||||
tenantId,
|
||||
actorId,
|
||||
placementMode = 'append_after_last_visible_component',
|
||||
conversationContext = [],
|
||||
onExecutionCommitted,
|
||||
}: {
|
||||
pageId: string;
|
||||
branchId: string;
|
||||
prompt: string;
|
||||
tenantId: string;
|
||||
actorId: string;
|
||||
placementMode?: PlacementMode;
|
||||
conversationContext?: Array<{ role: 'user' | 'assistant'; content: string }>;
|
||||
onExecutionCommitted?: (commit: {
|
||||
headRevision: number;
|
||||
components: CanvasComponent[];
|
||||
execution: PromptExecution;
|
||||
}) => void;
|
||||
}) => {
|
||||
const clientRequestId = `cli_${Date.now()}_${++requestIdRef.current}`;
|
||||
const now = new Date().toISOString();
|
||||
const optimistic: PromptExecution = {
|
||||
executionId: `pex_${clientRequestId}`,
|
||||
tenantId,
|
||||
pageId,
|
||||
branchId,
|
||||
actorId,
|
||||
prompt,
|
||||
intentClass: 'analytical',
|
||||
status: 'planning',
|
||||
modelRuntime: 'oracle_runtime',
|
||||
semanticModelVersion: 'oracle_semantic_v2026_04_08_01',
|
||||
warnings: [],
|
||||
createdAt: now,
|
||||
};
|
||||
|
||||
setInFlight(optimistic);
|
||||
setLastError(null);
|
||||
|
||||
try {
|
||||
setInFlight((prev) => (prev ? { ...prev, status: 'executing' } : prev));
|
||||
|
||||
const response = await submitPrompt(pageId, {
|
||||
clientRequestId,
|
||||
branchId,
|
||||
prompt,
|
||||
conversationContext,
|
||||
placementMode,
|
||||
});
|
||||
|
||||
const completed: PromptExecution = {
|
||||
...optimistic,
|
||||
executionId: response.executionId,
|
||||
status: response.status,
|
||||
summary: response.summary,
|
||||
warnings: response.warnings,
|
||||
componentsCreated: response.componentsCreated,
|
||||
completedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
onExecutionCommitted?.({
|
||||
headRevision: response.headRevision,
|
||||
components: response.components,
|
||||
execution: completed,
|
||||
});
|
||||
|
||||
setHistory((prev) => [...prev, { execution: completed, componentsCreated: response.componentsCreated }]);
|
||||
setInFlight(null);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'Prompt execution failed';
|
||||
const failed: PromptExecution = {
|
||||
...optimistic,
|
||||
status: 'failed',
|
||||
warnings: [msg],
|
||||
completedAt: new Date().toISOString(),
|
||||
};
|
||||
setHistory((prev) => [...prev, { execution: failed, componentsCreated: [] }]);
|
||||
setInFlight(null);
|
||||
setLastError(msg);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return { history, inFlight, lastError, submit, clearError: () => setLastError(null) };
|
||||
}
|
||||
120
app/src/oracle/hooks/useOraclePage.ts
Normal file
120
app/src/oracle/hooks/useOraclePage.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* useOraclePage — Page hydration, branch state, component projection
|
||||
* Owns the canonical page state including revision tracking and optimistic updates.
|
||||
*/
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import type { CanvasPage, CanvasComponent, OracleWSMessage } from '../types/canvas';
|
||||
import { fetchCanvasPage, connectPageSocket } from '../lib/oracleApiClient';
|
||||
|
||||
export interface OraclePageState {
|
||||
page: CanvasPage | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
isConnected: boolean;
|
||||
// Actions
|
||||
refresh: () => Promise<void>;
|
||||
optimisticallyAppendComponent: (comp: CanvasComponent) => void;
|
||||
applyRevision: (headRevision: number, components: CanvasComponent[]) => void;
|
||||
}
|
||||
|
||||
export function useOraclePage(pageId: string | null): OraclePageState {
|
||||
const [page, setPage] = useState<CanvasPage | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const disconnectRef = useRef<(() => void) | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!pageId) {
|
||||
setPage(null);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await fetchCanvasPage(pageId);
|
||||
setPage(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load page');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [pageId]);
|
||||
|
||||
// Connect WebSocket
|
||||
useEffect(() => {
|
||||
if (!pageId) {
|
||||
setIsConnected(false);
|
||||
return () => undefined;
|
||||
}
|
||||
const disconnect = connectPageSocket(pageId, {
|
||||
onMessage: (msg: OracleWSMessage) => handleWSMessage(msg),
|
||||
onReconnect: () => void load(),
|
||||
onClose: () => setIsConnected(false),
|
||||
});
|
||||
disconnectRef.current = disconnect;
|
||||
setIsConnected(true);
|
||||
return () => {
|
||||
disconnect();
|
||||
disconnectRef.current = null;
|
||||
};
|
||||
}, [pageId, load]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// Handle WS messages
|
||||
function handleWSMessage(msg: OracleWSMessage) {
|
||||
if (msg.type === 'oracle.page.revision.committed') {
|
||||
const { headRevision, components } = msg.payload as {
|
||||
headRevision: number;
|
||||
components: CanvasComponent[];
|
||||
};
|
||||
applyRevision(headRevision, components);
|
||||
} else if (msg.type === 'oracle.presence.updated') {
|
||||
setPage((prev) =>
|
||||
prev
|
||||
? {
|
||||
...prev,
|
||||
presence: {
|
||||
activeViewers: (msg.payload.activeViewers as number) ?? prev.presence.activeViewers,
|
||||
activeEditors: (msg.payload.activeEditors as number) ?? prev.presence.activeEditors,
|
||||
lastPresenceAt: msg.timestamp,
|
||||
},
|
||||
}
|
||||
: prev,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const optimisticallyAppendComponent = useCallback((comp: CanvasComponent) => {
|
||||
setPage((prev) => {
|
||||
if (!prev) return prev;
|
||||
// Prevent duplicate (idempotent insert)
|
||||
if (prev.components.some((c) => c.componentId === comp.componentId)) return prev;
|
||||
return { ...prev, components: [...prev.components, comp] };
|
||||
});
|
||||
}, []);
|
||||
|
||||
const applyRevision = useCallback(
|
||||
(headRevision: number, components: CanvasComponent[]) => {
|
||||
setPage((prev) =>
|
||||
prev ? { ...prev, headRevision, components } : prev,
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
return {
|
||||
page,
|
||||
isLoading,
|
||||
error,
|
||||
isConnected,
|
||||
refresh: load,
|
||||
optimisticallyAppendComponent,
|
||||
applyRevision,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user