186 lines
5.2 KiB
TypeScript
186 lines
5.2 KiB
TypeScript
import { apiFetch } from "@/lib/apiFetch";
|
|
import { config } from "@config/config";
|
|
|
|
import type {
|
|
ChatSessionSummary,
|
|
LoadedChatState,
|
|
Message,
|
|
} from "./GlobalChatbox.types";
|
|
import { cloneMessages } from "./globalChatboxUtils";
|
|
|
|
type BackendSessionPayload = {
|
|
id?: string;
|
|
title?: string;
|
|
created_at?: string | number;
|
|
updated_at?: string | number;
|
|
is_streaming?: boolean;
|
|
run_status?: string;
|
|
};
|
|
|
|
export const createEmptyChatState = (): LoadedChatState => ({
|
|
title: undefined,
|
|
isTitleManuallyEdited: false,
|
|
messages: [],
|
|
sessionId: undefined,
|
|
});
|
|
|
|
const sanitizeMessages = (messages: Message[] | undefined) =>
|
|
Array.isArray(messages) ? cloneMessages(messages) : [];
|
|
|
|
const compareSessionsByAnchorTime = (
|
|
left: Pick<ChatSessionSummary, "id" | "createdAt" | "updatedAt">,
|
|
right: Pick<ChatSessionSummary, "id" | "createdAt" | "updatedAt">,
|
|
) => {
|
|
const createdAtDiff = right.createdAt - left.createdAt;
|
|
if (createdAtDiff !== 0) return createdAtDiff;
|
|
|
|
const updatedAtDiff = right.updatedAt - left.updatedAt;
|
|
if (updatedAtDiff !== 0) return updatedAtDiff;
|
|
|
|
return right.id.localeCompare(left.id);
|
|
};
|
|
|
|
const toMillis = (value: string | number | undefined) =>
|
|
typeof value === "number" ? value : value ? new Date(value).getTime() : Date.now();
|
|
|
|
const normalizeTitle = (value?: string) => value?.trim() || "新对话";
|
|
|
|
const fetchBackendChatSessions = async (): Promise<ChatSessionSummary[]> => {
|
|
const response = await apiFetch(`${config.AGENT_URL}/api/v1/agent/chat/sessions`, {
|
|
method: "GET",
|
|
projectHeaderMode: "include",
|
|
skipAuthRedirect: true,
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(await response.text());
|
|
}
|
|
const payload = (await response.json()) as {
|
|
sessions?: BackendSessionPayload[];
|
|
};
|
|
return (payload.sessions ?? [])
|
|
.map((session) => ({
|
|
id: session.id ?? "",
|
|
title: normalizeTitle(session.title),
|
|
createdAt: toMillis(session.created_at),
|
|
updatedAt: toMillis(session.updated_at),
|
|
isStreaming: session.is_streaming,
|
|
runStatus: session.run_status,
|
|
}))
|
|
.filter((session) => Boolean(session.id))
|
|
.sort(compareSessionsByAnchorTime);
|
|
};
|
|
|
|
const fetchBackendChatSession = async (sessionId: string): Promise<LoadedChatState> => {
|
|
const response = await apiFetch(
|
|
`${config.AGENT_URL}/api/v1/agent/chat/session/${encodeURIComponent(sessionId)}`,
|
|
{
|
|
method: "GET",
|
|
projectHeaderMode: "include",
|
|
skipAuthRedirect: true,
|
|
},
|
|
);
|
|
if (!response.ok) {
|
|
if (response.status === 404) {
|
|
return createEmptyChatState();
|
|
}
|
|
throw new Error(await response.text());
|
|
}
|
|
const payload = (await response.json()) as {
|
|
id: string;
|
|
title?: string;
|
|
is_title_manually_edited?: boolean;
|
|
session_id?: string;
|
|
messages?: Message[];
|
|
is_streaming?: boolean;
|
|
run_status?: string;
|
|
};
|
|
return {
|
|
title: normalizeTitle(payload.title),
|
|
isTitleManuallyEdited: payload.is_title_manually_edited ?? false,
|
|
messages: sanitizeMessages(payload.messages),
|
|
sessionId: payload.session_id ?? payload.id,
|
|
isStreaming: payload.is_streaming ?? false,
|
|
runStatus: payload.run_status,
|
|
};
|
|
};
|
|
|
|
const updateBackendChatSessionTitle = async (
|
|
sessionId: string,
|
|
title: string,
|
|
isTitleManuallyEdited?: boolean,
|
|
) => {
|
|
const response = await apiFetch(
|
|
`${config.AGENT_URL}/api/v1/agent/chat/session/${encodeURIComponent(sessionId)}/title`,
|
|
{
|
|
method: "PATCH",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
title,
|
|
is_title_manually_edited: isTitleManuallyEdited,
|
|
}),
|
|
projectHeaderMode: "include",
|
|
skipAuthRedirect: true,
|
|
},
|
|
);
|
|
if (!response.ok) {
|
|
throw new Error(await response.text());
|
|
}
|
|
};
|
|
|
|
const deleteBackendChatSession = async (sessionId: string) => {
|
|
const response = await apiFetch(
|
|
`${config.AGENT_URL}/api/v1/agent/chat/session/${encodeURIComponent(sessionId)}`,
|
|
{
|
|
method: "DELETE",
|
|
projectHeaderMode: "include",
|
|
skipAuthRedirect: true,
|
|
},
|
|
);
|
|
if (!response.ok && response.status !== 404) {
|
|
throw new Error(await response.text());
|
|
}
|
|
};
|
|
|
|
export const listChatSessions = async (): Promise<ChatSessionSummary[]> => {
|
|
if (typeof window === "undefined") return [];
|
|
return await fetchBackendChatSessions();
|
|
};
|
|
|
|
export const updateChatSessionTitle = async (
|
|
sessionId: string,
|
|
title: string,
|
|
options?: {
|
|
isTitleManuallyEdited?: boolean;
|
|
},
|
|
): Promise<void> => {
|
|
if (typeof window === "undefined") return;
|
|
|
|
const normalizedTitle = title.trim();
|
|
if (!normalizedTitle) return;
|
|
await updateBackendChatSessionTitle(
|
|
sessionId,
|
|
normalizedTitle,
|
|
options?.isTitleManuallyEdited,
|
|
);
|
|
};
|
|
|
|
export const loadChatSessionById = async (
|
|
sessionId: string,
|
|
): Promise<LoadedChatState> => {
|
|
if (typeof window === "undefined") return createEmptyChatState();
|
|
|
|
return await fetchBackendChatSession(sessionId);
|
|
};
|
|
|
|
export const deleteChatSession = async (
|
|
sessionId: string,
|
|
): Promise<string | undefined> => {
|
|
if (typeof window === "undefined") return undefined;
|
|
|
|
await deleteBackendChatSession(sessionId);
|
|
const nextActiveSession = (await listChatSessions())[0];
|
|
return nextActiveSession?.id;
|
|
};
|