fix(rectification): align activity with real agent work
This commit is contained in:
@@ -21,7 +21,7 @@ export function AgentActivityStatus({
|
||||
return (
|
||||
<div className="agent-activity-status" role="status">
|
||||
<ThinkingOrb aria-hidden="true" state={state} size={20} />
|
||||
<span>{label}</span>
|
||||
<span key={label} className="agent-activity-status__text">{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,13 @@ export function AgentAvatar() {
|
||||
return <span className="agent-avatar" aria-hidden="true" />;
|
||||
}
|
||||
|
||||
export function ChatMessageRow({ message }: { readonly message: ChatMessageView }) {
|
||||
export function ChatMessageRow({
|
||||
message,
|
||||
showActivity = message.state !== "settled",
|
||||
}: Readonly<{
|
||||
message: ChatMessageView;
|
||||
showActivity?: boolean;
|
||||
}>) {
|
||||
const messageRow = useRef<HTMLElement>(null);
|
||||
const assistantLabel = message.state === "thinking"
|
||||
? "Jyotisha 正在分析"
|
||||
@@ -32,7 +38,7 @@ export function ChatMessageRow({ message }: { readonly message: ChatMessageView
|
||||
} as const)[message.activity.phase]
|
||||
: message.state === "thinking" ? "working" : "composing";
|
||||
const activityLabel = message.activity?.label
|
||||
?? (message.state === "thinking" ? "正在核对星盘信息…" : undefined);
|
||||
?? (message.state === "thinking" ? "正在处理…" : undefined);
|
||||
|
||||
useGSAP(() => {
|
||||
if (!messageRow.current) return;
|
||||
@@ -63,7 +69,7 @@ export function ChatMessageRow({ message }: { readonly message: ChatMessageView
|
||||
<div className="message-bubble">
|
||||
{message.role === "assistant" ? (
|
||||
<>
|
||||
{message.state !== "settled" && (
|
||||
{showActivity && (
|
||||
<AgentActivityStatus state={activityState} label={activityLabel} />
|
||||
)}
|
||||
{message.text && <ChatMessageContent text={message.text} />}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
"use client";
|
||||
|
||||
import { Check, ChevronDown } from "lucide-react";
|
||||
import type {
|
||||
PublicRectificationMethod,
|
||||
PublicRectificationTool,
|
||||
} from "@/lib/rectification-agentic/v9/public-receipt";
|
||||
|
||||
export type CompletedActivityReceiptView = Readonly<{
|
||||
steps: readonly PublicRectificationTool[];
|
||||
methods: readonly PublicRectificationMethod[];
|
||||
failedTool?: PublicRectificationTool;
|
||||
}>;
|
||||
|
||||
const TOOL_LABELS: Readonly<Record<PublicRectificationTool, string>> = {
|
||||
"rectification-read-case": "读取校正记录",
|
||||
"rectification-propose-evidence": "整理事件证据",
|
||||
"rectification-confirm-evidence": "确认事件证据",
|
||||
"rectification-revise-evidence": "修订事件证据",
|
||||
"rectification-compare-candidates": "比较候选时间",
|
||||
"rectification-read-diagnostics": "检查候选稳健性",
|
||||
"rectification-offer-candidates": "生成候选建议",
|
||||
"rectification-accept-candidate": "采用候选时间",
|
||||
"rectification-confirm-birth-time": "确认校正时间",
|
||||
"rectification-close-case": "完成校正记录",
|
||||
};
|
||||
|
||||
const METHOD_LABELS: Readonly<Record<PublicRectificationMethod, string>> = {
|
||||
"d1-rashi": "D1 本命盘",
|
||||
"d2-hora": "D2 财帛分盘",
|
||||
"d4-chaturthamsha": "D4 迁移分盘",
|
||||
"d9-navamsa": "D9 婚姻分盘",
|
||||
"d10-dashamsa": "D10 事业分盘",
|
||||
"d11-labhamsha": "D11 收益分盘",
|
||||
"d24-chaturvimshamsha": "D24 教育分盘",
|
||||
"d30-trimshamsha": "D30 健康压力分盘",
|
||||
"vimshottari-dasha": "Vimshottari",
|
||||
"narayana-dasha": "Narayana",
|
||||
gochara: "Gochara",
|
||||
ashtakavarga: "Ashtakavarga",
|
||||
shadbala: "Shadbala",
|
||||
"arudha-pada": "Arudha Pada",
|
||||
"functional-benefic-malefic": "本命功能吉凶星",
|
||||
};
|
||||
|
||||
const METHOD_GROUPS: readonly Readonly<{
|
||||
label: string;
|
||||
methods: readonly PublicRectificationMethod[];
|
||||
}>[] = [
|
||||
{
|
||||
label: "基础判断",
|
||||
methods: [
|
||||
"d1-rashi",
|
||||
"functional-benefic-malefic",
|
||||
"vimshottari-dasha",
|
||||
"narayana-dasha",
|
||||
"arudha-pada",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "主题分盘",
|
||||
methods: [
|
||||
"d2-hora",
|
||||
"d4-chaturthamsha",
|
||||
"d9-navamsa",
|
||||
"d10-dashamsa",
|
||||
"d11-labhamsha",
|
||||
"d24-chaturvimshamsha",
|
||||
"d30-trimshamsha",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "交叉验证",
|
||||
methods: ["gochara", "ashtakavarga", "shadbala"],
|
||||
},
|
||||
];
|
||||
|
||||
function failureLabel(tool: PublicRectificationTool): string {
|
||||
return tool === "rectification-compare-candidates"
|
||||
? "候选比较未完成,当前进度已保留"
|
||||
: `${TOOL_LABELS[tool]}未完成,当前进度已保留`;
|
||||
}
|
||||
|
||||
export function CompletedActivityReceipt({
|
||||
receipt,
|
||||
}: Readonly<{ receipt: CompletedActivityReceiptView }>) {
|
||||
const stepLabels = receipt.steps.map((tool) => TOOL_LABELS[tool]);
|
||||
const methodSet = new Set(receipt.methods);
|
||||
const methodGroups = METHOD_GROUPS.map((group) => ({
|
||||
label: group.label,
|
||||
methods: group.methods.filter((method) => methodSet.has(method)).map((method) => METHOD_LABELS[method]),
|
||||
})).filter((group) => group.methods.length > 0);
|
||||
const hasDetails = stepLabels.length > 0 || methodGroups.length > 0;
|
||||
const summary = receipt.failedTool
|
||||
? failureLabel(receipt.failedTool)
|
||||
: `本轮完成 · ${stepLabels.length} 个步骤 · ${receipt.methods.length} 项计算依据`;
|
||||
|
||||
if (!hasDetails) {
|
||||
return receipt.failedTool
|
||||
? <p className="rectification-activity-failure">{summary}</p>
|
||||
: null;
|
||||
}
|
||||
|
||||
return (
|
||||
<details className={`rectification-activity-receipt${receipt.failedTool ? " is-failed" : ""}`}>
|
||||
<summary>
|
||||
<span className="rectification-activity-receipt__summary-main">
|
||||
{receipt.failedTool ? (
|
||||
<span className="rectification-activity-receipt__failure-mark" aria-hidden="true">!</span>
|
||||
) : (
|
||||
<Check aria-hidden="true" size={14} strokeWidth={2.25} />
|
||||
)}
|
||||
<span>{summary}</span>
|
||||
</span>
|
||||
<span className="rectification-activity-receipt__toggle">
|
||||
查看详情
|
||||
<ChevronDown aria-hidden="true" size={14} />
|
||||
</span>
|
||||
</summary>
|
||||
<div className="rectification-activity-receipt__details">
|
||||
{stepLabels.length > 0 && (
|
||||
<section aria-label="执行步骤">
|
||||
<h3>执行步骤</h3>
|
||||
<p>{stepLabels.join(" · ")}</p>
|
||||
</section>
|
||||
)}
|
||||
{methodGroups.length > 0 && (
|
||||
<section aria-label="计算依据">
|
||||
<h3>计算依据</h3>
|
||||
<div className="rectification-activity-receipt__method-groups">
|
||||
{methodGroups.map((group) => (
|
||||
<p key={group.label}>
|
||||
<strong>{group.label}:</strong>
|
||||
{group.methods.join("、")}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,10 @@ import {
|
||||
} from "@/lib/rectification-agentic/v9/public-receipt";
|
||||
import type { PublicLanguageModel } from "@/lib/public-models";
|
||||
import { ChatMessageRow } from "./chat-message-row";
|
||||
import {
|
||||
CompletedActivityReceipt,
|
||||
type CompletedActivityReceiptView,
|
||||
} from "./completed-activity-receipt";
|
||||
import { ModelSelector } from "./model-selector";
|
||||
import { Button } from "./ui/button";
|
||||
import { Textarea } from "./ui/textarea";
|
||||
@@ -59,55 +63,43 @@ type RectificationAgenticChatProps = Readonly<{
|
||||
|
||||
type RenderMessage = ChatMessageView & {
|
||||
renderKey: string;
|
||||
/** V9 execution-receipt steps; separate from shared consultation activity. */
|
||||
receiptActivity?: ReceiptActivity;
|
||||
receiptStatus?: string;
|
||||
activeActivity?: PublicActivity;
|
||||
completedReceipt?: CompletedActivityReceiptView;
|
||||
};
|
||||
|
||||
type ReceiptActivity = Readonly<{
|
||||
steps: readonly string[];
|
||||
methods: readonly string[];
|
||||
type PublicActivity = Readonly<{
|
||||
tool: PublicRectificationTool;
|
||||
label: string;
|
||||
}>;
|
||||
|
||||
const TOOL_LABELS: Readonly<Record<PublicRectificationTool, string>> = {
|
||||
"rectification-read-case": "读取校正记录",
|
||||
"rectification-propose-evidence": "整理事件证据",
|
||||
"rectification-confirm-evidence": "确认事件证据",
|
||||
"rectification-revise-evidence": "修订事件证据",
|
||||
"rectification-compare-candidates": "比较候选时间",
|
||||
"rectification-read-diagnostics": "检查候选稳健性",
|
||||
"rectification-offer-candidates": "生成候选建议",
|
||||
"rectification-accept-candidate": "采用候选时间",
|
||||
"rectification-confirm-birth-time": "确认校正时间",
|
||||
"rectification-close-case": "关闭校正记录",
|
||||
const ACTIVE_TOOL_LABELS: Readonly<Record<PublicRectificationTool, string>> = {
|
||||
"rectification-read-case": "正在读取校正记录…",
|
||||
"rectification-propose-evidence": "正在整理事件证据…",
|
||||
"rectification-confirm-evidence": "正在确认事件证据…",
|
||||
"rectification-revise-evidence": "正在修订事件证据…",
|
||||
"rectification-compare-candidates": "正在比较候选时间…",
|
||||
"rectification-read-diagnostics": "正在检查候选稳健性…",
|
||||
"rectification-offer-candidates": "正在生成候选建议…",
|
||||
"rectification-accept-candidate": "正在采用候选时间…",
|
||||
"rectification-confirm-birth-time": "正在确认校正时间…",
|
||||
"rectification-close-case": "正在完成校正记录…",
|
||||
};
|
||||
|
||||
const METHOD_LABELS: Readonly<Record<PublicRectificationMethod, string>> = {
|
||||
"d1-rashi": "D1 本命盘",
|
||||
"d2-hora": "D2 财帛分盘",
|
||||
"d4-chaturthamsha": "D4 地产迁移分盘",
|
||||
"d9-navamsa": "D9 婚姻分盘",
|
||||
"d10-dashamsa": "D10 事业分盘",
|
||||
"d11-labhamsha": "D11 收益分盘",
|
||||
"d24-chaturvimshamsha": "D24 教育分盘",
|
||||
"d30-trimshamsha": "D30 健康压力分盘",
|
||||
"vimshottari-dasha": "Vimshottari 大运",
|
||||
"narayana-dasha": "Narayana 大运",
|
||||
gochara: "Gochara 行运",
|
||||
ashtakavarga: "Ashtakavarga",
|
||||
shadbala: "Shadbala",
|
||||
"arudha-pada": "Arudha Pada",
|
||||
"functional-benefic-malefic": "功能吉凶星",
|
||||
};
|
||||
|
||||
function activityFromReceipt(receipt: PersistedTurn["receipt"]): ReceiptActivity {
|
||||
function completedReceiptFromPersisted(receipt: PersistedTurn["receipt"]): CompletedActivityReceiptView {
|
||||
if (!receipt) return { steps: [], methods: [] };
|
||||
return {
|
||||
steps: [...new Set((receipt.tools ?? []).filter(isPublicRectificationTool).map((tool) => TOOL_LABELS[tool]))],
|
||||
methods: [...new Set((receipt.methods ?? []).filter(isPublicRectificationMethod).map((method) => METHOD_LABELS[method]))],
|
||||
steps: [...new Set((receipt.tools ?? []).filter(isPublicRectificationTool))],
|
||||
methods: [...new Set((receipt.methods ?? []).filter(isPublicRectificationMethod))],
|
||||
};
|
||||
}
|
||||
|
||||
function activityPhase(tool: PublicRectificationTool): NonNullable<ChatMessageView["activity"]>["phase"] {
|
||||
if (tool === "rectification-compare-candidates" || tool === "rectification-read-diagnostics") {
|
||||
return "chart-calculation";
|
||||
}
|
||||
return "evidence-validation";
|
||||
}
|
||||
|
||||
function messagesFromTurns(initialTurns: readonly PersistedTurn[]): RenderMessage[] {
|
||||
return initialTurns.flatMap((turn, index): RenderMessage[] => {
|
||||
const key = `persisted-${turn.id}-${index}`;
|
||||
@@ -117,8 +109,7 @@ function messagesFromTurns(initialTurns: readonly PersistedTurn[]): RenderMessag
|
||||
text: turn.text ?? "",
|
||||
renderKey: key,
|
||||
state: turn.status === "completed" ? "settled" : "thinking",
|
||||
receiptActivity: activityFromReceipt(turn.receipt),
|
||||
receiptStatus: turn.receipt?.status,
|
||||
completedReceipt: completedReceiptFromPersisted(turn.receipt),
|
||||
}];
|
||||
}
|
||||
return [{
|
||||
@@ -218,14 +209,15 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
...(action === "message"
|
||||
? [{ role: "user", text: trimmed, renderKey: userRenderKey, state: "settled" } satisfies RenderMessage]
|
||||
: []),
|
||||
{ role: "assistant", text: "", renderKey: assistantRenderKey, state: "thinking", receiptActivity: { steps: [], methods: [] } },
|
||||
{ role: "assistant", text: "", renderKey: assistantRenderKey, state: "thinking" },
|
||||
]);
|
||||
setDraft("");
|
||||
|
||||
let raw = "";
|
||||
let liveActivity: ReceiptActivity = { steps: [], methods: [] };
|
||||
const activitySteps = new Set<string>();
|
||||
const activityMethods = new Set<string>();
|
||||
let completedReceipt: CompletedActivityReceiptView = { steps: [], methods: [] };
|
||||
const completedSteps = new Set<PublicRectificationTool>();
|
||||
const completedMethods = new Set<PublicRectificationMethod>();
|
||||
let failedTool: PublicRectificationTool | undefined;
|
||||
try {
|
||||
const response = await fetch("/api/rectification/agent", {
|
||||
method: "POST",
|
||||
@@ -274,9 +266,16 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
buffer = lines.pop() ?? "";
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
let event: { type?: unknown; text?: unknown; message?: unknown; tool?: unknown; methods?: unknown };
|
||||
let event: {
|
||||
type?: unknown;
|
||||
status?: unknown;
|
||||
text?: unknown;
|
||||
message?: unknown;
|
||||
tool?: unknown;
|
||||
methods?: unknown;
|
||||
};
|
||||
try {
|
||||
event = JSON.parse(line) as { type?: unknown; text?: unknown; message?: unknown; tool?: unknown; methods?: unknown };
|
||||
event = JSON.parse(line) as typeof event;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
@@ -285,7 +284,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
raw += event.text;
|
||||
const parsed = parseAgentReplyBody(raw);
|
||||
setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey
|
||||
? { ...message, text: parsed.text, state: "streaming" }
|
||||
? { ...message, text: parsed.text, state: "streaming", activeActivity: undefined }
|
||||
: message));
|
||||
} else if (event.type === "run.failed") {
|
||||
streamFailed = true;
|
||||
@@ -294,20 +293,37 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
setError(typeof event.message === "string" ? event.message : "生时校正暂时不可用,请稍后再试。");
|
||||
} else if (event.type === "run.completed") {
|
||||
completed = true;
|
||||
} else {
|
||||
} else if (event.type === "tool.activity") {
|
||||
const tool = isPublicRectificationTool(event.tool) ? event.tool : null;
|
||||
if (tool) activitySteps.add(TOOL_LABELS[tool]);
|
||||
if (Array.isArray(event.methods)) {
|
||||
for (const method of event.methods) {
|
||||
if (isPublicRectificationMethod(method)) activityMethods.add(METHOD_LABELS[method]);
|
||||
}
|
||||
}
|
||||
if (tool || activityMethods.size !== liveActivity.methods.length) {
|
||||
liveActivity = { steps: [...activitySteps], methods: [...activityMethods] };
|
||||
if (!tool) continue;
|
||||
if (event.status === "started") {
|
||||
const activeActivity = { tool, label: ACTIVE_TOOL_LABELS[tool] } satisfies PublicActivity;
|
||||
setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey
|
||||
? { ...message, receiptActivity: liveActivity }
|
||||
? { ...message, activeActivity }
|
||||
: message));
|
||||
continue;
|
||||
}
|
||||
if (event.status === "completed") {
|
||||
completedSteps.add(tool);
|
||||
if (Array.isArray(event.methods)) {
|
||||
for (const method of event.methods) {
|
||||
if (isPublicRectificationMethod(method)) completedMethods.add(method);
|
||||
}
|
||||
}
|
||||
} else if (event.status === "failed") {
|
||||
failedTool = tool;
|
||||
}
|
||||
completedReceipt = {
|
||||
steps: [...completedSteps],
|
||||
methods: [...completedMethods],
|
||||
...(failedTool ? { failedTool } : {}),
|
||||
};
|
||||
setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey
|
||||
? {
|
||||
...message,
|
||||
activeActivity: message.activeActivity?.tool === tool ? undefined : message.activeActivity,
|
||||
}
|
||||
: message));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,9 +332,20 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
const succeeded = completed && !streamFailed && Boolean(parsed.text);
|
||||
setMessages((current) => succeeded
|
||||
? current.map((message) => message.renderKey === assistantRenderKey
|
||||
? { ...message, text: parsed.text, state: "settled", receiptActivity: liveActivity }
|
||||
? {
|
||||
...message,
|
||||
text: parsed.text,
|
||||
state: "settled",
|
||||
activeActivity: undefined,
|
||||
completedReceipt,
|
||||
}
|
||||
: message)
|
||||
: current.filter((message) => message.renderKey !== assistantRenderKey));
|
||||
if (!succeeded && failedTool) {
|
||||
setError(failedTool === "rectification-compare-candidates"
|
||||
? "候选比较未完成,当前进度已保留。"
|
||||
: "本轮处理未完成,当前进度已保留。请稍后再试。");
|
||||
}
|
||||
if (succeeded) {
|
||||
onMessagesChange?.([
|
||||
...(action === "message" ? [{ role: "user" as const, text: trimmed }] : []),
|
||||
@@ -396,23 +423,18 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
)}
|
||||
{messages.map((message) => (
|
||||
<div key={message.renderKey} className="rectification-message-wrap">
|
||||
{message.receiptActivity && (message.receiptActivity.steps.length > 0 || message.receiptActivity.methods.length > 0) && (
|
||||
<details className="rectification-activity">
|
||||
<summary>本轮做了什么</summary>
|
||||
{message.receiptActivity.steps.length > 0 && (
|
||||
<ol>
|
||||
{message.receiptActivity.steps.map((step) => <li key={step}>{step}</li>)}
|
||||
</ol>
|
||||
)}
|
||||
{message.receiptActivity.methods.length > 0 && (
|
||||
<div className="rectification-activity-methods">
|
||||
<span>使用技法</span>
|
||||
<ul>{message.receiptActivity.methods.map((method) => <li key={method}>{method}</li>)}</ul>
|
||||
</div>
|
||||
)}
|
||||
</details>
|
||||
<ChatMessageRow
|
||||
message={{
|
||||
...message,
|
||||
activity: message.activeActivity
|
||||
? { phase: activityPhase(message.activeActivity.tool), label: message.activeActivity.label }
|
||||
: undefined,
|
||||
}}
|
||||
showActivity={message.state === "thinking" || Boolean(message.activeActivity)}
|
||||
/>
|
||||
{message.state === "settled" && message.completedReceipt && (
|
||||
<CompletedActivityReceipt receipt={message.completedReceipt} />
|
||||
)}
|
||||
<ChatMessageRow message={message} />
|
||||
</div>
|
||||
))}
|
||||
{candidateResult?.selectionAllowed && candidateResult.candidates.length > 0 && (
|
||||
@@ -441,7 +463,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
</span>
|
||||
<span className="rectification-candidate-support">相对支持度 {candidate.relativeSupport}</span>
|
||||
<span className="rectification-candidate-action">
|
||||
{selected ? "已采用" : acceptingTime === candidate.time ? "保存中…" : candidateResult.selectedTime ? "改选为此时间" : "采用此时间"}
|
||||
{selected ? "已采用" : acceptingTime === candidate.time ? "正在采用…" : candidateResult.selectedTime ? "改选为此时间" : "采用此时间"}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user