fix: 表格的复制按钮被禁用的问题
This commit is contained in:
parent
c52b505354
commit
a62e65acfe
|
|
@ -1,14 +1,20 @@
|
|||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import type { AnchorHTMLAttributes } from "react";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { useCallback, useMemo, useState, type MouseEvent } from "react";
|
||||
import type {
|
||||
AnchorHTMLAttributes,
|
||||
ComponentPropsWithoutRef,
|
||||
ReactNode,
|
||||
} from "react";
|
||||
|
||||
import {
|
||||
MessageResponse,
|
||||
type MessageResponseProps,
|
||||
} from "@/components/ai-elements/message";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { streamdownPlugins } from "@/core/streamdown";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn, copyToClipboard } from "@/lib/utils";
|
||||
|
||||
import { CitationLink } from "../citations/citation-link";
|
||||
|
||||
|
|
@ -25,6 +31,89 @@ export type MarkdownContentProps = {
|
|||
components?: MessageResponseProps["components"];
|
||||
};
|
||||
|
||||
type TableData = {
|
||||
headers: string[];
|
||||
rows: string[][];
|
||||
};
|
||||
|
||||
function parseTableData(table: HTMLTableElement): TableData {
|
||||
const headers = Array.from(table.querySelectorAll("thead th")).map((cell) =>
|
||||
(cell.textContent ?? "").trim(),
|
||||
);
|
||||
const rows = Array.from(table.querySelectorAll("tbody tr")).map((row) =>
|
||||
Array.from(row.querySelectorAll("td")).map(
|
||||
(cell) => (cell.textContent ?? "").trim(),
|
||||
),
|
||||
);
|
||||
return { headers, rows };
|
||||
}
|
||||
|
||||
function toMarkdownTable(data: TableData): string {
|
||||
if (data.headers.length === 0) return "";
|
||||
const headerLine = `| ${data.headers.join(" | ")} |`;
|
||||
const dividerLine = `| ${data.headers.map(() => "---").join(" | ")} |`;
|
||||
const rowLines = data.rows.map((row) => `| ${row.join(" | ")} |`);
|
||||
return [headerLine, dividerLine, ...rowLines].join("\n");
|
||||
}
|
||||
|
||||
function MarkdownTable({
|
||||
className,
|
||||
children,
|
||||
isLoading,
|
||||
copyLabel,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<"table"> & {
|
||||
isLoading: boolean;
|
||||
copyLabel: string;
|
||||
}) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = useCallback(
|
||||
async (event: MouseEvent<HTMLButtonElement>) => {
|
||||
const wrapper = event.currentTarget.closest('[data-streamdown="table-wrapper"]');
|
||||
const table = wrapper?.querySelector("table");
|
||||
if (!(table instanceof HTMLTableElement)) return;
|
||||
|
||||
const markdown = toMarkdownTable(parseTableData(table));
|
||||
if (!markdown) return;
|
||||
|
||||
try {
|
||||
await copyToClipboard(markdown);
|
||||
setCopied(true);
|
||||
window.setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="my-4 flex flex-col space-y-2" data-streamdown="table-wrapper">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<button
|
||||
className="cursor-pointer p-1 text-muted-foreground transition-all hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={isLoading}
|
||||
onClick={handleCopy}
|
||||
title={copyLabel}
|
||||
type="button"
|
||||
>
|
||||
{copied ? <CheckIcon size={14} /> : <CopyIcon size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table
|
||||
className={cn("w-full border-collapse border border-border", className)}
|
||||
data-streamdown="table"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Renders markdown content. */
|
||||
export function MarkdownContent({
|
||||
content,
|
||||
|
|
@ -34,6 +123,8 @@ export function MarkdownContent({
|
|||
remarkPlugins = streamdownPlugins.remarkPlugins,
|
||||
components: componentsFromProps,
|
||||
}: MarkdownContentProps) {
|
||||
const { t } = useI18n();
|
||||
|
||||
const components = useMemo(() => {
|
||||
return {
|
||||
a: (props: AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
|
|
@ -58,9 +149,23 @@ export function MarkdownContent({
|
|||
/>
|
||||
);
|
||||
},
|
||||
table: ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<"table"> & { children?: ReactNode }) => (
|
||||
<MarkdownTable
|
||||
className={className}
|
||||
copyLabel={t.clipboard.copyToClipboard}
|
||||
isLoading={isLoading}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MarkdownTable>
|
||||
),
|
||||
...componentsFromProps,
|
||||
};
|
||||
}, [componentsFromProps]);
|
||||
}, [componentsFromProps, isLoading, t.clipboard.copyToClipboard]);
|
||||
|
||||
if (!content) return null;
|
||||
|
||||
|
|
@ -68,6 +173,7 @@ export function MarkdownContent({
|
|||
<MessageResponse
|
||||
className={className}
|
||||
isAnimating={isLoading}
|
||||
controls={{ table: false }}
|
||||
parseIncompleteMarkdown={!isLoading}
|
||||
remarkPlugins={remarkPlugins}
|
||||
rehypePlugins={rehypePlugins}
|
||||
|
|
|
|||
Loading…
Reference in New Issue