Dateien nach "competitor-analysis/components" hochladen
This commit is contained in:
57
competitor-analysis/components/Step3_Competitors.tsx
Normal file
57
competitor-analysis/components/Step3_Competitors.tsx
Normal file
@@ -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<Step3CompetitorsProps> = ({ candidates, onCandidatesChange, maxCompetitors, t }) => {
|
||||
const sortedCandidates = [...candidates].sort((a, b) => b.confidence - a.confidence);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4">{t.title}</h2>
|
||||
<p className="text-light-subtle dark:text-brand-light mb-6">
|
||||
{t.subtitle(maxCompetitors)}
|
||||
</p>
|
||||
|
||||
<EditableCard<CompetitorCandidate>
|
||||
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) => (
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<strong className={`text-light-text dark:text-white ${index < maxCompetitors ? 'text-green-600 dark:text-green-400' : ''}`}>{item.name}</strong>
|
||||
<a href={item.url} target="_blank" rel="noopener noreferrer" className="ml-2 text-blue-500 dark:text-blue-400 hover:underline text-sm">
|
||||
{t.visitButton}
|
||||
</a>
|
||||
<EvidencePopover evidence={item.evidence} />
|
||||
</div>
|
||||
<span className="text-xs font-mono bg-light-accent dark:bg-brand-accent text-light-text dark:text-white py-1 px-2 rounded-full">
|
||||
{(item.confidence * 100).toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-light-subtle dark:text-brand-light text-sm mt-1">{item.why}</p>
|
||||
{index === maxCompetitors - 1 && <div className="border-t-2 border-dashed border-red-500 mt-4 pt-2 text-red-500 dark:text-red-400 text-xs text-center font-bold">{t.shortlistBoundary}</div>}
|
||||
</div>
|
||||
)}
|
||||
t={t.editableCard}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Step3Competitors;
|
||||
97
competitor-analysis/components/Step4_Analysis.tsx
Normal file
97
competitor-analysis/components/Step4_Analysis.tsx
Normal file
@@ -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 = () => (<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>);
|
||||
|
||||
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 }) => (
|
||||
<div className="w-full bg-light-accent dark:bg-brand-accent rounded-full h-2.5">
|
||||
<div className="bg-brand-highlight h-2.5 rounded-full" style={{ width: `${score}%` }}></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Step4Analysis: React.FC<Step4AnalysisProps> = ({ analyses, t }) => {
|
||||
const sortedAnalyses = [...analyses].sort((a, b) => b.overlap_score - a.overlap_score);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4">{t.title}</h2>
|
||||
<p className="text-light-subtle dark:text-brand-light mb-6">
|
||||
{t.subtitle}
|
||||
</p>
|
||||
|
||||
<div className="space-y-6">
|
||||
{sortedAnalyses.map((analysis, index) => (
|
||||
<div key={index} className="bg-light-secondary dark:bg-brand-secondary p-6 rounded-lg shadow-lg">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-light-text dark:text-white">{analysis.competitor.name}</h3>
|
||||
<a href={analysis.competitor.url} target="_blank" rel="noopener noreferrer" className="text-blue-500 dark:text-blue-400 hover:underline text-sm">
|
||||
{analysis.competitor.url}
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => downloadJSON(analysis, `analysis_${analysis.competitor.name.replace(/ /g, '_')}.json`)}
|
||||
title={t.downloadJson_title}
|
||||
className="text-light-subtle dark:text-brand-light hover:text-light-text dark:hover:text-white p-1 rounded-full"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</button>
|
||||
<EvidencePopover evidence={analysis.evidence} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2">{t.portfolio}</h4>
|
||||
<ul className="list-disc list-inside text-light-subtle dark:text-brand-light text-sm space-y-1">
|
||||
{analysis.portfolio.map((p, i) => <li key={i}><strong>{p.product}:</strong> {p.purpose}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2">{t.differentiators}</h4>
|
||||
<ul className="list-disc list-inside text-light-subtle dark:text-brand-light text-sm space-y-1">
|
||||
{analysis.differentiators.map((d, i) => <li key={i}>{d}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2">{t.targetIndustries}</h4>
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{analysis.target_industries.map((ind, i) => (
|
||||
<span key={i} className="bg-light-accent dark:bg-brand-accent text-xs font-medium px-2.5 py-0.5 rounded-full">{ind}</span>
|
||||
))}
|
||||
</div>
|
||||
<span className="bg-gray-500 dark:bg-gray-600 text-white text-xs font-medium px-2.5 py-0.5 rounded-full">{analysis.delivery_model}</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2">{t.overlapScore}: {analysis.overlap_score}%</h4>
|
||||
<OverlapBar score={analysis.overlap_score} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Step4Analysis;
|
||||
BIN
competitor-analysis/components/Step5_Conclusion.tsx
Normal file
BIN
competitor-analysis/components/Step5_Conclusion.tsx
Normal file
Binary file not shown.
44
competitor-analysis/components/Step5_SilverBullets.tsx
Normal file
44
competitor-analysis/components/Step5_SilverBullets.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import type { SilverBullet } from '../types';
|
||||
|
||||
interface Step5SilverBulletsProps {
|
||||
silver_bullets: SilverBullet[];
|
||||
t: any;
|
||||
}
|
||||
|
||||
const Step5SilverBullets: React.FC<Step5SilverBulletsProps> = ({ silver_bullets, t }) => {
|
||||
if (!silver_bullets || silver_bullets.length === 0) {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4">{t.title}</h2>
|
||||
<p className="text-light-subtle dark:text-brand-light">{t.generating}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4">{t.title}</h2>
|
||||
<p className="text-light-subtle dark:text-brand-light mb-6">
|
||||
{t.subtitle}
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{silver_bullets.map((bullet, index) => (
|
||||
<div key={index} className="bg-light-secondary dark:bg-brand-secondary p-6 rounded-lg shadow-lg">
|
||||
<h3 className="text-lg font-semibold text-light-text dark:text-white mb-2">
|
||||
{t.cardTitle} <span className="text-brand-highlight">{bullet.competitor_name}</span>
|
||||
</h3>
|
||||
<blockquote className="border-l-4 border-brand-highlight pl-4">
|
||||
<p className="text-lg italic text-light-subtle dark:text-brand-light">
|
||||
"{bullet.statement}"
|
||||
</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Step5SilverBullets;
|
||||
148
competitor-analysis/components/Step6_Conclusion.tsx
Normal file
148
competitor-analysis/components/Step6_Conclusion.tsx
Normal file
@@ -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 <p>No data available for {title}.</p>;
|
||||
}
|
||||
|
||||
// Robustly get all unique column headers from all rows
|
||||
const columnSet = new Set<string>();
|
||||
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 (
|
||||
<div className="bg-light-secondary dark:bg-brand-secondary p-4 rounded-lg">
|
||||
<h3 className="text-lg font-bold mb-3">{title}</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="bg-light-accent dark:bg-brand-accent uppercase">
|
||||
<tr>
|
||||
<th scope="col" className="px-4 py-2"></th>
|
||||
{columns.map(col => <th key={col} scope="col" className="px-4 py-2 text-center whitespace-nowrap">{col}</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(row => (
|
||||
<tr key={row} className="border-b border-light-accent dark:border-brand-accent">
|
||||
<th scope="row" className="px-4 py-2 font-medium whitespace-nowrap">{row}</th>
|
||||
{columns.map(col => {
|
||||
// Safely access cell data to prevent crashes on malformed data
|
||||
const cellData = data[row]?.[col] || ' ';
|
||||
return (
|
||||
<td key={col} className="px-4 py-2 text-center text-green-600 dark:text-green-400 font-bold">
|
||||
{cellData}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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<string>();
|
||||
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<string, boolean>();
|
||||
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<Step6ConclusionProps> = ({ appState, t }) => {
|
||||
if (!appState || !appState.conclusion) return <p>{t.generating}</p>;
|
||||
const { conclusion, company } = appState;
|
||||
|
||||
const productMatrixForTable = transformMatrixData(conclusion.product_matrix);
|
||||
const industryMatrixForTable = transformMatrixData(conclusion.industry_matrix);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-2xl font-bold">{t.title}</h2>
|
||||
</div>
|
||||
|
||||
<p className="text-light-subtle dark:text-brand-light mb-6">{t.subtitle}</p>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<MatrixTable data={productMatrixForTable} title={t.productMatrix} />
|
||||
<MatrixTable data={industryMatrixForTable} title={t.industryMatrix} />
|
||||
</div>
|
||||
|
||||
<div className="bg-light-secondary dark:bg-brand-secondary p-4 rounded-lg">
|
||||
<h3 className="text-lg font-bold mb-3">{t.summary}</h3>
|
||||
<p className="text-light-subtle dark:text-brand-light">{conclusion.summary}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-light-secondary dark:bg-brand-secondary p-4 rounded-lg">
|
||||
<h3 className="text-lg font-bold mb-3">{t.opportunities(company.name)}</h3>
|
||||
<p className="text-light-subtle dark:text-brand-light">{conclusion.opportunities}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-light-secondary dark:bg-brand-secondary p-4 rounded-lg">
|
||||
<h3 className="text-lg font-bold mb-3">{t.nextQuestions}</h3>
|
||||
<ul className="list-disc list-inside text-light-subtle dark:text-brand-light space-y-1">
|
||||
{(conclusion.next_questions || []).map((q, i) => <li key={i}>{q}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Step6Conclusion;
|
||||
Reference in New Issue
Block a user