mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
refactor(frontend): reduce heading text size for plan preview content (#12620)
This commit is contained in:
@@ -156,4 +156,72 @@ describe("PlanPreview", () => {
|
||||
expect(container.textContent).toContain("Heading 1");
|
||||
expect(container.textContent).toContain("Heading 2");
|
||||
});
|
||||
|
||||
it("should use planHeadings components for h1 headings", () => {
|
||||
// Arrange
|
||||
const planContent = "# Main Title";
|
||||
|
||||
// Act
|
||||
const { container } = renderWithProviders(
|
||||
<PlanPreview planContent={planContent} />,
|
||||
);
|
||||
|
||||
// Assert
|
||||
const h1 = container.querySelector("h1");
|
||||
expect(h1).toBeInTheDocument();
|
||||
expect(h1).toHaveTextContent("Main Title");
|
||||
});
|
||||
|
||||
it("should use planHeadings components for h2 headings", () => {
|
||||
// Arrange
|
||||
const planContent = "## Section Title";
|
||||
|
||||
// Act
|
||||
const { container } = renderWithProviders(
|
||||
<PlanPreview planContent={planContent} />,
|
||||
);
|
||||
|
||||
// Assert
|
||||
const h2 = container.querySelector("h2");
|
||||
expect(h2).toBeInTheDocument();
|
||||
expect(h2).toHaveTextContent("Section Title");
|
||||
});
|
||||
|
||||
it("should use planHeadings components for h3 headings", () => {
|
||||
// Arrange
|
||||
const planContent = "### Subsection Title";
|
||||
|
||||
// Act
|
||||
const { container } = renderWithProviders(
|
||||
<PlanPreview planContent={planContent} />,
|
||||
);
|
||||
|
||||
// Assert
|
||||
const h3 = container.querySelector("h3");
|
||||
expect(h3).toBeInTheDocument();
|
||||
expect(h3).toHaveTextContent("Subsection Title");
|
||||
});
|
||||
|
||||
it("should use planHeadings components for all heading levels", () => {
|
||||
// Arrange
|
||||
const planContent = `# H1 Title
|
||||
## H2 Title
|
||||
### H3 Title
|
||||
#### H4 Title
|
||||
##### H5 Title
|
||||
###### H6 Title`;
|
||||
|
||||
// Act
|
||||
const { container } = renderWithProviders(
|
||||
<PlanPreview planContent={planContent} />,
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(container.querySelector("h1")).toBeInTheDocument();
|
||||
expect(container.querySelector("h2")).toBeInTheDocument();
|
||||
expect(container.querySelector("h3")).toBeInTheDocument();
|
||||
expect(container.querySelector("h4")).toBeInTheDocument();
|
||||
expect(container.querySelector("h5")).toBeInTheDocument();
|
||||
expect(container.querySelector("h6")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { planHeadings } from "#/components/features/markdown/plan-headings";
|
||||
|
||||
describe("planHeadings", () => {
|
||||
describe("h1", () => {
|
||||
it("should render h1 with correct text content", () => {
|
||||
// Arrange
|
||||
const H1 = planHeadings.h1;
|
||||
const text = "Main Heading";
|
||||
|
||||
// Act
|
||||
render(<H1>{text}</H1>);
|
||||
|
||||
// Assert
|
||||
const heading = screen.getByRole("heading", { level: 1 });
|
||||
expect(heading).toBeInTheDocument();
|
||||
expect(heading).toHaveTextContent(text);
|
||||
});
|
||||
|
||||
it("should handle undefined children gracefully", () => {
|
||||
// Arrange
|
||||
const H1 = planHeadings.h1;
|
||||
|
||||
// Act
|
||||
render(<H1>{undefined}</H1>);
|
||||
|
||||
// Assert
|
||||
const heading = screen.getByRole("heading", { level: 1 });
|
||||
expect(heading).toBeInTheDocument();
|
||||
expect(heading).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it("should render complex children content", () => {
|
||||
// Arrange
|
||||
const H1 = planHeadings.h1;
|
||||
|
||||
// Act
|
||||
render(
|
||||
<H1>
|
||||
<span>Nested</span> Content
|
||||
</H1>,
|
||||
);
|
||||
|
||||
// Assert
|
||||
const heading = screen.getByRole("heading", { level: 1 });
|
||||
expect(heading).toHaveTextContent("Nested Content");
|
||||
expect(heading.querySelector("span")).toHaveTextContent("Nested");
|
||||
});
|
||||
});
|
||||
|
||||
describe("h2", () => {
|
||||
it("should render h2 with correct text content", () => {
|
||||
// Arrange
|
||||
const H2 = planHeadings.h2;
|
||||
const text = "Section Heading";
|
||||
|
||||
// Act
|
||||
render(<H2>{text}</H2>);
|
||||
|
||||
// Assert
|
||||
const heading = screen.getByRole("heading", { level: 2 });
|
||||
expect(heading).toBeInTheDocument();
|
||||
expect(heading).toHaveTextContent(text);
|
||||
});
|
||||
|
||||
it("should handle null children gracefully", () => {
|
||||
// Arrange
|
||||
const H2 = planHeadings.h2;
|
||||
|
||||
// Act
|
||||
render(<H2>{null}</H2>);
|
||||
|
||||
// Assert
|
||||
const heading = screen.getByRole("heading", { level: 2 });
|
||||
expect(heading).toBeInTheDocument();
|
||||
expect(heading).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
|
||||
describe("h3", () => {
|
||||
it("should render h3 with correct text content", () => {
|
||||
// Arrange
|
||||
const H3 = planHeadings.h3;
|
||||
const text = "Subsection Heading";
|
||||
|
||||
// Act
|
||||
render(<H3>{text}</H3>);
|
||||
|
||||
// Assert
|
||||
const heading = screen.getByRole("heading", { level: 3 });
|
||||
expect(heading).toBeInTheDocument();
|
||||
expect(heading).toHaveTextContent(text);
|
||||
});
|
||||
});
|
||||
|
||||
describe("h4", () => {
|
||||
it("should render h4 with correct text content", () => {
|
||||
// Arrange
|
||||
const H4 = planHeadings.h4;
|
||||
const text = "Level 4 Heading";
|
||||
|
||||
// Act
|
||||
render(<H4>{text}</H4>);
|
||||
|
||||
// Assert
|
||||
const heading = screen.getByRole("heading", { level: 4 });
|
||||
expect(heading).toBeInTheDocument();
|
||||
expect(heading).toHaveTextContent(text);
|
||||
});
|
||||
});
|
||||
|
||||
describe("h5", () => {
|
||||
it("should render h5 with correct text content", () => {
|
||||
// Arrange
|
||||
const H5 = planHeadings.h5;
|
||||
const text = "Level 5 Heading";
|
||||
|
||||
// Act
|
||||
render(<H5>{text}</H5>);
|
||||
|
||||
// Assert
|
||||
const heading = screen.getByRole("heading", { level: 5 });
|
||||
expect(heading).toBeInTheDocument();
|
||||
expect(heading).toHaveTextContent(text);
|
||||
});
|
||||
});
|
||||
|
||||
describe("h6", () => {
|
||||
it("should render h6 with correct text content", () => {
|
||||
// Arrange
|
||||
const H6 = planHeadings.h6;
|
||||
const text = "Level 6 Heading";
|
||||
|
||||
// Act
|
||||
render(<H6>{text}</H6>);
|
||||
|
||||
// Assert
|
||||
const heading = screen.getByRole("heading", { level: 6 });
|
||||
expect(heading).toBeInTheDocument();
|
||||
expect(heading).toHaveTextContent(text);
|
||||
});
|
||||
});
|
||||
|
||||
describe("heading hierarchy", () => {
|
||||
it("should render all heading levels correctly in sequence", () => {
|
||||
// Arrange
|
||||
const H1 = planHeadings.h1;
|
||||
const H2 = planHeadings.h2;
|
||||
const H3 = planHeadings.h3;
|
||||
const H4 = planHeadings.h4;
|
||||
const H5 = planHeadings.h5;
|
||||
const H6 = planHeadings.h6;
|
||||
|
||||
// Act
|
||||
render(
|
||||
<div>
|
||||
<H1>Heading 1</H1>
|
||||
<H2>Heading 2</H2>
|
||||
<H3>Heading 3</H3>
|
||||
<H4>Heading 4</H4>
|
||||
<H5>Heading 5</H5>
|
||||
<H6>Heading 6</H6>
|
||||
</div>,
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
|
||||
"Heading 1",
|
||||
);
|
||||
expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent(
|
||||
"Heading 2",
|
||||
);
|
||||
expect(screen.getByRole("heading", { level: 3 })).toHaveTextContent(
|
||||
"Heading 3",
|
||||
);
|
||||
expect(screen.getByRole("heading", { level: 4 })).toHaveTextContent(
|
||||
"Heading 4",
|
||||
);
|
||||
expect(screen.getByRole("heading", { level: 5 })).toHaveTextContent(
|
||||
"Heading 5",
|
||||
);
|
||||
expect(screen.getByRole("heading", { level: 6 })).toHaveTextContent(
|
||||
"Heading 6",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import { USE_PLANNING_AGENT } from "#/utils/feature-flags";
|
||||
import { Typography } from "#/ui/typography";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { MarkdownRenderer } from "#/components/features/markdown/markdown-renderer";
|
||||
import { planHeadings } from "#/components/features/markdown/plan-headings";
|
||||
|
||||
const MAX_CONTENT_LENGTH = 300;
|
||||
|
||||
@@ -66,7 +67,7 @@ export function PlanPreview({
|
||||
>
|
||||
{truncatedContent && (
|
||||
<>
|
||||
<MarkdownRenderer includeStandard includeHeadings>
|
||||
<MarkdownRenderer includeStandard components={planHeadings}>
|
||||
{truncatedContent}
|
||||
</MarkdownRenderer>
|
||||
{planContent && planContent.length > MAX_CONTENT_LENGTH && (
|
||||
|
||||
35
frontend/src/components/features/markdown/plan-headings.tsx
Normal file
35
frontend/src/components/features/markdown/plan-headings.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from "react";
|
||||
|
||||
// Custom heading components for plan views with reduced font sizes and tighter spacing
|
||||
export const planHeadings = {
|
||||
h1: ({ children }: { children?: React.ReactNode }) => (
|
||||
<h1 className="text-lg text-white font-bold leading-6 mb-1.5 mt-3 first:mt-0">
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children }: { children?: React.ReactNode }) => (
|
||||
<h2 className="text-base font-semibold leading-5 text-white mb-1 mt-2.5 first:mt-0">
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
h3: ({ children }: { children?: React.ReactNode }) => (
|
||||
<h3 className="text-sm font-semibold text-white mb-1 mt-2 first:mt-0">
|
||||
{children}
|
||||
</h3>
|
||||
),
|
||||
h4: ({ children }: { children?: React.ReactNode }) => (
|
||||
<h4 className="text-sm font-semibold text-white mb-1 mt-2 first:mt-0">
|
||||
{children}
|
||||
</h4>
|
||||
),
|
||||
h5: ({ children }: { children?: React.ReactNode }) => (
|
||||
<h5 className="text-xs font-semibold text-white mb-0.5 mt-1.5 first:mt-0">
|
||||
{children}
|
||||
</h5>
|
||||
),
|
||||
h6: ({ children }: { children?: React.ReactNode }) => (
|
||||
<h6 className="text-xs font-medium text-gray-300 mb-0.5 mt-1.5 first:mt-0">
|
||||
{children}
|
||||
</h6>
|
||||
),
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import LessonPlanIcon from "#/icons/lesson-plan.svg?react";
|
||||
import { useConversationStore } from "#/stores/conversation-store";
|
||||
import { useScrollToBottom } from "#/hooks/use-scroll-to-bottom";
|
||||
import { MarkdownRenderer } from "#/components/features/markdown/markdown-renderer";
|
||||
import { planHeadings } from "#/components/features/markdown/plan-headings";
|
||||
import { useHandlePlanClick } from "#/hooks/use-handle-plan-click";
|
||||
|
||||
function PlannerTab() {
|
||||
@@ -23,7 +24,7 @@ function PlannerTab() {
|
||||
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
|
||||
className="flex flex-col w-full h-full p-4 overflow-auto"
|
||||
>
|
||||
<MarkdownRenderer includeStandard includeHeadings>
|
||||
<MarkdownRenderer includeStandard components={planHeadings}>
|
||||
{planContent}
|
||||
</MarkdownRenderer>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user