916 lines
43 KiB
TypeScript
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; |