vn6295337 Claude Opus 4.5 commited on
Commit
6fd7fcd
·
1 Parent(s): 6ab9879

feat: SWOT output as structured text parsed into HTML tables

Browse files

- Backend: Updated analyzer prompt OUTPUT FORMAT to use structured text
format: [M##] Metric: Value - Insight
- Backend: Updated revision prompt to match the same format
- Frontend: Added parseSwotLine() parser to extract ref, metric, insight
- Frontend: Replaced SWOT list display with HTML tables (3 columns)
- Frontend: Removed unused icon imports (Zap, AlertCircle)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Files changed (2) hide show
  1. frontend/src/App.tsx +139 -85
  2. src/nodes/analyzer.py +29 -32
frontend/src/App.tsx CHANGED
@@ -27,10 +27,8 @@ import {
27
  AlertTriangle,
28
  CheckCircle,
29
  XCircle,
30
- AlertCircle,
31
  BarChart3,
32
  RefreshCw,
33
- Zap,
34
  Play,
35
  Copy,
36
  Download,
@@ -94,6 +92,51 @@ const cleanMarkdown = (text: string): string => {
94
  .trim()
95
  }
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  const Index = () => {
98
  const [selectedStock, setSelectedStock] = useState<StockResult | null>(null)
99
  const [isLoading, setIsLoading] = useState(false)
@@ -781,119 +824,130 @@ Generated by Instant SWOT Agent`
781
  />
782
  )}
783
 
784
- {/* SWOT Analysis */}
785
  <div className="space-y-6">
786
- {/* Strengths */}
787
  <div>
788
  <h3 className="flex items-center gap-2 text-base font-semibold text-emerald-500 mb-3 border-b border-emerald-500/30 pb-2">
789
  <TrendingUp className="h-5 w-5" />
790
  Strengths
791
  </h3>
792
- <ul className="space-y-2 pl-1">
793
- {analysisResult.swot_data.strengths.map((item, i) => (
794
- <li key={i} className="flex gap-2 text-sm text-foreground">
795
- <CheckCircle className="h-4 w-4 text-emerald-500 shrink-0 mt-0.5" />
796
- <span>{cleanMarkdown(item)}</span>
797
- </li>
798
- ))}
799
- </ul>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
800
  </div>
801
 
802
- {/* Weaknesses */}
803
  <div>
804
  <h3 className="flex items-center gap-2 text-base font-semibold text-red-500 mb-3 border-b border-red-500/30 pb-2">
805
  <TrendingDown className="h-5 w-5" />
806
  Weaknesses
807
  </h3>
808
- <ul className="space-y-2 pl-1">
809
- {analysisResult.swot_data.weaknesses.map((item, i) => (
810
- <li key={i} className="flex gap-2 text-sm text-foreground">
811
- <XCircle className="h-4 w-4 text-red-500 shrink-0 mt-0.5" />
812
- <span>{cleanMarkdown(item)}</span>
813
- </li>
814
- ))}
815
- </ul>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
816
  </div>
817
 
818
- {/* Opportunities */}
819
  <div>
820
  <h3 className="flex items-center gap-2 text-base font-semibold text-blue-500 mb-3 border-b border-blue-500/30 pb-2">
821
  <Target className="h-5 w-5" />
822
  Opportunities
823
  </h3>
824
- <ul className="space-y-2 pl-1">
825
- {analysisResult.swot_data.opportunities.map((item, i) => (
826
- <li key={i} className="flex gap-2 text-sm text-foreground">
827
- <Zap className="h-4 w-4 text-blue-500 shrink-0 mt-0.5" />
828
- <span>{cleanMarkdown(item)}</span>
829
- </li>
830
- ))}
831
- </ul>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
832
  </div>
833
 
834
- {/* Threats */}
835
  <div>
836
  <h3 className="flex items-center gap-2 text-base font-semibold text-yellow-500 mb-3 border-b border-yellow-500/30 pb-2">
837
  <AlertTriangle className="h-5 w-5" />
838
  Threats
839
  </h3>
840
- <ul className="space-y-2 pl-1">
841
- {analysisResult.swot_data.threats.map((item, i) => (
842
- <li key={i} className="flex gap-2 text-sm text-foreground">
843
- <AlertCircle className="h-4 w-4 text-yellow-500 shrink-0 mt-0.5" />
844
- <span>{cleanMarkdown(item)}</span>
845
- </li>
846
- ))}
847
- </ul>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
848
  </div>
849
  </div>
850
 
851
- {/* Data Quality Notes */}
852
- <Card>
853
- <CardHeader>
854
- <CardTitle className="text-base flex items-center gap-2">
855
- <Target className="h-4 w-4" />
856
- Data Quality Notes
857
- </CardTitle>
858
- </CardHeader>
859
- <CardContent className="space-y-4">
860
- {analysisResult.quality_notes?.high_confidence?.length > 0 && (
861
- <div>
862
- <h4 className="font-medium text-sm mb-2">Metrics with High Confidence</h4>
863
- <ul className="text-sm text-muted-foreground space-y-1">
864
- {analysisResult.quality_notes.high_confidence.map((m: string, i: number) => (
865
- <li key={i}>- {m}</li>
866
- ))}
867
- </ul>
868
- </div>
869
- )}
870
- {analysisResult.quality_notes?.gaps_or_stale?.length > 0 && (
871
- <div>
872
- <h4 className="font-medium text-sm mb-2">Data Gaps or Staleness</h4>
873
- <ul className="text-sm text-muted-foreground space-y-1">
874
- {analysisResult.quality_notes.gaps_or_stale.map((m: string, i: number) => (
875
- <li key={i}>- {m}</li>
876
- ))}
877
- </ul>
878
- </div>
879
- )}
880
- {analysisResult.quality_notes?.assumptions?.length > 0 && (
881
- <div>
882
- <h4 className="font-medium text-sm mb-2">Assumptions Made</h4>
883
- <ul className="text-sm text-muted-foreground space-y-1">
884
- {analysisResult.quality_notes.assumptions.map((a: string, i: number) => (
885
- <li key={i}>- {a}</li>
886
- ))}
887
- </ul>
888
- </div>
889
- )}
890
- {(!analysisResult.quality_notes?.high_confidence?.length &&
891
- !analysisResult.quality_notes?.gaps_or_stale?.length &&
892
- !analysisResult.quality_notes?.assumptions?.length) && (
893
- <p className="text-sm text-muted-foreground">No quality notes available.</p>
894
- )}
895
- </CardContent>
896
- </Card>
897
  </div>
898
  )}
899
  </TabsContent>
 
27
  AlertTriangle,
28
  CheckCircle,
29
  XCircle,
 
30
  BarChart3,
31
  RefreshCw,
 
32
  Play,
33
  Copy,
34
  Download,
 
92
  .trim()
93
  }
94
 
95
+ // Parse structured SWOT line: "[M01] Revenue: $394.3B - Strong market position"
96
+ // Returns { ref, metric, insight } or null if line doesn't match pattern
97
+ interface SwotRow {
98
+ ref: string
99
+ metric: string
100
+ insight: string
101
+ }
102
+
103
+ const parseSwotLine = (line: string): SwotRow | null => {
104
+ // Clean markdown first
105
+ const cleaned = cleanMarkdown(line)
106
+
107
+ // Pattern: [M##] Metric: Value - Insight
108
+ // Also handle: [M##] Metric: Value | Insight (pipe separator)
109
+ const match = cleaned.match(/^\[?(M\d+)\]?\s*(.+?)\s*[-|]\s*(.+)$/i)
110
+ if (match) {
111
+ return {
112
+ ref: match[1],
113
+ metric: match[2].trim(),
114
+ insight: match[3].trim()
115
+ }
116
+ }
117
+
118
+ // Fallback: no ref pattern, just "Metric: Value - Insight"
119
+ const fallback = cleaned.match(/^(.+?)\s*[-|]\s*(.+)$/)
120
+ if (fallback) {
121
+ return {
122
+ ref: '',
123
+ metric: fallback[1].trim(),
124
+ insight: fallback[2].trim()
125
+ }
126
+ }
127
+
128
+ // Last resort: just return the cleaned line as insight
129
+ if (cleaned.length > 0) {
130
+ return {
131
+ ref: '',
132
+ metric: '',
133
+ insight: cleaned
134
+ }
135
+ }
136
+
137
+ return null
138
+ }
139
+
140
  const Index = () => {
141
  const [selectedStock, setSelectedStock] = useState<StockResult | null>(null)
142
  const [isLoading, setIsLoading] = useState(false)
 
824
  />
825
  )}
826
 
827
+ {/* SWOT Analysis - Tables */}
828
  <div className="space-y-6">
829
+ {/* Strengths Table */}
830
  <div>
831
  <h3 className="flex items-center gap-2 text-base font-semibold text-emerald-500 mb-3 border-b border-emerald-500/30 pb-2">
832
  <TrendingUp className="h-5 w-5" />
833
  Strengths
834
  </h3>
835
+ <table className="w-full text-sm border-collapse">
836
+ <thead>
837
+ <tr className="border-b border-border">
838
+ <th className="text-left py-2 px-2 w-12 text-muted-foreground font-medium">Ref</th>
839
+ <th className="text-left py-2 px-2 w-1/3 text-muted-foreground font-medium">Metric</th>
840
+ <th className="text-left py-2 px-2 text-muted-foreground font-medium">Insight</th>
841
+ </tr>
842
+ </thead>
843
+ <tbody>
844
+ {analysisResult.swot_data.strengths.map((item, i) => {
845
+ const parsed = parseSwotLine(item)
846
+ if (!parsed) return null
847
+ return (
848
+ <tr key={i} className="border-b border-border/50 hover:bg-emerald-500/5">
849
+ <td className="py-2 px-2 text-emerald-500 font-mono text-xs">{parsed.ref}</td>
850
+ <td className="py-2 px-2 text-foreground">{parsed.metric}</td>
851
+ <td className="py-2 px-2 text-muted-foreground">{parsed.insight}</td>
852
+ </tr>
853
+ )
854
+ })}
855
+ </tbody>
856
+ </table>
857
  </div>
858
 
859
+ {/* Weaknesses Table */}
860
  <div>
861
  <h3 className="flex items-center gap-2 text-base font-semibold text-red-500 mb-3 border-b border-red-500/30 pb-2">
862
  <TrendingDown className="h-5 w-5" />
863
  Weaknesses
864
  </h3>
865
+ <table className="w-full text-sm border-collapse">
866
+ <thead>
867
+ <tr className="border-b border-border">
868
+ <th className="text-left py-2 px-2 w-12 text-muted-foreground font-medium">Ref</th>
869
+ <th className="text-left py-2 px-2 w-1/3 text-muted-foreground font-medium">Metric</th>
870
+ <th className="text-left py-2 px-2 text-muted-foreground font-medium">Insight</th>
871
+ </tr>
872
+ </thead>
873
+ <tbody>
874
+ {analysisResult.swot_data.weaknesses.map((item, i) => {
875
+ const parsed = parseSwotLine(item)
876
+ if (!parsed) return null
877
+ return (
878
+ <tr key={i} className="border-b border-border/50 hover:bg-red-500/5">
879
+ <td className="py-2 px-2 text-red-500 font-mono text-xs">{parsed.ref}</td>
880
+ <td className="py-2 px-2 text-foreground">{parsed.metric}</td>
881
+ <td className="py-2 px-2 text-muted-foreground">{parsed.insight}</td>
882
+ </tr>
883
+ )
884
+ })}
885
+ </tbody>
886
+ </table>
887
  </div>
888
 
889
+ {/* Opportunities Table */}
890
  <div>
891
  <h3 className="flex items-center gap-2 text-base font-semibold text-blue-500 mb-3 border-b border-blue-500/30 pb-2">
892
  <Target className="h-5 w-5" />
893
  Opportunities
894
  </h3>
895
+ <table className="w-full text-sm border-collapse">
896
+ <thead>
897
+ <tr className="border-b border-border">
898
+ <th className="text-left py-2 px-2 w-12 text-muted-foreground font-medium">Ref</th>
899
+ <th className="text-left py-2 px-2 w-1/3 text-muted-foreground font-medium">Metric</th>
900
+ <th className="text-left py-2 px-2 text-muted-foreground font-medium">Insight</th>
901
+ </tr>
902
+ </thead>
903
+ <tbody>
904
+ {analysisResult.swot_data.opportunities.map((item, i) => {
905
+ const parsed = parseSwotLine(item)
906
+ if (!parsed) return null
907
+ return (
908
+ <tr key={i} className="border-b border-border/50 hover:bg-blue-500/5">
909
+ <td className="py-2 px-2 text-blue-500 font-mono text-xs">{parsed.ref}</td>
910
+ <td className="py-2 px-2 text-foreground">{parsed.metric}</td>
911
+ <td className="py-2 px-2 text-muted-foreground">{parsed.insight}</td>
912
+ </tr>
913
+ )
914
+ })}
915
+ </tbody>
916
+ </table>
917
  </div>
918
 
919
+ {/* Threats Table */}
920
  <div>
921
  <h3 className="flex items-center gap-2 text-base font-semibold text-yellow-500 mb-3 border-b border-yellow-500/30 pb-2">
922
  <AlertTriangle className="h-5 w-5" />
923
  Threats
924
  </h3>
925
+ <table className="w-full text-sm border-collapse">
926
+ <thead>
927
+ <tr className="border-b border-border">
928
+ <th className="text-left py-2 px-2 w-12 text-muted-foreground font-medium">Ref</th>
929
+ <th className="text-left py-2 px-2 w-1/3 text-muted-foreground font-medium">Metric</th>
930
+ <th className="text-left py-2 px-2 text-muted-foreground font-medium">Insight</th>
931
+ </tr>
932
+ </thead>
933
+ <tbody>
934
+ {analysisResult.swot_data.threats.map((item, i) => {
935
+ const parsed = parseSwotLine(item)
936
+ if (!parsed) return null
937
+ return (
938
+ <tr key={i} className="border-b border-border/50 hover:bg-yellow-500/5">
939
+ <td className="py-2 px-2 text-yellow-500 font-mono text-xs">{parsed.ref}</td>
940
+ <td className="py-2 px-2 text-foreground">{parsed.metric}</td>
941
+ <td className="py-2 px-2 text-muted-foreground">{parsed.insight}</td>
942
+ </tr>
943
+ )
944
+ })}
945
+ </tbody>
946
+ </table>
947
  </div>
948
  </div>
949
 
950
+ {/* Data Quality Notes - Hidden for now, will revisit later */}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
951
  </div>
952
  )}
953
  </TabsContent>
src/nodes/analyzer.py CHANGED
@@ -1348,17 +1348,29 @@ Weighted Score: {critique_details.get('weighted_score', 0):.1f} / 10
1348
 
1349
  ### OUTPUT INSTRUCTIONS
1350
 
1351
- Produce a complete, revised SWOT analysis using TABLE format:
1352
 
1353
  ## Strengths
1354
- | Ref | Metric | Insight |
1355
- |-----|--------|---------|
1356
- | M## | Metric: Value | Strategic insight in one sentence |
1357
 
1358
- (Same table format for Weaknesses, Opportunities, Threats)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1359
 
1360
  Do not:
1361
- - Use bullet points or **bold** labels - use tables only
1362
  - Include any preamble about revisions
1363
  - Reference the Critic's feedback in your output
1364
 
@@ -1404,42 +1416,27 @@ def _build_analyzer_prompt(company: str, ticker: str, formatted_data: str,
1404
 
1405
  === OUTPUT FORMAT ===
1406
 
1407
- Produce a SWOT analysis using TABLES with this exact structure:
1408
 
1409
  ## Strengths
1410
- | Ref | Metric | Insight |
1411
- |-----|--------|---------|
1412
- | M01 | Revenue: $394.3B | Strong market position with substantial scale |
1413
- | M02 | Net Margin: 24.3% | High profitability indicates pricing power |
1414
-
1415
- (Include 3-5 rows per section)
1416
 
1417
  ## Weaknesses
1418
- | Ref | Metric | Insight |
1419
- |-----|--------|---------|
1420
- | M04 | Debt/Equity: 1.87 | Elevated leverage increases financial risk |
1421
 
1422
  ## Opportunities
1423
- | Ref | Metric | Insight |
1424
- |-----|--------|---------|
1425
- | M12 | GDP Growth: 4.3% | Favorable macro environment for expansion |
1426
 
1427
  ## Threats
1428
- | Ref | Metric | Insight |
1429
- |-----|--------|---------|
1430
- | M13 | Interest Rate: 3.72% | Higher borrowing costs may impact margins |
1431
-
1432
- ## Data Quality Notes
1433
- - Metrics Used: [List key metrics analyzed]
1434
- - Data Gaps: [Any unavailable metrics]
1435
- - Confidence: [High/Medium/Low]
1436
 
1437
  CRITICAL REQUIREMENTS:
1438
- 1. Use TABLE format as shown above - NOT bullet points
1439
- 2. Every row MUST cite a metric reference [M##] in the Ref column
1440
- 3. Metric column: Name and value (e.g., "Revenue: $394.3B")
1441
- 4. Insight column: One concise sentence explaining strategic implication
1442
- 5. Use EXACT values from the METRIC REFERENCE TABLE - do NOT round"""
1443
 
1444
  return prompt, metric_lookup, ref_hash
1445
 
 
1348
 
1349
  ### OUTPUT INSTRUCTIONS
1350
 
1351
+ Produce a complete, revised SWOT analysis with this exact structure (3-5 points per section):
1352
 
1353
  ## Strengths
1354
+ - [M01] Revenue: $394.3B - Strong market position with substantial scale
1355
+ - [M02] Net Margin: 24.3% - High profitability indicates pricing power
 
1356
 
1357
+ ## Weaknesses
1358
+ - [M04] Debt/Equity: 1.87 - Elevated leverage increases financial risk
1359
+
1360
+ ## Opportunities
1361
+ - [M12] GDP Growth: 4.3% - Favorable macro environment for expansion
1362
+
1363
+ ## Threats
1364
+ - [M13] Interest Rate: 3.72% - Higher borrowing costs may impact margins
1365
+
1366
+ CRITICAL REQUIREMENTS:
1367
+ 1. Each point MUST start with metric reference in brackets: [M##]
1368
+ 2. Format: [M##] Metric: Value - Strategic insight
1369
+ 3. Use EXACT values from the METRIC REFERENCE TABLE - do NOT round
1370
+ 4. Keep insights concise (one sentence)
1371
+ 5. Include 3-5 points per section
1372
 
1373
  Do not:
 
1374
  - Include any preamble about revisions
1375
  - Reference the Critic's feedback in your output
1376
 
 
1416
 
1417
  === OUTPUT FORMAT ===
1418
 
1419
+ Produce a SWOT analysis with this exact structure (3-5 points per section):
1420
 
1421
  ## Strengths
1422
+ - [M01] Revenue: $394.3B - Strong market position with substantial scale
1423
+ - [M02] Net Margin: 24.3% - High profitability indicates pricing power
 
 
 
 
1424
 
1425
  ## Weaknesses
1426
+ - [M04] Debt/Equity: 1.87 - Elevated leverage increases financial risk
 
 
1427
 
1428
  ## Opportunities
1429
+ - [M12] GDP Growth: 4.3% - Favorable macro environment for expansion
 
 
1430
 
1431
  ## Threats
1432
+ - [M13] Interest Rate: 3.72% - Higher borrowing costs may impact margins
 
 
 
 
 
 
 
1433
 
1434
  CRITICAL REQUIREMENTS:
1435
+ 1. Each point MUST start with metric reference in brackets: [M##]
1436
+ 2. Format: [M##] Metric: Value - Strategic insight
1437
+ 3. Use EXACT values from the METRIC REFERENCE TABLE - do NOT round
1438
+ 4. Keep insights concise (one sentence)
1439
+ 5. Include 3-5 points per section"""
1440
 
1441
  return prompt, metric_lookup, ref_hash
1442