feat(FN-687): add markdown rendering to agent log viewer
- Add ReactMarkdown component to AgentLogViewer for rendering markdown content in agent logs - Remove className prop from ReactMarkdown for v10 compatibility (fix) - Add comprehensive tests for markdown rendering in agent log - Consolidate test files by removing redundant InlineCreateCard and QuickEntryBox tests
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import type { AgentLogEntry } from "@fusion/core";
|
||||
import { ProviderIcon } from "./ProviderIcon";
|
||||
import { useRef, useEffect } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
interface ModelInfo {
|
||||
provider?: string;
|
||||
@@ -190,7 +192,10 @@ export function AgentLogViewer({ entries, loading, executorModel, validatorModel
|
||||
opacity: 0.7,
|
||||
}}
|
||||
>
|
||||
{agentBadge}{entry.text}
|
||||
{agentBadge}
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{entry.text}
|
||||
</ReactMarkdown>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -258,7 +263,10 @@ export function AgentLogViewer({ entries, loading, executorModel, validatorModel
|
||||
// Default: text entries
|
||||
return (
|
||||
<span key={i} className="agent-log-text">
|
||||
{agentBadge}{entry.text}
|
||||
{agentBadge}
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{entry.text}
|
||||
</ReactMarkdown>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -360,4 +360,108 @@ describe("AgentLogViewer", () => {
|
||||
expect(viewer.scrollTop).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe("markdown rendering", () => {
|
||||
it("renders plain text without markdown correctly", () => {
|
||||
const entries = [
|
||||
makeEntry({ text: "Hello world, this is plain text." }),
|
||||
];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const textSpans = container.querySelectorAll(".agent-log-text");
|
||||
expect(textSpans).toHaveLength(1);
|
||||
expect(textSpans[0].textContent).toBe("Hello world, this is plain text.");
|
||||
});
|
||||
|
||||
it("renders inline markdown elements (bold, italic, inline code)", () => {
|
||||
const entries = [
|
||||
makeEntry({ text: "This is **bold** and *italic* with `inline code`." }),
|
||||
];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const textSpans = container.querySelectorAll(".agent-log-text");
|
||||
expect(textSpans).toHaveLength(1);
|
||||
// Check that the markdown elements are rendered
|
||||
const strong = textSpans[0].querySelector("strong");
|
||||
expect(strong).toBeTruthy();
|
||||
expect(strong!.textContent).toBe("bold");
|
||||
const em = textSpans[0].querySelector("em");
|
||||
expect(em).toBeTruthy();
|
||||
expect(em!.textContent).toBe("italic");
|
||||
const code = textSpans[0].querySelector("code");
|
||||
expect(code).toBeTruthy();
|
||||
expect(code!.textContent).toBe("inline code");
|
||||
});
|
||||
|
||||
it("renders code blocks with GFM support", () => {
|
||||
const entries = [
|
||||
makeEntry({ text: "```typescript\nconst x = 1;\n```" }),
|
||||
];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const textSpans = container.querySelectorAll(".agent-log-text");
|
||||
expect(textSpans).toHaveLength(1);
|
||||
// Check that code block is rendered
|
||||
const pre = textSpans[0].querySelector("pre");
|
||||
expect(pre).toBeTruthy();
|
||||
const code = pre!.querySelector("code");
|
||||
expect(code).toBeTruthy();
|
||||
expect(code!.textContent).toContain("const x = 1");
|
||||
});
|
||||
|
||||
it("renders GFM task lists", () => {
|
||||
const entries = [
|
||||
makeEntry({ text: "- [x] Completed task\n- [ ] Pending task" }),
|
||||
];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const textSpans = container.querySelectorAll(".agent-log-text");
|
||||
expect(textSpans).toHaveLength(1);
|
||||
// Check that task list is rendered
|
||||
const ul = textSpans[0].querySelector("ul");
|
||||
expect(ul).toBeTruthy();
|
||||
const taskListItems = ul!.querySelectorAll("li");
|
||||
expect(taskListItems).toHaveLength(2);
|
||||
// Check checkboxes
|
||||
const checkboxes = ul!.querySelectorAll('input[type="checkbox"]');
|
||||
expect(checkboxes).toHaveLength(2);
|
||||
expect((checkboxes[0] as HTMLInputElement).checked).toBe(true);
|
||||
expect((checkboxes[1] as HTMLInputElement).checked).toBe(false);
|
||||
});
|
||||
|
||||
it("renders blockquotes", () => {
|
||||
const entries = [
|
||||
makeEntry({ text: "> This is a blockquote\n> with multiple lines" }),
|
||||
];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const textSpans = container.querySelectorAll(".agent-log-text");
|
||||
expect(textSpans).toHaveLength(1);
|
||||
// Check that blockquote is rendered
|
||||
const blockquote = textSpans[0].querySelector("blockquote");
|
||||
expect(blockquote).toBeTruthy();
|
||||
expect(blockquote!.textContent).toContain("This is a blockquote");
|
||||
});
|
||||
|
||||
it("renders markdown in thinking entries", () => {
|
||||
const entries = [
|
||||
makeEntry({ text: "Let me think about **this problem**...", type: "thinking" }),
|
||||
];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const thinkingSpans = container.querySelectorAll(".agent-log-thinking");
|
||||
expect(thinkingSpans).toHaveLength(1);
|
||||
const strong = thinkingSpans[0].querySelector("strong");
|
||||
expect(strong).toBeTruthy();
|
||||
expect(strong!.textContent).toBe("this problem");
|
||||
});
|
||||
|
||||
it("renders mixed content with markdown and plain text", () => {
|
||||
const entries = [
|
||||
makeEntry({ text: "The code:\n\n```js\nconsole.log('hello');\n```\n\nworks!" }),
|
||||
];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const textSpans = container.querySelectorAll(".agent-log-text");
|
||||
expect(textSpans).toHaveLength(1);
|
||||
const pre = textSpans[0].querySelector("pre");
|
||||
expect(pre).toBeTruthy();
|
||||
// Plain text before and after should be preserved
|
||||
expect(textSpans[0].textContent).toContain("The code:");
|
||||
expect(textSpans[0].textContent).toContain("works!");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user