This commit is contained in:
2026-05-14 19:24:50 +00:00
parent af0628867f
commit a7f218e182
63 changed files with 10539 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
import { useCallback, useEffect, useState } from 'react';
import { api } from '@/api/client';
import type { Project } from '@/api/types';
export function useProjects() {
const [projects, setProjects] = useState<Project[] | null>(null);
const [error, setError] = useState<string | null>(null);
const refresh = useCallback(async () => {
try {
const list = await api.projects.list();
setProjects(list);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'failed to load projects');
}
}, []);
useEffect(() => {
void refresh();
}, [refresh]);
const add = useCallback(
async (body: { path: string; name?: string }) => {
const created = await api.projects.add(body);
await refresh();
return created;
},
[refresh]
);
const remove = useCallback(
async (id: string) => {
await api.projects.remove(id);
await refresh();
},
[refresh]
);
return { projects, error, refresh, add, remove };
}

View File

@@ -0,0 +1,139 @@
import { useEffect, useRef, useState } from 'react';
import type { Message, WsFrame } from '@/api/types';
interface State {
messages: Message[];
connected: boolean;
error: string | null;
}
function applyFrame(state: State, frame: WsFrame): State {
switch (frame.type) {
case 'snapshot': {
return { ...state, messages: frame.messages };
}
case 'message_started': {
const exists = state.messages.some((m) => m.id === frame.message_id);
if (exists) return state;
const newMsg: Message = {
id: frame.message_id,
session_id: '',
role: frame.role,
content: '',
tool_calls: null,
tool_results: null,
status: 'streaming',
last_seq: 0,
created_at: new Date().toISOString(),
};
return { ...state, messages: [...state.messages, newMsg] };
}
case 'delta': {
const next = state.messages.map((m) =>
m.id === frame.message_id ? { ...m, content: m.content + frame.content } : m
);
return { ...state, messages: next };
}
case 'tool_call': {
const next = state.messages.map((m) =>
m.id === frame.message_id
? { ...m, tool_calls: [...(m.tool_calls ?? []), frame.tool_call] }
: m
);
return { ...state, messages: next };
}
case 'tool_result': {
const exists = state.messages.some((m) => m.id === frame.tool_message_id);
if (exists) {
const next = state.messages.map((m) =>
m.id === frame.tool_message_id
? {
...m,
role: 'tool' as const,
tool_results: {
tool_call_id: frame.tool_call_id,
output: frame.output,
truncated: frame.truncated,
...(frame.error ? { error: frame.error } : {}),
},
status: 'complete' as const,
}
: m
);
return { ...state, messages: next };
}
const newMsg: Message = {
id: frame.tool_message_id,
session_id: '',
role: 'tool',
content: '',
tool_calls: null,
tool_results: {
tool_call_id: frame.tool_call_id,
output: frame.output,
truncated: frame.truncated,
...(frame.error ? { error: frame.error } : {}),
},
status: 'complete',
last_seq: 0,
created_at: new Date().toISOString(),
};
return { ...state, messages: [...state.messages, newMsg] };
}
case 'message_complete': {
const next = state.messages.map((m) =>
m.id === frame.message_id ? { ...m, status: 'complete' as const } : m
);
return { ...state, messages: next };
}
case 'error': {
const next = frame.message_id
? state.messages.map((m) =>
m.id === frame.message_id ? { ...m, status: 'failed' as const } : m
)
: state.messages;
return { ...state, messages: next, error: frame.error };
}
}
}
export function useSessionStream(sessionId: string | undefined) {
const [state, setState] = useState<State>({ messages: [], connected: false, error: null });
const wsRef = useRef<WebSocket | null>(null);
useEffect(() => {
if (!sessionId) return;
setState({ messages: [], connected: false, error: null });
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
const url = `${proto}://${window.location.host}/api/ws/sessions/${sessionId}`;
const ws = new WebSocket(url);
wsRef.current = ws;
ws.onopen = () => {
setState((s) => ({ ...s, connected: true, error: null }));
};
ws.onmessage = (ev) => {
try {
const frame = JSON.parse(typeof ev.data === 'string' ? ev.data : '') as WsFrame;
setState((s) => applyFrame(s, frame));
} catch (err) {
console.warn('bad ws frame', err);
}
};
ws.onerror = () => {
setState((s) => ({ ...s, error: 'websocket error' }));
};
ws.onclose = () => {
setState((s) => ({ ...s, connected: false }));
};
return () => {
wsRef.current = null;
ws.close();
};
}, [sessionId]);
return state;
}

View File

@@ -0,0 +1,46 @@
import { useCallback, useEffect, useState } from 'react';
import { api } from '@/api/client';
import type { Session } from '@/api/types';
export function useSessions(projectId: string | undefined) {
const [sessions, setSessions] = useState<Session[] | null>(null);
const [error, setError] = useState<string | null>(null);
const refresh = useCallback(async () => {
if (!projectId) {
setSessions(null);
return;
}
try {
const list = await api.sessions.listForProject(projectId);
setSessions(list);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'failed to load sessions');
}
}, [projectId]);
useEffect(() => {
void refresh();
}, [refresh]);
const create = useCallback(
async (body: { name?: string; model?: string; system_prompt?: string }) => {
if (!projectId) throw new Error('no project');
const created = await api.sessions.create(projectId, body);
await refresh();
return created;
},
[projectId, refresh]
);
const remove = useCallback(
async (id: string) => {
await api.sessions.remove(id);
await refresh();
},
[refresh]
);
return { sessions, error, refresh, create, remove };
}