feat(frontend): bind sessions to chart profiles
This commit is contained in:
@@ -11,7 +11,7 @@ export async function GET() {
|
||||
if (authError || !user) return NextResponse.json({ error: "请先登录" }, { status: 401 });
|
||||
const { data, error } = await supabase
|
||||
.from("chat_sessions")
|
||||
.select("id,title,theme,model_id,messages,session_type,rectification_case_id,updated_at")
|
||||
.select("id,title,theme,model_id,messages,session_type,rectification_case_id,chart_profile_id,chart_profile_name,chart_profile_role,updated_at")
|
||||
.eq("user_id", user.id)
|
||||
.order("updated_at", { ascending: false });
|
||||
if (error) return NextResponse.json({ error: "聊天记录暂时无法读取" }, { status: 500 });
|
||||
|
||||
@@ -814,6 +814,7 @@ button:disabled { cursor: default; opacity: .45; }
|
||||
.chat-header { z-index: 2; min-width: 0; display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 20px; border-bottom: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent); padding: 0 var(--space-8); background: var(--color-frosted); backdrop-filter: saturate(130%) blur(20px); text-align: left; }
|
||||
.chat-header > div { min-width: 0; }
|
||||
.chat-header strong { max-width: min(560px, 62vw); overflow: hidden; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; font-family: var(--font-display); font-size: var(--type-title-md); font-weight: 400; letter-spacing: -.3px; }
|
||||
.chat-header-subtitle { max-width: min(560px, 62vw); overflow: hidden; color: var(--color-muted-foreground); font-size: var(--type-caption); line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.chat-header-actions { min-width: max-content; display: flex; flex: 0 0 auto; align-items: center; gap: var(--space-2); white-space: nowrap; }
|
||||
.chat-header-actions > * { flex: 0 0 auto; }
|
||||
.credit-button { min-height: 44px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 0 11px; cursor: pointer; font-size: 13px; font-variant-numeric: tabular-nums; transition: background-color 120ms ease-out, transform 120ms ease-out; min-width: 64px; border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas-soft); color: var(--color-ink-secondary); font-weight: 500; }
|
||||
|
||||
@@ -221,6 +221,11 @@ type SynastryReportApiRecord = {
|
||||
created_at?: string;
|
||||
};
|
||||
type ChatSessionType = "consultation" | "birth_time_rectification";
|
||||
type ChatProfileBinding = {
|
||||
chartProfileId: string | null;
|
||||
chartProfileName: string | null;
|
||||
chartProfileRole: "self" | "other" | null;
|
||||
};
|
||||
type ChatSession = {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -230,6 +235,9 @@ type ChatSession = {
|
||||
updatedAt: number;
|
||||
sessionType: ChatSessionType;
|
||||
rectificationCaseId: string | null;
|
||||
chartProfileId: string | null;
|
||||
chartProfileName: string | null;
|
||||
chartProfileRole: "self" | "other" | null;
|
||||
};
|
||||
|
||||
type RequestError = { sessionId: string; message: string };
|
||||
@@ -415,6 +423,7 @@ function timestamp() {
|
||||
function createSession(
|
||||
modelId: string,
|
||||
sessionType: ChatSessionType = "consultation",
|
||||
chartBinding: ChatProfileBinding = { chartProfileId: null, chartProfileName: null, chartProfileRole: null },
|
||||
): ChatSession {
|
||||
return {
|
||||
id: globalThis.crypto.randomUUID(),
|
||||
@@ -425,6 +434,7 @@ function createSession(
|
||||
updatedAt: timestamp(),
|
||||
sessionType,
|
||||
rectificationCaseId: null,
|
||||
...chartBinding,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -513,6 +523,36 @@ function buildSelfChartRecord(profile: Profile): ChartLibraryRecord {
|
||||
return { id: "self", role: "self", profile: { ...profile, chartRelationship: "self" }, relationship: "self", updatedAt: timestamp() };
|
||||
}
|
||||
|
||||
function chartSnapshotForSession(
|
||||
chartId: string,
|
||||
library: readonly ChartLibraryRecord[],
|
||||
fallbackProfile: Profile,
|
||||
): ChatProfileBinding {
|
||||
const record = library.find((item) => item.id === chartId);
|
||||
if (record) {
|
||||
return {
|
||||
chartProfileId: record.id,
|
||||
chartProfileName: record.profile.name.trim() || (record.role === "self" ? "我" : "未命名资料"),
|
||||
chartProfileRole: record.role,
|
||||
};
|
||||
}
|
||||
if (chartId === "self") {
|
||||
return { chartProfileId: "self", chartProfileName: fallbackProfile.name.trim() || "我", chartProfileRole: "self" };
|
||||
}
|
||||
return { chartProfileId: chartId || null, chartProfileName: "未命名资料", chartProfileRole: chartId ? "other" : null };
|
||||
}
|
||||
|
||||
function sessionChartLabel(session: ChatSession, library: readonly ChartLibraryRecord[]) {
|
||||
if (!session.chartProfileId) return "未关联资料";
|
||||
const current = session.chartProfileId === "self" || library.some((record) => record.id === session.chartProfileId);
|
||||
const name = session.chartProfileName?.trim() || (session.chartProfileRole === "self" ? "我" : "未命名资料");
|
||||
return current ? name : `资料已删除 · ${name}`;
|
||||
}
|
||||
|
||||
function sessionSidebarTitle(session: ChatSession, library: readonly ChartLibraryRecord[]) {
|
||||
return `${sessionChartLabel(session, library)} · ${session.title || "新对话"}`;
|
||||
}
|
||||
|
||||
function upsertSelfChart(library: ChartLibraryRecord[], profile: Profile) {
|
||||
if (!profileReadyForLibrary(profile)) return library.filter((record) => record.role !== "self");
|
||||
const others = library.filter((record) => record.role !== "self");
|
||||
@@ -843,6 +883,9 @@ function readSessions(value: unknown, catalog: PublicLanguageModelCatalog | null
|
||||
const session = item as Partial<ChatSession> & {
|
||||
model_id?: unknown;
|
||||
rectification_case_id?: unknown;
|
||||
chart_profile_id?: unknown;
|
||||
chart_profile_name?: unknown;
|
||||
chart_profile_role?: unknown;
|
||||
session_type?: unknown;
|
||||
updated_at?: unknown;
|
||||
};
|
||||
@@ -887,6 +930,11 @@ function readSessions(value: unknown, catalog: PublicLanguageModelCatalog | null
|
||||
rectificationCaseId: typeof session.rectification_case_id === "string"
|
||||
? session.rectification_case_id
|
||||
: null,
|
||||
chartProfileId: typeof session.chart_profile_id === "string" ? session.chart_profile_id : null,
|
||||
chartProfileName: typeof session.chart_profile_name === "string" ? session.chart_profile_name : null,
|
||||
chartProfileRole: session.chart_profile_role === "self" || session.chart_profile_role === "other"
|
||||
? session.chart_profile_role
|
||||
: null,
|
||||
updatedAt: typeof session.updatedAt === "number"
|
||||
? session.updatedAt
|
||||
: typeof session.updated_at === "string"
|
||||
@@ -1654,6 +1702,9 @@ export default function Home() {
|
||||
updatedAt: timestamp(),
|
||||
sessionType: "consultation",
|
||||
rectificationCaseId: null,
|
||||
chartProfileId: "self",
|
||||
chartProfileName: previewProfile.name.trim() || "我",
|
||||
chartProfileRole: "self",
|
||||
};
|
||||
setAccount({
|
||||
user: { id: "preview-user", email: "preview@local.test" },
|
||||
@@ -1699,11 +1750,16 @@ export default function Home() {
|
||||
fetchSessions(controller.signal),
|
||||
]);
|
||||
const nextModelCatalog = modelCatalogResult.catalog;
|
||||
const nextProfile = readProfile(nextAccount.profile);
|
||||
const parsedSessions = readSessions(sessionsPayload, nextModelCatalog);
|
||||
let nextSessions = parsedSessions.sessions;
|
||||
if (nextSessions.length === 0) {
|
||||
if (controller.signal.aborted) return;
|
||||
const initialSession = createSession(nextModelCatalog?.defaultModelId ?? "");
|
||||
const initialSession = createSession(
|
||||
nextModelCatalog?.defaultModelId ?? "",
|
||||
"consultation",
|
||||
chartSnapshotForSession("self", [], nextProfile),
|
||||
);
|
||||
if (nextModelCatalog) {
|
||||
await writeChatSession(initialSession.id, {
|
||||
title: initialSession.title,
|
||||
@@ -1712,6 +1768,9 @@ export default function Home() {
|
||||
messages: initialSession.messages,
|
||||
session_type: initialSession.sessionType,
|
||||
rectification_case_id: initialSession.rectificationCaseId,
|
||||
chart_profile_id: initialSession.chartProfileId,
|
||||
chart_profile_name: initialSession.chartProfileName,
|
||||
chart_profile_role: initialSession.chartProfileRole,
|
||||
updated_at: new Date(initialSession.updatedAt).toISOString(),
|
||||
}, "create");
|
||||
}
|
||||
@@ -1760,7 +1819,6 @@ export default function Home() {
|
||||
|
||||
if (controller.signal.aborted) return;
|
||||
clearStaleClientReload(sessionStorage);
|
||||
const nextProfile = readProfile(nextAccount.profile);
|
||||
setAccount(nextAccount);
|
||||
setModelCatalog(nextModelCatalog);
|
||||
setProfile(nextProfile);
|
||||
@@ -2155,6 +2213,9 @@ export default function Home() {
|
||||
})),
|
||||
session_type: session.sessionType,
|
||||
rectification_case_id: session.rectificationCaseId,
|
||||
chart_profile_id: session.chartProfileId,
|
||||
chart_profile_name: session.chartProfileName,
|
||||
chart_profile_role: session.chartProfileRole,
|
||||
updated_at: new Date(session.updatedAt).toISOString(),
|
||||
};
|
||||
await writeChatSession(session.id, values, mode);
|
||||
@@ -2235,7 +2296,11 @@ export default function Home() {
|
||||
|
||||
async function startNewChat(): Promise<ChatSession | null> {
|
||||
if (!account || !modelCatalog || creatingSession) return null;
|
||||
const nextSession = createSession(modelCatalog.defaultModelId);
|
||||
const nextSession = createSession(
|
||||
modelCatalog.defaultModelId,
|
||||
"consultation",
|
||||
chartSnapshotForSession(activeChartId, chartLibrary, profile),
|
||||
);
|
||||
const previousSessionId = activeSession?.id ?? "";
|
||||
setCreatingSession(true);
|
||||
setSessions((current) => [nextSession, ...current]);
|
||||
@@ -2273,6 +2338,13 @@ export default function Home() {
|
||||
setDraft("");
|
||||
setDraftEntrypoint(null);
|
||||
setComposerNotice("");
|
||||
if (nextSession?.chartProfileId) {
|
||||
const boundChart = chartLibrary.find((record) => record.id === nextSession.chartProfileId);
|
||||
if (boundChart && boundChart.id !== activeChartId && accountId) {
|
||||
setActiveChartId(boundChart.id);
|
||||
localStorage.setItem(activeChartStorageKey(accountId), boundChart.id);
|
||||
}
|
||||
}
|
||||
if (nextSession?.sessionType === "birth_time_rectification") {
|
||||
setRectificationError("");
|
||||
if (nextSession.id !== rectificationSessionId) {
|
||||
@@ -2830,6 +2902,7 @@ export default function Home() {
|
||||
updatedAt: timestamp(),
|
||||
sessionType: "birth_time_rectification",
|
||||
rectificationCaseId: opened.caseId,
|
||||
...chartSnapshotForSession(activeChartId, chartLibrary, profile),
|
||||
};
|
||||
setSessions((current) => [merged, ...current.filter((session) => session.id !== merged.id)]);
|
||||
void persistSession(merged).catch(() => {});
|
||||
@@ -3859,7 +3932,7 @@ export default function Home() {
|
||||
|
||||
const sidebarSessions = visibleSessions.map((session) => ({
|
||||
id: session.id,
|
||||
title: session.title,
|
||||
title: sessionSidebarTitle(session, chartLibrary),
|
||||
pinned: pinnedSessionIds.includes(session.id),
|
||||
archived: archivedSessionIds.includes(session.id),
|
||||
}));
|
||||
@@ -4150,6 +4223,7 @@ export default function Home() {
|
||||
<SidebarTrigger placement="inset" />
|
||||
<div>
|
||||
<strong>{activeSession?.title || "新对话"}</strong>
|
||||
<span className="chat-header-subtitle">分析对象:{activeSession ? sessionChartLabel(activeSession, chartLibrary) : "未关联资料"}</span>
|
||||
</div>
|
||||
<div className="chat-header-actions">
|
||||
<div className="chat-header-rectification" data-rectification-header-slot="" ref={setRectificationHeaderSlot} />
|
||||
|
||||
Reference in New Issue
Block a user