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:
@@ -127,7 +127,6 @@ import {
|
||||
CONSULTATION_EVIDENCE_VALIDATION_LABEL,
|
||||
CONSULTATION_LOADING_METHOD_LABEL,
|
||||
} from "@/lib/consultation-activity-labels";
|
||||
import { writeChatSession } from "@/lib/chat-session-write-contract";
|
||||
import {
|
||||
SESSION_MISSING_NOTICE,
|
||||
clearLoginSessionReturn,
|
||||
@@ -210,7 +209,6 @@ import {
|
||||
resolveLookupBootstrap,
|
||||
payloadCode,
|
||||
payloadMessage,
|
||||
readDraftConsultation,
|
||||
readSessions,
|
||||
readStoredDailyStarlanguage,
|
||||
readStoredPendingConsultation,
|
||||
@@ -228,7 +226,7 @@ import {
|
||||
starterHomeLandingNeedsConsultation,
|
||||
type BootstrapPhase,
|
||||
} from "@/lib/home-bootstrap";
|
||||
import { findReusableEmptyConsultation } from "@/lib/session-list-filter";
|
||||
import { isUnsavedEmptyConsultation } from "@/lib/session-list-filter";
|
||||
|
||||
const BirthTimeRectification = dynamic(
|
||||
() => import("@/components/birth-time-rectification").then((module) => module.BirthTimeRectification),
|
||||
@@ -931,31 +929,12 @@ export default function Home() {
|
||||
const parsedSessions = readSessions(listBoot.rawRows, nextModelCatalog);
|
||||
setSessionsCursor(listBoot.cursor);
|
||||
let nextSessions = parsedSessions.sessions.length > 0 ? parsedSessions.sessions : listBoot.sessions;
|
||||
const draftSession = readDraftConsultation(listBoot.draftRow, nextModelCatalog);
|
||||
if (draftSession && !nextSessions.some((session) => session.id === draftSession.id)) {
|
||||
nextSessions = [...nextSessions, draftSession];
|
||||
}
|
||||
if (nextSessions.length === 0) {
|
||||
if (controller.signal.aborted) return;
|
||||
const initialSession = createSession(
|
||||
nextSessions = [createSession(
|
||||
nextModelCatalog?.defaultModelId ?? "",
|
||||
"consultation",
|
||||
chartSnapshotForSession("self", [], nextProfile),
|
||||
);
|
||||
if (nextModelCatalog) {
|
||||
await writeChatSession(initialSession.id, {
|
||||
title: initialSession.title,
|
||||
theme: initialSession.theme,
|
||||
model_id: initialSession.modelId,
|
||||
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,
|
||||
}, "create");
|
||||
}
|
||||
nextSessions = [initialSession];
|
||||
)];
|
||||
}
|
||||
nextSessions = applyLegacySessionControls(nextAccount.user.id, nextSessions);
|
||||
|
||||
@@ -1010,32 +989,13 @@ export default function Home() {
|
||||
bootstrapSelection.urlAction,
|
||||
);
|
||||
if (starterHomeLandingNeedsConsultation(nextSessions, landingSessionId, bootstrapSelection.urlAction)) {
|
||||
const reusable = findReusableEmptyConsultation(nextSessions);
|
||||
if (reusable) {
|
||||
landingSessionId = reusable.id;
|
||||
} else {
|
||||
if (controller.signal.aborted) return;
|
||||
const homeSession = createSession(
|
||||
nextModelCatalog?.defaultModelId ?? "",
|
||||
"consultation",
|
||||
chartSnapshotForSession("self", [], nextProfile),
|
||||
);
|
||||
if (nextModelCatalog) {
|
||||
await writeChatSession(homeSession.id, {
|
||||
title: homeSession.title,
|
||||
theme: homeSession.theme,
|
||||
model_id: homeSession.modelId,
|
||||
messages: [],
|
||||
session_type: homeSession.sessionType,
|
||||
rectification_case_id: homeSession.rectificationCaseId,
|
||||
chart_profile_id: homeSession.chartProfileId,
|
||||
chart_profile_name: homeSession.chartProfileName,
|
||||
chart_profile_role: homeSession.chartProfileRole,
|
||||
}, "create");
|
||||
}
|
||||
nextSessions = [homeSession, ...nextSessions];
|
||||
landingSessionId = homeSession.id;
|
||||
}
|
||||
const homeSession = createSession(
|
||||
nextModelCatalog?.defaultModelId ?? "",
|
||||
"consultation",
|
||||
chartSnapshotForSession("self", [], nextProfile),
|
||||
);
|
||||
nextSessions = [homeSession, ...nextSessions];
|
||||
landingSessionId = homeSession.id;
|
||||
}
|
||||
const activeListed = nextSessions.find((session) => session.id === landingSessionId)
|
||||
?? nextSessions[0];
|
||||
@@ -1062,7 +1022,12 @@ export default function Home() {
|
||||
setActiveSessionId(landingSessionId);
|
||||
if (bootstrapSelection.clearStoredReturn) clearLoginSessionReturn();
|
||||
if (bootstrapSelection.urlAction === "replace-clear") writeSessionUrl(null, "replace");
|
||||
if (bootstrapSelection.urlAction === "replace-selected") writeSessionUrl(bootstrapSelection.sessionId, "replace");
|
||||
if (bootstrapSelection.urlAction === "replace-selected") {
|
||||
const selected = nextSessions.find((session) => session.id === bootstrapSelection.sessionId);
|
||||
if (!selected || !isUnsavedEmptyConsultation(selected)) {
|
||||
writeSessionUrl(bootstrapSelection.sessionId, "replace");
|
||||
}
|
||||
}
|
||||
if (reservedConsultation?.status === "reserved") {
|
||||
const recoverySession = nextSessions.find((session) => session.id === reservedConsultation.sessionId);
|
||||
if (recoverySession) restoreConsultationRecovery(recoverySession, reservedConsultation.requestId, storedPending);
|
||||
|
||||
@@ -16,22 +16,9 @@ import {
|
||||
parseSessionCursor,
|
||||
sessionCursorFilter,
|
||||
} from "@/lib/session-cursor";
|
||||
const SESSION_LIST_COLUMNS = "id,title,theme,model_id,session_type,rectification_case_id,chart_profile_id,chart_profile_name,chart_profile_role,created_at,updated_at,pinned,archived_at";
|
||||
import { applyArchiveFilter, excludeEmptyConsultations } from "@/lib/session-list-filter";
|
||||
|
||||
function excludeEmptyConsultations<Query extends {
|
||||
not: (column: string, operator: string, value: unknown) => Query;
|
||||
}>(query: Query): Query {
|
||||
// Empty consultations are `messages = []`. Rectification rows keep an opening
|
||||
// turn, so excluding empty arrays leaves them in the list (BUG-928).
|
||||
return query.not("messages", "eq", []);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
const SESSION_LIST_COLUMNS = "id,title,theme,model_id,session_type,rectification_case_id,chart_profile_id,chart_profile_name,chart_profile_role,updated_at,pinned,archived_at";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
@@ -67,7 +54,6 @@ export async function GET(request: Request) {
|
||||
if (pageError) return NextResponse.json({ error: "聊天记录暂时无法读取" }, { status: 500 });
|
||||
|
||||
let pinnedRows: typeof pageRows = [];
|
||||
let draft: NonNullable<typeof pageRows>[number] | null = null;
|
||||
if (!cursor) {
|
||||
const { data: pinnedData, error: pinnedError } = await excludeEmptyConsultations(
|
||||
applyArchiveFilter(
|
||||
@@ -83,26 +69,12 @@ export async function GET(request: Request) {
|
||||
.order("id", { ascending: false });
|
||||
if (pinnedError) return NextResponse.json({ error: "聊天记录暂时无法读取" }, { status: 500 });
|
||||
pinnedRows = pinnedData ?? [];
|
||||
if (!archived) {
|
||||
const { data: draftData, error: draftError } = await supabase
|
||||
.from("chat_sessions")
|
||||
.select(SESSION_LIST_COLUMNS)
|
||||
.eq("user_id", user.id)
|
||||
.eq("session_type", "consultation")
|
||||
.eq("messages", [])
|
||||
.is("archived_at", null)
|
||||
.order("updated_at", { ascending: false })
|
||||
.order("id", { ascending: false })
|
||||
.limit(1);
|
||||
if (draftError) return NextResponse.json({ error: "聊天记录暂时无法读取" }, { status: 500 });
|
||||
draft = draftData?.[0] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
const nextCursor = nextSessionCursor(pageRows ?? [], limit);
|
||||
const page = (pageRows ?? []).slice(0, limit);
|
||||
const sessions = cursor ? page : [...(pinnedRows ?? []), ...page];
|
||||
return NextResponse.json({ sessions, nextCursor, draft });
|
||||
return NextResponse.json({ sessions, nextCursor });
|
||||
} catch (error) {
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json({ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 });
|
||||
|
||||
@@ -64,9 +64,11 @@ import { completedOnboardingTranscript, isProfileComplete, selectedBirthPlace }
|
||||
import { pendingConsultationStorageKey, timestamp } from "@/lib/home-types";
|
||||
import {
|
||||
consultSendBlockedByRectificationSession,
|
||||
fallbackSessionId,
|
||||
missingActiveSessionSendAction,
|
||||
SESSION_NOT_CONSULTATION_CODE,
|
||||
} from "@/lib/rectification-session-composer-guard";
|
||||
import { isUnsavedEmptyConsultation } from "@/lib/session-list-filter";
|
||||
import type {
|
||||
Account,
|
||||
AccountDialog,
|
||||
@@ -606,6 +608,22 @@ export function useConsultationRun(params: ConsultationRunParams) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isUnsavedEmptyConsultation(currentSession)) {
|
||||
try {
|
||||
await persistSession(currentSession, "create");
|
||||
} catch (caught) {
|
||||
const remaining = sessions.filter((session) => session.id !== currentSession.id);
|
||||
setSessions(remaining);
|
||||
const fallbackId = fallbackSessionId(remaining);
|
||||
setActiveSessionId(fallbackId);
|
||||
setRequestError({
|
||||
sessionId: fallbackId,
|
||||
message: caught instanceof Error ? caught.message : "新对话未能保存到云端。",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const [year, month, day] = profile.date.split("-").map(Number);
|
||||
const [hour, minute] = consultationRoute.time?.split(":").map(Number) ?? [];
|
||||
|
||||
|
||||
@@ -29,7 +29,11 @@ import {
|
||||
patchSessionModel,
|
||||
readSessions,
|
||||
} from "@/lib/home-cloud-sync";
|
||||
import { findReusableEmptyConsultation } from "@/lib/session-list-filter";
|
||||
import {
|
||||
isListedSidebarSession,
|
||||
isUnsavedEmptyConsultation,
|
||||
replaceUnsavedEmptyConsultations,
|
||||
} from "@/lib/session-list-filter";
|
||||
import { chartSnapshotForSession } from "@/lib/home-profile";
|
||||
import { beginSessionPageLoad, mergeSessionPage } from "@/lib/session-groups";
|
||||
import type { ConsultationEntrypoint } from "@/lib/consultation-entrypoint";
|
||||
@@ -110,7 +114,6 @@ export function useSessionManagement(params: SessionManagementParams) {
|
||||
setActiveChartId,
|
||||
setActiveSessionId,
|
||||
setBirthTimeConsultationConsent,
|
||||
setCreatingSession,
|
||||
setDraft,
|
||||
setDraftEntrypoint,
|
||||
setDraftTheme,
|
||||
@@ -131,12 +134,21 @@ export function useSessionManagement(params: SessionManagementParams) {
|
||||
const [sessionFullPrompt, setSessionFullPrompt] = useState<{ question: string; theme: Theme } | null>(null);
|
||||
sessionsRef.current = sessions;
|
||||
const loadMoreInFlight = useRef(false);
|
||||
const pendingCreateById = useRef(new Map<string, { continuedFromSessionId?: string }>());
|
||||
const cloudCreatedIds = useRef(new Set<string>());
|
||||
for (const session of sessions) {
|
||||
if (!isUnsavedEmptyConsultation(session)) cloudCreatedIds.current.add(session.id);
|
||||
}
|
||||
|
||||
const visibleSessions = sortSessions(sessions.filter((session) => (showArchivedSessions ? session.archivedAt : !session.archivedAt)
|
||||
&& (session.sessionType === "birth_time_rectification"
|
||||
|| session.messages.length > 0
|
||||
|| !session.messagesHydrated
|
||||
|| session.id === activeSessionId)));
|
||||
const visibleSessions = sortSessions(sessions.filter((session) => {
|
||||
if (showArchivedSessions) {
|
||||
return Boolean(session.archivedAt)
|
||||
&& (session.sessionType === "birth_time_rectification"
|
||||
|| session.messages.length > 0
|
||||
|| !session.messagesHydrated);
|
||||
}
|
||||
return isListedSidebarSession(session);
|
||||
}));
|
||||
|
||||
function updateSession(sessionId: string, change: (session: ChatSession) => ChatSession) {
|
||||
setSessions((current) => current.map((session) => (session.id === sessionId ? change(session) : session)));
|
||||
@@ -149,6 +161,9 @@ export function useSessionManagement(params: SessionManagementParams) {
|
||||
) {
|
||||
if (!account) throw new Error("账户尚未加载完成");
|
||||
if (process.env.NODE_ENV === "development" && uiPreview.current) return;
|
||||
if (mode === "create" && cloudCreatedIds.current.has(session.id)) return;
|
||||
const continuedFromSessionId = options?.continuedFromSessionId
|
||||
?? pendingCreateById.current.get(session.id)?.continuedFromSessionId;
|
||||
const values = mode === "create"
|
||||
? {
|
||||
title: session.title,
|
||||
@@ -160,8 +175,8 @@ export function useSessionManagement(params: SessionManagementParams) {
|
||||
chart_profile_id: session.chartProfileId,
|
||||
chart_profile_name: session.chartProfileName,
|
||||
chart_profile_role: session.chartProfileRole,
|
||||
...(options?.continuedFromSessionId
|
||||
? { continued_from_session_id: options.continuedFromSessionId }
|
||||
...(continuedFromSessionId
|
||||
? { continued_from_session_id: continuedFromSessionId }
|
||||
: {}),
|
||||
}
|
||||
: {
|
||||
@@ -173,6 +188,11 @@ export function useSessionManagement(params: SessionManagementParams) {
|
||||
chart_profile_role: session.chartProfileRole,
|
||||
};
|
||||
await writeChatSession(session.id, values, mode);
|
||||
if (mode === "create") {
|
||||
pendingCreateById.current.delete(session.id);
|
||||
cloudCreatedIds.current.add(session.id);
|
||||
if (!uiPreview.current) writeSessionUrl(session.id, "push");
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureSessionMessages(sessionId: string) {
|
||||
@@ -305,55 +325,24 @@ export function useSessionManagement(params: SessionManagementParams) {
|
||||
|
||||
async function startNewChat(options?: { continuedFromSessionId?: string }): Promise<ChatSession | null> {
|
||||
if (!account || !modelCatalog || creatingSession) return null;
|
||||
if (!options?.continuedFromSessionId) {
|
||||
const reusable = findReusableEmptyConsultation(sessions);
|
||||
if (reusable) {
|
||||
setActiveSessionId(reusable.id);
|
||||
if (!uiPreview.current) writeSessionUrl(reusable.id, "push");
|
||||
setDraft("");
|
||||
setDraftTheme(null);
|
||||
setDraftEntrypoint(null);
|
||||
setComposerNotice("");
|
||||
setRequestError(null);
|
||||
return reusable;
|
||||
}
|
||||
}
|
||||
const nextSession = {
|
||||
...createSession(modelCatalog.defaultModelId),
|
||||
...chartSnapshotForSession(activeChartId, chartLibrary, profile),
|
||||
};
|
||||
const previousSessionId = activeSession?.id ?? "";
|
||||
const previousHref = `${window.location.pathname}${window.location.search}`;
|
||||
setCreatingSession(true);
|
||||
setSessions((current) => [nextSession, ...current]);
|
||||
for (const session of sessions) {
|
||||
if (isUnsavedEmptyConsultation(session)) pendingCreateById.current.delete(session.id);
|
||||
}
|
||||
pendingCreateById.current.set(nextSession.id, {
|
||||
continuedFromSessionId: options?.continuedFromSessionId,
|
||||
});
|
||||
setSessions((current) => replaceUnsavedEmptyConsultations(current, nextSession));
|
||||
setActiveSessionId(nextSession.id);
|
||||
if (!uiPreview.current) writeSessionUrl(nextSession.id, "push");
|
||||
setDraft("");
|
||||
setDraftTheme(null);
|
||||
setDraftEntrypoint(null);
|
||||
setComposerNotice("");
|
||||
setRequestError(null);
|
||||
try {
|
||||
await persistSession(
|
||||
nextSession,
|
||||
"create",
|
||||
options?.continuedFromSessionId
|
||||
? { continuedFromSessionId: options.continuedFromSessionId }
|
||||
: undefined,
|
||||
);
|
||||
return nextSession;
|
||||
} catch (caught) {
|
||||
setSessions((current) => current.filter((session) => session.id !== nextSession.id));
|
||||
setActiveSessionId(previousSessionId);
|
||||
if (!uiPreview.current) window.history.replaceState(null, "", previousHref);
|
||||
setRequestError({
|
||||
sessionId: previousSessionId,
|
||||
message: caught instanceof Error ? caught.message : "新对话未能保存到云端。",
|
||||
});
|
||||
return null;
|
||||
} finally {
|
||||
setCreatingSession(false);
|
||||
}
|
||||
return nextSession;
|
||||
}
|
||||
|
||||
function selectSession(sessionId: string) {
|
||||
@@ -418,6 +407,7 @@ export function useSessionManagement(params: SessionManagementParams) {
|
||||
archived: showArchivedSessions,
|
||||
});
|
||||
const incoming = readSessions(page.sessions, modelCatalog).sessions;
|
||||
for (const session of incoming) cloudCreatedIds.current.add(session.id);
|
||||
setSessions((current) => mergeSessionPage(current, incoming));
|
||||
setSessionsCursor(page.nextCursor);
|
||||
} catch (caught) {
|
||||
@@ -434,6 +424,7 @@ export function useSessionManagement(params: SessionManagementParams) {
|
||||
try {
|
||||
const page = await fetchSessions(undefined, { archived: nextArchived });
|
||||
const parsed = readSessions(page.sessions, modelCatalog).sessions;
|
||||
for (const session of parsed) cloudCreatedIds.current.add(session.id);
|
||||
const active = sessionsRef.current.find((session) => session.id === activeSessionId);
|
||||
setSessions(active && !parsed.some((session) => session.id === active.id)
|
||||
? mergeHydratedSession(parsed, active)
|
||||
@@ -462,6 +453,7 @@ export function useSessionManagement(params: SessionManagementParams) {
|
||||
if (query.present && requestedId && !listed.some((session) => session.id === requestedId)) {
|
||||
void lookupSessionById(requestedId, modelCatalog).then((looked) => {
|
||||
if (looked.status === "found") {
|
||||
cloudCreatedIds.current.add(looked.session.id);
|
||||
setSessions((current) => mergeHydratedSession(current, looked.session));
|
||||
sessionSelectionSource.current = "history";
|
||||
selectSession(looked.session.id);
|
||||
|
||||
@@ -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