diff --git a/competitor-analysis/components/Step3_Competitors.tsx b/competitor-analysis/components/Step3_Competitors.tsx new file mode 100644 index 00000000..e9cf4383 --- /dev/null +++ b/competitor-analysis/components/Step3_Competitors.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import type { CompetitorCandidate } from '../types'; +import EvidencePopover from './EvidencePopover'; +import { EditableCard } from './EditableCard'; + +interface Step3CompetitorsProps { + candidates: CompetitorCandidate[]; + onCandidatesChange: (candidates: CompetitorCandidate[]) => void; + maxCompetitors: number; + t: any; +} + +const Step3Competitors: React.FC = ({ candidates, onCandidatesChange, maxCompetitors, t }) => { + const sortedCandidates = [...candidates].sort((a, b) => b.confidence - a.confidence); + + return ( +
+

{t.title}

+

+ {t.subtitle(maxCompetitors)} +

+ + + title={t.cardTitle} + items={sortedCandidates} + onItemsChange={onCandidatesChange} + fieldConfigs={[ + { key: 'name', label: t.nameLabel, type: 'text' }, + { key: 'url', label: 'URL', type: 'text' }, + { key: 'why', label: t.whyLabel, type: 'textarea' }, + ]} + newItemTemplate={{ name: '', url: '', confidence: 0.8, why: '', evidence: [] }} + renderDisplay={(item, index) => ( +
+
+
+ {item.name} + + {t.visitButton} + + +
+ + {(item.confidence * 100).toFixed(0)}% + +
+

{item.why}

+ {index === maxCompetitors - 1 &&
{t.shortlistBoundary}
} +
+ )} + t={t.editableCard} + /> +
+ ); +}; + +export default Step3Competitors; \ No newline at end of file diff --git a/competitor-analysis/components/Step4_Analysis.tsx b/competitor-analysis/components/Step4_Analysis.tsx new file mode 100644 index 00000000..5e30086c --- /dev/null +++ b/competitor-analysis/components/Step4_Analysis.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import type { Analysis } from '../types'; +import EvidencePopover from './EvidencePopover'; + +interface Step4AnalysisProps { + analyses: Analysis[]; + t: any; +} + +const DownloadIcon = () => (); + +const downloadJSON = (data: any, filename: string) => { + const jsonStr = JSON.stringify(data, null, 2); + const blob = new Blob([jsonStr], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); +}; + +const OverlapBar: React.FC<{ score: number }> = ({ score }) => ( +
+
+
+); + +const Step4Analysis: React.FC = ({ analyses, t }) => { + const sortedAnalyses = [...analyses].sort((a, b) => b.overlap_score - a.overlap_score); + + return ( +
+

{t.title}

+

+ {t.subtitle} +

+ +
+ {sortedAnalyses.map((analysis, index) => ( +
+
+
+

{analysis.competitor.name}

+ + {analysis.competitor.url} + +
+
+ + +
+
+ +
+
+

{t.portfolio}

+
    + {analysis.portfolio.map((p, i) =>
  • {p.product}: {p.purpose}
  • )} +
+
+
+

{t.differentiators}

+
    + {analysis.differentiators.map((d, i) =>
  • {d}
  • )} +
+
+
+

{t.targetIndustries}

+
+ {analysis.target_industries.map((ind, i) => ( + {ind} + ))} +
+ {analysis.delivery_model} +
+
+

{t.overlapScore}: {analysis.overlap_score}%

+ +
+
+
+ ))} +
+
+ ); +}; + +export default Step4Analysis; \ No newline at end of file diff --git a/competitor-analysis/components/Step5_Conclusion.tsx b/competitor-analysis/components/Step5_Conclusion.tsx new file mode 100644 index 00000000..06d74050 Binary files /dev/null and b/competitor-analysis/components/Step5_Conclusion.tsx differ diff --git a/competitor-analysis/components/Step5_SilverBullets.tsx b/competitor-analysis/components/Step5_SilverBullets.tsx new file mode 100644 index 00000000..652693f9 --- /dev/null +++ b/competitor-analysis/components/Step5_SilverBullets.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import type { SilverBullet } from '../types'; + +interface Step5SilverBulletsProps { + silver_bullets: SilverBullet[]; + t: any; +} + +const Step5SilverBullets: React.FC = ({ silver_bullets, t }) => { + if (!silver_bullets || silver_bullets.length === 0) { + return ( +
+

{t.title}

+

{t.generating}

+
+ ); + } + + return ( +
+

{t.title}

+

+ {t.subtitle} +

+ +
+ {silver_bullets.map((bullet, index) => ( +
+

+ {t.cardTitle} {bullet.competitor_name} +

+
+

+ "{bullet.statement}" +

+
+
+ ))} +
+
+ ); +}; + +export default Step5SilverBullets; \ No newline at end of file diff --git a/competitor-analysis/components/Step6_Conclusion.tsx b/competitor-analysis/components/Step6_Conclusion.tsx new file mode 100644 index 00000000..a28e16b0 --- /dev/null +++ b/competitor-analysis/components/Step6_Conclusion.tsx @@ -0,0 +1,148 @@ +import React from 'react'; +import type { AppState } from '../types'; + +interface Step6ConclusionProps { + appState: AppState | null; + t: any; +} + +const MatrixTable: React.FC<{ data: { [row: string]: { [col: string]: string } }, title: string }> = ({ data, title }) => { + if (!data || Object.keys(data).length === 0) { + return

No data available for {title}.

; + } + + // Robustly get all unique column headers from all rows + const columnSet = new Set(); + Object.values(data).forEach(rowData => { + if (rowData && typeof rowData === 'object') { + Object.keys(rowData).forEach(col => columnSet.add(col)); + } + }); + const columns = Array.from(columnSet).sort(); + const rows = Object.keys(data); + + return ( +
+

{title}

+
+ + + + + {columns.map(col => )} + + + + {rows.map(row => ( + + + {columns.map(col => { + // Safely access cell data to prevent crashes on malformed data + const cellData = data[row]?.[col] || ' '; + return ( + + ); + })} + + ))} + +
{col}
{row} + {cellData} +
+
+
+ ); +}; + +type ProductMatrixItem = { product: string; availability: { competitor: string; has_offering: boolean }[] }; +type IndustryMatrixItem = { industry: string; availability: { competitor: string; has_offering: boolean }[] }; + +const transformMatrixData = (matrixData: (ProductMatrixItem[] | IndustryMatrixItem[]) | undefined): { [row: string]: { [col: string]: string } } => { + const tableData: { [row: string]: { [col: string]: string } } = {}; + if (!matrixData || !Array.isArray(matrixData) || matrixData.length === 0) { + return tableData; + } + + const allCompetitors = new Set(); + matrixData.forEach(row => { + if (row && Array.isArray(row.availability)) { + row.availability.forEach(item => { + if (item && typeof item.competitor === 'string') { + allCompetitors.add(item.competitor); + } + }); + } + }); + const competitorList = Array.from(allCompetitors).sort(); + + matrixData.forEach(row => { + if (!row) return; + + const rowKey = 'product' in row ? row.product : ('industry' in row ? row.industry : undefined); + + if (typeof rowKey !== 'string' || !rowKey) { + return; + } + + const availabilityMap = new Map(); + if (Array.isArray(row.availability)) { + row.availability.forEach(item => { + if (item && typeof item.competitor === 'string') { + availabilityMap.set(item.competitor, item.has_offering); + } + }); + } + + const rowObject: { [col: string]: string } = {}; + competitorList.forEach(competitor => { + rowObject[competitor] = availabilityMap.get(competitor) ? '✓' : ' '; + }); + tableData[rowKey] = rowObject; + }); + + return tableData; +}; + +const Step6Conclusion: React.FC = ({ appState, t }) => { + if (!appState || !appState.conclusion) return

{t.generating}

; + const { conclusion, company } = appState; + + const productMatrixForTable = transformMatrixData(conclusion.product_matrix); + const industryMatrixForTable = transformMatrixData(conclusion.industry_matrix); + + return ( +
+
+

{t.title}

+
+ +

{t.subtitle}

+ +
+
+ + +
+ +
+

{t.summary}

+

{conclusion.summary}

+
+ +
+

{t.opportunities(company.name)}

+

{conclusion.opportunities}

+
+ +
+

{t.nextQuestions}

+
    + {(conclusion.next_questions || []).map((q, i) =>
  • {q}
  • )} +
+
+
+
+ ); +}; + +export default Step6Conclusion; \ No newline at end of file