feat(webui): segment transcript storage
This commit is contained in:
@@ -12,6 +12,16 @@ import { deriveTitle } from "@/lib/format";
|
||||
import type { ChatSummary, UIMessage, WorkspaceScopePayload } from "@/lib/types";
|
||||
|
||||
const EMPTY_MESSAGES: UIMessage[] = [];
|
||||
const INITIAL_HISTORY_PAGE_LIMIT = 160;
|
||||
const OLDER_HISTORY_PAGE_LIMIT = 120;
|
||||
|
||||
function persistedMessagesToUi(messages: UIMessage[]): UIMessage[] {
|
||||
return messages.map((m, idx) => ({
|
||||
...m,
|
||||
id: m.id ?? `hist-${idx}`,
|
||||
createdAt: typeof m.createdAt === "number" ? m.createdAt : Date.now(),
|
||||
}));
|
||||
}
|
||||
|
||||
/** Sidebar state: fetches the full session list and exposes create / delete actions. */
|
||||
export function useSessions(): {
|
||||
@@ -129,14 +139,19 @@ export function useSessions(): {
|
||||
export function useSessionHistory(key: string | null): {
|
||||
messages: UIMessage[];
|
||||
loading: boolean;
|
||||
loadingOlder: boolean;
|
||||
error: string | null;
|
||||
refresh: () => void;
|
||||
loadOlder: () => Promise<void>;
|
||||
hasMoreBefore: boolean;
|
||||
userMessageOffset: number;
|
||||
version: number;
|
||||
forkBoundaryMessageCount: number | null;
|
||||
/** ``true`` when the replayed transcript ends with a trace row (turn still in flight). */
|
||||
hasPendingToolCalls: boolean;
|
||||
} {
|
||||
const { token } = useClient();
|
||||
const loadingOlderRef = useRef(false);
|
||||
const [refreshSeq, setRefreshSeq] = useState(0);
|
||||
const refresh = useCallback(() => {
|
||||
setRefreshSeq((value) => value + 1);
|
||||
@@ -145,17 +160,25 @@ export function useSessionHistory(key: string | null): {
|
||||
key: string | null;
|
||||
messages: UIMessage[];
|
||||
loading: boolean;
|
||||
loadingOlder: boolean;
|
||||
error: string | null;
|
||||
hasPendingToolCalls: boolean;
|
||||
forkBoundaryMessageCount: number | null;
|
||||
beforeCursor: string | null;
|
||||
hasMoreBefore: boolean;
|
||||
userMessageOffset: number;
|
||||
version: number;
|
||||
}>({
|
||||
key: null,
|
||||
messages: [],
|
||||
loading: false,
|
||||
loadingOlder: false,
|
||||
error: null,
|
||||
hasPendingToolCalls: false,
|
||||
forkBoundaryMessageCount: null,
|
||||
beforeCursor: null,
|
||||
hasMoreBefore: false,
|
||||
userMessageOffset: 0,
|
||||
version: 0,
|
||||
});
|
||||
|
||||
@@ -165,9 +188,13 @@ export function useSessionHistory(key: string | null): {
|
||||
key: null,
|
||||
messages: [],
|
||||
loading: false,
|
||||
loadingOlder: false,
|
||||
error: null,
|
||||
hasPendingToolCalls: false,
|
||||
forkBoundaryMessageCount: null,
|
||||
beforeCursor: null,
|
||||
hasMoreBefore: false,
|
||||
userMessageOffset: 0,
|
||||
version: 0,
|
||||
});
|
||||
return;
|
||||
@@ -176,37 +203,44 @@ export function useSessionHistory(key: string | null): {
|
||||
// Mark the new key as loading immediately so callers never see stale
|
||||
// messages from the previous session during the render right after a switch.
|
||||
setState((prev) => prev.key === key
|
||||
? { ...prev, loading: true, error: null }
|
||||
? { ...prev, loading: true, loadingOlder: false, error: null }
|
||||
: {
|
||||
key,
|
||||
messages: [],
|
||||
loading: true,
|
||||
loadingOlder: false,
|
||||
error: null,
|
||||
hasPendingToolCalls: false,
|
||||
forkBoundaryMessageCount: null,
|
||||
beforeCursor: null,
|
||||
hasMoreBefore: false,
|
||||
userMessageOffset: 0,
|
||||
version: 0,
|
||||
});
|
||||
(async () => {
|
||||
try {
|
||||
const body = await fetchWebuiThread(token, key);
|
||||
const body = await fetchWebuiThread(token, key, {
|
||||
limit: INITIAL_HISTORY_PAGE_LIMIT,
|
||||
direction: "latest",
|
||||
});
|
||||
if (cancelled) return;
|
||||
if (!body?.messages?.length) {
|
||||
setState((prev) => ({
|
||||
key,
|
||||
messages: [],
|
||||
loading: false,
|
||||
loadingOlder: false,
|
||||
error: null,
|
||||
hasPendingToolCalls: false,
|
||||
forkBoundaryMessageCount: null,
|
||||
beforeCursor: null,
|
||||
hasMoreBefore: false,
|
||||
userMessageOffset: 0,
|
||||
version: prev.key === key ? prev.version + 1 : 1,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
const ui: UIMessage[] = body.messages.map((m, idx) => ({
|
||||
...m,
|
||||
id: m.id ?? `hist-${idx}`,
|
||||
createdAt: typeof m.createdAt === "number" ? m.createdAt : Date.now(),
|
||||
}));
|
||||
const ui = persistedMessagesToUi(body.messages);
|
||||
const last = ui[ui.length - 1];
|
||||
const hasPending = last?.kind === "trace";
|
||||
const forkBoundary = typeof body.fork_boundary_message_count === "number"
|
||||
@@ -216,9 +250,13 @@ export function useSessionHistory(key: string | null): {
|
||||
key,
|
||||
messages: ui,
|
||||
loading: false,
|
||||
loadingOlder: false,
|
||||
error: null,
|
||||
hasPendingToolCalls: hasPending,
|
||||
forkBoundaryMessageCount: forkBoundary,
|
||||
beforeCursor: body.page?.before_cursor ?? null,
|
||||
hasMoreBefore: body.page?.has_more_before === true,
|
||||
userMessageOffset: Math.max(0, body.page?.user_message_offset ?? 0),
|
||||
version: prev.key === key ? prev.version + 1 : 1,
|
||||
}));
|
||||
} catch (e) {
|
||||
@@ -228,9 +266,13 @@ export function useSessionHistory(key: string | null): {
|
||||
key,
|
||||
messages: [],
|
||||
loading: false,
|
||||
loadingOlder: false,
|
||||
error: null,
|
||||
hasPendingToolCalls: false,
|
||||
forkBoundaryMessageCount: null,
|
||||
beforeCursor: null,
|
||||
hasMoreBefore: false,
|
||||
userMessageOffset: 0,
|
||||
version: prev.key === key ? prev.version + 1 : 1,
|
||||
}));
|
||||
} else {
|
||||
@@ -238,9 +280,13 @@ export function useSessionHistory(key: string | null): {
|
||||
key,
|
||||
messages: [],
|
||||
loading: false,
|
||||
loadingOlder: false,
|
||||
error: (e as Error).message,
|
||||
hasPendingToolCalls: false,
|
||||
forkBoundaryMessageCount: null,
|
||||
beforeCursor: null,
|
||||
hasMoreBefore: false,
|
||||
userMessageOffset: 0,
|
||||
version: prev.key === key ? prev.version : 0,
|
||||
}));
|
||||
}
|
||||
@@ -251,12 +297,78 @@ export function useSessionHistory(key: string | null): {
|
||||
};
|
||||
}, [key, token, refreshSeq]);
|
||||
|
||||
const loadOlder = useCallback(async () => {
|
||||
if (!key || loadingOlderRef.current) return;
|
||||
const before = state.key === key ? state.beforeCursor : null;
|
||||
if (!before || !state.hasMoreBefore) return;
|
||||
loadingOlderRef.current = true;
|
||||
setState((prev) => prev.key === key ? { ...prev, loadingOlder: true, error: null } : prev);
|
||||
try {
|
||||
const body = await fetchWebuiThread(token, key, {
|
||||
limit: OLDER_HISTORY_PAGE_LIMIT,
|
||||
before,
|
||||
});
|
||||
setState((prev) => {
|
||||
if (prev.key !== key) return prev;
|
||||
if (!body?.messages?.length) {
|
||||
return {
|
||||
...prev,
|
||||
loadingOlder: false,
|
||||
hasMoreBefore: false,
|
||||
beforeCursor: null,
|
||||
};
|
||||
}
|
||||
const older = persistedMessagesToUi(body.messages);
|
||||
const olderBoundary = typeof body.fork_boundary_message_count === "number"
|
||||
? Math.max(0, Math.min(body.fork_boundary_message_count, older.length))
|
||||
: null;
|
||||
const shiftedBoundary = prev.forkBoundaryMessageCount === null
|
||||
? null
|
||||
: prev.forkBoundaryMessageCount + older.length;
|
||||
const nextMessages = [...older, ...prev.messages];
|
||||
const last = nextMessages[nextMessages.length - 1];
|
||||
return {
|
||||
...prev,
|
||||
messages: nextMessages,
|
||||
loadingOlder: false,
|
||||
error: null,
|
||||
hasPendingToolCalls: last?.kind === "trace",
|
||||
forkBoundaryMessageCount: olderBoundary ?? shiftedBoundary,
|
||||
beforeCursor: body.page?.before_cursor ?? null,
|
||||
hasMoreBefore: body.page?.has_more_before === true,
|
||||
userMessageOffset: Math.max(0, body.page?.user_message_offset ?? 0),
|
||||
version: prev.version + 1,
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
setState((prev) => prev.key === key
|
||||
? {
|
||||
...prev,
|
||||
loadingOlder: false,
|
||||
error: (e as Error).message,
|
||||
}
|
||||
: prev);
|
||||
} finally {
|
||||
loadingOlderRef.current = false;
|
||||
}
|
||||
}, [
|
||||
key,
|
||||
state.beforeCursor,
|
||||
state.hasMoreBefore,
|
||||
state.key,
|
||||
token,
|
||||
]);
|
||||
|
||||
if (!key) {
|
||||
return {
|
||||
messages: EMPTY_MESSAGES,
|
||||
loading: false,
|
||||
loadingOlder: false,
|
||||
error: null,
|
||||
refresh,
|
||||
loadOlder,
|
||||
hasMoreBefore: false,
|
||||
userMessageOffset: 0,
|
||||
version: 0,
|
||||
forkBoundaryMessageCount: null,
|
||||
hasPendingToolCalls: false,
|
||||
@@ -269,8 +381,12 @@ export function useSessionHistory(key: string | null): {
|
||||
return {
|
||||
messages: EMPTY_MESSAGES,
|
||||
loading: true,
|
||||
loadingOlder: false,
|
||||
error: null,
|
||||
refresh,
|
||||
loadOlder,
|
||||
hasMoreBefore: false,
|
||||
userMessageOffset: 0,
|
||||
version: 0,
|
||||
forkBoundaryMessageCount: null,
|
||||
hasPendingToolCalls: false,
|
||||
@@ -280,8 +396,12 @@ export function useSessionHistory(key: string | null): {
|
||||
return {
|
||||
messages: state.messages,
|
||||
loading: state.loading,
|
||||
loadingOlder: state.loadingOlder,
|
||||
error: state.error,
|
||||
refresh,
|
||||
loadOlder,
|
||||
hasMoreBefore: state.hasMoreBefore,
|
||||
userMessageOffset: state.userMessageOffset,
|
||||
version: state.version,
|
||||
forkBoundaryMessageCount: state.forkBoundaryMessageCount,
|
||||
hasPendingToolCalls: state.hasPendingToolCalls,
|
||||
|
||||
Reference in New Issue
Block a user