feat: Market Intel Database & UI Polish
- market_db_manager.py: Created SQLite manager for saving/loading projects. - server.cjs: Added API routes for project management. - geminiService.ts: Added client-side DB functions. - StepInput.tsx: Added 'Past Runs' sidebar to load previous audits. - App.tsx: Added auto-save functionality and full state hydration logic. - StepOutreach.tsx: Improved UI layout by merging generated campaigns and suggestions into one list.
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { Header } from './components/Header';
|
||||
import { StepInput } from './components/StepInput';
|
||||
import { StepStrategy } from './components/StepStrategy';
|
||||
@@ -8,7 +7,7 @@ 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 } from './services/geminiService';
|
||||
import { identifyCompetitors, analyzeCompanyWithStrategy, generateSearchStrategy, saveProject } from './services/geminiService';
|
||||
|
||||
const generateId = () => Math.random().toString(36).substr(2, 9);
|
||||
|
||||
@@ -16,23 +15,70 @@ 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>(''); // Added state for productContext
|
||||
const [referenceCity, setReferenceCity] = useState<string>(''); // Added state for referenceCity
|
||||
const [referenceCountry, setReferenceCountry] = useState<string>(''); // Added state for referenceCountry
|
||||
const [productContext, setProductContext] = useState<string>('');
|
||||
const [referenceCity, setReferenceCity] = useState<string>('');
|
||||
const [referenceCountry, setReferenceCountry] = useState<string>('');
|
||||
|
||||
// Data States
|
||||
const [strategy, setStrategy] = useState<SearchStrategy | null>(null);
|
||||
const [competitors, setCompetitors] = useState<Competitor[]>([]);
|
||||
const [categorizedCompetitors, setCategorizedCompetitors] = useState<{ localCompetitors: Competitor[], nationalCompetitors: Competitor[], internationalCompetitors: Competitor[] } | null>(null); // New state for categorized competitors
|
||||
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;
|
||||
|
||||
const dataToSave = {
|
||||
id: projectId,
|
||||
name: projectName || new URL(referenceUrl).hostname,
|
||||
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);
|
||||
@@ -48,14 +94,17 @@ const App: React.FC = () => {
|
||||
setLanguage(selectedLang);
|
||||
setReferenceUrl(url);
|
||||
setTargetMarket(market);
|
||||
setProductContext(productCtx); // Store productContext in state
|
||||
// referenceCity and referenceCountry are not yet extracted from input in StepInput, leaving empty for now.
|
||||
// Future improvement: extract from referenceUrl or add input fields.
|
||||
setReferenceCity('');
|
||||
setReferenceCountry('');
|
||||
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 {
|
||||
// 1. Generate Strategy first
|
||||
const generatedStrategy = await generateSearchStrategy(url, productCtx, selectedLang);
|
||||
setStrategy(generatedStrategy);
|
||||
setStep(AppStep.STRATEGY);
|
||||
@@ -67,36 +116,70 @@ const App: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Hydrate state from loaded project (DB or File)
|
||||
const handleLoadReport = (loadedStrategy: SearchStrategy, loadedResults: AnalysisResult[]) => {
|
||||
setStrategy(loadedStrategy);
|
||||
setAnalysisResults(loadedResults);
|
||||
// Reconstruct competitors list from results for consistency if user goes back
|
||||
const reconstructedCompetitors = loadedResults.map(r => ({
|
||||
id: generateId(),
|
||||
name: r.companyName,
|
||||
dataSource: r.dataSource
|
||||
}));
|
||||
setCompetitors(reconstructedCompetitors);
|
||||
setStep(AppStep.REPORT);
|
||||
// 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 {
|
||||
// 2. Identify Competitors based on Reference
|
||||
const idealCustomerProfile = finalStrategy.idealCustomerProfile; // Use ICP for lookalike search
|
||||
const idealCustomerProfile = finalStrategy.idealCustomerProfile;
|
||||
const identifiedCompetitors = await identifyCompetitors(referenceUrl, targetMarket, productContext, referenceCity, referenceCountry, idealCustomerProfile);
|
||||
setCategorizedCompetitors(identifiedCompetitors); // Store categorized competitors
|
||||
setCategorizedCompetitors(identifiedCompetitors);
|
||||
|
||||
// Flatten categorized competitors into a single list for existing StepReview component
|
||||
const flatCompetitors: Competitor[] = [
|
||||
...(identifiedCompetitors.localCompetitors || []),
|
||||
...(identifiedCompetitors.nationalCompetitors || []),
|
||||
...(identifiedCompetitors.internationalCompetitors || []),
|
||||
];
|
||||
|
||||
setCompetitors(flatCompetitors); // Set the flattened list for StepReview
|
||||
setCompetitors(flatCompetitors);
|
||||
setStep(AppStep.REVIEW_LIST);
|
||||
} catch (e) {
|
||||
alert("Failed to find companies.");
|
||||
@@ -135,23 +218,14 @@ const App: React.FC = () => {
|
||||
addLog(`> Analyzing ${comp.name} (${i + 1}/${competitors.length})`);
|
||||
|
||||
try {
|
||||
// Step-by-step logging to make it feel real and informative
|
||||
addLog(` 🔍 Searching official website for ${comp.name}...`);
|
||||
// The actual API call happens here. While waiting, the user sees the search log.
|
||||
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" ? "Verified" : (result.dataSource || "Unknown");
|
||||
const revenue = result.revenue || "N/A";
|
||||
const employees = result.employees || "N/A";
|
||||
const status = result.status || "Unknown";
|
||||
const tier = result.tier || "N/A";
|
||||
|
||||
const websiteStatus = result.dataSource === "Digital Trace Audit (Deep Dive)" ? "Verified" : (result.dataSource || "Unknown");
|
||||
addLog(` ✓ Found website: ${websiteStatus}`);
|
||||
addLog(` 📊 Estimated: ${revenue} revenue, ${employees} employees.`);
|
||||
addLog(` 🎯 Status: ${status} | Tier: ${tier}`);
|
||||
addLog(` ✅ Analysis complete for ${comp.name}.`);
|
||||
}
|
||||
|
||||
@@ -169,6 +243,7 @@ const App: React.FC = () => {
|
||||
}, [competitors, language, strategy]);
|
||||
|
||||
const handleRestart = () => {
|
||||
setProjectId(null); // Reset Project ID to start fresh
|
||||
setCompetitors([]);
|
||||
setAnalysisResults([]);
|
||||
setStrategy(null);
|
||||
@@ -180,7 +255,18 @@ const App: React.FC = () => {
|
||||
<Header showBack={step !== AppStep.INPUT && step !== AppStep.ANALYSIS} onBack={handleBack} />
|
||||
<main>
|
||||
{step === AppStep.INPUT && (
|
||||
<StepInput onSearch={handleInitialInput} onLoadReport={handleLoadReport} isLoading={isLoading} />
|
||||
<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 && (
|
||||
@@ -231,4 +317,4 @@ const App: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
export default App;
|
||||
Reference in New Issue
Block a user