mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-29 20:07:19 +02:00
🌗 refactor: Consistent Mermaid Theming for Inline and Artifact Renderers (#12055)
* refactor: consistent theming between inline and Artifacts Mermaid Diagram * refactor: Enhance Mermaid component with improved theming and security features - Updated Mermaid component to utilize useCallback for performance optimization. - Increased maximum zoom level from 4 to 10 for better diagram visibility. - Added security level configuration to Mermaid initialization for enhanced security. - Refactored theme handling to ensure consistent theming between inline and artifact diagrams. - Introduced unit tests for Mermaid configuration to validate flowchart settings and theme behavior. * refactor: Improve theme handling in useMermaid hook - Enhanced theme variable management by merging custom theme variables with default values for dark mode. - Ensured consistent theming across Mermaid diagrams by preserving existing theme configurations while applying new defaults. * refactor: Consolidate imports in mermaid test file - Combined multiple imports from the mermaid utility into a single statement for improved readability and organization in the test file. * feat: Add subgraph title contrast adjustment for Mermaid diagrams - Introduced a utility function to enhance text visibility on subgraph titles by adjusting the fill color based on background luminance. - Updated the Mermaid component to utilize this function, ensuring better contrast in rendered SVGs. - Added comprehensive unit tests to validate the contrast adjustment logic across various scenarios. * refactor: Update MermaidHeader component for improved button accessibility and styling - Replaced Button components with TooltipAnchor for better accessibility and user experience. - Consolidated button styles into a single class for consistency. - Enhanced the layout and spacing of the header for a cleaner appearance. * fix: hex color handling and improve contrast adjustment in Mermaid diagrams - Updated hexLuminance function to support 3-character hex shorthand by expanding it to 6 characters. - Refined the fixSubgraphTitleContrast function to avoid double semicolons in style attributes and ensure proper fill color adjustments based on background luminance. - Added unit tests to validate the handling of 3-character hex fills and the prevention of double semicolons in text styles. * chore: Simplify Virtual Scrolling Performance tests by removing performance timing checks - Removed performance timing checks and associated console logs from tests handling 1000 and 5000 agents. - Focused tests on verifying the correct rendering of virtual list items without measuring render time.
This commit is contained in:
parent
6ebee069c7
commit
f1eabdbdb7
8 changed files with 671 additions and 309 deletions
172
client/src/utils/__tests__/mermaid.test.ts
Normal file
172
client/src/utils/__tests__/mermaid.test.ts
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import {
|
||||
fixSubgraphTitleContrast,
|
||||
artifactFlowchartConfig,
|
||||
inlineFlowchartConfig,
|
||||
getMermaidFiles,
|
||||
} from '~/utils/mermaid';
|
||||
|
||||
const makeSvg = (clusters: string): Element => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg">${clusters}</svg>`,
|
||||
'image/svg+xml',
|
||||
);
|
||||
return doc.querySelector('svg')!;
|
||||
};
|
||||
|
||||
describe('mermaid config', () => {
|
||||
describe('flowchart config invariants', () => {
|
||||
it('inlineFlowchartConfig must have htmlLabels: false for blob URL <img> rendering', () => {
|
||||
expect(inlineFlowchartConfig.htmlLabels).toBe(false);
|
||||
});
|
||||
|
||||
it('artifactFlowchartConfig must have htmlLabels: true for direct DOM injection', () => {
|
||||
expect(artifactFlowchartConfig.htmlLabels).toBe(true);
|
||||
});
|
||||
|
||||
it('both configs share the same base layout settings', () => {
|
||||
expect(inlineFlowchartConfig.curve).toBe(artifactFlowchartConfig.curve);
|
||||
expect(inlineFlowchartConfig.nodeSpacing).toBe(artifactFlowchartConfig.nodeSpacing);
|
||||
expect(inlineFlowchartConfig.rankSpacing).toBe(artifactFlowchartConfig.rankSpacing);
|
||||
expect(inlineFlowchartConfig.padding).toBe(artifactFlowchartConfig.padding);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMermaidFiles', () => {
|
||||
const content = 'graph TD\n A-->B';
|
||||
|
||||
it('produces dark theme files when isDarkMode is true', () => {
|
||||
const files = getMermaidFiles(content, true);
|
||||
expect(files['/components/ui/MermaidDiagram.tsx']).toContain('theme: "dark"');
|
||||
expect(files['mermaid.css']).toContain('#212121');
|
||||
});
|
||||
|
||||
it('produces neutral theme files when isDarkMode is false', () => {
|
||||
const files = getMermaidFiles(content, false);
|
||||
expect(files['/components/ui/MermaidDiagram.tsx']).toContain('theme: "neutral"');
|
||||
expect(files['mermaid.css']).toContain('#FFFFFF');
|
||||
});
|
||||
|
||||
it('defaults to dark mode when isDarkMode is omitted', () => {
|
||||
const files = getMermaidFiles(content);
|
||||
expect(files['/components/ui/MermaidDiagram.tsx']).toContain('theme: "dark"');
|
||||
});
|
||||
|
||||
it('includes securityLevel in generated component', () => {
|
||||
const files = getMermaidFiles(content, true);
|
||||
expect(files['/components/ui/MermaidDiagram.tsx']).toContain('securityLevel: "strict"');
|
||||
});
|
||||
|
||||
it('includes all required file keys', () => {
|
||||
const files = getMermaidFiles(content, true);
|
||||
expect(files['diagram.mmd']).toBe(content);
|
||||
expect(files['App.tsx']).toBeDefined();
|
||||
expect(files['index.tsx']).toBeDefined();
|
||||
expect(files['/components/ui/MermaidDiagram.tsx']).toBeDefined();
|
||||
expect(files['mermaid.css']).toBeDefined();
|
||||
});
|
||||
|
||||
it('uses artifact flowchart config with htmlLabels: true', () => {
|
||||
const files = getMermaidFiles(content, true);
|
||||
expect(files['/components/ui/MermaidDiagram.tsx']).toContain('"htmlLabels": true');
|
||||
});
|
||||
|
||||
it('does not inject custom themeVariables into generated component', () => {
|
||||
const darkFiles = getMermaidFiles(content, true);
|
||||
const lightFiles = getMermaidFiles(content, false);
|
||||
expect(darkFiles['/components/ui/MermaidDiagram.tsx']).not.toContain('themeVariables');
|
||||
expect(lightFiles['/components/ui/MermaidDiagram.tsx']).not.toContain('themeVariables');
|
||||
});
|
||||
|
||||
it('handles empty content', () => {
|
||||
const files = getMermaidFiles('', true);
|
||||
expect(files['diagram.mmd']).toBe('# No mermaid diagram content provided');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fixSubgraphTitleContrast', () => {
|
||||
it('darkens title text on light subgraph backgrounds (fill attribute)', () => {
|
||||
const svg = makeSvg(
|
||||
'<g class="cluster"><rect fill="#FFF9C4"/><g class="cluster-label"><text fill="#E0E0E0">Title</text></g></g>',
|
||||
);
|
||||
fixSubgraphTitleContrast(svg);
|
||||
expect(svg.querySelector('text')!.getAttribute('style')).toContain('fill: #1a1a1a');
|
||||
});
|
||||
|
||||
it('darkens title text on light subgraph backgrounds (inline style fill)', () => {
|
||||
const svg = makeSvg(
|
||||
'<g class="cluster"><rect style="fill: #FFF9C4; stroke: #F9A825"/><g class="cluster-label"><text>Title</text></g></g>',
|
||||
);
|
||||
fixSubgraphTitleContrast(svg);
|
||||
expect(svg.querySelector('text')!.getAttribute('style')).toContain('fill: #1a1a1a');
|
||||
});
|
||||
|
||||
it('lightens title text on dark subgraph backgrounds', () => {
|
||||
const svg = makeSvg(
|
||||
'<g class="cluster"><rect fill="#1f2020"/><g class="cluster-label"><text fill="#222222">Title</text></g></g>',
|
||||
);
|
||||
fixSubgraphTitleContrast(svg);
|
||||
expect(svg.querySelector('text')!.getAttribute('style')).toContain('fill: #f0f0f0');
|
||||
});
|
||||
|
||||
it('leaves title text alone when contrast is already good', () => {
|
||||
const svg = makeSvg(
|
||||
'<g class="cluster"><rect fill="#FFF9C4"/><g class="cluster-label"><text fill="#333333">Title</text></g></g>',
|
||||
);
|
||||
fixSubgraphTitleContrast(svg);
|
||||
expect(svg.querySelector('text')!.getAttribute('style')).toBeNull();
|
||||
});
|
||||
|
||||
it('skips clusters without a rect', () => {
|
||||
const svg = makeSvg(
|
||||
'<g class="cluster"><g class="cluster-label"><text fill="#E0E0E0">Title</text></g></g>',
|
||||
);
|
||||
fixSubgraphTitleContrast(svg);
|
||||
expect(svg.querySelector('text')!.getAttribute('style')).toBeNull();
|
||||
});
|
||||
|
||||
it('skips clusters with non-hex fills', () => {
|
||||
const svg = makeSvg(
|
||||
'<g class="cluster"><rect fill="rgb(255,249,196)"/><g class="cluster-label"><text fill="#E0E0E0">Title</text></g></g>',
|
||||
);
|
||||
fixSubgraphTitleContrast(svg);
|
||||
expect(svg.querySelector('text')!.getAttribute('style')).toBeNull();
|
||||
});
|
||||
|
||||
it('sets dark fill when text has no explicit fill on light backgrounds', () => {
|
||||
const svg = makeSvg(
|
||||
'<g class="cluster"><rect style="fill:#FFF9C4"/><g class="cluster-label"><text>Title</text></g></g>',
|
||||
);
|
||||
fixSubgraphTitleContrast(svg);
|
||||
expect(svg.querySelector('text')!.getAttribute('style')).toContain('fill: #1a1a1a');
|
||||
});
|
||||
|
||||
it('preserves existing text style when appending fill override', () => {
|
||||
const svg = makeSvg(
|
||||
'<g class="cluster"><rect fill="#FFF9C4"/><g class="cluster-label"><text style="font-size: 14px" fill="#E0E0E0">Title</text></g></g>',
|
||||
);
|
||||
fixSubgraphTitleContrast(svg);
|
||||
const style = svg.querySelector('text')!.getAttribute('style')!;
|
||||
expect(style).toContain('font-size: 14px');
|
||||
expect(style).toContain('fill: #1a1a1a');
|
||||
});
|
||||
|
||||
it('handles 3-char hex shorthand fills', () => {
|
||||
const svg = makeSvg(
|
||||
'<g class="cluster"><rect fill="#FFC"/><g class="cluster-label"><text fill="#EEE">Title</text></g></g>',
|
||||
);
|
||||
fixSubgraphTitleContrast(svg);
|
||||
expect(svg.querySelector('text')!.getAttribute('style')).toContain('fill: #1a1a1a');
|
||||
});
|
||||
|
||||
it('avoids double semicolons when existing style has trailing semicolon', () => {
|
||||
const svg = makeSvg(
|
||||
'<g class="cluster"><rect fill="#FFF9C4"/><g class="cluster-label"><text style="font-size: 14px;" fill="#E0E0E0">Title</text></g></g>',
|
||||
);
|
||||
fixSubgraphTitleContrast(svg);
|
||||
const style = svg.querySelector('text')!.getAttribute('style')!;
|
||||
expect(style).not.toContain(';;');
|
||||
expect(style).toContain('fill: #1a1a1a');
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue