Skip to content

Commit e2b997c

Browse files
committed
Fix mermaid xychart rendering and ignore playwright artifacts
1 parent c865ded commit e2b997c

2 files changed

Lines changed: 101 additions & 2 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ backend/math_to_manim_temp/
6363
backend/test_gen_only.py
6464
backend/test_manim_tool.py
6565

66+
# Playwright artifacts
67+
output/playwright/
68+
6669
# Deployment Info (Generated)
6770
# API_ENDPOINT.md (Choosing to commit this for team visibility, uncomment if local only)
6871
fpai-cbcb2-firebase-adminsdk-fbsvc-5feb40f951.json

frontend/src/components/ui/RichText.tsx

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
YouTubeEmbedBlock,
2626
getVariantStyles,
2727
} from "@/components/block-types"
28-
import { parseGraphFromTable, type GraphType } from "@/lib/graphFromTable"
28+
import { parseGraphFromTable, type GraphType, type ParsedGraph } from "@/lib/graphFromTable"
2929
import { AsyncVideo } from "@/components/block-types/Video"
3030
import type { QuizQuestion } from "@/types/block"
3131
import type { BlockVariant, VariantStyle } from "@/types/block-types"
@@ -41,6 +41,58 @@ function parseGraphTag(line: string): GraphType | null {
4141
return match[1].toLowerCase() as GraphType
4242
}
4343

44+
function parseMermaidList(raw: string): string[] {
45+
const inner = raw.trim().replace(/^\[/, "").replace(/\]$/, "")
46+
if (!inner) return []
47+
const tokens: string[] = []
48+
const re = /"([^"]+)"|'([^']+)'|([^,\]]+)/g
49+
let match: RegExpExecArray | null
50+
while ((match = re.exec(inner)) !== null) {
51+
const value = (match[1] ?? match[2] ?? match[3] ?? "").trim()
52+
if (value) tokens.push(value)
53+
}
54+
return tokens
55+
}
56+
57+
function parseMermaidNumbers(raw: string): number[] {
58+
const inner = raw.trim().replace(/^\[/, "").replace(/\]$/, "")
59+
if (!inner) return []
60+
return inner
61+
.split(",")
62+
.map((part) => Number(part.trim()))
63+
.filter((value) => Number.isFinite(value))
64+
}
65+
66+
function parseMermaidXyChart(code: string): ParsedGraph | null {
67+
if (!/\bxychart-beta\b/i.test(code)) return null
68+
69+
const xAxisMatch = code.match(/^\s*x-axis\s+(.+)$/im)
70+
const yAxisMatch = code.match(/^\s*y-axis\s+"([^"]+)"/im)
71+
const barMatch = code.match(/^\s*bar\s+\[([^\]]+)\]/im)
72+
const lineMatch = code.match(/^\s*line\s+\[([^\]]+)\]/im)
73+
74+
const values = barMatch
75+
? parseMermaidNumbers(barMatch[1])
76+
: lineMatch
77+
? parseMermaidNumbers(lineMatch[1])
78+
: []
79+
if (values.length === 0) return null
80+
81+
const parsedLabels = xAxisMatch ? parseMermaidList(xAxisMatch[1]) : []
82+
const labels = Array.from({ length: values.length }, (_, idx) => parsedLabels[idx] || `Item ${idx + 1}`)
83+
const count = Math.min(labels.length, values.length)
84+
if (count === 0) return null
85+
86+
return {
87+
type: barMatch ? "bar" : "line",
88+
labels: labels.slice(0, count),
89+
series: [{
90+
name: yAxisMatch?.[1]?.trim() || "Value",
91+
values: values.slice(0, count),
92+
}],
93+
}
94+
}
95+
4496
function findNextIndex(haystack: string, from: number, needle: string): number {
4597
const idx = haystack.indexOf(needle, from)
4698
return idx >= 0 ? idx : Number.POSITIVE_INFINITY
@@ -1045,10 +1097,54 @@ export function Markdownish({
10451097
}
10461098
if (endIndex >= 0) {
10471099
i = endIndex
1100+
const code = collected.join("\n")
1101+
const lowerLang = lang.toLowerCase()
1102+
if (lowerLang === "mermaid") {
1103+
const mermaidGraph = parseMermaidXyChart(code)
1104+
if (mermaidGraph) {
1105+
const graphSnippet = `\`\`\`mermaid\n${code}\n\`\`\``
1106+
const graphActions = effectiveSourceBlockId
1107+
? resolveBlockActions("graph", { sourceBlockId: effectiveSourceBlockId }, {
1108+
chart: mermaidGraph,
1109+
markdownSnippet: graphSnippet,
1110+
})
1111+
: []
1112+
const graphAnalyze = graphActions.find((action) => action.request.actionType === "analyze")
1113+
const graphConvertActions = graphActions.filter((action) => action.request.actionType === "convert")
1114+
elements.push(
1115+
<GraphBlock
1116+
key={`code-graph-${i}`}
1117+
chart={mermaidGraph}
1118+
variant={variant}
1119+
COLORS={COLORS}
1120+
onAnalyze={
1121+
graphAnalyze && onBlockAction
1122+
? () => onBlockAction({ ...graphAnalyze.request, sourceBlockId: effectiveSourceBlockId })
1123+
: undefined
1124+
}
1125+
onConvertType={
1126+
graphConvertActions.length > 0 && onBlockAction
1127+
? (targetType) =>
1128+
(() => {
1129+
const matched =
1130+
graphConvertActions.find((action) => action.request.targetGraphType === targetType) ||
1131+
graphConvertActions[0]
1132+
onBlockAction({
1133+
...matched.request,
1134+
sourceBlockId: effectiveSourceBlockId,
1135+
})
1136+
})()
1137+
: undefined
1138+
}
1139+
/>,
1140+
)
1141+
continue
1142+
}
1143+
}
10481144
elements.push(
10491145
<CodeBlock
10501146
key={`code-${i}`}
1051-
code={collected.join("\n")}
1147+
code={code}
10521148
language={lang}
10531149
variant={variant}
10541150
COLORS={COLORS}

0 commit comments

Comments
 (0)