feat(KB-159): complete Step 4 — update frontend for streaming display

This commit is contained in:
gsxdsm
2026-03-30 08:55:07 -07:00
parent 7954c62201
commit bff0c13d85
7 changed files with 639 additions and 25 deletions

View File

@@ -649,6 +649,14 @@ export interface PlanningSession {
summary: PlanningSummary | null;
}
/** SSE event types for planning session streaming */
export type PlanningStreamEvent =
| { type: "thinking"; data: string }
| { type: "question"; data: PlanningQuestion }
| { type: "summary"; data: PlanningSummary }
| { type: "error"; data: string }
| { type: "complete"; data: Record<string, never> };
/** Start a new planning session with an initial plan */
export function startPlanning(initialPlan: string): Promise<PlanningSession> {
return api<PlanningSession>("/planning/start", {
@@ -657,6 +665,14 @@ export function startPlanning(initialPlan: string): Promise<PlanningSession> {
});
}
/** Start a new planning session with AI streaming support */
export function startPlanningStreaming(initialPlan: string): Promise<{ sessionId: string }> {
return api<{ sessionId: string }>("/planning/start-streaming", {
method: "POST",
body: JSON.stringify({ initialPlan }),
});
}
/** Submit a response to the current planning question */
export function respondToPlanning(
sessionId: string,
@@ -683,3 +699,107 @@ export function createTaskFromPlanning(sessionId: string): Promise<Task> {
body: JSON.stringify({ sessionId }),
});
}
/** Get the SSE stream URL for a planning session */
export function getPlanningStreamUrl(sessionId: string): string {
return `/api/planning/${encodeURIComponent(sessionId)}/stream`;
}
/** Connect to planning session SSE stream and handle events
*
* Returns an object with:
* - close: function to close the connection
* - reconnect: function to reconnect after error
*/
export function connectPlanningStream(
sessionId: string,
handlers: {
onThinking?: (data: string) => void;
onQuestion?: (data: PlanningQuestion) => void;
onSummary?: (data: PlanningSummary) => void;
onError?: (data: string) => void;
onComplete?: () => void;
}
): { close: () => void; isConnected: () => boolean } {
const url = getPlanningStreamUrl(sessionId);
const eventSource = new EventSource(url);
let isClosed = false;
eventSource.onopen = () => {
isClosed = false;
};
eventSource.onmessage = (event) => {
// Handle comment events (heartbeats)
if (event.data.startsWith(":")) return;
};
// Handle specific event types
eventSource.addEventListener("thinking", (event: Event) => {
try {
const messageEvent = event as MessageEvent;
const data = JSON.parse(messageEvent.data);
handlers.onThinking?.(data);
} catch {
const messageEvent = event as MessageEvent;
handlers.onThinking?.(messageEvent.data);
}
});
eventSource.addEventListener("question", (event: Event) => {
try {
const messageEvent = event as MessageEvent;
const data = JSON.parse(messageEvent.data) as PlanningQuestion;
handlers.onQuestion?.(data);
} catch (err) {
console.error("[planning] Failed to parse question event:", err);
}
});
eventSource.addEventListener("summary", (event: Event) => {
try {
const messageEvent = event as MessageEvent;
const data = JSON.parse(messageEvent.data) as PlanningSummary;
handlers.onSummary?.(data);
} catch (err) {
console.error("[planning] Failed to parse summary event:", err);
}
});
eventSource.addEventListener("error", (event: Event) => {
try {
const messageEvent = event as MessageEvent;
const data = JSON.parse(messageEvent.data);
handlers.onError?.(data.message || data);
} catch {
const messageEvent = event as MessageEvent;
handlers.onError?.(messageEvent.data || "Stream error");
}
close();
});
eventSource.addEventListener("complete", () => {
handlers.onComplete?.();
close();
});
// Handle connection errors
eventSource.onerror = () => {
if (!isClosed) {
handlers.onError?.("Connection lost");
close();
}
};
function close() {
if (!isClosed) {
isClosed = true;
eventSource.close();
}
}
return {
close,
isConnected: () => !isClosed && eventSource.readyState === EventSource.OPEN,
};
}