@@ -5,6 +5,7 @@ import { useEffect, useMemo } from "react";
55
66import { Card } from "@/components/ui" ;
77import { ItemLink } from "@/components/ItemLink" ;
8+ import { ProseWithLinks , hasInlineRefs } from "@/components/ProseWithLinks" ;
89import { SeverityBadge } from "@/components/SeverityBadge" ;
910import { SpellLink } from "@/components/SpellLink" ;
1011import { formatPercent } from "@/lib/format" ;
@@ -26,6 +27,12 @@ export function AnalysisCard({ analysis, locale }: Props) {
2627 // for the spell/item chips even when ad blockers stop wowhead's tooltip
2728 // script from auto-renaming the link.
2829 const nameMap = structured . _localized_names ?? { } ;
30+ // ``_talent_spell_ids`` is the per-analysis lookup the backend ships so
31+ // we can render ``[Label](talent:<traitNodeEntryId>)`` markdown links
32+ // as Wowhead spell links. WCL ships TraitNodeEntry IDs which collide
33+ // with old MoP spell IDs, so we MUST resolve to the underlying spell
34+ // ID before linking.
35+ const talentSpellIds = structured . _talent_spell_ids ?? { } ;
2936 const parseMetrics = structured . _parse_metrics ;
3037
3138 // Wowhead's tooltip script (loaded once at the layout level) only scans
@@ -88,7 +95,15 @@ export function AnalysisCard({ analysis, locale }: Props) {
8895 < div className = "space-y-4" >
8996 < Card >
9097 < div className = "flex flex-wrap items-baseline justify-between gap-3" >
91- < h2 className = "text-xl font-semibold" > { s . headline ?? "—" } </ h2 >
98+ < h2 className = "text-xl font-semibold" >
99+ < ProseWithLinks
100+ text = { s . headline ?? "—" }
101+ locale = { locale }
102+ nameMap = { nameMap }
103+ talentSpellIds = { talentSpellIds }
104+ className = ""
105+ />
106+ </ h2 >
92107 < div className = "flex flex-wrap items-baseline gap-2" >
93108 { parseMetrics ?. parse_percent !== null && parseMetrics ?. parse_percent !== undefined ? (
94109 < span
@@ -169,8 +184,19 @@ export function AnalysisCard({ analysis, locale }: Props) {
169184 ) }
170185 </ div >
171186 < h4 className = "mt-2 text-base font-semibold text-zinc-100" > { f . title } </ h4 >
172- < p className = "mt-1 whitespace-pre-line text-sm text-zinc-200" > { f . detail } </ p >
173- { f . related_spell_ids ?. length || f . related_item_ids ?. length ? (
187+ < ProseWithLinks
188+ text = { f . detail }
189+ locale = { locale }
190+ nameMap = { nameMap }
191+ talentSpellIds = { talentSpellIds }
192+ className = "mt-1 text-sm text-zinc-200"
193+ />
194+ { /* Chip list only as a fallback for legacy analyses without
195+ inline markdown links in their prose — modern outputs
196+ render the links inline above and the chips become
197+ redundant. */ }
198+ { ! hasInlineRefs ( f . detail ) &&
199+ ( f . related_spell_ids ?. length || f . related_item_ids ?. length ) ? (
174200 < div className = "mt-2 flex flex-wrap gap-2 text-xs" >
175201 { f . related_spell_ids ?. map ( ( id ) => (
176202 < SpellLink
@@ -196,12 +222,12 @@ export function AnalysisCard({ analysis, locale }: Props) {
196222 </ Card >
197223
198224 < div className = "grid gap-4 md:grid-cols-2" >
199- { textBlock ( t ( "analyze.rotation" ) , s . rotation_summary ) }
200- { textBlock ( t ( "analyze.cooldowns" ) , s . cooldown_usage_summary ) }
201- { textBlock ( t ( "analyze.stats" ) , s . stat_recommendations ) }
202- { textBlock ( t ( "analyze.talents" ) , s . talent_recommendations ) }
203- { textBlock ( t ( "analyze.gearTrinkets" ) , s . gear_and_trinket_notes ) }
204- { textBlock ( t ( "analyze.comparison" ) , s . comparison_to_top_logs ) }
225+ { proseBlock ( t ( "analyze.rotation" ) , s . rotation_summary , locale , nameMap , talentSpellIds ) }
226+ { proseBlock ( t ( "analyze.cooldowns" ) , s . cooldown_usage_summary , locale , nameMap , talentSpellIds ) }
227+ { proseBlock ( t ( "analyze.stats" ) , s . stat_recommendations , locale , nameMap , talentSpellIds ) }
228+ { proseBlock ( t ( "analyze.talents" ) , s . talent_recommendations , locale , nameMap , talentSpellIds ) }
229+ { proseBlock ( t ( "analyze.gearTrinkets" ) , s . gear_and_trinket_notes , locale , nameMap , talentSpellIds ) }
230+ { proseBlock ( t ( "analyze.comparison" ) , s . comparison_to_top_logs , locale , nameMap , talentSpellIds ) }
205231 </ div >
206232 </ div >
207233 ) ;
@@ -211,12 +237,23 @@ function severityRank(s: string): number {
211237 return ( { critical : 0 , high : 1 , medium : 2 , low : 3 , info : 4 } as Record < string , number > ) [ s ] ?? 5 ;
212238}
213239
214- function textBlock ( title : string , body : string | undefined ) {
240+ function proseBlock (
241+ title : string ,
242+ body : string | undefined ,
243+ locale : Locale ,
244+ nameMap : Record < string , string > ,
245+ talentSpellIds : Record < string , number > ,
246+ ) {
215247 if ( ! body ) return null ;
216248 return (
217249 < Card >
218250 < h3 className = "mb-2 text-sm font-semibold uppercase tracking-wide text-zinc-400" > { title } </ h3 >
219- < p className = "whitespace-pre-line text-sm text-zinc-200" > { body } </ p >
251+ < ProseWithLinks
252+ text = { body }
253+ locale = { locale }
254+ nameMap = { nameMap }
255+ talentSpellIds = { talentSpellIds }
256+ />
220257 </ Card >
221258 ) ;
222259}
0 commit comments