fix(web): persist thinking, title sessions distinctly, and send follow-ups from the answer
Thinking disappeared on failure and never reached session storage. Keep the sanitized chain on disk and on errors, and regroup the sidebar around reports, charts, favorites, and dated history titles. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -500,6 +500,7 @@ export async function POST(request: Request) {
|
||||
techniqueTruth: string,
|
||||
workflowReceipt: WorkflowReceipt,
|
||||
agentExecutionReceipt?: AgentExecutionReceipt,
|
||||
thinkingText?: string,
|
||||
): Promise<AgentSettlementResult> {
|
||||
try {
|
||||
const reply = parseAgentReply(
|
||||
@@ -507,9 +508,11 @@ export async function POST(request: Request) {
|
||||
createConsultationReplyMetadata({ question: visibleQuestion }),
|
||||
);
|
||||
if (!reply.text) throw new Error("empty_agent_reply");
|
||||
const persistedThinking = thinkingText?.trim().slice(0, 4_000);
|
||||
const responseMessage = {
|
||||
role: "assistant" as const,
|
||||
text: reply.text,
|
||||
...(persistedThinking ? { thinkingText: persistedThinking } : {}),
|
||||
techniqueTruth,
|
||||
workflowReceipt,
|
||||
...(agentExecutionReceipt ? { agentExecutionReceipt } : {}),
|
||||
@@ -755,12 +758,13 @@ export async function POST(request: Request) {
|
||||
headers: { "x-jyotish-birth-time-mode": consultationMode },
|
||||
onFirstActivity: markFirstActivity,
|
||||
onFirstOutput: markFirstText,
|
||||
onComplete: (output, agentExecutionReceipt) => settleRun(() => completeResponse(
|
||||
onComplete: (output, agentExecutionReceipt, thinkingText) => settleRun(() => completeResponse(
|
||||
output,
|
||||
mergeUsage(usages),
|
||||
generalDailyContext ? "public-panchanga-only" : "not-applicable",
|
||||
workflowReceipt,
|
||||
agentExecutionReceipt,
|
||||
thinkingText,
|
||||
), undefined),
|
||||
onError: (error) => settleRun(
|
||||
cancel,
|
||||
@@ -840,12 +844,13 @@ export async function POST(request: Request) {
|
||||
headers: { "x-jyotish-birth-time-mode": consultationMode },
|
||||
onFirstActivity: markFirstActivity,
|
||||
onFirstOutput: markFirstText,
|
||||
onComplete: (output, agentExecutionReceipt) => settleRun(() => completeResponse(
|
||||
onComplete: (output, agentExecutionReceipt, thinkingText) => settleRun(() => completeResponse(
|
||||
output,
|
||||
mergeUsage(usages),
|
||||
state.techniqueTruth ?? "declared-window",
|
||||
state.workflowReceipt ?? workflowReceipt,
|
||||
agentExecutionReceipt,
|
||||
thinkingText,
|
||||
), undefined),
|
||||
onError: (error) => settleRun(
|
||||
cancel,
|
||||
@@ -930,12 +935,13 @@ export async function POST(request: Request) {
|
||||
headers: { "x-jyotish-birth-time-mode": consultationMode },
|
||||
onFirstActivity: markFirstActivity,
|
||||
onFirstOutput: markFirstText,
|
||||
onComplete: (output, agentExecutionReceipt) => settleRun(() => completeResponse(
|
||||
onComplete: (output, agentExecutionReceipt, thinkingText) => settleRun(() => completeResponse(
|
||||
output,
|
||||
mergeUsage(usages),
|
||||
state.techniqueTruth ?? "unknown",
|
||||
state.workflowReceipt ?? workflowReceipt,
|
||||
agentExecutionReceipt,
|
||||
thinkingText,
|
||||
), undefined),
|
||||
onError: (error) => settleRun(
|
||||
cancel,
|
||||
|
||||
+106
-17
@@ -241,20 +241,50 @@ button:disabled { cursor: default; opacity: .45; }
|
||||
}
|
||||
.markdown-table tr:last-child th,
|
||||
.markdown-table tr:last-child td { border-bottom: 0; }
|
||||
.agent-activity-status {
|
||||
.agent-activity-status,
|
||||
.agent-thinking-panel {
|
||||
min-height: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
color: var(--color-ink-tertiary);
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
margin: 0 0 var(--space-3);
|
||||
color: var(--color-ink-secondary);
|
||||
font-size: var(--type-body-sm);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.agent-thinking-timeline {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.agent-thinking-step {
|
||||
display: grid;
|
||||
grid-template-columns: 20px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
min-height: 24px;
|
||||
}
|
||||
.agent-thinking-marker {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--color-success) 14%, var(--color-canvas));
|
||||
color: var(--color-success);
|
||||
}
|
||||
.agent-thinking-marker svg { width: 12px; height: 12px; }
|
||||
.agent-thinking-marker.is-live-marker {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
.agent-activity-status__row { min-height: 24px; display: flex; align-items: center; gap: var(--space-2); min-width: 0; width: 100%; }
|
||||
.agent-activity-status__live { min-height: 24px; display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
|
||||
.agent-activity-status canvas { flex: 0 0 auto; }
|
||||
.agent-activity-status__elapsed {
|
||||
.agent-activity-status canvas,
|
||||
.agent-thinking-marker canvas { flex: 0 0 auto; }
|
||||
.agent-activity-status__elapsed,
|
||||
.agent-thinking-elapsed {
|
||||
flex: 0 0 auto;
|
||||
color: var(--color-ink-tertiary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
@@ -288,7 +318,8 @@ button:disabled { cursor: default; opacity: .45; }
|
||||
from { background-position: 120% 0; }
|
||||
to { background-position: -120% 0; }
|
||||
}
|
||||
.agent-activity-status + .message-answer { margin-top: var(--space-2); }
|
||||
.agent-activity-status + .message-answer,
|
||||
.agent-thinking-panel + .message-answer { margin-top: var(--space-2); }
|
||||
.composer textarea::placeholder { color: var(--color-ink-tertiary); }
|
||||
.composer button svg { width: 19px; height: 19px; }
|
||||
.dialog-close svg { width: 19px; height: 19px; }
|
||||
@@ -465,18 +496,55 @@ button:disabled { cursor: default; opacity: .45; }
|
||||
.brand-row strong { font-weight: 400; }
|
||||
.brand-mark, .auth-brand span { width: 32px; height: 32px; border-radius: 50%; background: var(--color-canvas) url("/jyotish-logo.png") center / contain no-repeat; box-shadow: 0 0 0 1px oklch(0 0 0 / .1); }
|
||||
.auth-story-brand img { width: 32px; height: 32px; border-radius: 50%; object-fit: contain; box-shadow: 0 0 0 1px oklch(0 0 0 / .1); }
|
||||
.new-chat { width: 100%; min-height: 44px; display: flex; align-items: center; justify-content: center; gap: var(--space-2); padding: 0 var(--space-3); border: 0; background: var(--sidebar-primary); color: var(--sidebar-primary-foreground); cursor: pointer; font-size: var(--type-body-sm); transition: background-color 120ms ease-out, transform 120ms ease-out; margin: var(--space-4) 0 var(--space-2); border-radius: var(--radius-md); font-weight: 500; }
|
||||
.report-nav-button { width: 100%; min-height: 44px; display: flex; align-items: center; justify-content: center; gap: var(--space-2); padding: 0 var(--space-3); margin-bottom: var(--space-4); border: 1px solid var(--sidebar-border); border-radius: var(--radius-md); color: var(--sidebar-foreground); }
|
||||
.report-nav-button:hover { background: var(--sidebar-accent); color: var(--sidebar-accent-foreground); }
|
||||
.new-chat { width: 100%; min-height: 44px; display: flex; align-items: center; justify-content: center; gap: var(--space-2); padding: 0 var(--space-3); border: 0; background: transparent; color: var(--sidebar-foreground); cursor: pointer; font-size: var(--type-body-sm); transition: background-color 120ms ease-out, transform 120ms ease-out; margin: 0; border-radius: var(--radius-md); font-weight: 500; }
|
||||
.report-nav-button { width: 100%; min-height: 44px; display: flex; align-items: center; justify-content: center; gap: var(--space-2); padding: 0 var(--space-3); margin-bottom: var(--space-2); border: 0; border-radius: var(--radius-md); color: var(--sidebar-foreground); }
|
||||
.report-nav-button:hover,
|
||||
.new-chat:hover { background: var(--sidebar-accent); color: var(--sidebar-accent-foreground); }
|
||||
[data-state="expanded"] .new-chat,
|
||||
[data-state="expanded"] .report-nav-button { justify-content: flex-start; }
|
||||
.session-nav-header { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: var(--space-2); padding: 0 var(--space-3) var(--space-2); }
|
||||
.sidebar-label { min-height: 44px; display: flex; align-items: center; color: var(--sidebar-muted-foreground); font-size: var(--type-overline); font-weight: 500; letter-spacing: 1.5px; }
|
||||
.session-nav-header-inline { padding-top: 0; }
|
||||
.sidebar-section { margin: 0 0 var(--space-3); }
|
||||
.sidebar-section-summary {
|
||||
display: flex;
|
||||
min-height: 44px;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: 0 var(--space-3);
|
||||
list-style: none;
|
||||
color: var(--sidebar-muted-foreground);
|
||||
cursor: pointer;
|
||||
font-size: var(--type-overline);
|
||||
font-weight: 500;
|
||||
letter-spacing: 1.5px;
|
||||
}
|
||||
.sidebar-section-summary::-webkit-details-marker { display: none; }
|
||||
.sidebar-section-summary > svg { width: 16px; height: 16px; flex: 0 0 auto; }
|
||||
.sidebar-section-chevron { margin-left: auto; width: 16px; height: 16px; transition: transform 120ms ease-out; }
|
||||
.sidebar-section[open] .sidebar-section-chevron { transform: rotate(180deg); }
|
||||
.sidebar-empty { margin: 0; padding: 0 var(--space-3) var(--space-2); color: var(--color-ink-tertiary); font-size: var(--type-caption); }
|
||||
.chart-nav-list { display: flex; flex-wrap: wrap; gap: var(--space-2); padding: 0 var(--space-3) var(--space-2); }
|
||||
.chart-nav-chip {
|
||||
min-height: 32px;
|
||||
max-width: 100%;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--sidebar-border);
|
||||
border-radius: 999px;
|
||||
background: var(--color-canvas);
|
||||
color: var(--sidebar-foreground);
|
||||
cursor: pointer;
|
||||
font-size: var(--type-caption);
|
||||
}
|
||||
.chart-nav-chip:hover { background: var(--sidebar-accent); }
|
||||
.sidebar-label { min-height: 44px; display: flex; align-items: center; gap: var(--space-2); color: var(--sidebar-muted-foreground); font-size: var(--type-overline); font-weight: 500; letter-spacing: 1.5px; }
|
||||
.sidebar-label:focus-visible { outline: 3px solid color-mix(in srgb, var(--sidebar-ring) 56%, transparent); outline-offset: 2px; border-radius: var(--radius-sm); }
|
||||
.session-nav-toggle { min-height: 44px; padding: 0 var(--space-2); border: 0; border-radius: var(--radius-md); background: transparent; color: var(--sidebar-muted-foreground); cursor: pointer; font-size: var(--type-overline); transition: background-color 120ms ease-out, color 120ms ease-out; }
|
||||
.session-nav-toggle { min-height: 44px; min-width: 44px; display: inline-flex; align-items: center; justify-content: center; padding: 0 var(--space-2); border: 0; border-radius: var(--radius-md); background: transparent; color: var(--sidebar-muted-foreground); cursor: pointer; font-size: var(--type-overline); transition: background-color 120ms ease-out, color 120ms ease-out; }
|
||||
.session-nav-toggle svg { width: 16px; height: 16px; }
|
||||
.session-list { min-height: 0; display: flex; flex-direction: column; gap: var(--space-1); }
|
||||
.session-row { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) 44px; align-items: center; border-radius: var(--radius-lg); color: var(--sidebar-muted-foreground); transition: background-color 120ms ease-out, box-shadow 120ms ease-out, color 120ms ease-out; }
|
||||
.session-row:hover, .session-row:focus-within { background: var(--sidebar-accent); color: var(--sidebar-accent-foreground); }
|
||||
.session-row:has(.session-main[data-active="true"]) { background: var(--sidebar-accent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--sidebar-ring) 22%, transparent); color: var(--sidebar-accent-foreground); }
|
||||
.session-main { position: relative; width: 100%; min-height: 52px; display: grid; gap: 2px; padding: var(--space-2) var(--space-3) var(--space-2) var(--space-4); border: 0; border-radius: var(--radius-md); background: transparent; color: inherit; cursor: pointer; text-align: left; transition: background-color 120ms ease-out, color 120ms ease-out, transform 120ms ease-out; }
|
||||
.session-main { position: relative; width: 100%; min-height: 44px; display: grid; gap: 2px; padding: var(--space-2) var(--space-3) var(--space-2) var(--space-4); border: 0; border-radius: var(--radius-md); background: transparent; color: inherit; cursor: pointer; text-align: left; transition: background-color 120ms ease-out, color 120ms ease-out, transform 120ms ease-out; }
|
||||
.session-main[data-active="true"] { color: var(--sidebar-accent-foreground); background: transparent; }
|
||||
.session-main[data-active="true"]::before { position: absolute; border-radius: 3px; content: ""; top: var(--space-3); bottom: var(--space-3); left: var(--space-1); width: 2px; background: var(--sidebar-ring); }
|
||||
.session-title { min-width: 0; display: flex; align-items: center; gap: var(--space-1); overflow: hidden; line-height: 1.35; font-size: var(--type-body-sm); font-weight: 500; }
|
||||
@@ -518,7 +586,7 @@ button:disabled { cursor: default; opacity: .45; }
|
||||
[data-state="collapsed"] .brand-row { justify-content: center; gap: 0; padding: 0; }
|
||||
[data-state="collapsed"] [data-sidebar="menu"] { justify-items: center; }
|
||||
[data-state="collapsed"] [data-sidebar="menu-button"] { width: 44px; height: 44px; min-height: 44px; padding: 0; place-items: center; }
|
||||
[data-state="collapsed"] .new-chat { margin-block: var(--space-4) var(--space-6); }
|
||||
[data-state="collapsed"] .new-chat { margin-block: var(--space-2); }
|
||||
[data-state="collapsed"] .profile-trigger { width: 44px; height: 44px; min-height: 44px; margin-inline: auto; padding: 6px; display: grid; grid-template-columns: 32px; place-items: center; }
|
||||
[data-state="collapsed"] .sidebar-footer { padding-top: var(--space-3); }
|
||||
}
|
||||
@@ -764,6 +832,27 @@ button:disabled { cursor: default; opacity: .45; }
|
||||
margin: -4px 0 8px 42px;
|
||||
color: var(--color-ink-tertiary);
|
||||
}
|
||||
.conversation-follow-ups {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin: 0 0 12px var(--assistant-content-inset);
|
||||
}
|
||||
.conversation-follow-up {
|
||||
max-width: 100%;
|
||||
min-height: 44px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 999px;
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-ink);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
text-align: left;
|
||||
}
|
||||
.conversation-follow-up:hover { background: var(--color-canvas-muted); }
|
||||
.conversation-follow-up:disabled { cursor: not-allowed; opacity: .56; }
|
||||
.message-actions button {
|
||||
display: inline-flex;
|
||||
width: 26px;
|
||||
@@ -1108,7 +1197,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
.status-已过期, .status-已兑换, .empty-cell { color: var(--color-ink-tertiary); }
|
||||
|
||||
@media (hover: hover) {
|
||||
.new-chat:not(:disabled):hover { background: var(--color-surface-dark-raised); }
|
||||
.new-chat:not(:disabled):hover { background: var(--sidebar-accent); color: var(--sidebar-accent-foreground); }
|
||||
.composer button:not(:disabled):hover, .button-primary:not(:disabled):hover { background: var(--color-action-hover); }
|
||||
.model-selector-trigger:not(:disabled):hover { background: var(--color-canvas-muted); color: var(--color-ink); }
|
||||
.model-selector-option:hover { background: var(--color-canvas-soft); color: var(--color-ink); }
|
||||
|
||||
+118
-25
@@ -39,7 +39,9 @@ import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/s
|
||||
import { composerDraftSnapshot, setComposerDraft } from "@/lib/composer-draft";
|
||||
import { clearStaleClientReload } from "@/lib/stale-client-recovery";
|
||||
import { chinaLocations, type ProvinceNode } from "@/data/china-locations";
|
||||
import { parseAgentReply, resolveSessionTitle, type ReplyTheme } from "@/lib/agent-reply";
|
||||
import { ConversationFollowUps } from "@/components/conversation-follow-ups";
|
||||
import { parseAgentReply, isGenericSessionTitle, resolveSessionTitle, type ReplyTheme } from "@/lib/agent-reply";
|
||||
import { deriveConsultationFollowUps } from "@/lib/consultation-follow-ups";
|
||||
import {
|
||||
beamAvatarPalettes,
|
||||
beamAvatarSchema,
|
||||
@@ -755,16 +757,24 @@ function readSessions(value: unknown, catalog: PublicLanguageModelCatalog | null
|
||||
updated_at?: unknown;
|
||||
};
|
||||
const messages: Message[] = Array.isArray(session.messages)
|
||||
? session.messages.flatMap((message) => (
|
||||
message && typeof message === "object"
|
||||
&& ((message as Message).role === "user" || (message as Message).role === "assistant")
|
||||
&& typeof (message as Message).text === "string"
|
||||
? [{
|
||||
role: (message as Message).role,
|
||||
text: (message as Message).text.slice(0, 12000),
|
||||
}]
|
||||
: []
|
||||
))
|
||||
? session.messages.flatMap((message) => {
|
||||
if (!message || typeof message !== "object") return [];
|
||||
const stored = message as Message;
|
||||
if ((stored.role !== "user" && stored.role !== "assistant") || typeof stored.text !== "string") {
|
||||
return [];
|
||||
}
|
||||
const thinkingText = typeof stored.thinkingText === "string" && stored.thinkingText.trim()
|
||||
? stored.thinkingText.slice(0, 4000)
|
||||
: undefined;
|
||||
return [{
|
||||
role: stored.role,
|
||||
text: stored.text.slice(0, 12000),
|
||||
...(thinkingText ? { thinkingText } : {}),
|
||||
...(typeof stored.techniqueTruth === "string" ? { techniqueTruth: stored.techniqueTruth } : {}),
|
||||
...(stored.agentExecutionReceipt ? { agentExecutionReceipt: stored.agentExecutionReceipt } : {}),
|
||||
...(stored.workflowReceipt ? { workflowReceipt: stored.workflowReceipt } : {}),
|
||||
}];
|
||||
})
|
||||
: [];
|
||||
|
||||
if (typeof session.id !== "string") return [];
|
||||
@@ -775,7 +785,7 @@ function readSessions(value: unknown, catalog: PublicLanguageModelCatalog | null
|
||||
if (catalog && selection.fellBack) fallbackSessionIds.push(session.id);
|
||||
return [{
|
||||
id: session.id,
|
||||
title: typeof session.title === "string" ? session.title.slice(0, 36) : "新对话",
|
||||
title: typeof session.title === "string" ? session.title.slice(0, 48) : "新对话",
|
||||
theme: normalizeConsultationDomain(session.theme) ?? "general",
|
||||
modelId: selection.modelId,
|
||||
messages,
|
||||
@@ -1155,6 +1165,7 @@ export default function Home() {
|
||||
&& activeSession.id === rectificationSessionId;
|
||||
const visibleSessions = sessions
|
||||
.filter((session) => showArchivedSessions ? archivedSessionIds.includes(session.id) : !archivedSessionIds.includes(session.id))
|
||||
.filter((session) => session.sessionType === "birth_time_rectification" || session.messages.length > 0)
|
||||
.sort((left, right) => Number(pinnedSessionIds.includes(right.id)) - Number(pinnedSessionIds.includes(left.id)));
|
||||
const activeError = requestError && requestError.sessionId === activeSession?.id ? requestError.message : "";
|
||||
const isLoading = pendingSessionId === activeSession?.id;
|
||||
@@ -1375,8 +1386,12 @@ export default function Home() {
|
||||
? session
|
||||
: {
|
||||
...session,
|
||||
title: session.messages.length === 0 && session.title === "新对话"
|
||||
? resolveSessionTitle(question)
|
||||
title: session.messages.length === 0 && isGenericSessionTitle(session.title)
|
||||
? resolveSessionTitle(question, undefined, {
|
||||
entrypoint: stored?.entrypoint,
|
||||
theme: stored?.theme ?? session.theme,
|
||||
existingTitles: sessions.filter((item) => item.id !== session.id).map((item) => item.title),
|
||||
})
|
||||
: session.title,
|
||||
theme: stored?.theme ?? session.theme,
|
||||
messages: [...session.messages, { role: "user" as const, text: question }],
|
||||
@@ -1983,6 +1998,7 @@ export default function Home() {
|
||||
messages: session.messages.map((message) => ({
|
||||
role: message.role,
|
||||
text: message.text,
|
||||
thinkingText: message.thinkingText,
|
||||
techniqueTruth: message.techniqueTruth,
|
||||
agentExecutionReceipt: message.agentExecutionReceipt,
|
||||
workflowReceipt: message.workflowReceipt,
|
||||
@@ -2631,7 +2647,10 @@ export default function Home() {
|
||||
// never generates a Case id; it only mirrors the returned binding.
|
||||
const merged: ChatSession = {
|
||||
id: opened.sessionId,
|
||||
title: "生时校正",
|
||||
title: resolveSessionTitle("生时校正", undefined, {
|
||||
entrypoint: "birth_time_rectification",
|
||||
existingTitles: sessions.map((session) => session.title),
|
||||
}),
|
||||
theme: "general",
|
||||
modelId: modelCatalog.defaultModelId ?? "",
|
||||
messages: [],
|
||||
@@ -2640,6 +2659,7 @@ export default function Home() {
|
||||
rectificationCaseId: opened.caseId,
|
||||
};
|
||||
setSessions((current) => [merged, ...current.filter((session) => session.id !== merged.id)]);
|
||||
void persistSession(merged).catch(() => {});
|
||||
|
||||
setRectificationPendingQuestion(pendingConsultationQuestion);
|
||||
setDraft("");
|
||||
@@ -3075,8 +3095,12 @@ export default function Home() {
|
||||
: currentSession.messages);
|
||||
const userSession: ChatSession = {
|
||||
...currentSession,
|
||||
title: currentSession.messages.length === 0 && currentSession.title === "新对话"
|
||||
? resolveSessionTitle(question)
|
||||
title: currentSession.messages.length === 0 && isGenericSessionTitle(currentSession.title)
|
||||
? resolveSessionTitle(question, undefined, {
|
||||
entrypoint: consultEntrypoint,
|
||||
theme,
|
||||
existingTitles: sessions.filter((item) => item.id !== currentSession.id).map((item) => item.title),
|
||||
})
|
||||
: currentSession.title,
|
||||
theme,
|
||||
messages: questionAlreadyPresent ? preservedMessages : [...preservedMessages, { role: "user", text: question }],
|
||||
@@ -3209,6 +3233,7 @@ export default function Home() {
|
||||
}
|
||||
setStreamingReply({ sessionId, text: "" });
|
||||
let latestPartialReply = "";
|
||||
let thinking = "";
|
||||
try {
|
||||
const response = await fetch("/api/consult", {
|
||||
method: "POST",
|
||||
@@ -3270,7 +3295,6 @@ export default function Home() {
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let answer = "";
|
||||
let thinking = "";
|
||||
const updateStreamingAnswer = (activity?: AgentActivityView) => {
|
||||
const partialReply = parseAgentReply(answer).text;
|
||||
latestPartialReply = partialReply;
|
||||
@@ -3320,7 +3344,7 @@ export default function Home() {
|
||||
const parser = createNdjsonParser((event) => {
|
||||
if (event.type === "answer.delta") answer += event.text;
|
||||
if (event.type === "thinking.delta") {
|
||||
thinking += event.text;
|
||||
thinking = `${thinking}${event.text}`.slice(0, 4_000);
|
||||
setStreamingReply((current) => ({
|
||||
sessionId,
|
||||
text: current?.sessionId === sessionId ? current.text : parseAgentReply(answer).text,
|
||||
@@ -3386,7 +3410,14 @@ export default function Home() {
|
||||
void refreshAccount();
|
||||
return true;
|
||||
}
|
||||
if (!runCompleted && !truncatedFailure) throw new ConsultationResponseError(502, "Agent 回答未完成,本次不会保存为成功咨询。");
|
||||
if (!runCompleted && !truncatedFailure) {
|
||||
throw new ConsultationResponseError(
|
||||
502,
|
||||
thinking.trim()
|
||||
? "这次还没有生成可显示的回答。思考过程已保留,可以直接继续问。"
|
||||
: "Agent 回答未完成,本次不会保存为成功咨询。",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
@@ -3397,13 +3428,23 @@ export default function Home() {
|
||||
answer += decoder.decode();
|
||||
}
|
||||
if (controller.signal.aborted) return Boolean(latestPartialReply);
|
||||
if (!answer.trim()) throw new Error("Agent 没有返回内容,请重试。");
|
||||
const reply = parseAgentReply(answer);
|
||||
if (!reply.text) throw new Error("Agent 没有返回可显示的回答,请重试。");
|
||||
if (!reply.text) {
|
||||
throw thinking.trim()
|
||||
? new ConsultationResponseError(502, "这次还没有生成可显示的回答。思考过程已保留,可以直接继续问。")
|
||||
: new Error("Agent 没有返回可显示的回答,请重试。");
|
||||
}
|
||||
|
||||
const completedTitle = reply.title && !isGenericSessionTitle(reply.title)
|
||||
? resolveSessionTitle(question, reply.title, {
|
||||
entrypoint: consultEntrypoint,
|
||||
theme,
|
||||
existingTitles: sessions.filter((item) => item.id !== sessionId).map((item) => item.title),
|
||||
})
|
||||
: userSession.title;
|
||||
const completedSession: ChatSession = {
|
||||
...userSession,
|
||||
title: userSession.title,
|
||||
title: completedTitle,
|
||||
messages: [...userSession.messages, {
|
||||
role: "assistant",
|
||||
text: reply.text,
|
||||
@@ -3415,6 +3456,11 @@ export default function Home() {
|
||||
updatedAt: timestamp(),
|
||||
};
|
||||
updateSession(sessionId, () => completedSession);
|
||||
try {
|
||||
await persistSession(completedSession);
|
||||
} catch (error) {
|
||||
setComposerNotice(error instanceof Error ? error.message : "回答已生成,但云端同步暂时失败。");
|
||||
}
|
||||
setReplyOutcome({
|
||||
sessionId,
|
||||
phase: "completed",
|
||||
@@ -3446,6 +3492,18 @@ export default function Home() {
|
||||
if (restore) {
|
||||
updateSession(sessionId, () => restore);
|
||||
void persistSession(restore).catch(() => {});
|
||||
} else if (thinking.trim() || latestPartialReply) {
|
||||
const failedSession: ChatSession = {
|
||||
...userSession,
|
||||
messages: [...userSession.messages, {
|
||||
role: "assistant",
|
||||
text: latestPartialReply,
|
||||
thinkingText: thinking.trim() || undefined,
|
||||
}],
|
||||
updatedAt: timestamp(),
|
||||
};
|
||||
updateSession(sessionId, () => failedSession);
|
||||
void persistSession(failedSession).catch(() => {});
|
||||
}
|
||||
completeConsultationInterface(requestId);
|
||||
return false;
|
||||
@@ -3599,10 +3657,19 @@ export default function Home() {
|
||||
const sidebarSessions = visibleSessions.map((session) => ({
|
||||
id: session.id,
|
||||
title: session.title,
|
||||
messageCount: session.messages.length,
|
||||
pinned: pinnedSessionIds.includes(session.id),
|
||||
archived: archivedSessionIds.includes(session.id),
|
||||
}));
|
||||
const sidebarCharts = (chartLibrary.length > 0
|
||||
? chartLibrary
|
||||
: profile.name.trim()
|
||||
? [{ id: "self", role: "self" as const, profile, updatedAt: timestamp() }]
|
||||
: []
|
||||
).map((record) => ({
|
||||
id: record.id,
|
||||
name: record.profile.name.trim() || (record.role === "self" ? "我" : "对方"),
|
||||
role: record.role,
|
||||
}));
|
||||
const modalOpen = activeAccountDialog !== null || onboardingPaywallOpen;
|
||||
|
||||
return (
|
||||
@@ -3611,6 +3678,7 @@ export default function Home() {
|
||||
<span className="sr-only" role="status" aria-live="polite" aria-atomic="true">{replyAnnouncement}</span>
|
||||
<AppSidebar
|
||||
sessions={sidebarSessions}
|
||||
charts={sidebarCharts}
|
||||
activeSessionId={activeSession?.id ?? null}
|
||||
account={sidebarAccount}
|
||||
accountMenuOpen={accountMenuOpen}
|
||||
@@ -3646,6 +3714,14 @@ export default function Home() {
|
||||
onNewChat={() => void startNewChat()}
|
||||
onOpenReports={() => router.push("/reports")}
|
||||
onSelectSession={selectSession}
|
||||
onSelectChart={() => {
|
||||
setChartLibraryOpen(true);
|
||||
openAccountDialog("profile");
|
||||
}}
|
||||
onAddChart={() => {
|
||||
setChartLibraryOpen(true);
|
||||
openAccountDialog("profile");
|
||||
}}
|
||||
onOpenProfile={() => openAccountDialog("profile")}
|
||||
onOpenRedeem={() => router.push(membershipHref("account-menu"))}
|
||||
onOpenLogout={() => openAccountDialog("logout")}
|
||||
@@ -3856,7 +3932,7 @@ export default function Home() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="message-list" aria-busy={isLoading}>
|
||||
{chatMessageViews(activeSession.messages, isLoading, activeStreamingText, activeStreamingActivity, activeStreamingThinking).map((message, _, views) => {
|
||||
{chatMessageViews(activeSession.messages, isLoading, activeStreamingText, activeStreamingActivity, activeStreamingThinking).map((message, index, views) => {
|
||||
const showActions = message.role === "assistant"
|
||||
&& message.state === "settled"
|
||||
&& Boolean(message.text);
|
||||
@@ -3866,6 +3942,18 @@ export default function Home() {
|
||||
item.role === "assistant" && item.state === "settled" && Boolean(item.text)
|
||||
))?.renderKey
|
||||
: undefined;
|
||||
const previousQuestion = views[index - 1]?.role === "user" ? views[index - 1]?.text : "";
|
||||
const followUps = showActions
|
||||
&& message.renderKey === latestRegeneratableKey
|
||||
&& activeSession.sessionType === "consultation"
|
||||
&& previousQuestion
|
||||
? deriveConsultationFollowUps({
|
||||
question: previousQuestion,
|
||||
answer: message.text,
|
||||
theme: activeSession.theme,
|
||||
entrypoint: isGeneralDailyFortuneQuestion(previousQuestion) ? "daily_starlanguage" : null,
|
||||
})
|
||||
: [];
|
||||
return (
|
||||
<div className="message-entry" key={message.renderKey}>
|
||||
<ChatMessageRow message={message} />
|
||||
@@ -3885,6 +3973,11 @@ export default function Home() {
|
||||
onRegenerate={() => regenerateLatestAnswer(message.renderKey)}
|
||||
/>
|
||||
)}
|
||||
<ConversationFollowUps
|
||||
questions={followUps}
|
||||
disabled={productEntrypointsDisabled}
|
||||
onSelect={(question) => void send(question, activeSession.theme)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user