Fix report PDF placement and table scrolling

This commit is contained in:
134 2026-06-24 18:51:26 +08:00
parent 57f4d08c31
commit cd90c8074c
3 changed files with 137 additions and 10 deletions

View File

@ -262,11 +262,19 @@
}
.chat-card-x-scroll {
min-width: 0;
max-width: 100%;
-webkit-overflow-scrolling: touch;
scrollbar-gutter: stable;
scrollbar-width: thin;
scrollbar-color: rgba(148, 163, 184, 0.42) rgba(148, 163, 184, 0.14);
}
.chat-card-x-scroll > table,
.chat-card-x-scroll > svg {
max-width: none;
}
.chat-card-x-scroll::-webkit-scrollbar {
width: 10px;
height: 10px;

View File

@ -290,7 +290,7 @@ export function OpenClawChartCard({ chart }: { chart: OpenClawChartSpec }) {
};
return (
<div className="my-3 max-w-full overflow-hidden rounded-xl border border-border bg-background/70 shadow-sm whitespace-normal">
<div className="my-3 w-full min-w-0 max-w-full overflow-hidden rounded-xl border border-border bg-background/70 shadow-sm whitespace-normal">
<div className="flex items-start justify-between gap-3 border-b border-border/70 bg-muted/30 px-4 py-3">
<div className="min-w-0">
<div className="flex items-center gap-2 text-sm font-semibold text-foreground">
@ -314,7 +314,7 @@ export function OpenClawChartCard({ chart }: { chart: OpenClawChartSpec }) {
<span>下载数据</span>
</button>
</div>
<div className="chat-card-x-scroll overflow-x-auto px-3 pb-4 pt-3">
<div className="chat-card-x-scroll w-full min-w-0 overflow-x-auto px-3 pb-4 pt-3">
<svg
className="min-w-[720px] max-w-none"
role="img"

View File

@ -1,7 +1,11 @@
"use client";
import { ChevronDownIcon, DownloadIcon } from "lucide-react";
import type { ComponentProps, ReactNode } from "react";
import type {
ComponentProps,
PointerEvent as ReactPointerEvent,
ReactNode,
} from "react";
import { useRef, useState } from "react";
import { Streamdown } from "streamdown";
import { ImagePreview } from "@/components/image-preview";
@ -65,6 +69,93 @@ type DownloadableMarkdownTableProps = ComponentProps<"table"> & {
node?: unknown;
};
type MarkdownTableCellProps = ComponentProps<"td"> & {
node?: unknown;
};
type MarkdownTableHeaderCellProps = ComponentProps<"th"> & {
node?: unknown;
};
function HorizontalDragScrollArea({
children,
className,
...props
}: ComponentProps<"div">) {
const scrollRef = useRef<HTMLDivElement>(null);
const dragStateRef = useRef<{
pointerId: number;
startX: number;
scrollLeft: number;
hasDragged: boolean;
} | null>(null);
const suppressClickRef = useRef(false);
const endDrag = (event: ReactPointerEvent<HTMLDivElement>) => {
const state = dragStateRef.current;
const element = scrollRef.current;
if (!state || !element || state.pointerId !== event.pointerId) return;
if (state.hasDragged) {
suppressClickRef.current = true;
window.setTimeout(() => {
suppressClickRef.current = false;
}, 0);
}
if (element.hasPointerCapture(event.pointerId)) {
element.releasePointerCapture(event.pointerId);
}
dragStateRef.current = null;
};
return (
<div
ref={scrollRef}
className={cn(
"chat-card-x-scroll w-full min-w-0 overflow-x-auto overscroll-x-contain pb-2",
"cursor-grab select-auto touch-pan-x active:cursor-grabbing",
className
)}
onClickCapture={(event) => {
if (!suppressClickRef.current) return;
event.preventDefault();
event.stopPropagation();
}}
onPointerCancel={endDrag}
onPointerDown={(event) => {
if (event.button !== 0) return;
const element = scrollRef.current;
if (!element || element.scrollWidth <= element.clientWidth) return;
dragStateRef.current = {
pointerId: event.pointerId,
startX: event.clientX,
scrollLeft: element.scrollLeft,
hasDragged: false,
};
element.setPointerCapture(event.pointerId);
}}
onPointerMove={(event) => {
const state = dragStateRef.current;
const element = scrollRef.current;
if (!state || !element || state.pointerId !== event.pointerId) return;
const deltaX = event.clientX - state.startX;
if (Math.abs(deltaX) > 2) {
state.hasDragged = true;
event.preventDefault();
}
element.scrollLeft = state.scrollLeft - deltaX;
}}
onPointerUp={endDrag}
{...props}
>
{children}
</div>
);
}
const OPENCLAW_CHART_BLOCK_REGEX =
/```(?:openclaw-chart|openclaw_chart|claw-chart|claw_chart|chart)\s*\n[\s\S]*?```/i;
const MARKDOWN_TABLE_REGEX =
@ -313,7 +404,7 @@ function DownloadableMarkdownTable({
};
return (
<div className="my-3 max-w-full overflow-hidden rounded-xl border border-border bg-background/70 whitespace-normal">
<div className="my-3 w-full min-w-0 max-w-full overflow-hidden rounded-xl border border-border bg-background/70 whitespace-normal">
<div
className="flex items-center justify-end border-b border-border/70 bg-muted/40 px-2 py-1.5"
data-pdf-exclude="true"
@ -329,12 +420,12 @@ function DownloadableMarkdownTable({
<span>下载表格</span>
</button>
</div>
<div className="chat-card-x-scroll max-w-full overflow-x-auto overscroll-x-contain pb-2">
<HorizontalDragScrollArea>
<table
ref={tableRef}
className={cn(
"w-max min-w-full border-collapse text-sm",
"[&_td]:min-w-[12rem] [&_td]:align-top [&_td]:break-words [&_td]:whitespace-normal",
"w-max min-w-full table-auto border-collapse text-sm",
"[&_td]:min-w-[12rem] [&_td]:align-top [&_td]:whitespace-nowrap",
"[&_td:first-child]:min-w-[22rem] [&_th:first-child]:min-w-[22rem]",
"[&_th]:min-w-[12rem] [&_th]:whitespace-nowrap",
className
@ -343,11 +434,37 @@ function DownloadableMarkdownTable({
>
{children}
</table>
</div>
</HorizontalDragScrollArea>
</div>
);
}
function MarkdownTableCell({
className,
node: _node,
...props
}: MarkdownTableCellProps) {
return (
<td
className={cn("min-w-[12rem] align-top !whitespace-nowrap", className)}
{...props}
/>
);
}
function MarkdownTableHeaderCell({
className,
node: _node,
...props
}: MarkdownTableHeaderCellProps) {
return (
<th
className={cn("min-w-[12rem] !whitespace-nowrap", className)}
{...props}
/>
);
}
// 注意:移除了 memo 的自定义比较函数,确保 isProcessing* 状态变化时组件能正确重新渲染
// 之前的 memo 比较函数可能导致 Streamdown 内部的 ImagePreview 组件没有正确更新
export const Response = ({
@ -414,6 +531,8 @@ export const Response = ({
return <VideoPreview src={src} alt={alt} />;
},
table: DownloadableMarkdownTable,
td: MarkdownTableCell,
th: MarkdownTableHeaderCell,
};
const renderImagePreview = (src: string, alt: string | undefined, key: string) => (
@ -515,8 +634,9 @@ export const Response = ({
return (
<div className="w-full min-w-0">
<div ref={pdfContentRef}>{content}</div>
<div
className="not-prose mb-2 flex justify-end"
className="not-prose mt-4 flex justify-end"
data-pdf-exclude="true"
>
<ResponsePdfDownloadButton
@ -527,7 +647,6 @@ export const Response = ({
title={pdfTitle}
/>
</div>
<div ref={pdfContentRef}>{content}</div>
</div>
);
};