import { useEffect, useState } from "react"; import type { FC } from "react"; import { Modal, RadioGroup, Radio, Select, Toast } from "@douyinfe/semi-ui"; import type { RadioChangeEvent } from "@douyinfe/semi-ui/lib/es/radio"; import { traceApi } from "../../api/traceApi"; import type { KnowledgeLogEntry, FeedbackAction, KnowledgeFeedbackState } from "../../types/goal"; import type { KnowledgeFeedbackListItem } from "../../api/traceApi"; import styles from "./KnowledgeFeedbackModal.module.css"; interface KnowledgeFeedbackModalProps { visible: boolean; traceId: string; onClose: () => void; onSubmitted?: () => void; onNavigateToMessage?: (goalId: string, sequence: number) => void; } const EVAL_STATUS_OPTIONS = [ { value: "helpful", label: "✅ helpful(有效)" }, { value: "harmful", label: "🚫 harmful(有害)" }, { value: "unused", label: "⬜ unused(未使用)" }, { value: "irrelevant", label: "⚠️ irrelevant(无关)" }, { value: "neutral", label: "➖ neutral(中性)" }, ]; const EVAL_STATUS_BADGE: Record = { helpful: { label: "helpful", cls: styles.badgeHelpful }, harmful: { label: "harmful", cls: styles.badgeHarmful }, unused: { label: "unused", cls: styles.badgeNeutral }, irrelevant: { label: "irrelevant", cls: styles.badgeNeutral }, neutral: { label: "neutral", cls: styles.badgeNeutral }, }; export const KnowledgeFeedbackModal: FC = ({ visible, traceId, onClose, onSubmitted, onNavigateToMessage, }) => { const [entries, setEntries] = useState([]); const [feedbackMap, setFeedbackMap] = useState>({}); const [isLoading, setIsLoading] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [expandedIds, setExpandedIds] = useState>(new Set()); // 加载 knowledge_log,并去重(同一 knowledge_id 取最新注入) useEffect(() => { if (!visible || !traceId) return; setIsLoading(true); traceApi .fetchKnowledgeLog(traceId) .then((log) => { // 去重:同一 knowledge_id 保留最新注入的条目 const seen = new Map(); for (const entry of log.entries) { const existing = seen.get(entry.knowledge_id); if (!existing || entry.injected_at_sequence > existing.injected_at_sequence) { seen.set(entry.knowledge_id, entry); } } const deduped = Array.from(seen.values()); setEntries(deduped); // 默认所有条目选「跳过」 const initial: Record = {}; deduped.forEach((e) => { // 若已有用户反馈,恢复之前的选择 if (e.user_feedback) { initial[e.knowledge_id] = { action: e.user_feedback.action as FeedbackAction, eval_status: e.user_feedback.eval_status, feedback_text: e.user_feedback.feedback_text, }; } else { initial[e.knowledge_id] = { action: "skip" }; } }); setFeedbackMap(initial); }) .catch(() => { Toast.error("加载知识日志失败"); }) .finally(() => setIsLoading(false)); }, [visible, traceId]); const toggleExpand = (id: string) => { setExpandedIds((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; const setFeedback = (id: string, patch: Partial) => { setFeedbackMap((prev) => ({ ...prev, [id]: { ...prev[id], ...patch }, })); }; const confirmedCount = Object.values(feedbackMap).filter( (f) => f.action !== "skip" ).length; const handleSubmit = async () => { setIsSubmitting(true); const feedbackList: KnowledgeFeedbackListItem[] = entries .filter((e) => feedbackMap[e.knowledge_id]?.action !== "skip") .map((e) => ({ knowledge_id: e.knowledge_id, action: feedbackMap[e.knowledge_id].action, eval_status: feedbackMap[e.knowledge_id].eval_status, feedback_text: feedbackMap[e.knowledge_id].feedback_text, source: { trace_id: traceId, goal_id: e.goal_id, sequence: e.injected_at_sequence, feedback_by: "user", feedback_at: new Date().toISOString(), }, })); try { await traceApi.submitKnowledgeFeedback(traceId, { feedback_list: feedbackList }); localStorage.setItem(`knowledge_feedback_submitted_${traceId}`, "true"); Toast.success(`反馈提交成功,共反馈 ${feedbackList.length} 条知识`); onSubmitted?.(); onClose(); } catch { Toast.error("提交失败,请重试"); } finally { setIsSubmitting(false); } }; const footer = (
); return ( {isLoading ? (
加载中...
) : entries.length === 0 ? (
本次 Trace 暂无知识注入记录
) : ( <>
💡 提示:只需对有信心的知识做反馈,不确定的可以跳过(默认)
{entries.map((entry) => { const fb = feedbackMap[entry.knowledge_id] ?? { action: "skip" }; const isExpanded = expandedIds.has(entry.knowledge_id); const evalBadge = entry.eval_result?.eval_status ? EVAL_STATUS_BADGE[entry.eval_result.eval_status] : null; return (
{/* 卡片头部 */}
{entry.knowledge_id.slice(0, 28)}… { onNavigateToMessage(entry.goal_id, entry.injected_at_sequence); onClose(); } : undefined } > Goal {entry.goal_id} {" · "} seq {entry.injected_at_sequence} {evalBadge && ( Agent: {evalBadge.label} )} {!entry.eval_result && ( 未评估 )}
{/* 任务场景 */}
{entry.task}
{/* Agent 评估详情 */}
{entry.eval_result ? ( <> {typeof entry.eval_result.score === "number" && ( 评分 {entry.eval_result.score}/5 )} {entry.eval_result.reason ? (

{entry.eval_result.reason}

) : ( (无评估说明) )} ) : ( Agent 暂未评估此知识 )}
{/* 知识内容(可展开) */}
toggleExpand(entry.knowledge_id)} > {isExpanded ? "▲ 收起内容" : "▼ 展开内容"} {isExpanded && (
{entry.content}
)}
{/* 用户反馈选择 */}
您的反馈: setFeedback(entry.knowledge_id, { action: e.target.value as FeedbackAction }) } direction="horizontal" > 跳过(不确定) 同意 Agent 评估 不同意,重新评估
{/* 重新评估子表单 */} {fb.action === "override" && (
评估结果: value={fb.eval_status} onChange={(value) => { if (typeof value === "string") { setFeedback(entry.knowledge_id, { eval_status: value }); } }} optionList={EVAL_STATUS_OPTIONS} style={{ width: 220 }} placeholder="请选择评估结果" />
说明(可选):