Fix report PDF placement and table scrolling
This commit is contained in:
parent
57f4d08c31
commit
cd90c8074c
@ -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;
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user