fix(webui): use portal file reference tooltips

This commit is contained in:
Xubin Ren 2026-05-17 23:52:29 +08:00
parent 945f208d38
commit 361f31c0e4
3 changed files with 57 additions and 10 deletions

View File

@ -6,6 +6,7 @@ import remarkGfm from "remark-gfm";
import remarkMath from "remark-math"; import remarkMath from "remark-math";
import { CodeBlock } from "@/components/CodeBlock"; import { CodeBlock } from "@/components/CodeBlock";
import { FileReferenceChip, isLikelyFilePath } from "@/components/FileReferenceChip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import "katex/dist/katex.min.css"; import "katex/dist/katex.min.css";
@ -44,6 +45,9 @@ export default function MarkdownTextRenderer({
); );
} }
const raw = String(kids).replace(/\n$/, ""); const raw = String(kids).replace(/\n$/, "");
if (isLikelyFilePath(raw)) {
return <FileReferenceChip path={raw} />;
}
/** Plain fenced ``` blocks (no language) & wide one-liners: block monospace, not inline pill. */ /** Plain fenced ``` blocks (no language) & wide one-liners: block monospace, not inline pill. */
const widePlainBlock = raw.includes("\n") || raw.length > 120; const widePlainBlock = raw.includes("\n") || raw.length > 120;
if (widePlainBlock) { if (widePlainBlock) {

View File

@ -11,6 +11,7 @@ const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>, React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => ( >(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content <TooltipPrimitive.Content
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
@ -20,6 +21,7 @@ const TooltipContent = React.forwardRef<
)} )}
{...props} {...props}
/> />
</TooltipPrimitive.Portal>
)); ));
TooltipContent.displayName = TooltipPrimitive.Content.displayName; TooltipContent.displayName = TooltipPrimitive.Content.displayName;

View File

@ -1,4 +1,4 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import { MessageBubble } from "@/components/MessageBubble"; import { MessageBubble } from "@/components/MessageBubble";
@ -179,6 +179,47 @@ describe("MessageBubble", () => {
expect(screen.getByText("Body line.")).toBeInTheDocument(); expect(screen.getByText("Body line.")).toBeInTheDocument();
}); });
it("renders inline file paths as compact file references", async () => {
await import("@/components/MarkdownTextRenderer");
const message: UIMessage = {
id: "a-file-path",
role: "assistant",
content:
"改动在 `webui/src/components/MarkdownTextRenderer.tsx` 和 `/Users/renxubin/.nanobot/workspace/minecraft-fps/index.html`。",
createdAt: Date.now(),
};
try {
render(<MessageBubble message={message} />);
const references = await screen.findAllByTestId("inline-file-path");
expect(references).toHaveLength(2);
expect(references[0].parentElement).not.toHaveClass("translate-y-[0.08em]");
expect(references[0].parentElement).toHaveClass("align-[0.14em]");
expect(references[0]).toHaveTextContent("MarkdownTextRenderer.tsx");
expect(references[0]).not.toHaveTextContent("webui/src/components");
expect(screen.getByText("index.html")).toBeInTheDocument();
expect(references[1]).not.toHaveTextContent("/Users/renxubin");
expect(references[1]).not.toHaveAttribute("title");
expect(references[1]).toHaveAttribute(
"aria-label",
"/Users/renxubin/.nanobot/workspace/minecraft-fps/index.html",
);
vi.useFakeTimers();
fireEvent.pointerMove(references[1].parentElement!);
await act(async () => {
vi.advanceTimersByTime(500);
});
const tooltip = screen.getByRole("tooltip");
expect(tooltip).toHaveTextContent(
"/Users/renxubin/.nanobot/workspace/minecraft-fps/index.html",
);
} finally {
vi.useRealTimers();
}
});
it("renders assistant image media as a larger generated result", () => { it("renders assistant image media as a larger generated result", () => {
const message: UIMessage = { const message: UIMessage = {
id: "a-image", id: "a-image",