74 lines
3.8 KiB
TypeScript
74 lines
3.8 KiB
TypeScript
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";
|
||
import { canExpandAnswer } from "./answer-presentation";
|
||
|
||
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 || !canExpandAnswer([{ type: "answer-ui", block }])) 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>
|
||
);
|
||
}
|