feat: polish intelligent answer interactions
This commit is contained in:
@@ -1,18 +1,28 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Checkbox } from "@/shared/ui/checkbox";
|
||||
import { cn } from "@/shared/ui/cn";
|
||||
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/shared/ui/table";
|
||||
import { AskAboutContent } from "./answer-ui-interactions";
|
||||
import { AnswerReveal } from "./answer-motion";
|
||||
import { AskAboutContent, SelectForAnalysis } from "./answer-ui-interactions";
|
||||
|
||||
export function InteractiveDataTable({ title, columns, rows, source }: {
|
||||
import { useAnswerNodeState } from "./answer-view-state-provider";
|
||||
import type { AnswerTableState } from "./answer-view-state";
|
||||
|
||||
export function InteractiveDataTable({ title, columns, rows, source, state: controlledState, onStateChange }: {
|
||||
title: string; columns: string[]; rows: string[][]; source?: string;
|
||||
state?: Omit<AnswerTableState, "type">;
|
||||
onStateChange?: (patch: Partial<Omit<AnswerTableState, "type">>) => void;
|
||||
}) {
|
||||
const [query, setQuery] = useState("");
|
||||
const [sort, setSort] = useState<{ column: number; descending: boolean } | null>(null);
|
||||
const [selection, setSelection] = useState<number[]>([]);
|
||||
const [localState, patchLocal] = useAnswerNodeState<AnswerTableState>("Table", { title, columns, rows });
|
||||
const state = controlledState ?? localState;
|
||||
const patch = onStateChange ?? patchLocal;
|
||||
// The owning schema node migrates data changes. A complete restored snapshot
|
||||
// can change chart columns/range and restore a valid selection atomically.
|
||||
const { query, selection, sort } = state;
|
||||
const setSelection = (selection: number[]) => patch({ selection });
|
||||
const filtered = useMemo(() => {
|
||||
const items = rows.map((cells, id) => ({ cells, id }))
|
||||
.filter(({ cells }) => cells.join(" ").toLocaleLowerCase().includes(query.trim().toLocaleLowerCase()));
|
||||
@@ -30,7 +40,7 @@ export function InteractiveDataTable({ title, columns, rows, source }: {
|
||||
return <div className="min-w-0 space-y-2" data-interactive-table>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Input aria-label={`搜索${title}`} placeholder="搜索当前数据" value={query} className="h-9 min-w-0 flex-1 text-xs"
|
||||
onChange={(event) => { setQuery(event.currentTarget.value); setSelection([]); }} />
|
||||
onChange={(event) => { patch({ query: event.currentTarget.value, selection: [] }); }} />
|
||||
<span className="text-[11px] tabular-nums text-muted-foreground">{filtered.length} / {rows.length} 行</span>
|
||||
</div>
|
||||
<div role="region" aria-label={title} tabIndex={0}
|
||||
@@ -38,21 +48,23 @@ export function InteractiveDataTable({ title, columns, rows, source }: {
|
||||
<Table>
|
||||
<TableCaption className="caption-top font-medium">{title}</TableCaption>
|
||||
<TableHeader className="bg-muted/50"><TableRow>
|
||||
<TableHead scope="col" className="w-10"><span className="sr-only">选择行</span></TableHead>
|
||||
<TableHead scope="col" className="w-10"><Checkbox aria-label={`选择${title}的全部筛选结果`}
|
||||
checked={filtered.length > 0 && selected.length === Math.min(filtered.length, 100)} disabled={!filtered.length}
|
||||
onChange={(event) => setSelection(event.target.checked ? filtered.slice(0, 100).map(({ id }) => id) : [])} /></TableHead>
|
||||
{columns.map((column, index) => <TableHead key={index} scope="col" className="whitespace-nowrap"
|
||||
aria-sort={sort?.column === index ? sort.descending ? "descending" : "ascending" : "none"}>
|
||||
<Button size="sm" variant="ghost" className="h-9 gap-1 px-1 text-xs" aria-label={`按${column}排序`}
|
||||
onClick={() => setSort((current) => current?.column === index
|
||||
? current.descending ? null : { column: index, descending: true }
|
||||
: { column: index, descending: false })}>
|
||||
onClick={() => patch({ sort: sort?.column === index
|
||||
? sort.descending ? null : { column: index, descending: true }
|
||||
: { column: index, descending: false } })}>
|
||||
{column}{sort?.column === index ? sort.descending ? <ArrowDown size={12} /> : <ArrowUp size={12} /> : <ArrowUpDown size={12} />}
|
||||
</Button>
|
||||
</TableHead>)}
|
||||
</TableRow></TableHeader>
|
||||
<TableBody>
|
||||
{filtered.map(({ cells, id }) => <TableRow key={id} data-state={selection.includes(id) ? "selected" : undefined}>
|
||||
<TableCell className="w-10"><Checkbox aria-label={`选择第 ${id + 1} 行`} checked={selection.includes(id)}
|
||||
onChange={(event) => setSelection((current) => event.target.checked ? [...current, id] : current.filter((value) => value !== id))} /></TableCell>
|
||||
<TableCell className="w-10"><Checkbox aria-label={`选择第 ${id + 1} 行`} checked={selection.includes(id)} disabled={!selection.includes(id) && selection.length >= 100}
|
||||
onChange={(event) => setSelection(event.target.checked ? [...selection, id] : selection.filter((value) => value !== id))} /></TableCell>
|
||||
{cells.map((cell, index) => <TableCell key={index}
|
||||
className={cn("min-w-24", /^[-+]?\d+(\.\d+)?%?$/.test(cell) && "text-right tabular-nums")}>{cell}</TableCell>)}
|
||||
</TableRow>)}
|
||||
@@ -62,9 +74,14 @@ export function InteractiveDataTable({ title, columns, rows, source }: {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
{selected.length ? <div className="flex flex-wrap items-center justify-between gap-1">
|
||||
<span className="text-xs text-muted-foreground">已选 {selected.length} 行</span>
|
||||
<AskAboutContent title={title} text={`${quote}${source ? `\n来源:${source}` : ""}`} />
|
||||
</div> : <p className="text-[11px] text-muted-foreground">筛选与排序仅作用于已加载数据;勾选行可继续提问。</p>}
|
||||
<AnswerReveal open={selected.length > 0} className="flex flex-wrap items-center justify-between gap-1">
|
||||
<span className="text-xs text-muted-foreground">已选 {selected.length} 行{filtered.length > 100 ? " · 每次最多选择前 100 行" : ""}</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<Button type="button" variant="ghost" size="sm" className="h-9 text-xs" onClick={() => setSelection([])}>取消选行</Button>
|
||||
<SelectForAnalysis label="将所选行加入分析" title={title} text={`${quote}${source ? `\n来源:${source}` : ""}`} />
|
||||
<AskAboutContent title={title} text={`${quote}${source ? `\n来源:${source}` : ""}`} />
|
||||
</div>
|
||||
</AnswerReveal>
|
||||
{!selected.length && filtered.length ? <p className="text-[11px] text-muted-foreground">勾选数据行可加入分析或继续提问。</p> : null}
|
||||
</div>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user