feat: polish intelligent answer interactions

This commit is contained in:
2026-10-29 18:39:50 +08:00
parent 9474788db6
commit 1ee21d1c7f
26 changed files with 1762 additions and 327 deletions
@@ -0,0 +1,72 @@
import { useRef, type RefObject } from "react";
import { ChartNoAxesCombined, ClipboardPenLine, Gauge, MapPin, BookOpenCheck, Table2, ListTree, ChevronDown, CircleCheck } from "lucide-react";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/shared/ui/dropdown-menu";
import type { AnswerUiBlock } from "./contract";
const contentKinds = [
{ type: "Result", label: "结果", icon: CircleCheck, selector: "[data-answer-result]" },
{ type: "Metric", label: "指标", icon: Gauge, selector: ".answer-ui-metric" },
{ type: "Chart", label: "趋势", icon: ChartNoAxesCombined, selector: ".answer-ui-chart" },
{ type: "Table", label: "表格", icon: Table2, selector: "[data-answer-table]" },
{ type: "Evidence", label: "依据", icon: BookOpenCheck, selector: ".answer-ui-evidence" },
{ type: "MapLocation", label: "地图", icon: MapPin, selector: ".answer-ui-location" },
{ type: "FollowUpForm", label: "补充资料", icon: ClipboardPenLine, selector: ".answer-ui-form" }
] as const;
/** Navigate the mounted answer so chart selections and unsent forms survive. */
export function AnswerContentNav({ block, contentRef }: {
block: AnswerUiBlock;
contentRef: RefObject<HTMLDivElement | null>;
}) {
const navigationTarget = useRef<HTMLElement | null>(null);
const elements = Object.values(block.spec.elements);
const items = contentKinds.map((kind) => ({
...kind, count: elements.filter((element) => element.type === kind.type).length
})).filter((item) => item.count > 0);
if (items.length < 2) return null;
return (
<nav aria-label="回答内容导航" className="answer-content-nav">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="button" variant="ghost" size="sm" className="h-9 gap-1.5 px-2 text-xs font-normal">
<ListTree size={14} aria-hidden />浏览回答<ChevronDown size={12} aria-hidden />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" data-agent-theme="chat" className="min-w-48"
onCloseAutoFocus={(event) => {
const target = navigationTarget.current;
navigationTarget.current = null;
if (!target?.isConnected) return;
event.preventDefault();
target.tabIndex = -1;
target.focus({ preventScroll: true });
target.scrollIntoView({ block: "start", behavior: "instant" });
}}>
{items.map(({ type, label, icon: Icon, selector, count }) => (
<DropdownMenuItem key={type} className="min-h-10 gap-2 text-xs"
aria-label={`查看${label},${count}项`}
onSelect={() => {
const target = contentRef.current?.querySelector<HTMLElement>(selector) ?? null;
const section = target?.closest(".answer-task-support-section[data-state=closed]");
section?.querySelector<HTMLButtonElement>(":scope > button")?.click();
navigationTarget.current = target;
}}>
<Icon size={14} aria-hidden />{label}
<Badge variant="secondary" className="ml-auto min-w-4 justify-center rounded-sm border-0 px-1 py-0 text-[10px] tabular-nums">
{count}
</Badge>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<span className="text-[11px] text-muted-foreground">
{items.map((item, index) => <span key={item.type} className="inline-block whitespace-nowrap">
{index > 0 ? <span aria-hidden> · </span> : null}{item.label}
</span>)}
</span>
</nav>
);
}