diff --git a/general-market-intelligence/App.tsx b/general-market-intelligence/App.tsx index f4d7232e..c0cdbfc0 100644 --- a/general-market-intelligence/App.tsx +++ b/general-market-intelligence/App.tsx @@ -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.INPUT); const [isLoading, setIsLoading] = useState(false); const [language, setLanguage] = useState('de'); + + // Project State + const [projectId, setProjectId] = useState(null); + const [projectName, setProjectName] = useState(''); + + // Core Data const [referenceUrl, setReferenceUrl] = useState(''); const [targetMarket, setTargetMarket] = useState(''); - const [productContext, setProductContext] = useState(''); // Added state for productContext - const [referenceCity, setReferenceCity] = useState(''); // Added state for referenceCity - const [referenceCountry, setReferenceCountry] = useState(''); // Added state for referenceCountry + const [productContext, setProductContext] = useState(''); + const [referenceCity, setReferenceCity] = useState(''); + const [referenceCountry, setReferenceCountry] = useState(''); - // Data States const [strategy, setStrategy] = useState(null); const [competitors, setCompetitors] = useState([]); - 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([]); + const [processingState, setProcessingState] = useState({ currentCompany: '', progress: 0, total: 0, completed: 0 }); const [selectedCompanyForOutreach, setSelectedCompanyForOutreach] = useState(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 = () => {
{step === AppStep.INPUT && ( - + { + 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; \ No newline at end of file diff --git a/general-market-intelligence/components/StepInput.tsx b/general-market-intelligence/components/StepInput.tsx index a5a07e45..078cf278 100644 --- a/general-market-intelligence/components/StepInput.tsx +++ b/general-market-intelligence/components/StepInput.tsx @@ -1,8 +1,9 @@ -import React, { useState } from 'react'; -import { Search, ArrowRight, Loader2, Globe, Link as LinkIcon, Languages, Upload, FileText, X, FolderOpen, FileUp } from 'lucide-react'; +import React, { useState, useEffect } from 'react'; +import { Search, ArrowRight, Loader2, Globe, Link as LinkIcon, Languages, Upload, FileText, X, FolderOpen, FileUp, History, Clock } from 'lucide-react'; import { Language, AnalysisResult, SearchStrategy } from '../types'; import { parseMarkdownReport } from '../utils/reportParser'; +import { listProjects, loadProject } from '../services/geminiService'; interface StepInputProps { onSearch: (url: string, context: string, market: string, language: Language) => void; @@ -21,6 +22,38 @@ export const StepInput: React.FC = ({ onSearch, onLoadReport, is const [fileName, setFileName] = useState(''); const [market, setMarket] = useState(COUNTRIES[0]); const [language, setLanguage] = useState('de'); + + const [recentProjects, setRecentProjects] = useState([]); + const [isLoadingProjects, setIsLoadingProjects] = useState(false); + + useEffect(() => { + const fetchProjects = async () => { + setIsLoadingProjects(true); + try { + const projects = await listProjects(); + setRecentProjects(projects); + } catch (e) { + console.error("Failed to load projects", e); + } finally { + setIsLoadingProjects(false); + } + }; + fetchProjects(); + }, []); + + const handleProjectSelect = async (projectId: string) => { + try { + const projectData = await loadProject(projectId); + if (projectData && projectData.strategy && projectData.analysisResults) { + onLoadReport(projectData.strategy, projectData.analysisResults); + } else { + alert("Project data is incomplete or corrupted."); + } + } catch (e) { + console.error(e); + alert("Failed to load project."); + } + }; const handleFileUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; @@ -64,181 +97,222 @@ export const StepInput: React.FC = ({ onSearch, onLoadReport, is }; return ( -
-
-

Market Intelligence Agent

-
- - -
- - {activeMode === 'new' ? ( -

- Upload your Strategy Document to let AI design the perfect market audit. -

- ) : ( -

- Select an exported .md Report to continue working on an existing analysis. -

- )} -
- -
- {activeMode === 'new' ? ( -
-
- - - {!fileContent ? ( -
- - -
- ) : ( -
-
- -
-
-

{fileName}

-

Context loaded successfully

-
- -
- )} -
- -
- -
- setUrl(e.target.value)} - placeholder="e.g. https://www.reference-customer.com" - className="w-full pl-12 pr-4 py-3 rounded-xl border border-slate-200 focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/10 outline-none transition-all text-lg" - required - /> - -
-

Used to calibrate the search and find lookalikes.

-
- -
-
- -
- - -
-
- -
- -
- - -
-
-
- - + -
- ) : ( -
-
- -
-
-

Note:

-

- Loading an existing audit will take you directly to the Report view. You can then trigger new outreach campaigns for any company in the list. -

+ {activeMode === 'new' ? ( +

+ Upload your Strategy Document to let AI design the perfect market audit. +

+ ) : ( +

+ Select an exported .md Report to continue working on an existing analysis. +

+ )} +
+ +
+ {activeMode === 'new' ? ( +
+
+ + + {!fileContent ? ( +
+ + +
+ ) : ( +
+
+ +
+
+

{fileName}

+

Context loaded successfully

+
+ +
+ )} +
+ +
+ +
+ setUrl(e.target.value)} + placeholder="e.g. https://www.reference-customer.com" + className="w-full pl-12 pr-4 py-3 rounded-xl border border-slate-200 focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/10 outline-none transition-all text-lg" + required + /> + +
+

Used to calibrate the search and find lookalikes.

+
+ +
+
+ +
+ + +
+
+ +
+ +
+ + +
+
+
+ + +
+ ) : ( +
+
+ + +
+ +
+

Note:

+

+ Loading an existing audit will take you directly to the Report view. You can then trigger new outreach campaigns for any company in the list. +

+
-
- )} + )} +
+ + {/* RIGHT: Recent Projects Sidebar */} +
+
+
+ +

Past Runs

+
+ +
+ {isLoadingProjects ? ( +
+ +
+ ) : recentProjects.length > 0 ? ( + recentProjects.map((p) => ( + + )) + ) : ( +
+ No saved runs yet. +
+ )} +
+
+
+
); }; diff --git a/general-market-intelligence/components/StepOutreach.tsx b/general-market-intelligence/components/StepOutreach.tsx index 6a36ab3d..9e6947a5 100644 --- a/general-market-intelligence/components/StepOutreach.tsx +++ b/general-market-intelligence/components/StepOutreach.tsx @@ -194,61 +194,64 @@ export const StepOutreach: React.FC = ({ company, language, r
- {/* Sidebar Tabs */} -
-
-

Generated Campaigns

-
- - {/* Generated List */} -
- {emails.map((email, idx) => ( - + ))} + + {/* Separator for Suggestions */} + {availableRoles.length > 0 && ( +
+
+ + Suggestions + +
+
+ )} + + {/* Suggestions List */} + {availableRoles.map((role, i) => ( +
+ {role} + +
+ ))} +
-
{email.subject}
- - ))} -
- - {/* Suggestions List */} - {availableRoles.length > 0 && ( -
-
- -

Other Relevant Roles

-
-
- {availableRoles.map((role, i) => ( -
- {role} - -
- ))} -
-
- )} - {/* Content Area */}
diff --git a/general-market-intelligence/server.cjs b/general-market-intelligence/server.cjs index 95a50b46..32905fe0 100644 --- a/general-market-intelligence/server.cjs +++ b/general-market-intelligence/server.cjs @@ -213,6 +213,34 @@ app.post('/api/generate-outreach', async (req, res) => { } }); +// --- DATABASE ROUTES --- + +// Initialize DB on startup +const dbScript = path.join(__dirname, '..', 'market_db_manager.py'); +spawn('python3', [dbScript, 'init']); + +app.get('/api/projects', (req, res) => { + runPython([dbScript, 'list'], res); +}); + +app.get('/api/projects/:id', (req, res) => { + runPython([dbScript, 'load', req.params.id], res); +}); + +app.post('/api/save-project', (req, res) => { + const projectData = req.body; + const tmpDir = path.join(__dirname, 'tmp'); + if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir); + const tempFilePath = path.join(tmpDir, `save_${Date.now()}.json`); + + try { + fs.writeFileSync(tempFilePath, JSON.stringify(projectData)); + runPython([dbScript, 'save', tempFilePath], res, [tempFilePath]); + } catch (e) { + res.status(500).json({ error: 'Failed to write project data to disk' }); + } +}); + app.listen(PORT, () => { console.log(`Node.js API Bridge running on http://localhost:${PORT}`); }); \ No newline at end of file diff --git a/general-market-intelligence/services/geminiService.ts b/general-market-intelligence/services/geminiService.ts index 06bd8c74..2bd5800f 100644 --- a/general-market-intelligence/services/geminiService.ts +++ b/general-market-intelligence/services/geminiService.ts @@ -251,4 +251,28 @@ export const translateEmailDrafts = async (drafts: EmailDraft[], targetLanguage: // Dieser Teil muss noch im Python-Backend oder direkt im Frontend implementiert werden console.warn("translateEmailDrafts ist noch nicht im Python-Backend implementiert."); return drafts; -} \ No newline at end of file +} + +// --- PROJECT MANAGEMENT (DB) --- + +export const listProjects = async (): Promise => { + const response = await fetch(`${API_BASE_URL}/projects`); + if (!response.ok) throw new Error("Failed to list projects"); + return await response.json(); +}; + +export const loadProject = async (id: string): Promise => { + const response = await fetch(`${API_BASE_URL}/projects/${id}`); + if (!response.ok) throw new Error("Failed to load project"); + return await response.json(); +}; + +export const saveProject = async (data: any): Promise => { + const response = await fetch(`${API_BASE_URL}/save-project`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + if (!response.ok) throw new Error("Failed to save project"); + return await response.json(); +}; \ No newline at end of file diff --git a/market_db_manager.py b/market_db_manager.py new file mode 100644 index 00000000..7a167fff --- /dev/null +++ b/market_db_manager.py @@ -0,0 +1,105 @@ +import sqlite3 +import json +import os +import uuid +from datetime import datetime + +DB_PATH = "/app/market_intelligence.db" + +def get_db_connection(): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + return conn + +def init_db(): + conn = get_db_connection() + # Flexible schema: We store almost everything in a 'data' JSON column + conn.execute(''' + CREATE TABLE IF NOT EXISTS projects ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + data JSON NOT NULL + ) + ''') + conn.commit() + conn.close() + +def save_project(project_data): + """ + Saves a project. If 'id' exists in data, updates it. Otherwise creates new. + """ + conn = get_db_connection() + try: + project_id = project_data.get('id') + + # Extract a name for the list view (e.g. from companyName or referenceUrl) + # We assume the frontend passes a 'name' field, or we derive it. + name = project_data.get('name') or project_data.get('companyName') or "Untitled Project" + + if not project_id: + # Create New + project_id = str(uuid.uuid4()) + project_data['id'] = project_id + + conn.execute( + 'INSERT INTO projects (id, name, data) VALUES (?, ?, ?)', + (project_id, name, json.dumps(project_data)) + ) + else: + # Update Existing + conn.execute( + '''UPDATE projects + SET name = ?, data = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ?''', + (name, json.dumps(project_data), project_id) + ) + + conn.commit() + return {"id": project_id, "status": "saved"} + + except Exception as e: + return {"error": str(e)} + finally: + conn.close() + +def get_all_projects(): + conn = get_db_connection() + projects = conn.execute('SELECT id, name, created_at, updated_at FROM projects ORDER BY updated_at DESC').fetchall() + conn.close() + return [dict(ix) for ix in projects] + +def load_project(project_id): + conn = get_db_connection() + project = conn.execute('SELECT data FROM projects WHERE id = ?', (project_id,)).fetchone() + conn.close() + if project: + return json.loads(project['data']) + return None + +if __name__ == "__main__": + import sys + # Simple CLI for Node.js bridge + # Usage: python market_db_manager.py [init|list|save|load] [args...] + + mode = sys.argv[1] + + if mode == "init": + init_db() + print(json.dumps({"status": "initialized"})) + + elif mode == "list": + print(json.dumps(get_all_projects())) + + elif mode == "save": + # Data is passed as a JSON string file path to avoid command line length limits + data_file = sys.argv[2] + with open(data_file, 'r') as f: + data = json.load(f) + print(json.dumps(save_project(data))) + + elif mode == "load": + p_id = sys.argv[2] + result = load_project(p_id) + print(json.dumps(result if result else {"error": "Project not found"}))