fix(web): keep rectification sessions listed and persist chats on first send
Empty-consultation filtering is now session_type scoped so birth-time rows stay in the sidebar. Subtitles use updatedAt with sort/group/cursor. New chats stay local until the first send.
This commit is contained in:
@@ -248,30 +248,18 @@ export async function fetchDailyStarlanguage(signal: AbortSignal): Promise<Daily
|
||||
export type SessionListPage = {
|
||||
readonly sessions: unknown;
|
||||
readonly nextCursor: string | null;
|
||||
readonly draft: unknown;
|
||||
};
|
||||
|
||||
export function readSessionListPage(value: unknown): SessionListPage {
|
||||
if (Array.isArray(value)) return { sessions: value, nextCursor: null, draft: null };
|
||||
if (!value || typeof value !== "object") return { sessions: [], nextCursor: null, draft: null };
|
||||
const page = value as { sessions?: unknown; nextCursor?: unknown; draft?: unknown };
|
||||
if (Array.isArray(value)) return { sessions: value, nextCursor: null };
|
||||
if (!value || typeof value !== "object") return { sessions: [], nextCursor: null };
|
||||
const page = value as { sessions?: unknown; nextCursor?: unknown };
|
||||
return {
|
||||
sessions: page.sessions,
|
||||
nextCursor: typeof page.nextCursor === "string" && page.nextCursor ? page.nextCursor : null,
|
||||
draft: page.draft ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function readDraftConsultation(
|
||||
value: unknown,
|
||||
catalog: PublicLanguageModelCatalog | null,
|
||||
): ChatSession | null {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
const parsed = readSessions([{ ...(value as object), messages: [] }], catalog).sessions[0];
|
||||
if (!parsed || parsed.sessionType !== "consultation") return null;
|
||||
return { ...parsed, messages: [], messagesHydrated: true };
|
||||
}
|
||||
|
||||
export function readSessions(value: unknown, catalog: PublicLanguageModelCatalog | null): SessionReadResult {
|
||||
if (!Array.isArray(value)) return { sessions: [], fallbackSessionIds: [] };
|
||||
const fallbackSessionIds: string[] = [];
|
||||
|
||||
@@ -24,7 +24,6 @@ import { toSidebarSessionRow } from "@/lib/session-sidebar-row";
|
||||
export type SessionListBoot = {
|
||||
readonly sessions: ChatSession[];
|
||||
readonly rawRows: unknown;
|
||||
readonly draftRow: unknown;
|
||||
readonly cursor: string | null;
|
||||
readonly account: Account | null;
|
||||
readonly signedOut: boolean;
|
||||
@@ -96,16 +95,15 @@ async function loadSessionList(signal: AbortSignal): Promise<SessionListBoot> {
|
||||
fetch("/api/account", { signal, cache: "no-store" }),
|
||||
]);
|
||||
if (sessionResponse.status === 401 || accountResponse.status === 401) {
|
||||
return { sessions: [], rawRows: [], draftRow: null, cursor: null, account: null, signedOut: true };
|
||||
return { sessions: [], rawRows: [], cursor: null, account: null, signedOut: true };
|
||||
}
|
||||
const sessionPayload = await sessionResponse.json().catch(() => null) as {
|
||||
sessions?: unknown;
|
||||
nextCursor?: unknown;
|
||||
draft?: unknown;
|
||||
} | null;
|
||||
const accountPayload = await accountResponse.json().catch(() => null);
|
||||
if (!sessionResponse.ok || !accountResponse.ok) {
|
||||
return { sessions: [], rawRows: [], draftRow: null, cursor: null, account: null, signedOut: false };
|
||||
return { sessions: [], rawRows: [], cursor: null, account: null, signedOut: false };
|
||||
}
|
||||
const rawRows = Array.isArray(sessionPayload?.sessions) ? sessionPayload.sessions : [];
|
||||
const parsed = readSessions(rawRows, null);
|
||||
@@ -113,7 +111,6 @@ async function loadSessionList(signal: AbortSignal): Promise<SessionListBoot> {
|
||||
return {
|
||||
sessions: parsed.sessions,
|
||||
rawRows,
|
||||
draftRow: sessionPayload?.draft ?? null,
|
||||
cursor,
|
||||
account: accountPayload as Account,
|
||||
signedOut: false,
|
||||
@@ -145,7 +142,7 @@ export function SessionListProvider({ children }: { children: ReactNode }) {
|
||||
})
|
||||
.catch(() => {
|
||||
if (controller.signal.aborted) return;
|
||||
bootRef.current = { sessions: [], rawRows: [], draftRow: null, cursor: null, account: null, signedOut: false };
|
||||
bootRef.current = { sessions: [], rawRows: [], cursor: null, account: null, signedOut: false };
|
||||
setSettled(true);
|
||||
readyPack.resolve();
|
||||
});
|
||||
|
||||
@@ -1,14 +1,35 @@
|
||||
import type { ChatSession } from "@/lib/home-types";
|
||||
|
||||
export function findReusableEmptyConsultation(
|
||||
sessions: readonly ChatSession[],
|
||||
): ChatSession | undefined {
|
||||
return sessions.find((session) => (
|
||||
session.sessionType === "consultation"
|
||||
/** PostgREST `or()` that keeps rectification rows and non-empty consultations. */
|
||||
export const EMPTY_CONSULTATION_LIST_FILTER = "session_type.neq.consultation,messages.neq.[]";
|
||||
|
||||
export function excludeEmptyConsultations<Query extends {
|
||||
or: (expression: string) => Query;
|
||||
}>(query: Query): Query {
|
||||
// Rectification content lives on the case table; chat_sessions.messages is
|
||||
// always []. Only unpublished consultation drafts are `messages = []`.
|
||||
return query.or(EMPTY_CONSULTATION_LIST_FILTER);
|
||||
}
|
||||
|
||||
export function applyArchiveFilter<Query extends {
|
||||
is: (column: string, value: null) => Query;
|
||||
not: (column: string, operator: string, value: null) => Query;
|
||||
}>(query: Query, archived: boolean): Query {
|
||||
return archived ? query.not("archived_at", "is", null) : query.is("archived_at", null);
|
||||
}
|
||||
|
||||
export function isUnsavedEmptyConsultation(session: ChatSession): boolean {
|
||||
return session.sessionType === "consultation"
|
||||
&& !session.archivedAt
|
||||
&& session.messagesHydrated
|
||||
&& session.messages.length === 0
|
||||
));
|
||||
&& session.messages.length === 0;
|
||||
}
|
||||
|
||||
export function replaceUnsavedEmptyConsultations(
|
||||
sessions: readonly ChatSession[],
|
||||
nextSession: ChatSession,
|
||||
): ChatSession[] {
|
||||
return [nextSession, ...sessions.filter((session) => !isUnsavedEmptyConsultation(session))];
|
||||
}
|
||||
|
||||
export function isListedSidebarSession(session: ChatSession): boolean {
|
||||
@@ -17,3 +38,13 @@ export function isListedSidebarSession(session: ChatSession): boolean {
|
||||
if (!session.messagesHydrated) return true;
|
||||
return session.messages.length > 0;
|
||||
}
|
||||
|
||||
export function cloudListIncludesSession(input: {
|
||||
sessionType: ChatSession["sessionType"];
|
||||
messagesEmpty: boolean;
|
||||
archived: boolean;
|
||||
archivedView: boolean;
|
||||
}): boolean {
|
||||
if (input.sessionType === "consultation" && input.messagesEmpty) return false;
|
||||
return input.archivedView ? input.archived : !input.archived;
|
||||
}
|
||||
|
||||
@@ -11,8 +11,7 @@ export function sessionSidebarSubtitle(
|
||||
session: ChatSession,
|
||||
library: readonly ChartLibraryRecord[] = [],
|
||||
): string {
|
||||
const created = session.createdAt || session.updatedAt;
|
||||
const clock = Number.isFinite(created) ? shanghaiDateTimeLabel(new Date(created)) : "";
|
||||
const clock = Number.isFinite(session.updatedAt) ? shanghaiDateTimeLabel(new Date(session.updatedAt)) : "";
|
||||
const parts: string[] = [];
|
||||
if (clock) parts.push(clock);
|
||||
if (session.chartProfileRole && session.chartProfileRole !== "self" && session.chartProfileId) {
|
||||
|
||||
Reference in New Issue
Block a user