336 lines
12 KiB
TypeScript
336 lines
12 KiB
TypeScript
import React, { useState, useCallback, useEffect } from 'react';
|
|
import { Header } from './components/Header';
|
|
import { StepInput } from './components/StepInput';
|
|
import { StepStrategy } from './components/StepStrategy';
|
|
import { StepReview } from './components/StepReview';
|
|
import { StepProcessing } from './components/StepProcessing';
|
|
import { StepReport } from './components/StepReport';
|
|
import { StepOutreach } from './components/StepOutreach';
|
|
import { AppStep, Competitor, AnalysisResult, AnalysisState, Language, SearchStrategy } from './types';
|
|
import { identifyCompetitors, analyzeCompanyWithStrategy, generateSearchStrategy, saveProject } from './services/geminiService';
|
|
|
|
const generateId = () => Math.random().toString(36).substr(2, 9);
|
|
|
|
const App: React.FC = () => {
|
|
const [step, setStep] = useState<AppStep>(AppStep.INPUT);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [language, setLanguage] = useState<Language>('de');
|
|
|
|
// Project State
|
|
const [projectId, setProjectId] = useState<string | null>(null);
|
|
const [projectName, setProjectName] = useState<string>('');
|
|
|
|
// Core Data
|
|
const [referenceUrl, setReferenceUrl] = useState<string>('');
|
|
const [targetMarket, setTargetMarket] = useState<string>('');
|
|
const [productContext, setProductContext] = useState<string>('');
|
|
const [referenceCity, setReferenceCity] = useState<string>('');
|
|
const [referenceCountry, setReferenceCountry] = useState<string>('');
|
|
|
|
const [strategy, setStrategy] = useState<SearchStrategy | null>(null);
|
|
const [competitors, setCompetitors] = useState<Competitor[]>([]);
|
|
const [categorizedCompetitors, setCategorizedCompetitors] = useState<{ localCompetitors: Competitor[], nationalCompetitors: Competitor[], internationalCompetitors: Competitor[] } | null>(null);
|
|
const [analysisResults, setAnalysisResults] = useState<AnalysisResult[]>([]);
|
|
|
|
const [processingState, setProcessingState] = useState<AnalysisState>({
|
|
currentCompany: '', progress: 0, total: 0, completed: 0
|
|
});
|
|
|
|
const [selectedCompanyForOutreach, setSelectedCompanyForOutreach] = useState<AnalysisResult | null>(null);
|
|
|
|
// --- Auto-Save Effect ---
|
|
useEffect(() => {
|
|
// Don't save on initial load or reset
|
|
if (step === AppStep.INPUT) return;
|
|
|
|
const saveData = async () => {
|
|
if (!referenceUrl) return;
|
|
|
|
let dynamicName = projectName;
|
|
try {
|
|
const refHost = new URL(referenceUrl).hostname.replace('www.', '');
|
|
|
|
if (analysisResults && analysisResults.length > 0) {
|
|
// Use the first analyzed company as the title anchor
|
|
dynamicName = `${analysisResults[0].companyName} (Ref: ${refHost})`;
|
|
} else if (competitors && competitors.length > 0) {
|
|
dynamicName = `Search: ${refHost} Lookalikes`;
|
|
} else if (!projectName || projectName === "New Project") {
|
|
dynamicName = `Draft: ${refHost}`;
|
|
}
|
|
} catch (e) {
|
|
dynamicName = projectName || "Untitled Project";
|
|
}
|
|
|
|
const dataToSave = {
|
|
id: projectId,
|
|
name: dynamicName,
|
|
created_at: new Date().toISOString(), // DB updates updated_at automatically
|
|
currentStep: step,
|
|
language,
|
|
referenceUrl,
|
|
targetMarket,
|
|
productContext,
|
|
strategy,
|
|
competitors,
|
|
categorizedCompetitors,
|
|
analysisResults
|
|
};
|
|
|
|
try {
|
|
const result = await saveProject(dataToSave);
|
|
if (result.id && !projectId) {
|
|
setProjectId(result.id);
|
|
console.log("Project created with ID:", result.id);
|
|
}
|
|
} catch (e) {
|
|
console.error("Auto-save failed", e);
|
|
}
|
|
};
|
|
|
|
// Simple debounce to avoid spamming save on every keystroke/state change
|
|
const timer = setTimeout(saveData, 1000);
|
|
return () => clearTimeout(timer);
|
|
|
|
}, [step, strategy, competitors, analysisResults, referenceUrl, projectName]);
|
|
|
|
|
|
const handleBack = () => {
|
|
if (step === AppStep.STRATEGY) setStep(AppStep.INPUT);
|
|
else if (step === AppStep.REVIEW_LIST) setStep(AppStep.STRATEGY);
|
|
else if (step === AppStep.REPORT) setStep(AppStep.REVIEW_LIST);
|
|
else if (step === AppStep.OUTREACH) {
|
|
setStep(AppStep.REPORT);
|
|
setSelectedCompanyForOutreach(null);
|
|
}
|
|
};
|
|
|
|
const handleInitialInput = async (url: string, productCtx: string, market: string, selectedLang: Language) => {
|
|
setIsLoading(true);
|
|
setLanguage(selectedLang);
|
|
setReferenceUrl(url);
|
|
setTargetMarket(market);
|
|
setProductContext(productCtx);
|
|
|
|
// Set explicit name for new project
|
|
try {
|
|
const hostname = new URL(url).hostname.replace('www.', '');
|
|
setProjectName(hostname);
|
|
} catch (e) {
|
|
setProjectName("New Project");
|
|
}
|
|
|
|
try {
|
|
const generatedStrategy = await generateSearchStrategy(url, productCtx, selectedLang);
|
|
setStrategy(generatedStrategy);
|
|
setStep(AppStep.STRATEGY);
|
|
} catch (error) {
|
|
alert("Failed to generate strategy. Please try again.");
|
|
console.error(error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
// Hydrate state from loaded project (DB or File)
|
|
const handleLoadReport = (loadedStrategy: SearchStrategy, loadedResults: AnalysisResult[]) => {
|
|
// NOTE: This signature is from the old StepInput prop.
|
|
// Ideally StepInput should pass the FULL project object if loaded from DB.
|
|
// But for backward compatibility with file load, we keep it.
|
|
// If loaded from DB via StepInput -> handleProjectSelect, we need a new handler.
|
|
// See below 'handleLoadProjectData'
|
|
|
|
setStrategy(loadedStrategy);
|
|
setAnalysisResults(loadedResults);
|
|
|
|
// Reconstruct competitors list from results for consistency
|
|
const reconstructedCompetitors = loadedResults.map(r => ({
|
|
id: generateId(),
|
|
name: r.companyName,
|
|
dataSource: r.dataSource
|
|
}));
|
|
setCompetitors(reconstructedCompetitors);
|
|
|
|
setStep(AppStep.REPORT);
|
|
};
|
|
|
|
// NEW: Full Project Hydration
|
|
const handleLoadProjectData = (data: any) => {
|
|
setProjectId(data.id);
|
|
setProjectName(data.name);
|
|
setReferenceUrl(data.referenceUrl || '');
|
|
setTargetMarket(data.targetMarket || '');
|
|
setProductContext(data.productContext || '');
|
|
setLanguage(data.language || 'de');
|
|
|
|
if (data.strategy) setStrategy(data.strategy);
|
|
if (data.competitors) setCompetitors(data.competitors);
|
|
if (data.categorizedCompetitors) setCategorizedCompetitors(data.categorizedCompetitors);
|
|
if (data.analysisResults) setAnalysisResults(data.analysisResults);
|
|
|
|
// Jump to the last relevant step
|
|
// If results exist -> Report. Else if competitors -> Review. Else -> Strategy.
|
|
if (data.analysisResults && data.analysisResults.length > 0) {
|
|
setStep(AppStep.REPORT);
|
|
} else if (data.competitors && data.competitors.length > 0) {
|
|
setStep(AppStep.REVIEW_LIST);
|
|
} else if (data.strategy) {
|
|
setStep(AppStep.STRATEGY);
|
|
} else {
|
|
setStep(AppStep.INPUT);
|
|
}
|
|
};
|
|
|
|
const handleStrategyConfirm = async (finalStrategy: SearchStrategy) => {
|
|
setStrategy(finalStrategy);
|
|
setIsLoading(true);
|
|
try {
|
|
const idealCustomerProfile = finalStrategy.idealCustomerProfile;
|
|
const identifiedCompetitors = await identifyCompetitors(referenceUrl, targetMarket, productContext, referenceCity, referenceCountry, idealCustomerProfile);
|
|
setCategorizedCompetitors(identifiedCompetitors);
|
|
|
|
const flatCompetitors: Competitor[] = [
|
|
...(identifiedCompetitors.localCompetitors || []),
|
|
...(identifiedCompetitors.nationalCompetitors || []),
|
|
...(identifiedCompetitors.internationalCompetitors || []),
|
|
];
|
|
|
|
setCompetitors(flatCompetitors);
|
|
setStep(AppStep.REVIEW_LIST);
|
|
} catch (e) {
|
|
alert("Failed to find companies.");
|
|
console.error(e);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleRemoveCompetitor = (id: string) => {
|
|
setCompetitors(prev => prev.filter(c => c.id !== id));
|
|
};
|
|
|
|
const handleAddCompetitor = (name: string) => {
|
|
setCompetitors(prev => [...prev, { id: generateId(), name }]);
|
|
};
|
|
|
|
const runAnalysis = useCallback(async () => {
|
|
if (!strategy) return;
|
|
setStep(AppStep.ANALYSIS);
|
|
setAnalysisResults([]);
|
|
setProcessingState({ currentCompany: '', progress: 0, total: competitors.length, completed: 0, terminalLogs: ['🚀 Starting Deep Tech Audit session...'] });
|
|
|
|
const results: AnalysisResult[] = [];
|
|
|
|
const addLog = (msg: string) => {
|
|
setProcessingState(prev => ({
|
|
...prev,
|
|
terminalLogs: [...(prev.terminalLogs || []), msg]
|
|
}));
|
|
};
|
|
|
|
for (let i = 0; i < competitors.length; i++) {
|
|
const comp = competitors[i];
|
|
setProcessingState(prev => ({ ...prev, currentCompany: comp.name }));
|
|
addLog(`> Analyzing ${comp.name} (${i + 1}/${competitors.length})`);
|
|
|
|
try {
|
|
addLog(` 🔍 Searching official website for ${comp.name}...`);
|
|
const result = await analyzeCompanyWithStrategy(comp.name, strategy, language);
|
|
|
|
if (result.dataSource === "Error") {
|
|
addLog(` ❌ Error: Could not process ${comp.name}.`);
|
|
} else {
|
|
const websiteStatus = result.dataSource === "Digital Trace Audit (Deep Dive)" ? "Verified" : (result.dataSource || "Unknown");
|
|
addLog(` ✓ Found website: ${websiteStatus}`);
|
|
addLog(` ✅ Analysis complete for ${comp.name}.`);
|
|
}
|
|
|
|
results.push(result);
|
|
} catch (e) {
|
|
console.error(`Failed to analyze ${comp.name}`);
|
|
addLog(` ❌ Fatal error analyzing ${comp.name}.`);
|
|
}
|
|
setProcessingState(prev => ({ ...prev, completed: i + 1 }));
|
|
}
|
|
|
|
addLog(`✨ Audit session finished. Generating final report...`);
|
|
setAnalysisResults(results);
|
|
setStep(AppStep.REPORT);
|
|
}, [competitors, language, strategy]);
|
|
|
|
const handleRestart = () => {
|
|
setProjectId(null); // Reset Project ID to start fresh
|
|
setCompetitors([]);
|
|
setAnalysisResults([]);
|
|
setStrategy(null);
|
|
setStep(AppStep.INPUT);
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-slate-50 text-slate-900 font-sans">
|
|
<Header showBack={step !== AppStep.INPUT && step !== AppStep.ANALYSIS} onBack={handleBack} />
|
|
<main>
|
|
{step === AppStep.INPUT && (
|
|
<StepInput
|
|
onSearch={handleInitialInput}
|
|
// We overload onLoadReport to accept full project data if available
|
|
onLoadReport={(stratOrFullProject, results) => {
|
|
if ('id' in stratOrFullProject) {
|
|
handleLoadProjectData(stratOrFullProject);
|
|
} else {
|
|
handleLoadReport(stratOrFullProject as SearchStrategy, results);
|
|
}
|
|
}}
|
|
isLoading={isLoading}
|
|
/>
|
|
)}
|
|
|
|
{step === AppStep.STRATEGY && strategy && (
|
|
<StepStrategy strategy={strategy} onConfirm={handleStrategyConfirm} />
|
|
)}
|
|
|
|
{step === AppStep.REVIEW_LIST && (
|
|
<StepReview
|
|
competitors={competitors}
|
|
categorizedCompetitors={categorizedCompetitors}
|
|
onAdd={handleAddCompetitor}
|
|
onRemove={handleRemoveCompetitor}
|
|
onConfirm={runAnalysis}
|
|
hasResults={analysisResults.length > 0}
|
|
onShowReport={() => setStep(AppStep.REPORT)}
|
|
/>
|
|
)}
|
|
|
|
{step === AppStep.ANALYSIS && (
|
|
<StepProcessing state={processingState} />
|
|
)}
|
|
|
|
{step === AppStep.REPORT && strategy && (
|
|
<StepReport
|
|
results={analysisResults}
|
|
strategy={strategy}
|
|
onRestart={handleRestart}
|
|
onBack={handleBack}
|
|
language={language}
|
|
onStartOutreach={(company) => {
|
|
setSelectedCompanyForOutreach(company);
|
|
setStep(AppStep.OUTREACH);
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{step === AppStep.OUTREACH && selectedCompanyForOutreach && (
|
|
<StepOutreach
|
|
company={selectedCompanyForOutreach}
|
|
language={language}
|
|
referenceUrl={referenceUrl}
|
|
onBack={handleBack}
|
|
knowledgeBase={productContext}
|
|
/>
|
|
)}
|
|
</main>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default App; |