mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-14 06:43:53 +00:00
fix(webui): polish links and thought timing
This commit is contained in:
parent
d5692bf94c
commit
e8d4aff5be
@ -272,7 +272,7 @@ function InlineLinkPreviewRow({ link }: { link: InlineLinkPreview }) {
|
|||||||
aria-label={`Open link: ${label}`}
|
aria-label={`Open link: ${label}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"not-prose inline-flex max-w-full items-center gap-2 align-baseline",
|
"not-prose inline-flex max-w-full items-center gap-2 align-baseline",
|
||||||
"text-primary no-underline underline-offset-2 hover:underline",
|
"text-blue-600 no-underline underline-offset-2 hover:underline dark:text-blue-300",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@ -410,7 +410,7 @@ export default function MarkdownTextRenderer({
|
|||||||
href={href}
|
href={href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
className="text-primary underline underline-offset-2 hover:opacity-80"
|
className="text-blue-600 underline underline-offset-2 hover:text-blue-700 dark:text-blue-300 dark:hover:text-blue-200"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{markdownChildren}
|
{markdownChildren}
|
||||||
@ -508,7 +508,7 @@ export default function MarkdownTextRenderer({
|
|||||||
"prose-ul:my-2 prose-ol:my-2 prose-li:my-0.5",
|
"prose-ul:my-2 prose-ol:my-2 prose-li:my-0.5",
|
||||||
"prose-blockquote:my-3 prose-blockquote:border-l-2 prose-blockquote:font-normal",
|
"prose-blockquote:my-3 prose-blockquote:border-l-2 prose-blockquote:font-normal",
|
||||||
"prose-blockquote:not-italic prose-blockquote:text-foreground/80",
|
"prose-blockquote:not-italic prose-blockquote:text-foreground/80",
|
||||||
"prose-a:text-primary prose-a:underline-offset-2 hover:prose-a:opacity-80",
|
"prose-a:text-blue-600 prose-a:underline-offset-2 hover:prose-a:text-blue-700 dark:prose-a:text-blue-300 dark:hover:prose-a:text-blue-200",
|
||||||
"prose-hr:my-6",
|
"prose-hr:my-6",
|
||||||
"prose-pre:my-0 prose-pre:bg-transparent prose-pre:p-0",
|
"prose-pre:my-0 prose-pre:bg-transparent prose-pre:p-0",
|
||||||
"prose-code:before:content-none prose-code:after:content-none prose-code:font-normal",
|
"prose-code:before:content-none prose-code:after:content-none prose-code:font-normal",
|
||||||
|
|||||||
@ -573,7 +573,7 @@ export function ReasoningBubble({
|
|||||||
"prose-headings:mt-2 prose-headings:mb-1 prose-headings:font-medium",
|
"prose-headings:mt-2 prose-headings:mb-1 prose-headings:font-medium",
|
||||||
"prose-headings:text-muted-foreground/92 prose-strong:text-muted-foreground",
|
"prose-headings:text-muted-foreground/92 prose-strong:text-muted-foreground",
|
||||||
"prose-h1:text-[15px] prose-h2:text-[13.5px] prose-h3:text-[12.5px] prose-h4:text-[12px]",
|
"prose-h1:text-[15px] prose-h2:text-[13.5px] prose-h3:text-[12.5px] prose-h4:text-[12px]",
|
||||||
"prose-a:text-muted-foreground/95 prose-a:underline hover:prose-a:opacity-90",
|
"prose-a:text-blue-600 prose-a:underline hover:prose-a:text-blue-700 dark:prose-a:text-blue-300 dark:hover:prose-a:text-blue-200",
|
||||||
"prose-code:text-[0.92em]",
|
"prose-code:text-[0.92em]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export function ReasoningRow({
|
|||||||
"prose-headings:mt-2 prose-headings:mb-1 prose-headings:font-medium",
|
"prose-headings:mt-2 prose-headings:mb-1 prose-headings:font-medium",
|
||||||
"prose-headings:text-muted-foreground/88 prose-strong:text-muted-foreground",
|
"prose-headings:text-muted-foreground/88 prose-strong:text-muted-foreground",
|
||||||
"prose-h1:text-[15px] prose-h2:text-[13.5px] prose-h3:text-[12.5px] prose-h4:text-[12px]",
|
"prose-h1:text-[15px] prose-h2:text-[13.5px] prose-h3:text-[12.5px] prose-h4:text-[12px]",
|
||||||
"prose-a:text-muted-foreground/95 prose-a:underline hover:prose-a:opacity-90",
|
"prose-a:text-blue-600 prose-a:underline hover:prose-a:text-blue-700 dark:prose-a:text-blue-300 dark:hover:prose-a:text-blue-200",
|
||||||
"prose-code:text-[0.92em]",
|
"prose-code:text-[0.92em]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -145,7 +145,17 @@ function closeReasoningStream(prev: UIMessage[]): UIMessage[] {
|
|||||||
for (let i = prev.length - 1; i >= 0; i -= 1) {
|
for (let i = prev.length - 1; i >= 0; i -= 1) {
|
||||||
const candidate = prev[i];
|
const candidate = prev[i];
|
||||||
if (!candidate.reasoningStreaming) continue;
|
if (!candidate.reasoningStreaming) continue;
|
||||||
const merged: UIMessage = { ...candidate, reasoningStreaming: false };
|
const latencyMs =
|
||||||
|
candidate.latencyMs === undefined
|
||||||
|
&& Number.isFinite(candidate.createdAt)
|
||||||
|
&& candidate.createdAt > 1_000_000_000_000
|
||||||
|
? Math.max(0, Math.round(Date.now() - candidate.createdAt))
|
||||||
|
: candidate.latencyMs;
|
||||||
|
const merged: UIMessage = {
|
||||||
|
...candidate,
|
||||||
|
reasoningStreaming: false,
|
||||||
|
...(latencyMs !== undefined ? { latencyMs } : {}),
|
||||||
|
};
|
||||||
return [...prev.slice(0, i), merged, ...prev.slice(i + 1)];
|
return [...prev.slice(0, i), merged, ...prev.slice(i + 1)];
|
||||||
}
|
}
|
||||||
return prev;
|
return prev;
|
||||||
|
|||||||
@ -4,6 +4,14 @@ import { describe, expect, it } from "vitest";
|
|||||||
import MarkdownTextRenderer from "@/components/MarkdownTextRenderer";
|
import MarkdownTextRenderer from "@/components/MarkdownTextRenderer";
|
||||||
|
|
||||||
describe("MarkdownTextRenderer", () => {
|
describe("MarkdownTextRenderer", () => {
|
||||||
|
it("renders clickable markdown links in blue", () => {
|
||||||
|
render(<MarkdownTextRenderer>[local server](http://127.0.0.1:7891/)</MarkdownTextRenderer>);
|
||||||
|
|
||||||
|
const link = screen.getByRole("link", { name: "local server" });
|
||||||
|
expect(link).toHaveAttribute("href", "http://127.0.0.1:7891/");
|
||||||
|
expect(link).toHaveClass("text-blue-600", "dark:text-blue-300");
|
||||||
|
});
|
||||||
|
|
||||||
it("does not wrap complete fenced code blocks in an extra pre", () => {
|
it("does not wrap complete fenced code blocks in an extra pre", () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<MarkdownTextRenderer highlightCode={false}>
|
<MarkdownTextRenderer highlightCode={false}>
|
||||||
|
|||||||
@ -1039,6 +1039,41 @@ describe("useNanobotStream", () => {
|
|||||||
expect(result.current.messages[1].reasoningStreaming).toBe(false);
|
expect(result.current.messages[1].reasoningStreaming).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("stamps completed live Thought blocks with their own latency", async () => {
|
||||||
|
const dateNow = vi.spyOn(Date, "now");
|
||||||
|
let now = Date.UTC(2026, 5, 1, 0, 0, 0);
|
||||||
|
dateNow.mockImplementation(() => now);
|
||||||
|
try {
|
||||||
|
const fake = fakeClient();
|
||||||
|
const { result } = renderHook(() => useNanobotStream("chat-r5-lat", EMPTY_MESSAGES), {
|
||||||
|
wrapper: wrap(fake.client),
|
||||||
|
});
|
||||||
|
await act(async () => {});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fake.emit("chat-r5-lat", {
|
||||||
|
event: "reasoning_delta",
|
||||||
|
chat_id: "chat-r5-lat",
|
||||||
|
text: "Thinking through the tests.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise<void>((resolve) => window.requestAnimationFrame(() => resolve()));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.messages[0].createdAt).toBe(now);
|
||||||
|
now += 2100;
|
||||||
|
act(() => {
|
||||||
|
fake.emit("chat-r5-lat", { event: "reasoning_end", chat_id: "chat-r5-lat" });
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.messages[0].reasoningStreaming).toBe(false);
|
||||||
|
expect(result.current.messages[0].latencyMs).toBe(2100);
|
||||||
|
} finally {
|
||||||
|
dateNow.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps alternating reasoning and answer deltas in separate ordered blocks", async () => {
|
it("keeps alternating reasoning and answer deltas in separate ordered blocks", async () => {
|
||||||
const fake = fakeClient();
|
const fake = fakeClient();
|
||||||
const { result } = renderHook(() => useNanobotStream("chat-r5b", EMPTY_MESSAGES), {
|
const { result } = renderHook(() => useNanobotStream("chat-r5b", EMPTY_MESSAGES), {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user