Skip to content

Commit 07cdd77

Browse files
committed
Fix WQIS markdown rendering: parse **bold**, *italic*, and * bullet points
1 parent e8c07d6 commit 07cdd77

2 files changed

Lines changed: 48 additions & 10 deletions

File tree

src/components/ai/WqisReportModal.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,33 @@ const LABELS: Record<Period, string> = { '7d': 'Weekly', '30d': 'Monthly', '90d'
66
interface ReportData { period: Period; report: string; generatedAt: string; }
77
interface Props { onClose: () => void; }
88

9+
function InlineMarkdown({ text }: { text: string }) {
10+
const parts: React.ReactNode[] = [];
11+
let remaining = text;
12+
let key = 0;
13+
while (remaining.length > 0) {
14+
const match = remaining.match(/(\*{3})(.+?)\1|(\*{2})(.+?)\3|(\*{1})(.+?)\5/);
15+
if (!match) { parts.push(remaining); break; }
16+
const idx = match.index!;
17+
if (idx > 0) parts.push(remaining.slice(0, idx));
18+
if (match[1] === '***') parts.push(<strong key={key++} className="font-semibold italic">{match[2]}</strong>);
19+
else if (match[3] === '**') parts.push(<strong key={key++} className="font-semibold">{match[4]}</strong>);
20+
else parts.push(<em key={key++}>{match[6]}</em>);
21+
remaining = remaining.slice(idx + match[0].length);
22+
}
23+
return <>{parts}</>;
24+
}
25+
926
function Lines({ text }: { text: string }) {
1027
return (
1128
<div className="space-y-2 text-sm text-[#374151] dark:text-[#E5E7EB]">
1229
{text.split('\n').map((line, i) => {
13-
if (line.startsWith('# ')) return <h2 key={i} className="text-base font-semibold text-[#111827] dark:text-[#F3F4F6] mt-4">{line.slice(2)}</h2>;
14-
if (line.startsWith('## ')) return <h3 key={i} className="text-sm font-semibold text-[#1F2937] dark:text-[#E5E7EB] mt-3">{line.slice(3)}</h3>;
15-
if (line.startsWith('- ') || line.startsWith('• ')) return <li key={i} className="ml-4 list-disc">{line.slice(2)}</li>;
16-
if (line.trim() === '') return <div key={i} className="h-1" />;
17-
return <p key={i} className="leading-relaxed">{line}</p>;
30+
const trimmed = line.trimStart();
31+
if (trimmed.startsWith('# ')) return <h2 key={i} className="text-base font-semibold text-[#111827] dark:text-[#F3F4F6] mt-4">{trimmed.slice(2)}</h2>;
32+
if (trimmed.startsWith('## ')) return <h3 key={i} className="text-sm font-semibold text-[#1F2937] dark:text-[#E5E7EB] mt-3"><InlineMarkdown text={trimmed.slice(3)} /></h3>;
33+
if (trimmed.startsWith('- ') || trimmed.startsWith('• ') || trimmed.startsWith('* ')) return <li key={i} className="ml-4 list-disc leading-relaxed"><InlineMarkdown text={trimmed.slice(2)} /></li>;
34+
if (trimmed === '') return <div key={i} className="h-1" />;
35+
return <p key={i} className="leading-relaxed"><InlineMarkdown text={line} /></p>;
1836
})}
1937
</div>
2038
);

src/components/ai/WqisStationAnalysis.tsx

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,35 @@ import { useState, useEffect, useCallback } from 'react';
44
interface AnalysisData { stationId: string; analysis: string; analyzedAt: string; }
55
interface Props { stationId: string; stationName?: string; onClose: () => void; }
66

7+
function InlineMarkdown({ text }: { text: string }) {
8+
// Convert inline **bold**, *italic*, and ***bold italic*** to React elements
9+
const parts: React.ReactNode[] = [];
10+
let remaining = text;
11+
let key = 0;
12+
while (remaining.length > 0) {
13+
// Match ***bold italic***, **bold**, or *italic* (in that priority order)
14+
const match = remaining.match(/(\*{3})(.+?)\1|(\*{2})(.+?)\3|(\*{1})(.+?)\5/);
15+
if (!match) { parts.push(remaining); break; }
16+
const idx = match.index!;
17+
if (idx > 0) parts.push(remaining.slice(0, idx));
18+
if (match[1] === '***') parts.push(<strong key={key++} className="font-semibold italic">{match[2]}</strong>);
19+
else if (match[3] === '**') parts.push(<strong key={key++} className="font-semibold">{match[4]}</strong>);
20+
else parts.push(<em key={key++}>{match[6]}</em>);
21+
remaining = remaining.slice(idx + match[0].length);
22+
}
23+
return <>{parts}</>;
24+
}
25+
726
function Lines({ text }: { text: string }) {
827
return (
928
<div className="space-y-2 text-sm text-[#374151] dark:text-[#E5E7EB]">
1029
{text.split('\n').map((line, i) => {
11-
if (line.startsWith('# ')) return <h2 key={i} className="text-base font-semibold text-[#111827] dark:text-[#F3F4F6] mt-4 first:mt-0">{line.slice(2)}</h2>;
12-
if (line.startsWith('## ')) return <h3 key={i} className="text-sm font-semibold text-[#1F2937] dark:text-[#E5E7EB] mt-3">{line.slice(3)}</h3>;
13-
if (line.startsWith('- ') || line.startsWith('• ')) return <li key={i} className="ml-4 list-disc leading-relaxed">{line.slice(2)}</li>;
14-
if (line.trim() === '') return <div key={i} className="h-1" />;
15-
return <p key={i} className="leading-relaxed">{line}</p>;
30+
const trimmed = line.trimStart();
31+
if (trimmed.startsWith('# ')) return <h2 key={i} className="text-base font-semibold text-[#111827] dark:text-[#F3F4F6] mt-4 first:mt-0"><InlineMarkdown text={trimmed.slice(2)} /></h2>;
32+
if (trimmed.startsWith('## ')) return <h3 key={i} className="text-sm font-semibold text-[#1F2937] dark:text-[#E5E7EB] mt-3"><InlineMarkdown text={trimmed.slice(3)} /></h3>;
33+
if (trimmed.startsWith('- ') || trimmed.startsWith('• ') || trimmed.startsWith('* ')) return <li key={i} className="ml-4 list-disc leading-relaxed"><InlineMarkdown text={trimmed.slice(2)} /></li>;
34+
if (trimmed === '') return <div key={i} className="h-1" />;
35+
return <p key={i} className="leading-relaxed"><InlineMarkdown text={line} /></p>;
1636
})}
1737
</div>
1838
);

0 commit comments

Comments
 (0)