Fix report PDF placement and table scrolling
This commit is contained in:
parent
57f4d08c31
commit
cd90c8074c
@ -262,11 +262,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-card-x-scroll {
|
.chat-card-x-scroll {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: rgba(148, 163, 184, 0.42) rgba(148, 163, 184, 0.14);
|
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 {
|
.chat-card-x-scroll::-webkit-scrollbar {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
|
|||||||
@ -290,7 +290,7 @@ export function OpenClawChartCard({ chart }: { chart: OpenClawChartSpec }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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="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="min-w-0">
|
||||||
<div className="flex items-center gap-2 text-sm font-semibold text-foreground">
|
<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>
|
<span>下载数据</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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
|
<svg
|
||||||
className="min-w-[720px] max-w-none"
|
className="min-w-[720px] max-w-none"
|
||||||
role="img"
|
role="img"
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ChevronDownIcon, DownloadIcon } from "lucide-react";
|
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 { useRef, useState } from "react";
|
||||||
import { Streamdown } from "streamdown";
|
import { Streamdown } from "streamdown";
|
||||||
import { ImagePreview } from "@/components/image-preview";
|
import { ImagePreview } from "@/components/image-preview";
|
||||||
@ -65,6 +69,93 @@ type DownloadableMarkdownTableProps = ComponentProps<"table"> & {
|
|||||||
node?: unknown;
|
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 =
|
const OPENCLAW_CHART_BLOCK_REGEX =
|
||||||
/```(?:openclaw-chart|openclaw_chart|claw-chart|claw_chart|chart)\s*\n[\s\S]*?```/i;
|
/```(?:openclaw-chart|openclaw_chart|claw-chart|claw_chart|chart)\s*\n[\s\S]*?```/i;
|
||||||
const MARKDOWN_TABLE_REGEX =
|
const MARKDOWN_TABLE_REGEX =
|
||||||
@ -313,7 +404,7 @@ function DownloadableMarkdownTable({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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
|
<div
|
||||||
className="flex items-center justify-end border-b border-border/70 bg-muted/40 px-2 py-1.5"
|
className="flex items-center justify-end border-b border-border/70 bg-muted/40 px-2 py-1.5"
|
||||||
data-pdf-exclude="true"
|
data-pdf-exclude="true"
|
||||||
@ -329,12 +420,12 @@ function DownloadableMarkdownTable({
|
|||||||
<span>下载表格</span>
|
<span>下载表格</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="chat-card-x-scroll max-w-full overflow-x-auto overscroll-x-contain pb-2">
|
<HorizontalDragScrollArea>
|
||||||
<table
|
<table
|
||||||
ref={tableRef}
|
ref={tableRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-max min-w-full border-collapse text-sm",
|
"w-max min-w-full table-auto border-collapse text-sm",
|
||||||
"[&_td]:min-w-[12rem] [&_td]:align-top [&_td]:break-words [&_td]:whitespace-normal",
|
"[&_td]:min-w-[12rem] [&_td]:align-top [&_td]:whitespace-nowrap",
|
||||||
"[&_td:first-child]:min-w-[22rem] [&_th:first-child]:min-w-[22rem]",
|
"[&_td:first-child]:min-w-[22rem] [&_th:first-child]:min-w-[22rem]",
|
||||||
"[&_th]:min-w-[12rem] [&_th]:whitespace-nowrap",
|
"[&_th]:min-w-[12rem] [&_th]:whitespace-nowrap",
|
||||||
className
|
className
|
||||||
@ -343,11 +434,37 @@ function DownloadableMarkdownTable({
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</HorizontalDragScrollArea>
|
||||||
</div>
|
</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 的自定义比较函数,确保 isProcessing* 状态变化时组件能正确重新渲染
|
||||||
// 之前的 memo 比较函数可能导致 Streamdown 内部的 ImagePreview 组件没有正确更新
|
// 之前的 memo 比较函数可能导致 Streamdown 内部的 ImagePreview 组件没有正确更新
|
||||||
export const Response = ({
|
export const Response = ({
|
||||||
@ -414,6 +531,8 @@ export const Response = ({
|
|||||||
return <VideoPreview src={src} alt={alt} />;
|
return <VideoPreview src={src} alt={alt} />;
|
||||||
},
|
},
|
||||||
table: DownloadableMarkdownTable,
|
table: DownloadableMarkdownTable,
|
||||||
|
td: MarkdownTableCell,
|
||||||
|
th: MarkdownTableHeaderCell,
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderImagePreview = (src: string, alt: string | undefined, key: string) => (
|
const renderImagePreview = (src: string, alt: string | undefined, key: string) => (
|
||||||
@ -515,8 +634,9 @@ export const Response = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full min-w-0">
|
<div className="w-full min-w-0">
|
||||||
|
<div ref={pdfContentRef}>{content}</div>
|
||||||
<div
|
<div
|
||||||
className="not-prose mb-2 flex justify-end"
|
className="not-prose mt-4 flex justify-end"
|
||||||
data-pdf-exclude="true"
|
data-pdf-exclude="true"
|
||||||
>
|
>
|
||||||
<ResponsePdfDownloadButton
|
<ResponsePdfDownloadButton
|
||||||
@ -527,7 +647,6 @@ export const Response = ({
|
|||||||
title={pdfTitle}
|
title={pdfTitle}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div ref={pdfContentRef}>{content}</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user