Files
next-tjwater-frontend/src/features/agent/answer-ui/answer-content-nav.tsx
T
jiang c0e4231b83
Generic Container CI/CD / test-build-publish (push) Successful in 1m52s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 1m52s
feat: expand intelligent investigation workspace
2026-10-30 17:30:19 +08:00

74 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}