fix(rectification): render candidates and keep agent replies natural
This commit is contained in:
@@ -2,8 +2,13 @@
|
||||
|
||||
import { ArrowUp } from "lucide-react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { parseAgentReply } from "@/lib/agent-reply";
|
||||
import { parseAgentReplyBody } from "@/lib/agent-reply";
|
||||
import type { ChatMessage, ChatMessageView } from "@/lib/chat-message-view";
|
||||
import {
|
||||
isRecommendedRectificationCandidate,
|
||||
parseRectificationCandidateResult,
|
||||
type RectificationCandidateResult,
|
||||
} from "@/lib/rectification-candidate-result";
|
||||
import { membershipHref } from "@/lib/membership";
|
||||
import {
|
||||
isPublicRectificationMethod,
|
||||
@@ -32,16 +37,7 @@ type PersistedTurn = Readonly<{
|
||||
}> | null;
|
||||
}>;
|
||||
|
||||
type CandidateResult = Readonly<{
|
||||
resultId: string;
|
||||
candidates: readonly Readonly<{ rank: number; time: string; relative_support: number; tied_minute_count: number }>[];
|
||||
overallConfidence: "low" | "medium" | "high";
|
||||
selectionAllowed: boolean;
|
||||
confirmationAllowed: boolean;
|
||||
representativeTime: string | null;
|
||||
selectedTime: string | null;
|
||||
selectionKind: string | null;
|
||||
}> | null;
|
||||
type CandidateResult = RectificationCandidateResult | null;
|
||||
|
||||
type RectificationAgenticChatProps = Readonly<{
|
||||
caseId: string;
|
||||
@@ -161,7 +157,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
const [savedStatus, setSavedStatus] = useState<"accepted" | "confirmed" | null>(null);
|
||||
const [candidateResult, setCandidateResult] = useState<CandidateResult>(null);
|
||||
const [acceptingTime, setAcceptingTime] = useState<string | null>(null);
|
||||
const [suggestions, setSuggestions] = useState<string[]>([]);
|
||||
const conversation = useRef<HTMLElement>(null);
|
||||
const composer = useRef<HTMLTextAreaElement>(null);
|
||||
const keyCounter = useRef(0);
|
||||
@@ -192,18 +187,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
);
|
||||
if (!response.ok) return null;
|
||||
const payload = await response.json().catch(() => null);
|
||||
const latest = payload?.latest_result;
|
||||
if (!latest || typeof latest.result_id !== "string") return null;
|
||||
return {
|
||||
resultId: latest.result_id,
|
||||
candidates: Array.isArray(latest.candidates) ? latest.candidates : [],
|
||||
overallConfidence: latest.overall_confidence === "high" || latest.overall_confidence === "medium" ? latest.overall_confidence : "low",
|
||||
selectionAllowed: latest.selection_allowed === true,
|
||||
confirmationAllowed: latest.confirmation_allowed === true,
|
||||
representativeTime: typeof latest.representative_time === "string" ? latest.representative_time : null,
|
||||
selectedTime: typeof latest.selected_time === "string" ? latest.selected_time : null,
|
||||
selectionKind: typeof latest.selection_kind === "string" ? latest.selection_kind : null,
|
||||
};
|
||||
return parseRectificationCandidateResult(payload?.latest_result);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -221,7 +205,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
const trimmed = action === "message" ? messageText.trim() : "";
|
||||
if ((action === "message" && !trimmed) || busy || readonly) return;
|
||||
setError("");
|
||||
setSuggestions([]);
|
||||
setPending(true);
|
||||
|
||||
keyCounter.current += 1;
|
||||
@@ -300,11 +283,10 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
if (typeof event.type !== "string") continue;
|
||||
if (event.type === "answer.delta" && typeof event.text === "string") {
|
||||
raw += event.text;
|
||||
const parsed = parseAgentReply(raw, "general");
|
||||
const parsed = parseAgentReplyBody(raw);
|
||||
setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey
|
||||
? { ...message, text: parsed.text, state: "streaming" }
|
||||
: message));
|
||||
setSuggestions(parsed.suggestions);
|
||||
} else if (event.type === "run.failed") {
|
||||
streamFailed = true;
|
||||
} else if (event.type === "error") {
|
||||
@@ -330,18 +312,17 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = parseAgentReply(raw, "general");
|
||||
const parsed = parseAgentReplyBody(raw);
|
||||
const succeeded = completed && !streamFailed && Boolean(parsed.text);
|
||||
setMessages((current) => succeeded
|
||||
? current.map((message) => message.renderKey === assistantRenderKey
|
||||
? { ...message, text: parsed.text, suggestions: parsed.suggestions, state: "settled", receiptActivity: liveActivity }
|
||||
? { ...message, text: parsed.text, state: "settled", receiptActivity: liveActivity }
|
||||
: message)
|
||||
: current.filter((message) => message.renderKey !== assistantRenderKey));
|
||||
setSuggestions(succeeded ? parsed.suggestions : []);
|
||||
if (succeeded) {
|
||||
onMessagesChange?.([
|
||||
...(action === "message" ? [{ role: "user" as const, text: trimmed }] : []),
|
||||
{ role: "assistant", text: parsed.text, suggestions: parsed.suggestions },
|
||||
{ role: "assistant", text: parsed.text },
|
||||
]);
|
||||
onCompleted?.();
|
||||
await loadCandidate().then((result) => {
|
||||
@@ -444,7 +425,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
<div className="rectification-candidate-list">
|
||||
{candidateResult.candidates.map((candidate) => {
|
||||
const selected = candidateResult.selectedTime === candidate.time;
|
||||
const recommended = !candidateResult.selectedTime && candidate.rank === 1;
|
||||
const recommended = isRecommendedRectificationCandidate(candidateResult, candidate);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
@@ -458,7 +439,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
{selected && <span className="rectification-candidate-badge">已采用</span>}
|
||||
{recommended && <span className="rectification-candidate-badge">当前推荐</span>}
|
||||
</span>
|
||||
<span className="rectification-candidate-support">相对支持度 {candidate.relative_support}%</span>
|
||||
<span className="rectification-candidate-support">相对支持度 {candidate.relativeSupport}</span>
|
||||
<span className="rectification-candidate-action">
|
||||
{selected ? "已采用" : acceptingTime === candidate.time ? "保存中…" : candidateResult.selectedTime ? "改选为此时间" : "采用此时间"}
|
||||
</span>
|
||||
@@ -484,14 +465,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
</section>
|
||||
|
||||
<div className="composer-wrap">
|
||||
{suggestions.length > 0 && !busy && (
|
||||
<div className="composer-suggestions" aria-label="推荐继续提问">
|
||||
{suggestions.map((question) => (
|
||||
<button key={question} type="button" onClick={() => void send("message", question)}>{question}</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form className="composer" onSubmit={submit}>
|
||||
<Textarea
|
||||
ref={composer}
|
||||
|
||||
Reference in New Issue
Block a user