Skip to content

Commit 13016ee

Browse files
committed
fix(web-evals): fix JSX syntax error and use safe React elements for log highlighting
- Fixed malformed JSX in formatLogContent function (duplicate nested div elements) - Replaced HTML string injection with proper React elements for XSS-safe syntax highlighting - Addresses review feedback about dangerouslySetInnerHTML security concern
1 parent 382d25f commit 13016ee

File tree

1 file changed

+85
-48
lines changed
  • apps/web-evals/src/app/runs/[id]

1 file changed

+85
-48
lines changed

apps/web-evals/src/app/runs/[id]/run.tsx

Lines changed: 85 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -42,60 +42,97 @@ function getToolAbbreviation(toolName: string): string {
4242
.join("")
4343
}
4444

45-
// Escape HTML to prevent XSS
46-
function escapeHtml(text: string): string {
47-
return text
48-
.replace(/&/g, "&")
49-
.replace(/</g, "&lt;")
50-
.replace(/>/g, "&gt;")
51-
.replace(/"/g, "&quot;")
52-
.replace(/'/g, "&#039;")
45+
// Pattern definitions for syntax highlighting
46+
type HighlightPattern = {
47+
pattern: RegExp
48+
className: string
49+
// If true, wraps the entire match; if a number, wraps that capture group
50+
wrapGroup?: number
5351
}
5452

55-
// Format log content with basic highlighting (XSS-safe)
56-
function formatLogContent(log: string): React.ReactNode[] {
57-
const lines = log.split("\n")
58-
return lines.map((line, index) => {
59-
// First escape the entire line to prevent XSS
60-
let formattedLine = escapeHtml(line)
61-
62-
// Highlight timestamps [YYYY-MM-DDTHH:MM:SS.sssZ]
63-
formattedLine = formattedLine.replace(
64-
/\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)\]/g,
65-
'<span class="text-blue-400">[$1]</span>',
66-
)
53+
const HIGHLIGHT_PATTERNS: HighlightPattern[] = [
54+
// Timestamps [YYYY-MM-DDTHH:MM:SS.sssZ]
55+
{ pattern: /\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)\]/g, className: "text-blue-400" },
56+
// Log levels
57+
{ pattern: /\|\s*(INFO)\s*\|/g, className: "text-green-400", wrapGroup: 1 },
58+
{ pattern: /\|\s*(WARN|WARNING)\s*\|/g, className: "text-yellow-400", wrapGroup: 1 },
59+
{ pattern: /\|\s*(ERROR)\s*\|/g, className: "text-red-400", wrapGroup: 1 },
60+
{ pattern: /\|\s*(DEBUG)\s*\|/g, className: "text-gray-400", wrapGroup: 1 },
61+
// Task identifiers
62+
{ pattern: /(taskCreated|taskFocused|taskStarted|taskCompleted|EvalPass|EvalFail)/g, className: "text-purple-400" },
63+
// Message arrows
64+
{ pattern: //g, className: "text-cyan-400" },
65+
]
66+
67+
// Format a single line with syntax highlighting using React elements (XSS-safe)
68+
function formatLine(line: string): React.ReactNode[] {
69+
// Find all matches with their positions
70+
type Match = { start: number; end: number; text: string; className: string }
71+
const matches: Match[] = []
72+
73+
for (const { pattern, className, wrapGroup } of HIGHLIGHT_PATTERNS) {
74+
// Reset regex state
75+
pattern.lastIndex = 0
76+
let regexMatch
77+
while ((regexMatch = pattern.exec(line)) !== null) {
78+
const capturedText = wrapGroup !== undefined ? regexMatch[wrapGroup] : regexMatch[0]
79+
// Skip if capture group didn't match
80+
if (!capturedText) continue
81+
const start =
82+
wrapGroup !== undefined ? regexMatch.index + regexMatch[0].indexOf(capturedText) : regexMatch.index
83+
matches.push({
84+
start,
85+
end: start + capturedText.length,
86+
text: capturedText,
87+
className,
88+
})
89+
}
90+
}
91+
92+
// Sort matches by position and filter overlapping ones
93+
matches.sort((a, b) => a.start - b.start)
94+
const filteredMatches: Match[] = []
95+
for (const m of matches) {
96+
const lastMatch = filteredMatches[filteredMatches.length - 1]
97+
if (!lastMatch || m.start >= lastMatch.end) {
98+
filteredMatches.push(m)
99+
}
100+
}
67101

68-
// Highlight log levels
69-
formattedLine = formattedLine.replace(/\|\s*(INFO)\s*\|/g, '| <span class="text-green-400">$1</span> |')
70-
formattedLine = formattedLine.replace(
71-
/\|\s*(WARN|WARNING)\s*\|/g,
72-
'| <span class="text-yellow-400">$1</span> |',
73-
)
74-
formattedLine = formattedLine.replace(/\|\s*(ERROR)\s*\|/g, '| <span class="text-red-400">$1</span> |')
75-
formattedLine = formattedLine.replace(/\|\s*(DEBUG)\s*\|/g, '| <span class="text-gray-400">$1</span> |')
102+
// Build result with highlighted spans
103+
const result: React.ReactNode[] = []
104+
let currentPos = 0
76105

77-
// Highlight task identifiers like taskCreated, taskFocused, etc.
78-
formattedLine = formattedLine.replace(
79-
/(taskCreated|taskFocused|taskStarted|taskCompleted|EvalPass|EvalFail)/g,
80-
'<span class="text-purple-400">$1</span>',
106+
for (const [i, m] of filteredMatches.entries()) {
107+
// Add text before this match
108+
if (m.start > currentPos) {
109+
result.push(line.slice(currentPos, m.start))
110+
}
111+
// Add highlighted match
112+
result.push(
113+
<span key={`${i}-${m.start}`} className={m.className}>
114+
{m.text}
115+
</span>,
81116
)
117+
currentPos = m.end
118+
}
82119

83-
// Highlight message arrows (escaped as &rarr; after escapeHtml)
84-
formattedLine = formattedLine.replace(//g, '<span class="text-cyan-400">→</span>')
85-
86-
return (
87-
<div
88-
key={index}
89-
className="hover:bg-white/5"
90-
<div
91-
key={index}
92-
className="hover:bg-white/5"
93-
>
94-
{formattedLine || " "}
95-
</div>
96-
/>
97-
)
98-
})
120+
// Add remaining text
121+
if (currentPos < line.length) {
122+
result.push(line.slice(currentPos))
123+
}
124+
125+
return result.length > 0 ? result : [line]
126+
}
127+
128+
// Format log content with basic highlighting (XSS-safe - no dangerouslySetInnerHTML)
129+
function formatLogContent(log: string): React.ReactNode[] {
130+
const lines = log.split("\n")
131+
return lines.map((line, index) => (
132+
<div key={index} className="hover:bg-white/5">
133+
{line ? formatLine(line) : " "}
134+
</div>
135+
))
99136
}
100137

101138
export function Run({ run }: { run: Run }) {

0 commit comments

Comments
 (0)