fix: restore rectification agent message actions

This commit is contained in:
Jesse_Chen
2026-07-30 00:41:48 +08:00
parent a5f409a500
commit 0a5b8cd239
15 changed files with 513 additions and 4 deletions
@@ -1,6 +1,6 @@
"use client";
import { ArrowUp } from "lucide-react";
import { ArrowUp, Check, Copy, RotateCcw, ThumbsDown, ThumbsUp } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { useRectificationV4 } from "@/hooks/use-rectification-v4";
import type { ChatMessageView } from "@/lib/chat-message-view";
@@ -29,6 +29,28 @@ type RectificationV4PanelProps = Readonly<{
onContinueOriginalQuestion?: (continuation: RectificationV4Continuation) => void;
}>;
export function toggleRectificationFeedback(
current: "up" | "down" | undefined,
requested: "up" | "down",
): "up" | "down" | undefined {
return current === requested ? undefined : requested;
}
export function canRegenerateRectificationMessage(input: Readonly<{
message: ChatMessageView;
currentMessageKey: string | null;
deploymentMode: RectificationV4ApiResponse["case"]["deploymentMode"] | null;
busy: boolean;
canAnswer: boolean;
}>): boolean {
return input.deploymentMode === "v5_agent"
&& input.message.role === "assistant"
&& input.message.state === "settled"
&& input.message.renderKey === input.currentMessageKey
&& !input.busy
&& input.canAnswer;
}
export function rectificationV4ChatMessages(
data: RectificationV4ApiResponse | null,
processing: boolean,
@@ -129,6 +151,9 @@ export function RectificationV4Panel(props: RectificationV4PanelProps) {
onPendingChange: props.onPendingChange,
});
const [draft, setDraft] = useState("");
const [feedback, setFeedback] = useState<Record<string, "up" | "down" | undefined>>({});
const [copiedMessageKey, setCopiedMessageKey] = useState<string | null>(null);
const [regeneratingMessageKey, setRegeneratingMessageKey] = useState<string | null>(null);
const composer = useRef<HTMLTextAreaElement>(null);
const conversationEnd = useRef<HTMLDivElement>(null);
const caseValue = controller.data?.case;
@@ -145,6 +170,10 @@ export function RectificationV4Panel(props: RectificationV4PanelProps) {
&& !processing
&& !controller.pending
&& ["awaiting_answer", "range_ready"].includes(caseValue?.status ?? "");
const currentMessageKey = caseValue?.currentQuestion
? `rectification-current-${caseValue.currentQuestion.id}`
: null;
const busy = processing || controller.pending || regeneratingMessageKey !== null;
const canAcceptRange = caseValue?.status === "range_ready"
&& Boolean(caseValue.latestSnapshot?.canAcceptRange)
&& !caseValue.acceptedRange;
@@ -174,6 +203,27 @@ export function RectificationV4Panel(props: RectificationV4PanelProps) {
if (result) setDraft("");
}
async function copyMessage(message: ChatMessageView) {
try {
await navigator.clipboard.writeText(message.text);
setCopiedMessageKey(message.renderKey);
window.setTimeout(() => setCopiedMessageKey((current) => (
current === message.renderKey ? null : current
)), 1_500);
} catch {
// Clipboard permission failures must not interrupt the conversation.
}
}
async function regenerateMessage(messageKey: string) {
setRegeneratingMessageKey(messageKey);
try {
await controller.regenerate();
} finally {
setRegeneratingMessageKey((current) => current === messageKey ? null : current);
}
}
function continueOriginalQuestion() {
if (!caseValue?.acceptedRange || !handoff) return;
props.onContinueOriginalQuestion?.({
@@ -189,7 +239,71 @@ export function RectificationV4Panel(props: RectificationV4PanelProps) {
<>
<section className="conversation" aria-label="生时校正对话" aria-busy={processing || controller.pending}>
<div className="message-list" aria-live="polite">
{messages.map((message) => <ChatMessageRow key={message.renderKey} message={message} />)}
{messages.map((message) => {
const showActions = caseValue?.deploymentMode === "v5_agent"
&& message.role === "assistant"
&& message.state === "settled"
&& Boolean(message.text);
const regenerating = regeneratingMessageKey === message.renderKey;
const canRegenerate = canRegenerateRectificationMessage({
message,
currentMessageKey,
deploymentMode: caseValue?.deploymentMode ?? null,
busy,
canAnswer,
});
return (
<div className="rectification-message-entry" key={message.renderKey}>
<ChatMessageRow message={regenerating
? { ...message, text: "", state: "thinking" }
: message} />
{showActions && !regenerating && (
<div className="rectification-message-actions" aria-label="Agent 回答操作">
<button
aria-label="赞"
aria-pressed={feedback[message.renderKey] === "up"}
className={feedback[message.renderKey] === "up" ? "is-active" : ""}
title="赞"
type="button"
onClick={() => setFeedback((current) => ({
...current,
[message.renderKey]: toggleRectificationFeedback(current[message.renderKey], "up"),
}))}
>
<ThumbsUp aria-hidden="true" />
</button>
<button
aria-label="踩"
aria-pressed={feedback[message.renderKey] === "down"}
className={feedback[message.renderKey] === "down" ? "is-active" : ""}
title="踩"
type="button"
onClick={() => setFeedback((current) => ({
...current,
[message.renderKey]: toggleRectificationFeedback(current[message.renderKey], "down"),
}))}
>
<ThumbsDown aria-hidden="true" />
</button>
<button aria-label="复制回答" title="复制" type="button" onClick={() => void copyMessage(message)}>
{copiedMessageKey === message.renderKey
? <Check aria-hidden="true" />
: <Copy aria-hidden="true" />}
</button>
<button
aria-label="重新生成回答"
disabled={!canRegenerate}
title={canRegenerate ? "重新生成" : "只能重新生成当前问题"}
type="button"
onClick={() => void regenerateMessage(message.renderKey)}
>
<RotateCcw aria-hidden="true" />
</button>
</div>
)}
</div>
);
})}
{controller.error && <p className="error-message" role="alert">{controller.error}</p>}
<div ref={conversationEnd} />
</div>