e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
233 lines
6.3 KiB
TypeScript
233 lines
6.3 KiB
TypeScript
import { apiFetch, apiUrl } from "@/lib/api";
|
|
import { invalidateClientCache, withClientCache } from "@/lib/client-cache";
|
|
import type { LLMSelection, StreamEvent } from "@/lib/unified-ws";
|
|
|
|
export interface SessionMessage {
|
|
id: number;
|
|
session_id: string;
|
|
role: "user" | "assistant" | "system";
|
|
content: string;
|
|
capability?: string;
|
|
events: StreamEvent[];
|
|
attachments: Array<{
|
|
type: string;
|
|
filename?: string;
|
|
base64?: string;
|
|
url?: string;
|
|
mime_type?: string;
|
|
id?: string;
|
|
extracted_text?: string;
|
|
generated?: boolean;
|
|
size_bytes?: number;
|
|
}>;
|
|
metadata?: Record<string, unknown>;
|
|
created_at: number;
|
|
/** Edit-branching: id of the message this row continues. `null` for the
|
|
* first message in a session. Siblings share the same parent. */
|
|
parent_message_id?: number | null;
|
|
}
|
|
|
|
export interface SessionSummary {
|
|
id: string;
|
|
session_id: string;
|
|
title: string;
|
|
created_at: number;
|
|
updated_at: number;
|
|
message_count: number;
|
|
last_message: string;
|
|
status?:
|
|
| "idle"
|
|
| "running"
|
|
| "completed"
|
|
| "failed"
|
|
| "cancelled"
|
|
| "rejected";
|
|
active_turn_id?: string;
|
|
preferences?: {
|
|
capability?: string;
|
|
tools?: string[];
|
|
knowledge_bases?: string[];
|
|
language?: string;
|
|
llm_selection?: LLMSelection | null;
|
|
/** Session-level persona preference; "" / absent = Default (no persona). */
|
|
persona?: string;
|
|
/** Edit-branching: maps a parent_message_id → the child id currently
|
|
* shown at that branch point. Missing keys default to the latest
|
|
* sibling (most recently created child). */
|
|
selected_branches?: Record<string, number>;
|
|
};
|
|
}
|
|
|
|
export interface ActiveTurnSummary {
|
|
id: string;
|
|
turn_id: string;
|
|
session_id: string;
|
|
capability: string;
|
|
status: "running" | "completed" | "failed" | "cancelled" | "rejected";
|
|
error: string;
|
|
created_at: number;
|
|
updated_at: number;
|
|
finished_at?: number | null;
|
|
last_seq: number;
|
|
}
|
|
|
|
export interface SessionDetail {
|
|
id: string;
|
|
session_id: string;
|
|
title: string;
|
|
created_at: number;
|
|
updated_at: number;
|
|
status?:
|
|
| "idle"
|
|
| "running"
|
|
| "completed"
|
|
| "failed"
|
|
| "cancelled"
|
|
| "rejected";
|
|
active_turn_id?: string;
|
|
compressed_summary?: string;
|
|
summary_up_to_msg_id?: number;
|
|
preferences?: {
|
|
capability?: string;
|
|
tools?: string[];
|
|
knowledge_bases?: string[];
|
|
language?: string;
|
|
llm_selection?: LLMSelection | null;
|
|
/** Session-level persona preference; "" / absent = Default (no persona). */
|
|
persona?: string;
|
|
/** Edit-branching: maps a parent_message_id → the child id currently
|
|
* shown at that branch point. Missing keys default to the latest
|
|
* sibling (most recently created child). */
|
|
selected_branches?: Record<string, number>;
|
|
};
|
|
messages: SessionMessage[];
|
|
active_turns?: ActiveTurnSummary[];
|
|
}
|
|
|
|
export interface QuizResultItem {
|
|
question_id?: string;
|
|
question: string;
|
|
question_type?: string;
|
|
options?: Record<string, string>;
|
|
user_answer: string;
|
|
correct_answer: string;
|
|
explanation?: string;
|
|
difficulty?: string;
|
|
is_correct: boolean;
|
|
}
|
|
|
|
async function expectJson<T>(response: Response): Promise<T> {
|
|
if (response.status === 401 && typeof window !== "undefined") {
|
|
const next = encodeURIComponent(window.location.pathname);
|
|
window.location.href = `/login?next=${next}`;
|
|
return new Promise(() => {});
|
|
}
|
|
if (!response.ok) {
|
|
throw new Error(`Request failed: ${response.status}`);
|
|
}
|
|
return response.json() as Promise<T>;
|
|
}
|
|
|
|
export async function listSessions(
|
|
limit = 50,
|
|
offset = 0,
|
|
options?: { force?: boolean },
|
|
): Promise<SessionSummary[]> {
|
|
const qs = new URLSearchParams({
|
|
limit: String(limit),
|
|
offset: String(offset),
|
|
});
|
|
return withClientCache<SessionSummary[]>(
|
|
`sessions:${limit}:${offset}`,
|
|
async () => {
|
|
const response = await apiFetch(
|
|
apiUrl(`/api/v1/sessions?${qs.toString()}`),
|
|
{
|
|
cache: "no-store",
|
|
},
|
|
);
|
|
const data = await expectJson<{ sessions: SessionSummary[] }>(response);
|
|
return data.sessions ?? [];
|
|
},
|
|
{
|
|
force: options?.force,
|
|
ttlMs: 15_000,
|
|
},
|
|
);
|
|
}
|
|
|
|
export async function getSession(
|
|
sessionId: string,
|
|
signal?: AbortSignal,
|
|
): Promise<SessionDetail> {
|
|
const response = await apiFetch(apiUrl(`/api/v1/sessions/${sessionId}`), {
|
|
cache: "no-store",
|
|
signal,
|
|
});
|
|
return expectJson<SessionDetail>(response);
|
|
}
|
|
|
|
export async function updateSessionTitle(
|
|
sessionId: string,
|
|
title: string,
|
|
): Promise<SessionDetail> {
|
|
const response = await apiFetch(apiUrl(`/api/v1/sessions/${sessionId}`), {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ title }),
|
|
});
|
|
const data = await expectJson<{ session: SessionDetail }>(response);
|
|
invalidateClientCache("sessions:");
|
|
return data.session;
|
|
}
|
|
|
|
export async function deleteSession(sessionId: string): Promise<void> {
|
|
const response = await apiFetch(apiUrl(`/api/v1/sessions/${sessionId}`), {
|
|
method: "DELETE",
|
|
});
|
|
await expectJson<{ deleted: boolean }>(response);
|
|
invalidateClientCache("sessions:");
|
|
}
|
|
|
|
export async function recordQuizResults(
|
|
sessionId: string,
|
|
answers: QuizResultItem[],
|
|
turnId?: string | null,
|
|
): Promise<void> {
|
|
const response = await apiFetch(
|
|
apiUrl(`/api/v1/sessions/${sessionId}/quiz-results`),
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ answers, turn_id: turnId || "" }),
|
|
},
|
|
);
|
|
await expectJson<{ recorded: boolean }>(response);
|
|
}
|
|
|
|
export async function deleteMessage(
|
|
sessionId: string,
|
|
messageId: number,
|
|
): Promise<void> {
|
|
const response = await apiFetch(
|
|
apiUrl(`/api/v1/sessions/${sessionId}/messages/${messageId}`),
|
|
{ method: "DELETE" },
|
|
);
|
|
await expectJson<{ deleted: boolean }>(response);
|
|
}
|
|
|
|
export async function updateBranchSelection(
|
|
sessionId: string,
|
|
selectedBranches: Record<string, number>,
|
|
): Promise<void> {
|
|
const response = await apiFetch(
|
|
apiUrl(`/api/v1/sessions/${sessionId}/branch-selection`),
|
|
{
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ selected_branches: selectedBranches }),
|
|
},
|
|
);
|
|
await expectJson<{ selected_branches: Record<string, number> }>(response);
|
|
}
|