Skip to content

Commit 137bf53

Browse files
Programcodeclaude
andcommitted
fix: harden AttackResults against malformed persisted results
Previously, accessing result.weaknesses.filter(...) and similar threw when a persisted attackResult from a prior schema was missing fields, which manifested as a fully blank page after navigating back to /app/attack. Now derives weaknesses/fallaciesDetected/crossExQuestions/strengthAreas as safe arrays and gates the weighing-analysis section so a stale store renders an empty/partial state instead of crashing the route. Independent of Part 1; safe to land anywhere on the branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 22dabe5 commit 137bf53

1 file changed

Lines changed: 62 additions & 51 deletions

File tree

‎src/components/features/attack/AttackResults.tsx‎

Lines changed: 62 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,18 @@ export function AttackResults({ result, onDiscuss }: AttackResultsProps) {
8585
const [isExportingGDocs, setIsExportingGDocs] = useState(false);
8686
const [showDriveAuth, setShowDriveAuth] = useState(false);
8787

88-
const criticalCount = result.weaknesses.filter(w => w.severity === 'critical').length;
89-
const majorCount = result.weaknesses.filter(w => w.severity === 'major').length;
90-
const minorCount = result.weaknesses.filter(w => w.severity === 'minor').length;
88+
// Guard against malformed/legacy persisted results so navigating back to
89+
// this page renders an empty state instead of crashing the route.
90+
const weaknesses = Array.isArray(result?.weaknesses) ? result.weaknesses : [];
91+
const fallaciesDetected = Array.isArray(result?.fallaciesDetected) ? result.fallaciesDetected : [];
92+
const crossExQuestions = Array.isArray(result?.crossExQuestions) ? result.crossExQuestions : [];
93+
const overallAssessment = result?.overallAssessment ?? { summary: '', score: 0, strengthAreas: [] };
94+
const strengthAreas = Array.isArray(overallAssessment.strengthAreas) ? overallAssessment.strengthAreas : [];
95+
const weighingAnalysis = result?.weighingAnalysis;
96+
97+
const criticalCount = weaknesses.filter(w => w.severity === 'critical').length;
98+
const majorCount = weaknesses.filter(w => w.severity === 'major').length;
99+
const minorCount = weaknesses.filter(w => w.severity === 'minor').length;
91100

92101
const handleDocxExport = async () => {
93102
const success = await exportAttackToDocx(result);
@@ -128,22 +137,22 @@ export function AttackResults({ result, onDiscuss }: AttackResultsProps) {
128137
{/* Overall Assessment */}
129138
<SectionCard
130139
title={t('attackResults.overall')}
131-
description={result.overallAssessment.summary}
140+
description={overallAssessment.summary}
132141
>
133142
<div className="space-y-4">
134143
<div>
135144
<div className="flex items-center justify-between mb-2">
136145
<span className="text-sm font-medium text-foreground">{t('attackResults.caseStrength')}</span>
137146
<span className={cn(
138147
'text-lg font-bold',
139-
result.overallAssessment.score >= 70 ? 'text-primary' :
140-
result.overallAssessment.score >= 50 ? 'text-amber-500' : 'text-destructive'
148+
overallAssessment.score >= 70 ? 'text-primary' :
149+
overallAssessment.score >= 50 ? 'text-amber-500' : 'text-destructive'
141150
)}>
142-
{result.overallAssessment.score}/100
151+
{overallAssessment.score}/100
143152
</span>
144153
</div>
145154
<Progress
146-
value={result.overallAssessment.score}
155+
value={overallAssessment.score}
147156
className="h-3"
148157
/>
149158
<div className="flex flex-wrap items-center justify-end gap-2 mt-3">
@@ -199,9 +208,9 @@ export function AttackResults({ result, onDiscuss }: AttackResultsProps) {
199208
<div>
200209
<h4 className="text-sm font-medium text-muted-foreground mb-2">{t('attackResults.strengths')}</h4>
201210
<ul className="space-y-1">
202-
{result.overallAssessment.strengthAreas.map((strength, i) => (
203-
<li key={i} className="flex items-start gap-2 text-sm">
204-
<span className="text-primary mt-1">✓</span>
211+
{strengthAreas.map((strength, i) => (
212+
<li key={i} className="flex items-baseline gap-2 text-sm">
213+
<span className="text-primary leading-none">✓</span>
205214
<span className="text-foreground">{strength}</span>
206215
</li>
207216
))}
@@ -213,26 +222,26 @@ export function AttackResults({ result, onDiscuss }: AttackResultsProps) {
213222
{/* Weaknesses */}
214223
< SectionCard
215224
title={t('attackResults.weaknesses')}
216-
description={`${result.weaknesses.length} ${t('attackResults.weaknessesCount')}`
225+
description={`${weaknesses.length} ${t('attackResults.weaknessesCount')}`
217226
}
218227
icon={< AlertCircle className="h-5 w-5" />}
219228
>
220229
<div className="space-y-4">
221-
{result.weaknesses.map((weakness) => (
230+
{weaknesses.map((weakness) => (
222231
<WeaknessCard key={weakness.id} weakness={weakness} />
223232
))}
224233
</div>
225234
</SectionCard >
226235

227236
{/* Fallacies */}
228237
{
229-
result.fallaciesDetected.length > 0 && (
238+
fallaciesDetected.length > 0 && (
230239
<SectionCard
231240
title={t('attackResults.fallacies')}
232241
description={t('attackResults.fallaciesDesc')}
233242
>
234243
<div className="space-y-3">
235-
{result.fallaciesDetected.map((fallacy, i) => (
244+
{fallaciesDetected.map((fallacy, i) => (
236245
<div key={i} className="rounded-lg border border-border p-3">
237246
<div className="flex items-center gap-2 mb-1">
238247
<Badge variant="destructive">{fallacy.name}</Badge>
@@ -253,7 +262,7 @@ export function AttackResults({ result, onDiscuss }: AttackResultsProps) {
253262
icon={<MessageSquare className="h-5 w-5" />}
254263
>
255264
<div className="space-y-4">
256-
{result.crossExQuestions.map((q, i) => (
265+
{crossExQuestions.map((q, i) => (
257266
<div key={i} className="rounded-lg border border-border p-4">
258267
<p className="font-medium text-foreground mb-2">"{q.question}"</p>
259268
<div className="grid gap-2 text-sm">
@@ -272,42 +281,44 @@ export function AttackResults({ result, onDiscuss }: AttackResultsProps) {
272281
</SectionCard>
273282

274283
{/* Weighing Analysis */}
275-
<SectionCard
276-
title={t('attackResults.weighing')}
277-
description={t('attackResults.weighingDesc')}
278-
icon={<Scale className="h-5 w-5" />}
279-
>
280-
<div className="space-y-4">
281-
<div>
282-
<h4 className="text-sm font-medium text-muted-foreground mb-1">{t('attackResults.currentWeighing')}</h4>
283-
<p className="text-foreground">{result.weighingAnalysis.currentWeighing}</p>
284-
</div>
285-
<Separator />
286-
<div>
287-
<h4 className="text-sm font-medium text-muted-foreground mb-2">{t('attackResults.missingComparisons')}</h4>
288-
<ul className="space-y-1">
289-
{result.weighingAnalysis.missingComparisons.map((comparison, i) => (
290-
<li key={i} className="flex items-start gap-2 text-sm">
291-
<span className="text-destructive mt-1">•</span>
292-
<span className="text-foreground">{comparison}</span>
293-
</li>
294-
))}
295-
</ul>
296-
</div>
297-
<Separator />
298-
<div>
299-
<h4 className="text-sm font-medium text-muted-foreground mb-2">{t('attackResults.improvements')}</h4>
300-
<ul className="space-y-1">
301-
{result.weighingAnalysis.improvementSuggestions.map((suggestion, i) => (
302-
<li key={i} className="flex items-start gap-2 text-sm">
303-
<span className="text-primary mt-1">→</span>
304-
<span className="text-foreground">{suggestion}</span>
305-
</li>
306-
))}
307-
</ul>
284+
{weighingAnalysis && (
285+
<SectionCard
286+
title={t('attackResults.weighing')}
287+
description={t('attackResults.weighingDesc')}
288+
icon={<Scale className="h-5 w-5" />}
289+
>
290+
<div className="space-y-4">
291+
<div>
292+
<h4 className="text-sm font-medium text-muted-foreground mb-1">{t('attackResults.currentWeighing')}</h4>
293+
<p className="text-foreground">{weighingAnalysis.currentWeighing}</p>
294+
</div>
295+
<Separator />
296+
<div>
297+
<h4 className="text-sm font-medium text-muted-foreground mb-2">{t('attackResults.missingComparisons')}</h4>
298+
<ul className="space-y-1">
299+
{(weighingAnalysis.missingComparisons ?? []).map((comparison, i) => (
300+
<li key={i} className="flex items-start gap-2 text-sm">
301+
<span className="text-destructive mt-1">•</span>
302+
<span className="text-foreground">{comparison}</span>
303+
</li>
304+
))}
305+
</ul>
306+
</div>
307+
<Separator />
308+
<div>
309+
<h4 className="text-sm font-medium text-muted-foreground mb-2">{t('attackResults.improvements')}</h4>
310+
<ul className="space-y-1">
311+
{(weighingAnalysis.improvementSuggestions ?? []).map((suggestion, i) => (
312+
<li key={i} className="flex items-start gap-2 text-sm">
313+
<span className="text-primary mt-1">→</span>
314+
<span className="text-foreground">{suggestion}</span>
315+
</li>
316+
))}
317+
</ul>
318+
</div>
308319
</div>
309-
</div>
310-
</SectionCard>
320+
</SectionCard>
321+
)}
311322
</StaggeredContainer >
312323
);
313324
}

0 commit comments

Comments
 (0)