feat: polish intelligent answer interactions
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user