Files
Brancheneinstufung2/gtm-architect/App.tsx

916 lines
43 KiB
TypeScript

import React, { useState, useEffect, useRef } from 'react';
import { Layout } from './components/Layout';
import { Phase, AppState, Phase1Data, Phase2Data, Phase3Data, Phase4Data, Phase6Data, Language, Theme } from './types';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { AlertTriangle, ArrowRight, ArrowLeft, Check, Database, Globe, Search, ShieldAlert, Cpu, Building2, UserCircle, Briefcase, Zap, Terminal, Target, Crosshair, Loader2, Plus, X, Download, ShieldCheck, Image as ImageIcon, Copy, Sparkles, Upload, CheckCircle, PenTool, Eraser, Undo, Save, RefreshCw, Pencil, Trash2, History } from 'lucide-react';
const TRANSLATIONS = {
en: {
phase1: 'Product & Constraints',
phase2: 'ICP Discovery',
phase3: 'Whale Hunting',
phase4: 'Strategy Matrix',
phase5: 'Asset Generation',
phase6: 'Sales Enablement',
initTitle: 'Initialize New Product Sequence',
inputLabel: 'Product URL or Technical Specification',
inputPlaceholder: 'Paste raw technical data, brochure text, or product URL here...',
startBtn: 'Start Analysis',
backBtn: 'Back',
processing: 'Processing...',
features: 'Detected Features',
constraints: 'Hard Constraints',
conflictTitle: 'Portfolio Conflict Detected',
conflictPass: 'Portfolio Conflict Check Passed: Unique Value Proposition Identified.',
confirmICP: 'Confirm & Proceed to ICP',
targetId: 'Target Identification',
dataProxies: 'Data Proxies (Digital Footprints)',
method: 'Method',
huntWhales: 'Lock Targets & Hunt Whales',
region: 'Region: DACH (Germany, Austria, Switzerland)',
whales: 'Key Accounts (Whales)',
roles: 'Buying Center Roles',
confirmStrat: 'Confirm Accounts & Strategize',
stratAngles: 'Strategic Angles',
segment: 'Segment',
pain: 'Pain Point',
angle: 'Our Angle',
diff: 'Differentiation',
writeCopy: 'Generate Assets',
genAssets: 'Generated Strategy Report & Assets',
complete: 'Process Complete. Strategy locked.',
restart: 'Start New Session',
addPlaceholder: 'Add item...',
download: 'Download Full Report (.md)',
downloadAsset: 'Download Asset',
toPhase6: 'Proceed to Sales Enablement (Phase 6)',
phase6Title: 'Sales Enablement & Visuals',
battlecards: 'Kill-Critique Battlecards',
visuals: 'Visual Briefings (Midjourney Prompts)',
objection: 'Objection',
response: 'Response Script',
copyPrompt: 'Copy Prompt',
copied: 'Copied!',
genImage: 'Generate Concept',
editImage: 'Edit & Iterate',
regenerateSketch: 'Regenerate with Sketch',
cancelEdit: 'Cancel Edit',
generating: 'Generating...',
uploadImage: 'Product Reference Images',
uploadHint: 'Upload multiple photos (Front, Side, Detail) for better 3D understanding.',
imageUploaded: 'Reference Images',
canvasClear: 'Clear',
canvasMode: 'Sketch Mode',
addName: 'Name...',
addRationale: 'Rationale...',
addTarget: 'Target criteria...',
addMethod: 'Method...',
addAccount: 'Account name...',
addRole: 'Job title...',
save: 'Save Project',
load: 'Load Project',
history: 'Project History',
noProjects: 'No saved projects found.',
delete: 'Delete',
loading: [
"Initializing quantum analysis engines...",
"Parsing raw technical specifications...",
"Simulating physical constraints & boundary conditions...",
"Cross-referencing internal product matrix...",
"Detecting potential portfolio conflicts...",
"Synthesizing GTM parameters..."
]
},
de: {
phase1: 'Produkt & Constraints',
phase2: 'ICP Entdeckung',
phase3: 'Whale Hunting',
phase4: 'Strategie-Matrix',
phase5: 'Asset Generierung',
phase6: 'Sales Enablement',
initTitle: 'Neue Produktsequenz initialisieren',
inputLabel: 'Produkt-URL oder Technische Spezifikation',
inputPlaceholder: 'Füge hier rohe technische Daten, Broschürentexte oder Produkt-URL ein...',
startBtn: 'Analyse starten',
backBtn: 'Zurück',
processing: 'Verarbeite...',
features: 'Erkannte Features',
constraints: 'Harte Constraints',
conflictTitle: 'Portfolio-Konflikt erkannt',
conflictPass: 'Portfolio-Konfliktprüfung bestanden: Alleinstellungsmerkmal identifiziert.',
confirmICP: 'Bestätigen & weiter zu ICP',
targetId: 'Zielgruppen-Identifikation',
dataProxies: 'Data Proxies (Digitale Spuren)',
method: 'Methode',
huntWhales: 'Ziele fixieren & Whales jagen',
region: 'Region: DACH (Deutschland, Österreich, Schweiz)',
whales: 'Key Accounts (Whales)',
roles: 'Buying Center Rollen',
confirmStrat: 'Accounts bestätigen & Strategie',
stratAngles: 'Strategische Angles',
segment: 'Segment',
pain: 'Pain Point',
angle: 'Unser Angle',
diff: 'Differenzierung',
writeCopy: 'Report & Assets generieren',
genAssets: 'Generierter Strategie-Report & Assets',
complete: 'Prozess abgeschlossen. Strategie fixiert.',
restart: 'Neue Session starten',
addPlaceholder: 'Punkt hinzufügen...',
download: 'Gesamtbericht herunterladen (.md)',
downloadAsset: 'Bild speichern',
toPhase6: 'Weiter zu Sales Enablement (Phase 6)',
phase6Title: 'Sales Enablement & Visuals',
battlecards: 'Kill-Critique Battlecards',
visuals: 'Visual Briefings (Midjourney Prompts)',
objection: 'Einwand',
response: 'Antwort-Skript',
copyPrompt: 'Prompt kopieren',
copied: 'Kopiert!',
genImage: 'Konzept erstellen',
editImage: 'Zeichnen & Iterieren',
regenerateSketch: 'Mit Skizze neu generieren',
cancelEdit: 'Abbrechen',
generating: 'Generiere...',
uploadImage: 'Produkt-Referenzbilder',
uploadHint: 'Lade mehrere Fotos hoch (Front, Seite, Detail), um das 3D-Verständnis zu verbessern.',
imageUploaded: 'Referenzbilder',
canvasClear: 'Leeren',
canvasMode: 'Zeichenmodus',
addName: 'Name...',
addRationale: 'Begründung...',
addTarget: 'Zielkriterium...',
addMethod: 'Suchmethode...',
addAccount: 'Firmenname...',
addRole: 'Jobtitel...',
save: 'Projekt speichern',
load: 'Projekt laden',
history: 'Projekt Historie',
noProjects: 'Keine gespeicherten Projekte gefunden.',
delete: 'Löschen',
loading: [
"Initialisiere Quanten-Analyse-Engines...",
"Analysiere technische Spezifikationen...",
"Simuliere physikalische Constraints...",
"Prüfe interne Produkt-Matrix...",
"Erkenne potenzielle Portfolio-Konflikte...",
"Synthetisiere GTM-Parameter..."
]
}
};
const App: React.FC = () => {
const [state, setState] = useState<AppState>({
currentPhase: Phase.Input,
isLoading: false,
history: [],
productInput: '',
productImages: [],
language: 'de',
theme: 'light'
});
const [language, setLanguage] = useState<Language>('de');
const [theme, setTheme] = useState<Theme>('light');
const [error, setError] = useState<string | null>(null);
const [loadingMessage, setLoadingMessage] = useState("");
// History / DB State
const [showHistory, setShowHistory] = useState(false);
const [savedProjects, setSavedProjects] = useState<any[]>([]);
// Input State
const [newFeatureInput, setNewFeatureInput] = useState("");
const [newConstraintInput, setNewConstraintInput] = useState("");
const [newICPName, setNewICPName] = useState("");
const [newICPRationale, setNewICPRationale] = useState("");
const [newProxyTarget, setNewProxyTarget] = useState("");
const [newProxyMethod, setNewProxyMethod] = useState("");
const [newAccountInputs, setNewAccountInputs] = useState<Record<number, string>>({});
const [newRoleInput, setNewRoleInput] = useState("");
const [copiedPromptIndex, setCopiedPromptIndex] = useState<number | null>(null);
// Image Gen State
const [generatingImages, setGeneratingImages] = useState<Record<number, boolean>>({});
const [generatedImages, setGeneratedImages] = useState<Record<number, string>>({});
const [editingIndex, setEditingIndex] = useState<number | null>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const [isDrawing, setIsDrawing] = useState(false);
const [brushColor, setBrushColor] = useState('#ef4444');
const [brushSize, setBrushSize] = useState(4);
const labels = TRANSLATIONS[language];
useEffect(() => {
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}, [theme]);
useEffect(() => {
if (!state.isLoading) return;
const messages = labels.loading;
let i = 0;
setLoadingMessage(messages[0]);
const interval = setInterval(() => {
i = (i + 1) % messages.length;
setLoadingMessage(messages[i]);
}, 2500);
return () => clearInterval(interval);
}, [state.isLoading, labels.loading]);
useEffect(() => {
if (editingIndex !== null && canvasRef.current && generatedImages[editingIndex]) {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;
ctx?.drawImage(img, 0, 0);
};
img.src = generatedImages[editingIndex];
}
}, [editingIndex, generatedImages]);
const toggleTheme = () => setTheme(prev => prev === 'light' ? 'dark' : 'light');
const goBack = () => {
if (state.currentPhase > Phase.Input) {
setState(s => ({ ...s, currentPhase: s.currentPhase - 1 }));
}
};
const getMaxAllowedPhase = (): Phase => {
if (state.phase6Result) return Phase.SalesEnablement;
if (state.phase5Result) return Phase.AssetGeneration;
if (state.phase4Result) return Phase.Strategy;
if (state.phase3Result) return Phase.WhaleHunting;
if (state.phase2Result) return Phase.ICPDiscovery;
if (state.phase1Result) return Phase.ProductAnalysis;
return Phase.Input;
};
const handlePhaseSelect = (phase: Phase) => {
if (state.isLoading) return;
setState(s => ({ ...s, currentPhase: phase }));
};
const callBackend = async (mode: string, data: any) => {
const response = await fetch('./api/gtm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode, data })
});
if (!response.ok) {
const err = await response.json();
throw new Error(err.details || 'Backend error');
}
return await response.json();
};
// --- DB Handlers ---
const fetchProjects = async () => {
try {
const projects = await callBackend('list_projects', {});
setSavedProjects(projects);
} catch (e) {
console.error("Failed to list projects", e);
}
};
const handleSaveProject = async () => {
try {
// Clone state to avoid mutation issues
const projectData = { ...state, language, theme };
await callBackend('save_project', projectData);
// Optional: Show success notification
const btn = document.getElementById('save-btn');
if(btn) {
const originalText = btn.innerHTML;
btn.innerHTML = `<span class='flex items-center gap-2'><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg> Saved!</span>`;
setTimeout(() => btn.innerHTML = originalText, 2000);
}
} catch (e: any) {
setError("Save failed: " + e.message);
}
};
const handleLoadProject = async (id: string) => {
try {
const project = await callBackend('load_project', { id });
if (project) {
setState(project);
if (project.language) setLanguage(project.language);
if (project.theme) setTheme(project.theme);
setShowHistory(false);
}
} catch (e: any) {
setError("Load failed: " + e.message);
}
};
const handleDeleteProject = async (id: string, e: React.MouseEvent) => {
e.stopPropagation();
if (!confirm("Are you sure you want to delete this project?")) return;
try {
await callBackend('delete_project', { id });
setSavedProjects(prev => prev.filter(p => p.id !== id));
} catch (e: any) {
setError("Delete failed: " + e.message);
}
};
const toggleHistory = () => {
if (!showHistory) {
fetchProjects();
}
setShowHistory(!showHistory);
};
// --- Phase Handlers ---
const handlePhase1Submit = async () => {
if (!state.productInput.trim()) return;
setState(s => ({ ...s, isLoading: true }));
setError(null);
try {
const result = await callBackend('analyze_product', { productInput: state.productInput, language });
setState(s => ({ ...s, currentPhase: Phase.ProductAnalysis, phase1Result: result, isLoading: false }));
} catch (e: any) {
setError(e.message || "Analysis failed");
setState(s => ({ ...s, isLoading: false }));
}
};
const handlePhase2Submit = async () => {
if (!state.phase1Result) return;
setState(s => ({ ...s, isLoading: true }));
try {
const result = await callBackend('discover_icps', { phase1Result: state.phase1Result, language });
setState(s => ({ ...s, currentPhase: Phase.ICPDiscovery, phase2Result: result, isLoading: false }));
} catch (e: any) {
setError(e.message);
setState(s => ({ ...s, isLoading: false }));
}
};
const handlePhase3Submit = async () => {
if (!state.phase2Result) return;
setState(s => ({ ...s, isLoading: true }));
try {
const result = await callBackend('hunt_whales', { phase2Result: state.phase2Result, language });
setState(s => ({ ...s, currentPhase: Phase.WhaleHunting, phase3Result: result, isLoading: false }));
} catch (e: any) {
setError(e.message);
setState(s => ({ ...s, isLoading: false }));
}
};
const handlePhase4Submit = async () => {
if (!state.phase3Result || !state.phase1Result) return;
setState(s => ({ ...s, isLoading: true }));
try {
const result = await callBackend('develop_strategy', { phase3Result: state.phase3Result, phase1Result: state.phase1Result, language });
setState(s => ({ ...s, currentPhase: Phase.Strategy, phase4Result: result, isLoading: false }));
} catch (e: any) {
setError(e.message);
setState(s => ({ ...s, isLoading: false }));
}
};
const handlePhase5Submit = async () => {
if (!state.phase4Result || !state.phase3Result || !state.phase2Result || !state.phase1Result) return;
setState(s => ({ ...s, isLoading: true }));
try {
const result = await callBackend('generate_assets', {
phase4Result: state.phase4Result,
phase3Result: state.phase3Result,
phase2Result: state.phase2Result,
phase1Result: state.phase1Result,
language
});
setState(s => ({ ...s, currentPhase: Phase.AssetGeneration, phase5Result: result, isLoading: false }));
} catch (e: any) {
setError(e.message);
setState(s => ({ ...s, isLoading: false }));
}
};
const handlePhase6Submit = async () => {
if (!state.phase4Result || !state.phase3Result || !state.phase1Result) return;
setState(s => ({ ...s, isLoading: true }));
try {
const result = await callBackend('generate_sales_enablement', {
phase4Result: state.phase4Result,
phase3Result: state.phase3Result,
phase1Result: state.phase1Result,
language
});
setState(s => ({ ...s, currentPhase: Phase.SalesEnablement, phase6Result: result, isLoading: false }));
} catch (e: any) {
setError(e.message);
setState(s => ({ ...s, isLoading: false }));
}
};
const downloadReport = () => {
if (!state.phase5Result || !state.phase6Result) return;
let fullReport = state.phase5Result;
fullReport += `\n\n# SALES ENABLEMENT & VISUALS (PHASE 6)\n\n`;
fullReport += `## Kill-Critique Battlecards\n\n`;
state.phase6Result.battlecards.forEach(card => {
fullReport += `### Persona: ${card.persona}\n`;
fullReport += `> **Objection:** "${card.objection}"\n\n`;
fullReport += `**Response:** ${card.responseScript}\n\n---\n\n`;
});
fullReport += `## Visual Briefings (Prompts)\n\n`;
state.phase6Result.visualPrompts.forEach(prompt => {
fullReport += `### ${prompt.title}\n`;
fullReport += `*Context: ${prompt.context}*\n\n`;
fullReport += `\
\
${prompt.prompt}\n\
\
`;
});
const element = document.createElement("a");
const file = new Blob([fullReport], {type: 'text/markdown'});
element.href = URL.createObjectURL(file);
element.download = `roboplanet-gtm-strategy-${new Date().toISOString().slice(0,10)}.md`;
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
};
const downloadAsset = (dataUrl: string, index: number) => {
const element = document.createElement("a");
element.href = dataUrl;
element.download = `roboplanet-visual-${index + 1}.png`;
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
};
const copyToClipboard = (text: string, index: number) => {
navigator.clipboard.writeText(text);
setCopiedPromptIndex(index);
setTimeout(() => setCopiedPromptIndex(null), 2000);
};
const handleGenerateImage = async (prompt: string, index: number, overrideReference?: string) => {
setError("Bildgenerierung ist in der lokalen Version aktuell deaktiviert. Bitte nutzen Sie die Prompts direkt in Midjourney/DALL-E.");
};
const handleImageUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (files && files.length > 0) {
Array.from(files).forEach(file => {
const reader = new FileReader();
reader.onloadend = () => {
if (reader.result) {
setState(s => ({ ...s, productImages: [...s.productImages, reader.result as string] }));
}
};
reader.readAsDataURL(file);
});
}
};
const removeUploadedImage = (index: number) => {
setState(s => ({ ...s, productImages: s.productImages.filter((_, i) => i !== index) }));
};
const startDrawing = (e: any) => {
setIsDrawing(true);
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = (('touches' in e ? e.touches[0].clientX : e.clientX) - rect.left) * scaleX;
const y = (('touches' in e ? e.touches[0].clientY : e.clientY) - rect.top) * scaleY;
ctx.beginPath();
ctx.moveTo(x, y);
};
const draw = (e: any) => {
if (!isDrawing) return;
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = (('touches' in e ? e.touches[0].clientX : e.clientX) - rect.left) * scaleX;
const y = (('touches' in e ? e.touches[0].clientY : e.clientY) - rect.top) * scaleY;
ctx.lineTo(x, y);
ctx.strokeStyle = brushColor;
ctx.lineWidth = brushSize;
ctx.lineCap = 'round';
ctx.stroke();
};
const stopDrawing = () => setIsDrawing(false);
const clearCanvas = () => {
const canvas = canvasRef.current;
if (canvas && editingIndex !== null) {
const ctx = canvas.getContext('2d');
if (ctx) {
const img = new Image();
img.onload = () => {
ctx.clearRect(0,0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
}
img.src = generatedImages[editingIndex];
}
}
};
const handleRegenerateWithSketch = (index: number, prompt: string) => {
const canvas = canvasRef.current;
if (canvas) {
const sketchData = canvas.toDataURL('image/png');
handleGenerateImage(prompt, index, sketchData);
}
};
// --- List Mutation Handlers ---
const addFeature = () => {
if (!newFeatureInput.trim() || !state.phase1Result) return;
setState(s => ({
...s,
phase1Result: s.phase1Result ? { ...s.phase1Result, features: [...s.phase1Result.features, newFeatureInput.trim()] } : undefined
}));
setNewFeatureInput("");
};
const removeFeature = (index: number) => setState(s => ({
...s,
phase1Result: s.phase1Result ? { ...s.phase1Result, features: s.phase1Result.features.filter((_, i) => i !== index) } : undefined
}));
const addConstraint = () => {
if (!newConstraintInput.trim() || !state.phase1Result) return;
setState(s => ({
...s,
phase1Result: s.phase1Result ? { ...s.phase1Result, constraints: [...s.phase1Result.constraints, newConstraintInput.trim()] } : undefined
}));
setNewConstraintInput("");
};
const removeConstraint = (index: number) => setState(s => ({
...s,
phase1Result: s.phase1Result ? { ...s.phase1Result, constraints: s.phase1Result.constraints.filter((_, i) => i !== index) } : undefined
}));
const addICP = () => {
if (!newICPName.trim() || !newICPRationale.trim() || !state.phase2Result) return;
setState(s => ({
...s,
phase2Result: s.phase2Result ? { ...s.phase2Result, icps: [...s.phase2Result.icps, { name: newICPName.trim(), rationale: newICPRationale.trim() }] } : undefined
}));
setNewICPName("");
setNewICPRationale("");
};
const removeICP = (index: number) => setState(s => ({
...s,
phase2Result: s.phase2Result ? { ...s.phase2Result, icps: s.phase2Result.icps.filter((_, i) => i !== index) } : undefined
}));
const addProxy = () => {
if (!newProxyTarget.trim() || !newProxyMethod.trim() || !state.phase2Result) return;
setState(s => ({
...s,
phase2Result: s.phase2Result ? { ...s.phase2Result, dataProxies: [...s.phase2Result.dataProxies, { target: newProxyTarget.trim(), method: newProxyMethod.trim() }] } : undefined
}));
setNewProxyTarget("");
setNewProxyMethod("");
};
const removeProxy = (index: number) => setState(s => ({
...s,
phase2Result: s.phase2Result ? { ...s.phase2Result, dataProxies: s.phase2Result.dataProxies.filter((_, i) => i !== index) } : undefined
}));
const addAccount = (groupIndex: number) => {
const val = newAccountInputs[groupIndex];
if (!val?.trim() || !state.phase3Result) return;
const newWhales = [...state.phase3Result.whales];
newWhales[groupIndex] = { ...newWhales[groupIndex], accounts: [...newWhales[groupIndex].accounts, val.trim()] };
setState(s => ({ ...s, phase3Result: s.phase3Result ? { ...s.phase3Result, whales: newWhales } : undefined }));
setNewAccountInputs(prev => ({...prev, [groupIndex]: ""}));
};
const removeAccount = (groupIndex: number, accountIndex: number) => {
if (!state.phase3Result) return;
const newWhales = [...state.phase3Result.whales];
newWhales[groupIndex] = { ...newWhales[groupIndex], accounts: newWhales[groupIndex].accounts.filter((_, i) => i !== accountIndex) };
setState(s => ({ ...s, phase3Result: s.phase3Result ? { ...s.phase3Result, whales: newWhales } : undefined }));
};
const addRole = () => {
if (!newRoleInput.trim() || !state.phase3Result) return;
setState(s => ({ ...s, phase3Result: s.phase3Result ? { ...s.phase3Result, roles: [...s.phase3Result.roles, newRoleInput.trim()] } : undefined }));
setNewRoleInput("");
};
const removeRole = (index: number) => setState(s => ({ ...s, phase3Result: s.phase3Result ? { ...s.phase3Result, roles: s.phase3Result.roles.filter((_, i) => i !== index) } : undefined }));
// --- Render Functions ---
const renderInputPhase = () => (
<div className="flex flex-col items-center justify-center min-h-[60vh] animate-in fade-in slide-in-from-bottom-4 duration-700">
<div className="w-full max-w-2xl p-8 rounded-xl shadow-2xl transition-colors duration-300 bg-white border border-slate-200 dark:bg-robo-800 dark:border-robo-700">
<h2 className="text-2xl font-bold mb-6 flex items-center gap-2 text-slate-800 dark:text-white">
<Terminal className="text-blue-600 dark:text-robo-accent" /> {labels.initTitle}
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-500 dark:text-slate-400 mb-2">{labels.inputLabel}</label>
<textarea
className="w-full h-40 rounded-lg p-4 font-mono text-sm resize-none focus:ring-2 focus:outline-none transition-colors bg-slate-50 border border-slate-300 text-slate-900 focus:ring-blue-500 dark:bg-robo-900 dark:border-robo-700 dark:text-slate-200 dark:focus:ring-robo-500"
placeholder={labels.inputPlaceholder}
value={state.productInput}
onChange={(e) => setState(s => ({ ...s, productInput: e.target.value }))}
disabled={state.isLoading}
/>
</div>
<div className="p-4 rounded-lg border border-dashed transition-colors bg-slate-50 border-slate-300 dark:bg-robo-900/50 dark:border-robo-600">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2 flex items-center gap-2">
<ImageIcon size={16}/> {labels.uploadImage}
</label>
<div className="grid grid-cols-4 gap-4 mb-4">
{state.productImages.map((img, idx) => (
<div key={idx} className="relative group aspect-square rounded-lg overflow-hidden border border-slate-200 dark:border-robo-700 bg-white">
<img src={img} alt={`Ref ${idx}`} className="w-full h-full object-cover" />
<button
onClick={() => removeUploadedImage(idx)}
className="absolute top-1 right-1 bg-red-500 text-white p-1 rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
>
<X size={12}/>
</button>
</div>
))}
<label className="flex flex-col items-center justify-center aspect-square rounded-lg border-2 border-dashed border-slate-300 dark:border-robo-600 hover:border-blue-400 dark:hover:border-robo-400 cursor-pointer transition-colors bg-slate-50 dark:bg-robo-900/30">
<input
type="file"
accept="image/*"
multiple
onChange={handleImageUpload}
className="hidden"
/>
<Plus className="text-slate-400 mb-1" size={24}/>
<span className="text-xs text-slate-500">Add</span>
</label>
</div>
<p className="text-xs text-slate-500 text-center">{labels.uploadHint}</p>
</div>
<button
onClick={handlePhase1Submit}
disabled={state.isLoading || !state.productInput}
className="w-full font-bold py-3 px-4 rounded-lg transition-all flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-50 dark:bg-robo-500 dark:hover:bg-robo-400"
>
{state.isLoading ? <span className="flex items-center gap-2"><Loader2 className="animate-spin" /> {labels.processing}</span> : <>{labels.startBtn} <ArrowRight size={18} /></>}</button>
{state.isLoading && (
<div className="mt-4 p-4 rounded-lg border font-mono text-sm animate-in fade-in slide-in-from-top-2 bg-white border-slate-200 dark:bg-robo-900/80 dark:border-robo-700">
<div className="flex items-center gap-3 text-emerald-600 dark:text-emerald-400">
<div className="relative flex h-3 w-3">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-3 w-3 bg-emerald-500"></span>
</div>
<span className="animate-pulse">{loadingMessage}</span>
</div>
</div>
)}
</div>
</div>
</div>
);
const renderPhase1 = () => (
<div className="space-y-6 animate-in fade-in">
<div className="p-6 rounded-xl border transition-colors bg-white border-slate-200 dark:bg-robo-800 dark:border-robo-700">
<h2 className="text-xl font-bold mb-4 flex items-center gap-2 text-slate-900 dark:text-white"><Cpu className="text-purple-500" /> {labels.features} & Constraints</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h3 className="text-sm font-bold uppercase tracking-wider text-slate-500 mb-2">{labels.features}</h3>
<ul className="space-y-2">
{state.phase1Result?.features.map((f, i) => (
<li key={i} className="flex justify-between items-center group text-sm p-2 rounded border bg-slate-50 dark:bg-robo-900 border-slate-200 dark:border-robo-700">
<span>{f}</span>
<button onClick={() => removeFeature(i)} className="opacity-0 group-hover:opacity-100 text-red-500"><X size={14}/></button>
</li>
))}
</ul>
<div className="flex gap-2 mt-3">
<input value={newFeatureInput} onChange={e => setNewFeatureInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && addFeature()} className="flex-1 text-sm border rounded px-3 py-2 dark:bg-robo-900 dark:text-white" placeholder={labels.addPlaceholder}/>
<button onClick={addFeature} className="p-2 bg-slate-100 dark:bg-robo-700 rounded"><Plus size={16}/></button>
</div>
</div>
<div>
<h3 className="text-sm font-bold uppercase tracking-wider text-slate-500 mb-2">{labels.constraints}</h3>
<ul className="space-y-2">
{state.phase1Result?.constraints.map((c, i) => (
<li key={i} className="flex justify-between items-center group text-sm p-2 rounded border bg-red-50 dark:bg-robo-900 border-red-200 dark:border-red-900/30">
<span>{c}</span>
<button onClick={() => removeConstraint(i)} className="opacity-0 group-hover:opacity-100 text-red-600"><X size={14}/></button>
</li>
))}
</ul>
<div className="flex gap-2 mt-3">
<input value={newConstraintInput} onChange={e => setNewConstraintInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && addConstraint()} className="flex-1 text-sm border rounded px-3 py-2 dark:bg-robo-900 dark:text-white" placeholder={labels.addPlaceholder}/>
<button onClick={addConstraint} className="p-2 bg-slate-100 dark:bg-robo-700 rounded"><Plus size={16}/></button>
</div>
</div>
</div>
</div>
<div className="flex justify-between pt-4">
<button onClick={goBack} disabled={state.isLoading} className="text-sm font-bold text-slate-500 flex items-center gap-2"><ArrowLeft size={16}/> {labels.backBtn}</button>
<button onClick={handlePhase2Submit} disabled={state.isLoading} className="font-bold py-3 px-6 rounded-lg bg-slate-900 text-white dark:bg-white dark:text-robo-900 flex items-center gap-2">
{state.isLoading ? <Loader2 className="animate-spin"/> : <>{labels.confirmICP} <ArrowRight size={18} /></>}</button>
</div>
</div>
);
const renderPhase2 = () => (
<div className="space-y-6 animate-in fade-in">
<h2 className="text-2xl font-bold flex items-center gap-2 text-slate-900 dark:text-white"><Target className="text-blue-600"/> {labels.targetId}</h2>
{state.phase2Result?.icps.map((icp, i) => (
<div key={i} className="relative p-6 rounded-xl border bg-white dark:bg-robo-800 border-slate-200 group">
<button onClick={() => removeICP(i)} className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 text-slate-500"><X size={16}/></button>
<h3 className="text-xl font-bold mb-2 dark:text-white">{i + 1}. {icp.name}</h3>
<p className="text-sm text-slate-600 dark:text-slate-300">{icp.rationale}</p>
</div>
))}
<div className="flex justify-between pt-4">
<button onClick={goBack} className="text-sm font-bold text-slate-500 flex items-center gap-2"><ArrowLeft size={16}/> {labels.backBtn}</button>
<button onClick={handlePhase3Submit} className="font-bold py-3 px-6 rounded-lg bg-slate-900 text-white dark:bg-white dark:text-robo-900 flex items-center gap-2">
{state.isLoading ? <Loader2 className="animate-spin"/> : <>{labels.huntWhales} <Crosshair size={18} /></>}</button>
</div>
</div>
);
const renderPhase3 = () => (
<div className="space-y-8 animate-in fade-in">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{state.phase3Result?.whales.map((w, idx) => (
<div key={idx} className="p-6 rounded-xl border bg-white dark:bg-robo-800 border-slate-200">
<h4 className="font-bold mb-4 dark:text-white uppercase text-xs tracking-widest text-slate-500">{w.industry}</h4>
<ul className="space-y-2">
{w.accounts.map((acc, aidx) => (
<li key={aidx} className="flex justify-between items-center group p-2 bg-slate-50 dark:bg-robo-900 rounded border border-slate-200 dark:border-robo-700">
<span className="text-sm font-medium dark:text-slate-200">{acc}</span>
<button onClick={() => removeAccount(idx, aidx)} className="opacity-0 group-hover:opacity-100 text-red-500"><X size={14}/></button>
</li>
))}
</ul>
</div>
))}
</div>
<div className="flex justify-between pt-4">
<button onClick={goBack} className="text-sm font-bold text-slate-500 flex items-center gap-2"><ArrowLeft size={16}/> {labels.backBtn}</button>
<button onClick={handlePhase4Submit} className="font-bold py-3 px-6 rounded-lg bg-slate-900 text-white dark:bg-white dark:text-robo-900 flex items-center gap-2">
{state.isLoading ? <Loader2 className="animate-spin"/> : <>{labels.confirmStrat} <Zap size={18} /></>}</button>
</div>
</div>
);
const renderPhase4 = () => (
<div className="space-y-8 animate-in fade-in">
<h2 className="text-2xl font-bold flex items-center gap-2 dark:text-white"><Zap className="text-yellow-500"/> {labels.stratAngles}</h2>
<div className="overflow-x-auto rounded-xl border border-slate-200 dark:border-robo-700 bg-white dark:bg-robo-800">
<table className="w-full text-left">
<thead className="bg-slate-100 dark:bg-robo-900 text-xs uppercase text-slate-500">
<tr><th className="p-4">{labels.segment}</th><th className="p-4">{labels.pain}</th><th className="p-4">{labels.angle}</th></tr>
</thead>
<tbody className="divide-y divide-slate-200 dark:divide-robo-700">
{state.phase4Result?.strategyMatrix.map((row, i) => (
<tr key={i} className="text-sm">
<td className="p-4 font-bold dark:text-white">{row.segment}</td>
<td className="p-4 text-red-600 dark:text-red-400">{row.painPoint}</td>
<td className="p-4 font-medium text-emerald-600 dark:text-emerald-400">{row.angle}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="flex justify-between pt-4">
<button onClick={goBack} className="text-sm font-bold text-slate-500 flex items-center gap-2"><ArrowLeft size={16}/> {labels.backBtn}</button>
<button onClick={handlePhase5Submit} className="font-bold py-3 px-6 rounded-lg bg-slate-900 text-white dark:bg-white dark:text-robo-900 flex items-center gap-2">
{state.isLoading ? <Loader2 className="animate-spin"/> : <>{labels.writeCopy} <Terminal size={18} /></>}</button>
</div>
</div>
);
const renderPhase5 = () => (
<div className="space-y-6 animate-in fade-in">
<div className="p-8 rounded-xl border font-mono text-sm bg-slate-50 dark:bg-robo-900 border-slate-200 dark:border-robo-700 dark:text-slate-300 overflow-x-auto leading-relaxed">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{state.phase5Result || ''}</ReactMarkdown>
</div>
<div className="flex justify-between pt-4">
<button onClick={goBack} className="text-sm font-bold text-slate-500 flex items-center gap-2"><ArrowLeft size={16}/> {labels.backBtn}</button>
<button onClick={handlePhase6Submit} className="font-bold py-3 px-6 rounded-lg bg-slate-900 text-white dark:bg-white dark:text-robo-900 flex items-center gap-2">
{state.isLoading ? <Loader2 className="animate-spin"/> : <>{labels.toPhase6} <ShieldCheck size={18} /></>}</button>
</div>
</div>
);
const renderPhase6 = () => (
<div className="space-y-8 animate-in fade-in">
<h2 className="text-2xl font-bold flex items-center gap-2 dark:text-white"><ShieldCheck className="text-emerald-600"/> {labels.phase6Title}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{state.phase6Result?.battlecards.map((card, i) => (
<div key={i} className="p-6 rounded-xl border bg-white dark:bg-robo-800 border-slate-200 shadow-sm">
<div className="font-bold mb-4 flex items-center gap-2 dark:text-white"><UserCircle size={16}/> {card.persona}</div>
<div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-100 rounded-lg text-sm italic mb-3 text-red-800 dark:text-red-300">"{card.objection}"</div>
<div className="p-3 bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-100 rounded-lg text-sm text-emerald-800 dark:text-emerald-300">{card.responseScript}</div>
</div>
))}
</div>
<div className="flex justify-between pt-4">
<button onClick={goBack} className="text-sm font-bold text-slate-500 flex items-center gap-2"><ArrowLeft size={16}/> {labels.backBtn}</button>
<button onClick={() => window.location.reload()} className="text-sm font-medium hover:underline text-slate-500">{labels.restart}</button>
</div>
</div>
);
return (
<Layout
currentPhase={state.currentPhase}
maxAllowedPhase={getMaxAllowedPhase()}
onPhaseSelect={handlePhaseSelect}
theme={theme}
toggleTheme={toggleTheme}
language={language}
setLanguage={setLanguage}
labels={labels}
>
{/* Top Bar Actions */}
<div className="absolute top-6 right-8 flex gap-2 z-50">
<button
id="save-btn"
onClick={handleSaveProject}
className="p-2 rounded-lg bg-white dark:bg-robo-800 border border-slate-200 dark:border-robo-700 hover:bg-slate-50 dark:hover:bg-robo-700 text-slate-600 dark:text-slate-300 shadow-sm transition-all"
title={labels.save}
>
<Save size={20}/>
</button>
<button
onClick={toggleHistory}
className="p-2 rounded-lg bg-white dark:bg-robo-800 border border-slate-200 dark:border-robo-700 hover:bg-slate-50 dark:hover:bg-robo-700 text-slate-600 dark:text-slate-300 shadow-sm transition-all"
title={labels.history}
>
<History size={20}/>
</button>
</div>
{/* History Modal */}
{showHistory && (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/50 backdrop-blur-sm animate-in fade-in">
<div className="w-full max-w-lg bg-white dark:bg-robo-900 rounded-xl shadow-2xl overflow-hidden border border-slate-200 dark:border-robo-700 flex flex-col max-h-[80vh]">
<div className="p-4 border-b border-slate-100 dark:border-robo-700 flex justify-between items-center bg-slate-50 dark:bg-robo-800">
<h3 className="font-bold flex items-center gap-2 dark:text-white"><History size={18}/> {labels.history}</h3>
<button onClick={() => setShowHistory(false)} className="p-1 hover:bg-slate-200 dark:hover:bg-robo-600 rounded-full"><X size={18}/></button>
</div>
<div className="overflow-y-auto p-4 flex-1">
{savedProjects.length === 0 ? (
<div className="text-center text-slate-500 py-8 italic">{labels.noProjects}</div>
) : (
<div className="space-y-3">
{savedProjects.map((p) => (
<div key={p.id} onClick={() => handleLoadProject(p.id)} className="group cursor-pointer p-4 rounded-lg border border-slate-200 dark:border-robo-700 hover:border-blue-400 dark:hover:border-robo-500 bg-slate-50 dark:bg-robo-800/50 transition-all hover:shadow-md relative">
<div className="font-bold text-slate-800 dark:text-white mb-1 pr-8 truncate">{p.name}</div>
<div className="text-xs text-slate-500 flex justify-between">
<span>{new Date(p.updated_at).toLocaleString()}</span>
<button
onClick={(e) => handleDeleteProject(p.id, e)}
className="text-red-400 hover:text-red-600 p-1 -m-1 rounded opacity-0 group-hover:opacity-100 transition-opacity"
title={labels.delete}
>
<Trash2 size={14}/>
</button>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
)}
{error && <div className="mb-6 p-4 rounded-lg flex items-center gap-3 bg-red-50 border border-red-200 text-red-700"><ShieldAlert size={20} />{error}</div>}
{state.currentPhase === Phase.Input && renderInputPhase()}
{state.currentPhase === Phase.ProductAnalysis && renderPhase1()}
{state.currentPhase === Phase.ICPDiscovery && renderPhase2()}
{state.currentPhase === Phase.WhaleHunting && renderPhase3()}
{state.currentPhase === Phase.Strategy && renderPhase4()}
{state.currentPhase === Phase.AssetGeneration && renderPhase5()}
{state.currentPhase === Phase.SalesEnablement && renderPhase6()}
</Layout>
);
};
export default App;