- Backend: Implemented secondary extraction phase for structured specs (JSON schema). - Backend: Added strict normalization rules (min, cm, kg). - Frontend: Added 'Phase1Data' interface update for specs. - Frontend: Implemented new UI component for 'Technical Specifications' in Phase 1. - Frontend: Updated header and sidebar to display 'v2.5' build marker. - Docs: Updated architectural documentation.
2198 lines
106 KiB
TypeScript
2198 lines
106 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import { Layout } from './components/Layout';
|
|
import { Phase, AppState, Phase1Data, Phase2Data, Phase3Data, Phase4Data, Phase6Data, Phase7Data, Phase8Data, Phase9Data, Language, Theme, ProjectHistoryItem } from './types';
|
|
import * as Gemini from './geminiService';
|
|
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, LayoutTemplate, TrendingUp, Shield, Languages, Clock, History, FileText } from 'lucide-react';
|
|
|
|
const TRANSLATIONS = {
|
|
en: {
|
|
// ... existing ...
|
|
historyTitle: 'Recent Sessions',
|
|
loadBtn: 'Load',
|
|
noSessions: 'No history found.',
|
|
// ... existing ...
|
|
phase1: 'Product & Constraints',
|
|
phase2: 'ICP Discovery',
|
|
phase3: 'Whale Hunting',
|
|
phase4: 'Strategy Matrix',
|
|
phase5: 'Asset Generation',
|
|
phase6: 'Sales Enablement',
|
|
phase7: 'Vertical Landing Page',
|
|
phase8: 'Business Case (ROI)',
|
|
phase9: 'Feature-to-Value Translator',
|
|
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',
|
|
translateBtn: 'Translate Report to English',
|
|
downloadEn: 'Download English Report (.md)',
|
|
translating: 'Translating...',
|
|
toPhase6: 'Proceed to Sales Enablement (Phase 6)',
|
|
toPhase7: 'Proceed to Landing Pages (Phase 7)',
|
|
toPhase8: 'Proceed to Business Case (Phase 8)',
|
|
toPhase9: 'Proceed to Feature-to-Value (Phase 9)',
|
|
phase6Title: 'Sales Enablement & Visuals',
|
|
phase7Title: 'Vertical Landing Pages',
|
|
phase8Title: 'Business Case Builder',
|
|
phase9Title: 'Feature-to-Value Translator',
|
|
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',
|
|
// New placeholders
|
|
addName: 'Name...',
|
|
addRationale: 'Rationale...',
|
|
addTarget: 'Target criteria...',
|
|
addMethod: 'Method...',
|
|
addAccount: 'Account name...',
|
|
addRole: 'Job title...',
|
|
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: {
|
|
historyTitle: 'Letzte Sitzungen',
|
|
loadBtn: 'Laden',
|
|
noSessions: 'Keine Historie gefunden.',
|
|
phase1: 'Produkt & Constraints',
|
|
phase2: 'ICP Entdeckung',
|
|
phase3: 'Whale Hunting',
|
|
phase4: 'Strategie-Matrix',
|
|
phase5: 'Asset Generierung',
|
|
phase6: 'Sales Enablement',
|
|
phase7: 'Vertical Landing Page',
|
|
phase8: 'Business Case (ROI)',
|
|
phase9: 'Feature-to-Value Translator',
|
|
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',
|
|
translateBtn: 'Bericht ins Englische übersetzen',
|
|
downloadEn: 'Englischen Bericht herunterladen (.md)',
|
|
translating: 'Übersetze...',
|
|
toPhase6: 'Weiter zu Sales Enablement (Phase 6)',
|
|
toPhase7: 'Weiter zu Landing Pages (Phase 7)',
|
|
toPhase8: 'Weiter zu Business Case (Phase 8)',
|
|
toPhase9: 'Weiter zu Feature-to-Value (Phase 9)',
|
|
phase6Title: 'Sales Enablement & Visuals',
|
|
phase7Title: 'Vertical Landing Pages',
|
|
phase8Title: 'Business Case Builder',
|
|
phase9Title: 'Feature-to-Value Translator',
|
|
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',
|
|
// New placeholders
|
|
addName: 'Name...',
|
|
addRationale: 'Begründung...',
|
|
addTarget: 'Zielkriterium...',
|
|
addMethod: 'Suchmethode...',
|
|
addAccount: 'Firmenname...',
|
|
addRole: 'Jobtitel...',
|
|
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: [], // Now an array
|
|
language: 'light', // temporary init, fix in useEffect
|
|
theme: 'light' // Default to light
|
|
});
|
|
|
|
// Fix initial state type matching
|
|
const [language, setLanguage] = useState<Language>('de'); // Default to German per prompt
|
|
const [theme, setTheme] = useState<Theme>('light');
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [loadingMessage, setLoadingMessage] = useState("");
|
|
const [isTranslating, setIsTranslating] = useState(false);
|
|
|
|
// Session Management
|
|
const [sessions, setSessions] = useState<ProjectHistoryItem[]>([]);
|
|
|
|
// Local state for adding new items (Human in the Loop inputs)
|
|
// Phase 1
|
|
const [newFeatureInput, setNewFeatureInput] = useState("");
|
|
const [newConstraintInput, setNewConstraintInput] = useState("");
|
|
|
|
// Phase 2
|
|
const [newICPName, setNewICPName] = useState("");
|
|
const [newICPRationale, setNewICPRationale] = useState("");
|
|
const [newProxyTarget, setNewProxyTarget] = useState("");
|
|
const [newProxyMethod, setNewProxyMethod] = useState("");
|
|
// Phase 3
|
|
// Changed to use a map to track input per group index
|
|
const [newAccountInputs, setNewAccountInputs] = useState<Record<number, string>>({});
|
|
const [newRoleInput, setNewRoleInput] = useState("");
|
|
|
|
const [copiedPromptIndex, setCopiedPromptIndex] = useState<number | null>(null);
|
|
|
|
// Phase 6 Image Generation State
|
|
const [generatingImages, setGeneratingImages] = useState<Record<number, boolean>>({});
|
|
const [generatedImages, setGeneratedImages] = useState<Record<number, string>>({});
|
|
const [aspectRatio, setAspectRatio] = useState('16:9');
|
|
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
const [isDrawing, setIsDrawing] = useState(false);
|
|
const [brushColor, setBrushColor] = useState('#ef4444'); // Red for annotations
|
|
const [brushSize, setBrushSize] = useState(4);
|
|
|
|
const labels = TRANSLATIONS[language];
|
|
|
|
// Apply theme to body
|
|
useEffect(() => {
|
|
if (theme === 'dark') {
|
|
document.documentElement.classList.add('dark');
|
|
} else {
|
|
document.documentElement.classList.remove('dark');
|
|
}
|
|
}, [theme]);
|
|
|
|
// Load Sessions on Mount
|
|
useEffect(() => {
|
|
const fetchSessions = async () => {
|
|
try {
|
|
const res = await Gemini.listSessions();
|
|
setSessions(res.projects);
|
|
} catch (e) {
|
|
console.error("Failed to load sessions", e);
|
|
}
|
|
};
|
|
fetchSessions();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!state.isLoading && !isTranslating) 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, isTranslating, labels.loading]);
|
|
|
|
// Canvas Initialization for Editing
|
|
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 }));
|
|
}
|
|
};
|
|
|
|
// Determine the highest phase the user has completed data for.
|
|
// This allows navigation back and forth.
|
|
const getMaxAllowedPhase = (): Phase => {
|
|
if (state.phase9Result) return Phase.TechTranslator;
|
|
if (state.phase8Result) return Phase.BusinessCase;
|
|
if (state.phase7Result) return Phase.LandingPage;
|
|
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; // Prevent navigation while generating
|
|
setState(s => ({ ...s, currentPhase: phase }));
|
|
};
|
|
|
|
const handleLoadSession = async (projectId: string) => {
|
|
setLoadingMessage("Loading Session...");
|
|
setState(s => ({ ...s, isLoading: true }));
|
|
try {
|
|
const data = await Gemini.loadSession(projectId);
|
|
const phases = data.phases || {};
|
|
|
|
setState(s => ({
|
|
...s,
|
|
isLoading: false,
|
|
currentPhase: Phase.ProductAnalysis,
|
|
projectId: projectId,
|
|
productInput: phases.phase1_result?.rawAnalysis || "",
|
|
phase1Result: phases.phase1_result,
|
|
phase2Result: phases.phase2_result,
|
|
phase3Result: phases.phase3_result,
|
|
phase4Result: phases.phase4_result,
|
|
phase5Result: phases.phase5_result,
|
|
phase6Result: phases.phase6_result,
|
|
phase7Result: phases.phase7_result,
|
|
phase8Result: phases.phase8_result,
|
|
phase9Result: phases.phase9_result,
|
|
}));
|
|
} catch (e: any) {
|
|
setError("Failed to load session: " + e.message);
|
|
setState(s => ({ ...s, isLoading: false }));
|
|
}
|
|
};
|
|
|
|
const handleDeleteSession = async (e: React.MouseEvent, projectId: string) => {
|
|
e.stopPropagation();
|
|
if (!window.confirm("Delete this session permanently?")) return;
|
|
|
|
try {
|
|
await Gemini.deleteSession(projectId);
|
|
setSessions(prev => prev.filter(s => s.id !== projectId));
|
|
} catch (e: any) {
|
|
setError("Failed to delete session: " + e.message);
|
|
}
|
|
};
|
|
|
|
const handleLoadMarkdown = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = (event) => {
|
|
const content = event.target?.result as string;
|
|
if (content) {
|
|
setState(s => ({
|
|
...s,
|
|
currentPhase: Phase.AssetGeneration,
|
|
phase5Result: { report: content }
|
|
}));
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
};
|
|
|
|
const generateFullReportMarkdown = (): string => {
|
|
if (!state.phase5Result || !state.phase5Result.report) return "";
|
|
|
|
let fullReport = state.phase5Result.report;
|
|
|
|
if (state.phase6Result) {
|
|
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 += `\`\`\`\n${prompt.prompt}\n\`\`\`\n\n`;
|
|
});
|
|
}
|
|
|
|
if (state.phase7Result) {
|
|
fullReport += `\n\n# VERTICAL LANDING PAGES (PHASE 7)\n\n`;
|
|
state.phase7Result.landingPages.forEach(lp => {
|
|
fullReport += `## ${lp.industry}\n`;
|
|
fullReport += `**Headline:** ${lp.headline}\n\n`;
|
|
fullReport += `**Subline:** ${lp.subline}\n\n`;
|
|
fullReport += `**Benefits:**\n`;
|
|
lp.bullets.forEach(b => fullReport += `- ${b}\n`);
|
|
fullReport += `\n**CTA:** ${lp.cta}\n\n---\n\n`;
|
|
});
|
|
}
|
|
|
|
if (state.phase8Result) {
|
|
fullReport += `\n\n# BUSINESS CASE & ROI (PHASE 8)\n\n`;
|
|
state.phase8Result.businessCases.forEach(bc => {
|
|
fullReport += `## ${bc.industry}\n`;
|
|
fullReport += `**Cost Driver:** ${bc.costDriver}\n\n`;
|
|
fullReport += `**Efficiency Gain:** ${bc.efficiencyGain}\n\n`;
|
|
fullReport += `**Risk Argument:** ${bc.riskArgument}\n\n---\n\n`;
|
|
});
|
|
}
|
|
|
|
if (state.phase9Result) {
|
|
fullReport += `\n\n# FEATURE-TO-VALUE TRANSLATOR (PHASE 9)\n\n`;
|
|
fullReport += `| Feature | The Story (Benefit) | Headline |\n`;
|
|
fullReport += `| :--- | :--- | :--- |\n`;
|
|
state.phase9Result.techTranslations.forEach(tt => {
|
|
fullReport += `| ${tt.feature} | ${tt.story} | ${tt.headline} |\n`;
|
|
});
|
|
}
|
|
return fullReport;
|
|
};
|
|
|
|
const downloadReport = (content?: string, filenamePrefix: string = "") => {
|
|
const reportContent = content || generateFullReportMarkdown();
|
|
if (!reportContent) return;
|
|
|
|
const element = document.createElement("a");
|
|
const file = new Blob([reportContent], {type: 'text/markdown'});
|
|
element.href = URL.createObjectURL(file);
|
|
element.download = `roboplanet-gtm-strategy-${filenamePrefix}${new Date().toISOString().slice(0,10)}.md`;
|
|
document.body.appendChild(element);
|
|
element.click();
|
|
document.body.removeChild(element);
|
|
};
|
|
|
|
const handleTranslateReport = async () => {
|
|
const fullReport = generateFullReportMarkdown();
|
|
if (!fullReport) return;
|
|
|
|
setIsTranslating(true);
|
|
setError(null);
|
|
try {
|
|
const translated = await Gemini.translateReportToEnglish(fullReport);
|
|
setState(s => ({ ...s, translatedReport: translated }));
|
|
} catch (e: any) {
|
|
setError("Translation failed: " + e.message);
|
|
} finally {
|
|
setIsTranslating(false);
|
|
}
|
|
};
|
|
|
|
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);
|
|
};
|
|
|
|
// --- Image Generation Handlers ---
|
|
|
|
const handleGenerateImage = async (prompt: string, index: number, overrideReference?: string) => {
|
|
setGeneratingImages(prev => ({ ...prev, [index]: true }));
|
|
try {
|
|
// If sketching, use only the sketch as the strong reference
|
|
// If not sketching, use the array of uploaded product images
|
|
const refImages = overrideReference ? [overrideReference] : state.productImages;
|
|
|
|
const imageUrl = await Gemini.generateConceptImage(prompt, refImages, aspectRatio);
|
|
setGeneratedImages(prev => ({ ...prev, [index]: imageUrl }));
|
|
|
|
// If we were editing, close edit mode
|
|
if (editingIndex === index) {
|
|
setEditingIndex(null);
|
|
}
|
|
} catch (e: any) {
|
|
console.error("Failed to generate image", e);
|
|
setError("Image generation failed: " + e.message);
|
|
} finally {
|
|
setGeneratingImages(prev => ({ ...prev, [index]: false }));
|
|
}
|
|
};
|
|
|
|
const handleImageUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const files = event.target.files;
|
|
if (files && files.length > 0) {
|
|
Array.from(files).forEach((file: 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)
|
|
}));
|
|
};
|
|
|
|
// --- Drawing / Editing Handlers ---
|
|
|
|
const startDrawing = (e: React.MouseEvent<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
|
|
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: React.MouseEvent<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
|
|
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);
|
|
}
|
|
};
|
|
|
|
// --- Handlers ---
|
|
|
|
const handlePhase1Submit = async () => {
|
|
if (!state.productInput.trim()) return;
|
|
setState(s => ({ ...s, isLoading: true }));
|
|
setError(null);
|
|
try {
|
|
const result = await Gemini.analyzeProduct(state.productInput, language);
|
|
setState(s => ({ ...s, currentPhase: Phase.ProductAnalysis, phase1Result: result, isLoading: false, projectId: result.projectId })); // Save projectId!
|
|
} catch (e: any) {
|
|
setError(e.message || "Analysis failed");
|
|
setState(s => ({ ...s, isLoading: false }));
|
|
}
|
|
};
|
|
|
|
const handlePhase2Submit = async () => {
|
|
if (!state.phase1Result || !state.projectId) return;
|
|
setState(s => ({ ...s, isLoading: true }));
|
|
try {
|
|
const result = await Gemini.discoverICPs(state.phase1Result, language, state.projectId);
|
|
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 || !state.projectId) return;
|
|
setState(s => ({ ...s, isLoading: true }));
|
|
try {
|
|
const result = await Gemini.huntWhales(state.phase2Result, language, state.projectId);
|
|
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 || !state.projectId) return;
|
|
setState(s => ({ ...s, isLoading: true }));
|
|
try {
|
|
const result = await Gemini.developStrategy(state.phase3Result, state.phase1Result, language, state.projectId);
|
|
setState(s => ({ ...s, currentPhase: Phase.Strategy, phase4Result: result, isLoading: false }));
|
|
} catch (e: any) {
|
|
setError(e.message);
|
|
setState(s => ({ ...s, isLoading: false }));
|
|
}
|
|
};
|
|
|
|
const handlePhase5Submit = async () => {
|
|
// Requires all previous data for the final report
|
|
if (!state.phase4Result || !state.phase3Result || !state.phase2Result || !state.phase1Result || !state.projectId) return;
|
|
setState(s => ({ ...s, isLoading: true }));
|
|
try {
|
|
const result = await Gemini.generateAssets(
|
|
state.phase4Result,
|
|
state.phase3Result,
|
|
state.phase2Result,
|
|
state.phase1Result,
|
|
language,
|
|
state.projectId
|
|
);
|
|
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 || !state.projectId) return;
|
|
setState(s => ({ ...s, isLoading: true }));
|
|
try {
|
|
const result = await Gemini.generateSalesEnablement(
|
|
state.phase4Result,
|
|
state.phase3Result,
|
|
state.phase1Result,
|
|
language,
|
|
state.projectId
|
|
);
|
|
setState(s => ({ ...s, currentPhase: Phase.SalesEnablement, phase6Result: result, isLoading: false }));
|
|
} catch (e: any) {
|
|
setError(e.message);
|
|
setState(s => ({ ...s, isLoading: false }));
|
|
}
|
|
};
|
|
|
|
const handlePhase7Submit = async () => {
|
|
if (!state.phase4Result || !state.phase2Result || !state.projectId) return;
|
|
setState(s => ({ ...s, isLoading: true }));
|
|
try {
|
|
const result = await Gemini.generateLandingPageCopy(
|
|
state.phase4Result,
|
|
state.phase2Result,
|
|
language,
|
|
state.projectId
|
|
);
|
|
setState(s => ({ ...s, currentPhase: Phase.LandingPage, phase7Result: result, isLoading: false }));
|
|
} catch (e: any) {
|
|
setError(e.message);
|
|
setState(s => ({ ...s, isLoading: false }));
|
|
}
|
|
};
|
|
|
|
const handlePhase8Submit = async () => {
|
|
if (!state.phase2Result || !state.phase1Result || !state.projectId) return;
|
|
setState(s => ({ ...s, isLoading: true }));
|
|
try {
|
|
const result = await Gemini.buildBusinessCase(
|
|
state.phase2Result,
|
|
state.phase1Result,
|
|
language,
|
|
state.projectId
|
|
);
|
|
setState(s => ({ ...s, currentPhase: Phase.BusinessCase, phase8Result: result, isLoading: false }));
|
|
} catch (e: any) {
|
|
setError(e.message);
|
|
setState(s => ({ ...s, isLoading: false }));
|
|
}
|
|
};
|
|
|
|
const handlePhase9Submit = async () => {
|
|
if (!state.phase1Result || !state.phase4Result || !state.projectId) return;
|
|
setState(s => ({ ...s, isLoading: true }));
|
|
try {
|
|
const result = await Gemini.translateTech(
|
|
state.phase1Result,
|
|
state.phase4Result,
|
|
language,
|
|
state.projectId
|
|
);
|
|
setState(s => ({ ...s, currentPhase: Phase.TechTranslator, phase9Result: result, isLoading: false }));
|
|
} catch (e: any) {
|
|
setError(e.message);
|
|
setState(s => ({ ...s, isLoading: false }));
|
|
}
|
|
};
|
|
|
|
// --- List Mutation Handlers (Phase 1) ---
|
|
|
|
const addFeature = () => {
|
|
if (!newFeatureInput.trim() || !state.phase1Result) return;
|
|
const newItem = newFeatureInput.trim();
|
|
setState(s => ({
|
|
...s,
|
|
phase1Result: s.phase1Result ? {
|
|
...s.phase1Result,
|
|
features: [...s.phase1Result.features, newItem]
|
|
} : 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;
|
|
const newItem = newConstraintInput.trim();
|
|
setState(s => ({
|
|
...s,
|
|
phase1Result: s.phase1Result ? {
|
|
...s.phase1Result,
|
|
constraints: [...s.phase1Result.constraints, newItem]
|
|
} : undefined
|
|
}));
|
|
setNewConstraintInput("");
|
|
};
|
|
|
|
const removeConstraint = (index: number) => {
|
|
setState(s => ({
|
|
...s,
|
|
phase1Result: s.phase1Result ? {
|
|
...s.phase1Result,
|
|
constraints: s.phase1Result.constraints.filter((_, i) => i !== index)
|
|
} : undefined
|
|
}));
|
|
};
|
|
|
|
// --- List Mutation Handlers (Phase 2) ---
|
|
|
|
const addICP = () => {
|
|
if (!newICPName.trim() || !newICPRationale.trim() || !state.phase2Result) return;
|
|
const newItem = { name: newICPName.trim(), rationale: newICPRationale.trim() };
|
|
setState(s => ({
|
|
...s,
|
|
phase2Result: s.phase2Result ? {
|
|
...s.phase2Result,
|
|
icps: [...s.phase2Result.icps, newItem]
|
|
} : 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;
|
|
const newItem = { target: newProxyTarget.trim(), method: newProxyMethod.trim() };
|
|
setState(s => ({
|
|
...s,
|
|
phase2Result: s.phase2Result ? {
|
|
...s.phase2Result,
|
|
dataProxies: [...s.phase2Result.dataProxies, newItem]
|
|
} : undefined
|
|
}));
|
|
setNewProxyTarget("");
|
|
setNewProxyMethod("");
|
|
};
|
|
|
|
const removeProxy = (index: number) => {
|
|
setState(s => ({
|
|
...s,
|
|
phase2Result: s.phase2Result ? {
|
|
...s.phase2Result,
|
|
dataProxies: s.phase2Result.dataProxies.filter((_, i) => i !== index)
|
|
} : undefined
|
|
}));
|
|
};
|
|
|
|
// --- List Mutation Handlers (Phase 3) ---
|
|
|
|
const addAccount = (groupIndex: number) => {
|
|
const inputValue = newAccountInputs[groupIndex];
|
|
if (!inputValue?.trim() || !state.phase3Result) return;
|
|
|
|
const newItem = inputValue.trim();
|
|
|
|
const newWhales = [...state.phase3Result.whales];
|
|
newWhales[groupIndex] = {
|
|
...newWhales[groupIndex],
|
|
accounts: [...newWhales[groupIndex].accounts, newItem]
|
|
};
|
|
|
|
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;
|
|
const newItem = newRoleInput.trim();
|
|
setState(s => ({
|
|
...s,
|
|
phase3Result: s.phase3Result ? {
|
|
...s.phase3Result,
|
|
roles: [...s.phase3Result.roles, newItem]
|
|
} : undefined
|
|
}));
|
|
setNewRoleInput("");
|
|
};
|
|
|
|
const removeRole = (index: number) => {
|
|
setState(s => ({
|
|
...s,
|
|
phase3Result: s.phase3Result ? {
|
|
...s.phase3Result,
|
|
roles: s.phase3Result.roles.filter((_, i) => i !== index)
|
|
} : undefined
|
|
}));
|
|
};
|
|
|
|
const handlePromptChange = (index: number, newText: string) => {
|
|
if (!state.phase6Result) return;
|
|
|
|
const newPrompts = [...state.phase6Result.visualPrompts];
|
|
newPrompts[index] = { ...newPrompts[index], prompt: newText };
|
|
|
|
setState(s => ({
|
|
...s,
|
|
phase6Result: s.phase6Result ? {
|
|
...s.phase6Result,
|
|
visualPrompts: newPrompts
|
|
} : undefined
|
|
}));
|
|
};
|
|
|
|
// --- Render Helpers ---
|
|
|
|
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-4xl grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
|
|
{/* LEFT COLUMN: Input */}
|
|
<div className="md:col-span-2 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 className="mt-2 h-1 w-full bg-slate-200 dark:bg-robo-800 rounded-full overflow-hidden">
|
|
<div className="h-full bg-emerald-500/50 animate-pulse w-2/3 rounded-full"></div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* RIGHT COLUMN: History */}
|
|
<div className="p-6 rounded-xl border flex flex-col h-full
|
|
bg-slate-50 border-slate-200
|
|
dark:bg-robo-900/30 dark:border-robo-700
|
|
">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="font-bold flex items-center gap-2 text-slate-700 dark:text-slate-300">
|
|
<History size={18}/> {labels.historyTitle}
|
|
</h3>
|
|
<label className="p-1.5 rounded-lg hover:bg-blue-100 dark:hover:bg-robo-800 text-blue-600 dark:text-robo-400 cursor-pointer transition-colors" title="Load Markdown File">
|
|
<input type="file" accept=".md" onChange={handleLoadMarkdown} className="hidden" />
|
|
<Upload size={16}/>
|
|
</label>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto space-y-2 pr-2">
|
|
{sessions.length === 0 ? (
|
|
<div className="text-sm text-slate-400 italic text-center py-10">{labels.noSessions}</div>
|
|
) : (
|
|
sessions.map((session) => (
|
|
<div key={session.id}
|
|
onClick={() => handleLoadSession(session.id)}
|
|
className="p-3 rounded-lg border cursor-pointer group transition-all relative
|
|
bg-white border-slate-200 hover:border-blue-400 hover:shadow-md
|
|
dark:bg-robo-800 dark:border-robo-700 dark:hover:border-robo-500
|
|
">
|
|
<button
|
|
onClick={(e) => handleDeleteSession(e, session.id)}
|
|
className="absolute top-2 right-2 p-1.5 rounded-md opacity-0 group-hover:opacity-100 hover:bg-red-50 dark:hover:bg-red-900/30 text-slate-400 hover:text-red-500 transition-all"
|
|
>
|
|
<Trash2 size={14}/>
|
|
</button>
|
|
<div className="font-medium text-sm text-slate-800 dark:text-slate-200 line-clamp-1 mb-1 pr-6 group-hover:text-blue-600 dark:group-hover:text-robo-400">
|
|
{session.name}
|
|
</div>
|
|
<div className="flex items-center gap-2 text-xs text-slate-500 dark:text-slate-500">
|
|
<Clock size={12}/>
|
|
{new Date(session.updated_at + "Z").toLocaleDateString()}
|
|
</div>
|
|
</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 dark:text-purple-400" /> {labels.features} & Constraints
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{/* Features List */}
|
|
<div>
|
|
<h3 className="text-sm font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 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 transition-colors
|
|
bg-slate-50 border-slate-200 text-slate-700
|
|
dark:bg-robo-900 dark:border-robo-700 dark:text-slate-200
|
|
">
|
|
<div className="flex items-start gap-2">
|
|
<div className="w-1.5 h-1.5 rounded-full bg-green-500 mt-1.5 shrink-0" />
|
|
<span>{f}</span>
|
|
</div>
|
|
<button
|
|
onClick={() => removeFeature(i)}
|
|
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-red-100 text-red-500 rounded transition-all dark:hover:bg-red-900/30"
|
|
>
|
|
<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 outline-none focus:ring-2 focus:ring-blue-500
|
|
bg-white border-slate-200 text-slate-800
|
|
dark:bg-robo-900 dark:border-robo-600 dark:text-white
|
|
"
|
|
placeholder={labels.addPlaceholder}
|
|
/>
|
|
<button onClick={addFeature} className="p-2 bg-slate-100 dark:bg-robo-700 rounded hover:bg-slate-200 dark:hover:bg-robo-600 text-slate-600 dark:text-slate-300">
|
|
<Plus size={16}/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Constraints List */}
|
|
<div>
|
|
<h3 className="text-sm font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 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 transition-colors
|
|
bg-red-50 border-red-200 text-red-700
|
|
dark:bg-robo-900 dark:border-red-900/30 dark:text-red-200
|
|
">
|
|
<div className="flex items-start gap-2">
|
|
<div className="w-1.5 h-1.5 rounded-full bg-red-500 mt-1.5 shrink-0" />
|
|
<span>{c}</span>
|
|
</div>
|
|
<button
|
|
onClick={() => removeConstraint(i)}
|
|
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-red-100 text-red-600 rounded transition-all dark:hover:bg-red-900/50"
|
|
>
|
|
<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 outline-none focus:ring-2 focus:ring-red-500
|
|
bg-white border-slate-200 text-slate-800
|
|
dark:bg-robo-900 dark:border-robo-600 dark:text-white
|
|
"
|
|
placeholder={labels.addPlaceholder}
|
|
/>
|
|
<button onClick={addConstraint} className="p-2 bg-slate-100 dark:bg-robo-700 rounded hover:bg-slate-200 dark:hover:bg-robo-600 text-slate-600 dark:text-slate-300">
|
|
<Plus size={16}/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* NEW: Hard Facts Specs Display */}
|
|
{state.phase1Result?.specs && 'metadata' in state.phase1Result.specs && (
|
|
<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">
|
|
<Database className="text-blue-500" /> Technical Specifications (Hard Facts)
|
|
</h2>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
|
{/* Core Data */}
|
|
<div className="p-4 bg-slate-50 dark:bg-robo-900 rounded-lg border border-slate-200 dark:border-robo-700">
|
|
<h3 className="text-xs font-bold uppercase tracking-wider text-slate-500 mb-3">Core Data</h3>
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex justify-between border-b border-slate-200 dark:border-robo-800 pb-1">
|
|
<span className="text-slate-500">Model</span>
|
|
<span className="font-bold text-slate-800 dark:text-slate-200">{state.phase1Result.specs.metadata.brand} {state.phase1Result.specs.metadata.model_name}</span>
|
|
</div>
|
|
<div className="flex justify-between border-b border-slate-200 dark:border-robo-800 pb-1">
|
|
<span className="text-slate-500">Category</span>
|
|
<span className="capitalize text-slate-800 dark:text-slate-200">{state.phase1Result.specs.metadata.category}</span>
|
|
</div>
|
|
<div className="flex justify-between border-b border-slate-200 dark:border-robo-800 pb-1">
|
|
<span className="text-slate-500">Runtime</span>
|
|
<span className="text-slate-800 dark:text-slate-200">{state.phase1Result.specs.core_specs.battery_runtime_min ? `${state.phase1Result.specs.core_specs.battery_runtime_min} min` : '-'}</span>
|
|
</div>
|
|
<div className="flex justify-between border-b border-slate-200 dark:border-robo-800 pb-1">
|
|
<span className="text-slate-500">Weight</span>
|
|
<span className="text-slate-800 dark:text-slate-200">{state.phase1Result.specs.core_specs.weight_kg ? `${state.phase1Result.specs.core_specs.weight_kg} kg` : '-'}</span>
|
|
</div>
|
|
<div className="flex justify-between border-b border-slate-200 dark:border-robo-800 pb-1">
|
|
<span className="text-slate-500">Dimensions</span>
|
|
<span className="text-slate-800 dark:text-slate-200">
|
|
{state.phase1Result.specs.core_specs.dimensions_cm.l ? `${state.phase1Result.specs.core_specs.dimensions_cm.l}x${state.phase1Result.specs.core_specs.dimensions_cm.w}x${state.phase1Result.specs.core_specs.dimensions_cm.h} cm` : '-'}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-slate-500">Navigation</span>
|
|
<span className="text-slate-800 dark:text-slate-200">{state.phase1Result.specs.core_specs.navigation_type || '-'}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Layer Data */}
|
|
<div className="p-4 bg-slate-50 dark:bg-robo-900 rounded-lg border border-slate-200 dark:border-robo-700">
|
|
<h3 className="text-xs font-bold uppercase tracking-wider text-slate-500 mb-3">Performance Layer</h3>
|
|
|
|
{!state.phase1Result.specs.layers.cleaning && !state.phase1Result.specs.layers.service && !state.phase1Result.specs.layers.security && (
|
|
<div className="text-sm text-slate-400 italic">No specific layer data detected.</div>
|
|
)}
|
|
|
|
{state.phase1Result.specs.layers.cleaning && (
|
|
<div className="space-y-2 text-sm mb-4">
|
|
<div className="text-emerald-600 font-bold mb-1 flex items-center gap-1"><Sparkles size={12}/> Cleaning Mode</div>
|
|
<div className="flex justify-between"><span className="text-slate-500">Area Perf:</span> <span className="text-slate-800 dark:text-slate-200">{state.phase1Result.specs.layers.cleaning.area_performance_sqm_h || '-'} m²/h</span></div>
|
|
<div className="flex justify-between"><span className="text-slate-500">Fresh Water:</span> <span className="text-slate-800 dark:text-slate-200">{state.phase1Result.specs.layers.cleaning.fresh_water_l || '-'} L</span></div>
|
|
</div>
|
|
)}
|
|
{state.phase1Result.specs.layers.service && (
|
|
<div className="space-y-2 text-sm mb-4">
|
|
<div className="text-blue-600 font-bold mb-1 flex items-center gap-1"><Briefcase size={12}/> Service Mode</div>
|
|
<div className="flex justify-between"><span className="text-slate-500">Payload:</span> <span className="text-slate-800 dark:text-slate-200">{state.phase1Result.specs.layers.service.max_payload_kg || '-'} kg</span></div>
|
|
<div className="flex justify-between"><span className="text-slate-500">Trays:</span> <span className="text-slate-800 dark:text-slate-200">{state.phase1Result.specs.layers.service.number_of_trays || '-'}</span></div>
|
|
</div>
|
|
)}
|
|
{state.phase1Result.specs.layers.security && (
|
|
<div className="space-y-2 text-sm">
|
|
<div className="text-red-600 font-bold mb-1 flex items-center gap-1"><Shield size={12}/> Security Mode</div>
|
|
<div className="flex justify-between"><span className="text-slate-500">Night Vision:</span> <span className="text-slate-800 dark:text-slate-200">{state.phase1Result.specs.layers.security.night_vision ? 'Yes' : 'No'}</span></div>
|
|
<div className="flex justify-between"><span className="text-slate-500">Cameras:</span> <span className="text-slate-800 dark:text-slate-200">{state.phase1Result.specs.layers.security.camera_types.join(', ') || '-'}</span></div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Extended Features */}
|
|
{state.phase1Result.specs.extended_features.length > 0 && (
|
|
<div>
|
|
<h3 className="text-xs font-bold uppercase tracking-wider text-slate-500 mb-2">Extended Features</h3>
|
|
<div className="flex flex-wrap gap-2">
|
|
{state.phase1Result.specs.extended_features.map((feat, idx) => (
|
|
<span key={idx} className="px-2 py-1 bg-slate-100 dark:bg-robo-900 text-xs rounded border border-slate-200 dark:border-robo-700 text-slate-700 dark:text-slate-300">
|
|
<span className="font-bold">{feat.feature}:</span> {feat.value} {feat.unit}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{state.phase1Result?.conflictCheck.hasConflict ? (
|
|
<div className="border p-6 rounded-xl flex gap-4 transition-colors
|
|
bg-red-50 border-red-200
|
|
dark:bg-red-950/50 dark:border-red-500/50
|
|
">
|
|
<AlertTriangle className="text-red-600 dark:text-red-500 shrink-0" size={32} />
|
|
<div>
|
|
<h3 className="font-bold text-lg text-red-800 dark:text-red-200">{labels.conflictTitle}</h3>
|
|
<p className="mt-1 text-red-700 dark:text-red-300">{state.phase1Result.conflictCheck.details}</p>
|
|
{state.phase1Result.conflictCheck.relatedProduct && (
|
|
<p className="text-sm font-mono mt-2 inline-block px-2 py-1 rounded
|
|
bg-red-100 text-red-800
|
|
dark:bg-red-900/50 dark:text-red-200
|
|
">
|
|
Conflict with: {state.phase1Result.conflictCheck.relatedProduct}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="border p-4 rounded-xl flex items-center gap-3 transition-colors
|
|
bg-emerald-50 border-emerald-200
|
|
dark:bg-emerald-950/30 dark:border-emerald-500/30
|
|
">
|
|
<Check className="text-emerald-600 dark:text-emerald-500" />
|
|
<span className="text-emerald-800 dark:text-emerald-200">{labels.conflictPass}</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex justify-between pt-4">
|
|
<button
|
|
onClick={goBack}
|
|
disabled={state.isLoading}
|
|
className="text-sm font-bold text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-white flex items-center gap-2 px-4 py-2"
|
|
>
|
|
<ArrowLeft size={16}/> {labels.backBtn}
|
|
</button>
|
|
<button
|
|
onClick={handlePhase2Submit}
|
|
disabled={state.isLoading}
|
|
className="font-bold py-3 px-6 rounded-lg transition-all flex items-center gap-2
|
|
bg-slate-900 text-white hover:bg-slate-700
|
|
dark:bg-white dark:text-robo-900 dark:hover:bg-slate-200"
|
|
>
|
|
{state.isLoading ? (
|
|
<span className="flex items-center gap-2"><Loader2 className="animate-spin"/> {labels.processing}</span>
|
|
) : (
|
|
<> {labels.confirmICP} <ArrowRight size={18} /></>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderPhase2 = () => (
|
|
<div className="space-y-6 animate-in fade-in">
|
|
<div className="grid grid-cols-1 gap-6">
|
|
<h2 className="text-2xl font-bold flex items-center gap-2 text-slate-900 dark:text-white">
|
|
<Target className="text-blue-600 dark:text-robo-accent"/> {labels.targetId}
|
|
</h2>
|
|
{state.phase2Result?.icps.map((icp, i) => (
|
|
<div key={i} className="relative p-6 rounded-xl border transition-colors cursor-default group
|
|
bg-white border-slate-200 hover:border-blue-400
|
|
dark:bg-robo-800 dark:border-robo-700 dark:hover:border-robo-500
|
|
">
|
|
<button
|
|
onClick={() => removeICP(i)}
|
|
className="absolute top-4 right-4 p-1.5 rounded-full opacity-0 group-hover:opacity-100 transition-opacity
|
|
bg-slate-100 hover:bg-red-100 text-slate-500 hover:text-red-500
|
|
dark:bg-robo-700 dark:hover:bg-red-900/50 dark:text-slate-400 dark:hover:text-red-400"
|
|
>
|
|
<X size={16}/>
|
|
</button>
|
|
<h3 className="text-xl font-bold mb-2 transition-colors pr-8
|
|
text-slate-800 group-hover:text-blue-600
|
|
dark:text-white dark:group-hover:text-robo-400
|
|
">
|
|
{i + 1}. {icp.name}
|
|
</h3>
|
|
<p className="text-sm leading-relaxed text-slate-600 dark:text-slate-300">{icp.rationale}</p>
|
|
</div>
|
|
))}
|
|
{/* ADD ICP INPUT */}
|
|
<div className="p-4 rounded-xl border border-dashed transition-colors
|
|
bg-slate-50 border-slate-300
|
|
dark:bg-robo-900/50 dark:border-robo-600
|
|
">
|
|
<div className="flex flex-col gap-2">
|
|
<input
|
|
value={newICPName}
|
|
onChange={e => setNewICPName(e.target.value)}
|
|
className="text-sm border rounded px-3 py-2 outline-none focus:ring-2 focus:ring-blue-500
|
|
bg-white border-slate-200 text-slate-800
|
|
dark:bg-robo-900 dark:border-robo-600 dark:text-white"
|
|
placeholder={labels.addName}
|
|
/>
|
|
<div className="flex gap-2">
|
|
<input
|
|
value={newICPRationale}
|
|
onChange={e => setNewICPRationale(e.target.value)}
|
|
onKeyDown={e => e.key === 'Enter' && addICP()}
|
|
className="flex-1 text-sm border rounded px-3 py-2 outline-none focus:ring-2 focus:ring-blue-500
|
|
bg-white border-slate-200 text-slate-800
|
|
dark:bg-robo-900 dark:border-robo-600 dark:text-white"
|
|
placeholder={labels.addRationale}
|
|
/>
|
|
<button onClick={addICP} className="p-2 bg-blue-100 dark:bg-robo-600 rounded hover:bg-blue-200 dark:hover:bg-robo-500 text-blue-700 dark:text-white">
|
|
<Plus size={16}/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-6 rounded-xl border transition-colors
|
|
bg-slate-100 border-slate-200
|
|
dark:bg-slate-900 dark:border-slate-700
|
|
">
|
|
<h3 className="text-sm font-bold uppercase tracking-wider mb-4 flex items-center gap-2 text-slate-500 dark:text-slate-400">
|
|
<Database size={16}/> {labels.dataProxies}
|
|
</h3>
|
|
<div className="space-y-3">
|
|
{state.phase2Result?.dataProxies.map((proxy, i) => (
|
|
<div key={i} className="flex gap-4 items-center p-3 rounded-lg group relative
|
|
bg-white border border-slate-200
|
|
dark:bg-black/20 dark:border-transparent
|
|
">
|
|
<div className="font-mono text-xs shrink-0 whitespace-nowrap px-2 py-1 rounded
|
|
bg-slate-100 text-blue-600
|
|
dark:bg-robo-900 dark:text-robo-400
|
|
">
|
|
PROXY_0{i+1}
|
|
</div>
|
|
<div className="flex-1">
|
|
<div className="font-medium text-slate-800 dark:text-slate-200">{proxy.target}</div>
|
|
<div className="text-xs mt-0.5 text-slate-500">{labels.method}: {proxy.method}</div>
|
|
</div>
|
|
<button
|
|
onClick={() => removeProxy(i)}
|
|
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-red-100 text-red-500 rounded transition-all dark:hover:bg-red-900/30"
|
|
>
|
|
<X size={14}/>
|
|
</button>
|
|
</div>
|
|
))}
|
|
{/* ADD PROXY INPUT */}
|
|
<div className="flex gap-2 mt-2">
|
|
<input
|
|
value={newProxyTarget}
|
|
onChange={e => setNewProxyTarget(e.target.value)}
|
|
className="flex-1 text-sm border rounded px-3 py-2 outline-none focus:ring-2 focus:ring-blue-500
|
|
bg-white border-slate-200 text-slate-800
|
|
dark:bg-robo-900 dark:border-robo-600 dark:text-white"
|
|
placeholder={labels.addTarget}
|
|
/>
|
|
<input
|
|
value={newProxyMethod}
|
|
onChange={e => setNewProxyMethod(e.target.value)}
|
|
onKeyDown={e => e.key === 'Enter' && addProxy()}
|
|
className="flex-1 text-sm border rounded px-3 py-2 outline-none focus:ring-2 focus:ring-blue-500
|
|
bg-white border-slate-200 text-slate-800
|
|
dark:bg-robo-900 dark:border-robo-600 dark:text-white"
|
|
placeholder={labels.addMethod}
|
|
/>
|
|
<button onClick={addProxy} className="p-2 bg-slate-200 dark:bg-robo-700 rounded hover:bg-slate-300 dark:hover:bg-robo-600 text-slate-700 dark:text-white">
|
|
<Plus size={16}/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-between pt-4">
|
|
<button
|
|
onClick={goBack}
|
|
disabled={state.isLoading}
|
|
className="text-sm font-bold text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-white flex items-center gap-2 px-4 py-2"
|
|
>
|
|
<ArrowLeft size={16}/> {labels.backBtn}
|
|
</button>
|
|
<button
|
|
onClick={handlePhase3Submit}
|
|
disabled={state.isLoading}
|
|
className="font-bold py-3 px-6 rounded-lg transition-all flex items-center gap-2
|
|
bg-slate-900 text-white hover:bg-slate-700
|
|
dark:bg-white dark:text-robo-900 dark:hover:bg-slate-200"
|
|
>
|
|
{state.isLoading ? (
|
|
<span className="flex items-center gap-2"><Loader2 className="animate-spin"/> {labels.processing}</span>
|
|
) : (
|
|
<> {labels.huntWhales} <Crosshair size={18} /></>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderPhase3 = () => (
|
|
<div className="space-y-8 animate-in fade-in">
|
|
<div className="flex gap-2 items-center p-2 rounded border inline-flex
|
|
bg-emerald-50 text-emerald-700 border-emerald-200
|
|
dark:bg-emerald-950/20 dark:text-emerald-400 dark:border-emerald-900/50
|
|
">
|
|
<Globe size={16}/> {labels.region}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-8">
|
|
{/* Whales Groups */}
|
|
<div className="space-y-6">
|
|
<h3 className="text-xl font-bold flex items-center gap-2 text-slate-800 dark:text-white">
|
|
<Building2 className="text-amber-500 dark:text-amber-400"/> {labels.whales}
|
|
</h3>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{state.phase3Result?.whales.map((whaleGroup, groupIdx) => (
|
|
<div key={groupIdx} className="p-6 rounded-xl border transition-colors flex flex-col
|
|
bg-white border-slate-200
|
|
dark:bg-robo-800 dark:border-robo-700
|
|
">
|
|
<div className="mb-4 pb-2 border-b border-slate-100 dark:border-robo-600">
|
|
<h4 className="font-bold text-slate-700 dark:text-slate-200 flex items-center gap-2">
|
|
<span className="bg-amber-100 text-amber-600 dark:bg-amber-900/30 dark:text-amber-400 px-2 py-0.5 rounded text-xs uppercase tracking-wider">Industry</span>
|
|
{whaleGroup.industry}
|
|
</h4>
|
|
</div>
|
|
|
|
<ul className="space-y-3 flex-1">
|
|
{whaleGroup.accounts.map((acc, accIdx) => (
|
|
<li key={accIdx} className="flex items-center justify-between group p-3 rounded-lg border transition-colors
|
|
bg-slate-50 border-slate-200
|
|
dark:bg-robo-900 dark:border-robo-800
|
|
">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 rounded flex items-center justify-center font-bold text-xs shrink-0
|
|
bg-slate-200 text-slate-500
|
|
dark:bg-slate-700 dark:text-slate-400
|
|
">
|
|
{acc.substring(0,2).toUpperCase()}
|
|
</div>
|
|
<span className="font-medium text-slate-800 dark:text-slate-200">{acc}</span>
|
|
</div>
|
|
<button
|
|
onClick={() => removeAccount(groupIdx, accIdx)}
|
|
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-red-100 text-red-500 rounded transition-all dark:hover:bg-red-900/30"
|
|
>
|
|
<X size={14}/>
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
<div className="flex gap-2 mt-4 pt-2 border-t border-slate-100 dark:border-robo-700/50">
|
|
<input
|
|
value={newAccountInputs[groupIdx] || ""}
|
|
onChange={e => setNewAccountInputs({...newAccountInputs, [groupIdx]: e.target.value})}
|
|
onKeyDown={e => e.key === 'Enter' && addAccount(groupIdx)}
|
|
className="flex-1 text-sm border rounded px-3 py-2 outline-none focus:ring-2 focus:ring-amber-500
|
|
bg-slate-50 border-slate-200 text-slate-800
|
|
dark:bg-robo-900 dark:border-robo-600 dark:text-white
|
|
"
|
|
placeholder={labels.addAccount}
|
|
/>
|
|
<button onClick={() => addAccount(groupIdx)} className="p-2 bg-slate-100 dark:bg-robo-700 rounded hover:bg-slate-200 dark:hover:bg-robo-600 text-slate-600 dark:text-slate-300">
|
|
<Plus size={16}/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Personas */}
|
|
<div className="p-6 rounded-xl border transition-colors
|
|
bg-white border-slate-200
|
|
dark:bg-robo-800 dark:border-robo-700
|
|
">
|
|
<h3 className="text-lg font-bold mb-4 flex items-center gap-2 text-slate-800 dark:text-white">
|
|
<UserCircle className="text-blue-500 dark:text-blue-400"/> {labels.roles}
|
|
</h3>
|
|
<ul className="space-y-3">
|
|
{state.phase3Result?.roles.map((role: any, i) => (
|
|
<li key={i} className="flex items-center justify-between group p-3 rounded-lg border transition-colors
|
|
bg-slate-50 border-slate-200
|
|
dark:bg-robo-900 dark:border-robo-800
|
|
">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-1.5 rounded-full
|
|
bg-blue-100 text-blue-500
|
|
dark:bg-blue-900/30 dark:text-blue-400
|
|
">
|
|
<Briefcase size={14}/>
|
|
</div>
|
|
<div>
|
|
<span className="font-medium text-slate-800 dark:text-slate-200 block">
|
|
{typeof role === 'object' ? role.role : role}
|
|
</span>
|
|
{typeof role === 'object' && role.description && (
|
|
<span className="text-xs text-slate-500 dark:text-slate-400 block mt-0.5">
|
|
{role.description}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => removeRole(i)}
|
|
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-red-100 text-red-500 rounded transition-all dark:hover:bg-red-900/30"
|
|
>
|
|
<X size={14}/>
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<div className="flex gap-2 mt-3">
|
|
<input
|
|
value={newRoleInput}
|
|
onChange={e => setNewRoleInput(e.target.value)}
|
|
onKeyDown={e => e.key === 'Enter' && addRole()}
|
|
className="flex-1 text-sm border rounded px-3 py-2 outline-none focus:ring-2 focus:ring-blue-500
|
|
bg-white border-slate-200 text-slate-800
|
|
dark:bg-robo-900 dark:border-robo-600 dark:text-white
|
|
"
|
|
placeholder={labels.addRole}
|
|
/>
|
|
<button onClick={addRole} className="p-2 bg-slate-100 dark:bg-robo-700 rounded hover:bg-slate-200 dark:hover:bg-robo-600 text-slate-600 dark:text-slate-300">
|
|
<Plus size={16}/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-between pt-4">
|
|
<button
|
|
onClick={goBack}
|
|
disabled={state.isLoading}
|
|
className="text-sm font-bold text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-white flex items-center gap-2 px-4 py-2"
|
|
>
|
|
<ArrowLeft size={16}/> {labels.backBtn}
|
|
</button>
|
|
<button
|
|
onClick={handlePhase4Submit}
|
|
disabled={state.isLoading}
|
|
className="font-bold py-3 px-6 rounded-lg transition-all flex items-center gap-2
|
|
bg-slate-900 text-white hover:bg-slate-700
|
|
dark:bg-white dark:text-robo-900 dark:hover:bg-slate-200"
|
|
>
|
|
{state.isLoading ? (
|
|
<span className="flex items-center gap-2"><Loader2 className="animate-spin"/> {labels.processing}</span>
|
|
) : (
|
|
<> {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 text-slate-900 dark:text-white">
|
|
<Zap className="text-yellow-500 dark:text-yellow-400"/> {labels.stratAngles}
|
|
</h2>
|
|
|
|
<div className="overflow-x-auto rounded-xl border transition-colors border-slate-200 dark:border-robo-700">
|
|
<table className="w-full text-left border-collapse">
|
|
<thead>
|
|
<tr className="text-xs uppercase tracking-wider
|
|
bg-slate-100 text-slate-500
|
|
dark:bg-robo-900 dark:text-slate-400
|
|
">
|
|
<th className="p-4 border-b border-slate-200 dark:border-robo-700">{labels.segment}</th>
|
|
<th className="p-4 border-b border-slate-200 dark:border-robo-700">{labels.pain}</th>
|
|
<th className="p-4 border-b border-slate-200 dark:border-robo-700">{labels.angle}</th>
|
|
<th className="p-4 border-b border-slate-200 dark:border-robo-700">{labels.diff}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-200 dark:divide-robo-700 bg-white dark:bg-robo-800">
|
|
{state.phase4Result?.strategyMatrix.map((row, i) => (
|
|
<tr key={i} className="hover:bg-slate-50 dark:hover:bg-robo-700/50 transition-colors">
|
|
<td className="p-4 font-bold text-slate-800 dark:text-white">{row.segment}</td>
|
|
<td className="p-4 text-sm text-red-600 dark:text-red-300">{row.painPoint}</td>
|
|
<td className="p-4 font-medium text-emerald-600 dark:text-emerald-300">{row.angle}</td>
|
|
<td className="p-4 text-sm italic border-l border-slate-100 dark:border-robo-700 text-slate-500 dark:text-slate-400">{row.differentiation}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div className="flex justify-between pt-4">
|
|
<button
|
|
onClick={goBack}
|
|
disabled={state.isLoading}
|
|
className="text-sm font-bold text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-white flex items-center gap-2 px-4 py-2"
|
|
>
|
|
<ArrowLeft size={16}/> {labels.backBtn}
|
|
</button>
|
|
<button
|
|
onClick={handlePhase5Submit}
|
|
disabled={state.isLoading}
|
|
className="font-bold py-3 px-6 rounded-lg transition-all flex items-center gap-2
|
|
bg-slate-900 text-white hover:bg-slate-700
|
|
dark:bg-white dark:text-robo-900 dark:hover:bg-slate-200"
|
|
>
|
|
{state.isLoading ? (
|
|
<span className="flex items-center gap-2"><Loader2 className="animate-spin"/> {labels.processing}</span>
|
|
) : (
|
|
<> {labels.writeCopy} <Terminal size={18} /></>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderPhase5 = () => (
|
|
<div className="space-y-6 animate-in fade-in">
|
|
<div className="flex justify-between items-center">
|
|
<h2 className="text-2xl font-bold flex items-center gap-2 text-slate-900 dark:text-white">
|
|
<Terminal className="text-pink-600 dark:text-pink-400"/> {labels.genAssets}
|
|
</h2>
|
|
<div className="flex items-center gap-4">
|
|
<div className="text-xs font-mono text-slate-500">format: markdown</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-8 rounded-xl border font-mono text-sm leading-relaxed overflow-x-auto shadow-inner transition-colors
|
|
bg-slate-50 border-slate-200 text-slate-800
|
|
dark:bg-slate-900 dark:border-slate-700 dark:text-slate-300
|
|
">
|
|
<ReactMarkdown
|
|
remarkPlugins={[remarkGfm]}
|
|
components={{
|
|
h1: ({node, ...props}) => <h1 className="text-2xl font-bold mt-6 mb-4 border-b pb-2 text-slate-900 border-slate-300 dark:text-white dark:border-slate-700" {...props} />,
|
|
h2: ({node, ...props}) => <h2 className="text-xl font-bold mt-6 mb-3 text-blue-600 dark:text-robo-400" {...props} />,
|
|
h3: ({node, ...props}) => <h3 className="text-lg font-bold mt-4 mb-2 text-slate-800 dark:text-slate-200" {...props} />,
|
|
p: ({node, ...props}) => <p className="mb-4" {...props} />,
|
|
ul: ({node, ...props}) => <ul className="list-disc pl-5 mb-4" {...props} />,
|
|
li: ({node, ...props}) => <li className="mb-1" {...props} />,
|
|
strong: ({node, ...props}) => <strong className="font-bold text-emerald-600 dark:text-emerald-400" {...props} />,
|
|
// Table support
|
|
table: ({node, ...props}) => (
|
|
<div className="overflow-x-auto my-6 rounded-lg border border-slate-200 dark:border-slate-700 shadow-sm">
|
|
<table className="w-full text-left border-collapse text-sm" {...props} />
|
|
</div>
|
|
),
|
|
thead: ({node, ...props}) => (
|
|
<thead className="bg-slate-50 dark:bg-robo-800 text-slate-500 dark:text-slate-400 uppercase text-xs font-bold tracking-wider" {...props} />
|
|
),
|
|
tbody: ({node, ...props}) => (
|
|
<tbody className="bg-white dark:bg-robo-900 divide-y divide-slate-200 dark:divide-slate-700" {...props} />
|
|
),
|
|
tr: ({node, ...props}) => (
|
|
<tr className="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors" {...props} />
|
|
),
|
|
th: ({node, ...props}) => (
|
|
<th className="px-6 py-3 border-b border-slate-200 dark:border-slate-700 whitespace-nowrap" {...props} />
|
|
),
|
|
td: ({node, ...props}) => (
|
|
<td className="px-6 py-4 border-b border-slate-200 dark:border-slate-700 align-top text-slate-700 dark:text-slate-300" {...props} />
|
|
),
|
|
}}
|
|
>
|
|
{state.phase5Result?.report || ''}
|
|
</ReactMarkdown>
|
|
</div>
|
|
|
|
<div className="flex justify-between pt-4">
|
|
<button
|
|
onClick={goBack}
|
|
disabled={state.isLoading}
|
|
className="text-sm font-bold text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-white flex items-center gap-2 px-4 py-2"
|
|
>
|
|
<ArrowLeft size={16}/> {labels.backBtn}
|
|
</button>
|
|
<button
|
|
onClick={handlePhase6Submit}
|
|
disabled={state.isLoading}
|
|
className="font-bold py-3 px-6 rounded-lg transition-all flex items-center gap-2
|
|
bg-slate-900 text-white hover:bg-slate-700
|
|
dark:bg-white dark:text-robo-900 dark:hover:bg-slate-200"
|
|
>
|
|
{state.isLoading ? (
|
|
<span className="flex items-center gap-2"><Loader2 className="animate-spin"/> {labels.processing}</span>
|
|
) : (
|
|
<> {labels.toPhase6} <ShieldCheck size={18} /></>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderPhase6 = () => (
|
|
<div className="space-y-8 animate-in fade-in">
|
|
<div className="flex justify-between items-center">
|
|
<h2 className="text-2xl font-bold flex items-center gap-2 text-slate-900 dark:text-white">
|
|
<ShieldCheck className="text-emerald-600 dark:text-emerald-400"/> {labels.phase6Title}
|
|
</h2>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-8">
|
|
{/* Battlecards */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-bold flex items-center gap-2 text-slate-800 dark:text-white">
|
|
<ShieldAlert className="text-red-500"/> {labels.battlecards}
|
|
</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{state.phase6Result?.battlecards.map((card, i) => (
|
|
<div key={i} className="rounded-xl border shadow-sm overflow-hidden flex flex-col
|
|
bg-white border-slate-200
|
|
dark:bg-robo-800 dark:border-robo-700
|
|
">
|
|
<div className="p-4 bg-slate-100 dark:bg-robo-900 border-b border-slate-200 dark:border-robo-700 flex justify-between items-center">
|
|
<span className="font-bold text-slate-700 dark:text-slate-300 flex items-center gap-2">
|
|
<UserCircle size={16}/> {card.persona}
|
|
</span>
|
|
</div>
|
|
<div className="p-5 flex-1 flex flex-col gap-4">
|
|
<div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-100 dark:border-red-900/30 rounded-lg text-red-800 dark:text-red-300 text-sm italic">
|
|
<span className="font-bold not-italic block mb-1 text-xs uppercase tracking-wide text-red-500">{labels.objection}</span>
|
|
"{card.objection}"
|
|
</div>
|
|
<div className="p-3 bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-100 dark:border-emerald-900/30 rounded-lg text-emerald-800 dark:text-emerald-300 text-sm">
|
|
<span className="font-bold block mb-1 text-xs uppercase tracking-wide text-emerald-600 dark:text-emerald-500">{labels.response}</span>
|
|
{card.responseScript}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Visual Prompts */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-bold flex items-center gap-2 text-slate-800 dark:text-white">
|
|
<ImageIcon className="text-blue-500"/> {labels.visuals}
|
|
</h3>
|
|
<div className="space-y-4">
|
|
{state.phase6Result?.visualPrompts.map((prompt, i) => (
|
|
<div key={i} className="rounded-xl border p-5 transition-colors
|
|
bg-slate-50 border-slate-200
|
|
dark:bg-robo-900/50 dark:border-robo-700
|
|
">
|
|
<div className="flex flex-col gap-4">
|
|
<div className="flex justify-between items-start">
|
|
<div>
|
|
<h4 className="font-bold text-slate-800 dark:text-white">{prompt.title}</h4>
|
|
<p className="text-xs text-slate-500 dark:text-slate-400 uppercase tracking-wide mt-1">{prompt.context}</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<select
|
|
value={aspectRatio}
|
|
onChange={e => setAspectRatio(e.target.value)}
|
|
className="text-xs font-bold rounded bg-white dark:bg-robo-800 border border-slate-200 dark:border-robo-600 hover:border-blue-400 dark:hover:border-robo-500 transition-colors text-slate-600 dark:text-slate-300 focus:ring-2 focus:ring-purple-500 outline-none"
|
|
>
|
|
<option value="16:9">16:9 (Landscape)</option>
|
|
<option value="9:16">9:16 (Portrait)</option>
|
|
<option value="1:1">1:1 (Square)</option>
|
|
<option value="4:3">4:3 (Classic)</option>
|
|
</select>
|
|
|
|
<button
|
|
onClick={() => handleGenerateImage(prompt.prompt, i)}
|
|
disabled={generatingImages[i]}
|
|
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-bold rounded bg-purple-100 dark:bg-purple-900/30 border border-purple-200 dark:border-purple-800 hover:bg-purple-200 dark:hover:bg-purple-900/50 text-purple-700 dark:text-purple-300 transition-colors disabled:opacity-50"
|
|
>
|
|
{generatingImages[i] ? <Loader2 size={14} className="animate-spin"/> : <Sparkles size={14}/>}
|
|
{generatingImages[i] ? labels.generating : labels.genImage}
|
|
</button>
|
|
<button
|
|
onClick={() => copyToClipboard(prompt.prompt, i)}
|
|
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-bold rounded bg-white dark:bg-robo-800 border border-slate-200 dark:border-robo-600 hover:border-blue-400 dark:hover:border-robo-500 transition-colors text-slate-600 dark:text-slate-300"
|
|
>
|
|
{copiedPromptIndex === i ? <Check size={14} className="text-emerald-500"/> : <Copy size={14}/>}
|
|
{copiedPromptIndex === i ? labels.copied : labels.copyPrompt}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<textarea
|
|
className="w-full h-32 text-xs p-3 rounded bg-white dark:bg-black/30 text-slate-600 dark:text-slate-400 leading-relaxed border border-slate-200 dark:border-robo-800 focus:ring-2 focus:ring-purple-500 outline-none resize-none font-mono"
|
|
value={prompt.prompt}
|
|
onChange={(e) => handlePromptChange(i, e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
{generatedImages[i] && (
|
|
<div className="mt-4 animate-in fade-in slide-in-from-bottom-2 relative group">
|
|
{editingIndex === i ? (
|
|
<div className="relative border border-blue-400 rounded-lg overflow-hidden bg-slate-100 dark:bg-slate-900">
|
|
<div className="absolute top-2 left-2 z-10 flex gap-2 bg-white/90 dark:bg-black/80 p-1.5 rounded-lg shadow-sm backdrop-blur-sm">
|
|
<button onClick={() => setBrushColor('#ef4444')} className={`w-6 h-6 rounded-full border-2 ${brushColor === '#ef4444' ? 'border-slate-900 dark:border-white scale-110' : 'border-transparent'}`} style={{backgroundColor: '#ef4444'}} />
|
|
<button onClick={() => setBrushColor('#3b82f6')} className={`w-6 h-6 rounded-full border-2 ${brushColor === '#3b82f6' ? 'border-slate-900 dark:border-white scale-110' : 'border-transparent'}`} style={{backgroundColor: '#3b82f6'}} />
|
|
<button onClick={() => setBrushColor('#10b981')} className={`w-6 h-6 rounded-full border-2 ${brushColor === '#10b981' ? 'border-slate-900 dark:border-white scale-110' : 'border-transparent'}`} style={{backgroundColor: '#10b981'}} />
|
|
<div className="w-px h-6 bg-slate-300 dark:bg-slate-700 mx-1" />
|
|
<button onClick={clearCanvas} className="p-1 hover:bg-slate-200 dark:hover:bg-slate-700 rounded text-slate-600 dark:text-slate-300" title={labels.canvasClear}>
|
|
<Eraser size={16}/>
|
|
</button>
|
|
</div>
|
|
|
|
<canvas
|
|
ref={canvasRef}
|
|
className="w-full h-auto cursor-crosshair touch-none block"
|
|
onMouseDown={startDrawing}
|
|
onMouseMove={draw}
|
|
onMouseUp={stopDrawing}
|
|
onMouseLeave={stopDrawing}
|
|
onTouchStart={startDrawing}
|
|
onTouchMove={draw}
|
|
onTouchEnd={stopDrawing}
|
|
/>
|
|
|
|
<div className="absolute bottom-2 right-2 z-10 flex gap-2">
|
|
<button
|
|
onClick={() => setEditingIndex(null)}
|
|
className="px-3 py-1.5 text-xs font-bold bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 rounded shadow-md hover:bg-slate-100 dark:hover:bg-slate-700"
|
|
>
|
|
{labels.cancelEdit}
|
|
</button>
|
|
<button
|
|
onClick={() => handleRegenerateWithSketch(i, prompt.prompt)}
|
|
className="px-3 py-1.5 text-xs font-bold bg-blue-600 text-white rounded shadow-md hover:bg-blue-700 flex items-center gap-1.5"
|
|
>
|
|
<RefreshCw size={14}/> {labels.regenerateSketch}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="relative">
|
|
<img
|
|
src={generatedImages[i]}
|
|
alt={`Generated concept for ${prompt.title}`}
|
|
className="w-full h-auto rounded-lg shadow-md border border-slate-200 dark:border-robo-700 block"
|
|
/>
|
|
<div className="absolute top-2 right-2 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<button
|
|
onClick={() => setEditingIndex(i)}
|
|
className="bg-white/90 dark:bg-black/70 hover:bg-white dark:hover:bg-black p-2 rounded-full text-slate-800 dark:text-white shadow-sm backdrop-blur-sm"
|
|
title={labels.editImage}
|
|
>
|
|
<Pencil size={16}/>
|
|
</button>
|
|
<button
|
|
onClick={() => downloadAsset(generatedImages[i], i)}
|
|
className="bg-white/90 dark:bg-black/70 hover:bg-white dark:hover:bg-black p-2 rounded-full text-slate-800 dark:text-white shadow-sm backdrop-blur-sm"
|
|
title={labels.downloadAsset}
|
|
>
|
|
<Download size={16}/>
|
|
</button>
|
|
</div>
|
|
<p className="text-xs text-slate-500 mt-2 italic text-center">Generated by Gemini 2.5 Flash Image (Nano Banana)</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-4 rounded-lg border flex items-center justify-between transition-colors
|
|
bg-white border-slate-200
|
|
dark:bg-robo-800 dark:border-robo-700
|
|
">
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={goBack}
|
|
disabled={state.isLoading}
|
|
className="text-sm font-bold text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-white flex items-center gap-2 px-4 py-2"
|
|
>
|
|
<ArrowLeft size={16}/> {labels.backBtn}
|
|
</button>
|
|
<span className="text-sm text-slate-600 dark:text-slate-400 self-center border-l pl-2 border-slate-300 dark:border-slate-600">{labels.complete}</span>
|
|
</div>
|
|
<button
|
|
onClick={handlePhase7Submit}
|
|
className="font-bold py-3 px-6 rounded-lg transition-all flex items-center gap-2
|
|
bg-slate-900 text-white hover:bg-slate-700
|
|
dark:bg-white dark:text-robo-900 dark:hover:bg-slate-200"
|
|
>
|
|
{state.isLoading ? (
|
|
<span className="flex items-center gap-2"><Loader2 className="animate-spin"/> {labels.processing}</span>
|
|
) : (
|
|
<> {labels.toPhase7} <LayoutTemplate size={18} /></>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderPhase7 = () => (
|
|
<div className="space-y-8 animate-in fade-in">
|
|
<h2 className="text-2xl font-bold flex items-center gap-2 text-slate-900 dark:text-white">
|
|
<LayoutTemplate className="text-purple-600 dark:text-purple-400"/> {labels.phase7Title}
|
|
</h2>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
{state.phase7Result?.landingPages.map((lp, i) => (
|
|
<div key={i} className="flex flex-col rounded-xl border overflow-hidden shadow-lg transition-colors
|
|
bg-white border-slate-200
|
|
dark:bg-robo-800 dark:border-robo-700
|
|
">
|
|
<div className="p-6 bg-slate-900 text-white">
|
|
<span className="inline-block px-2 py-1 mb-3 text-xs font-bold uppercase tracking-wider bg-purple-500 rounded text-white">
|
|
{lp.industry}
|
|
</span>
|
|
<h3 className="text-2xl font-bold leading-tight">{lp.headline}</h3>
|
|
<p className="mt-3 text-purple-100 opacity-90 text-sm">{lp.subline}</p>
|
|
</div>
|
|
<div className="p-6 flex-1 flex flex-col">
|
|
<ul className="space-y-3 mb-6 flex-1">
|
|
{lp.bullets.map((bullet, bi) => (
|
|
<li key={bi} className="flex items-start gap-3 text-sm text-slate-700 dark:text-slate-300">
|
|
<CheckCircle size={16} className="text-green-500 mt-0.5 shrink-0"/>
|
|
<span>{bullet}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<button className="w-full py-3 bg-purple-600 hover:bg-purple-700 text-white font-bold rounded-lg transition-colors">
|
|
{lp.cta}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="p-4 rounded-lg border flex items-center justify-between transition-colors
|
|
bg-white border-slate-200
|
|
dark:bg-robo-800 dark:border-robo-700
|
|
">
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={goBack}
|
|
disabled={state.isLoading}
|
|
className="text-sm font-bold text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-white flex items-center gap-2 px-4 py-2"
|
|
>
|
|
<ArrowLeft size={16}/> {labels.backBtn}
|
|
</button>
|
|
</div>
|
|
<button
|
|
onClick={handlePhase8Submit}
|
|
disabled={state.isLoading}
|
|
className="font-bold py-3 px-6 rounded-lg transition-all flex items-center gap-2
|
|
bg-slate-900 text-white hover:bg-slate-700
|
|
dark:bg-white dark:text-robo-900 dark:hover:bg-slate-200"
|
|
>
|
|
{state.isLoading ? (
|
|
<span className="flex items-center gap-2"><Loader2 className="animate-spin"/> {labels.processing}</span>
|
|
) : (
|
|
<> {labels.toPhase8} <TrendingUp size={18} /></>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderPhase8 = () => (
|
|
<div className="space-y-8 animate-in fade-in">
|
|
<h2 className="text-2xl font-bold flex items-center gap-2 text-slate-900 dark:text-white">
|
|
<TrendingUp className="text-green-600 dark:text-green-400"/> {labels.phase8Title}
|
|
</h2>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{state.phase8Result?.businessCases.map((bc, i) => (
|
|
<div key={i} className="rounded-xl border p-6 transition-colors flex flex-col gap-4
|
|
bg-white border-slate-200
|
|
dark:bg-robo-800 dark:border-robo-700
|
|
">
|
|
<div className="flex items-center gap-3 pb-4 border-b border-slate-100 dark:border-robo-700">
|
|
<div className="p-2 bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400 rounded-lg">
|
|
<Building2 size={20}/>
|
|
</div>
|
|
<h3 className="font-bold text-lg text-slate-800 dark:text-white">{bc.industry}</h3>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<span className="text-xs font-bold uppercase tracking-wider text-red-500">Cost Driver (Pain)</span>
|
|
<p className="mt-1 text-sm text-slate-700 dark:text-slate-300 font-medium">{bc.costDriver}</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-xs font-bold uppercase tracking-wider text-green-500">Efficiency Gain (Gain)</span>
|
|
<p className="mt-1 text-sm text-slate-700 dark:text-slate-300 font-medium">{bc.efficiencyGain}</p>
|
|
</div>
|
|
<div className="p-3 bg-slate-50 dark:bg-robo-900 rounded-lg border border-slate-100 dark:border-robo-700">
|
|
<span className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 flex items-center gap-1.5 mb-1">
|
|
<ShieldCheck size={12}/> Risk Argument
|
|
</span>
|
|
<p className="text-sm text-slate-600 dark:text-slate-400 italic">"{bc.riskArgument}"</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="p-4 rounded-lg border flex items-center justify-between transition-colors
|
|
bg-white border-slate-200
|
|
dark:bg-robo-800 dark:border-robo-700
|
|
">
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={goBack}
|
|
disabled={state.isLoading}
|
|
className="text-sm font-bold text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-white flex items-center gap-2 px-4 py-2"
|
|
>
|
|
<ArrowLeft size={16}/> {labels.backBtn}
|
|
</button>
|
|
</div>
|
|
<button
|
|
onClick={handlePhase9Submit}
|
|
disabled={state.isLoading}
|
|
className="font-bold py-3 px-6 rounded-lg transition-all flex items-center gap-2
|
|
bg-slate-900 text-white hover:bg-slate-700
|
|
dark:bg-white dark:text-robo-900 dark:hover:bg-slate-200"
|
|
>
|
|
{state.isLoading ? (
|
|
<span className="flex items-center gap-2"><Loader2 className="animate-spin"/> {labels.processing}</span>
|
|
) : (
|
|
<> {labels.toPhase9} <Shield size={18} /></>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderPhase9 = () => (
|
|
<div className="space-y-8 animate-in fade-in">
|
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
|
<h2 className="text-2xl font-bold flex items-center gap-2 text-slate-900 dark:text-white">
|
|
<Shield className="text-blue-600 dark:text-blue-400"/> {labels.phase9Title}
|
|
</h2>
|
|
<div className="flex flex-wrap gap-2">
|
|
{language === 'de' && (
|
|
<>
|
|
<button
|
|
onClick={handleTranslateReport}
|
|
disabled={isTranslating}
|
|
className="flex items-center gap-2 px-3 py-1.5 text-sm font-bold rounded-lg transition-all bg-white border border-slate-300 text-slate-700 hover:bg-slate-50 dark:bg-robo-800 dark:border-robo-600 dark:text-slate-300 dark:hover:bg-robo-700"
|
|
>
|
|
{isTranslating ? (
|
|
<><Loader2 size={14} className="animate-spin"/> {labels.translating}</>
|
|
) : (
|
|
<><Languages size={14}/> {labels.translateBtn}</>
|
|
)}
|
|
</button>
|
|
{state.translatedReport && (
|
|
<button
|
|
onClick={() => downloadReport(state.translatedReport, "EN-")}
|
|
className="flex items-center gap-2 px-3 py-1.5 text-sm font-bold rounded-lg transition-all
|
|
bg-emerald-600 text-white hover:bg-emerald-700
|
|
dark:bg-emerald-600 dark:hover:bg-emerald-500"
|
|
>
|
|
<Download size={14}/> {labels.downloadEn}
|
|
</button>
|
|
)}
|
|
</>
|
|
)}
|
|
<button
|
|
onClick={() => downloadReport()}
|
|
className="flex items-center gap-2 px-3 py-1.5 text-sm font-bold rounded-lg transition-all
|
|
bg-slate-900 text-white hover:bg-slate-700
|
|
dark:bg-white dark:text-robo-900 dark:hover:bg-slate-200"
|
|
>
|
|
<Download size={14}/> {labels.download}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="overflow-x-auto rounded-xl border border-slate-200 dark:border-robo-700">
|
|
<table className="w-full text-left border-collapse">
|
|
<thead>
|
|
<tr className="bg-slate-100 dark:bg-robo-900 text-xs uppercase tracking-wider text-slate-500 dark:text-slate-400 font-bold">
|
|
<th className="p-4 border-b border-slate-200 dark:border-robo-700 w-1/4">Tech Feature</th>
|
|
<th className="p-4 border-b border-slate-200 dark:border-robo-700 w-1/2">The Story (Benefit)</th>
|
|
<th className="p-4 border-b border-slate-200 dark:border-robo-700 w-1/4">Headline</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-200 dark:divide-robo-700 bg-white dark:bg-robo-800">
|
|
{state.phase9Result?.techTranslations.map((item, i) => (
|
|
<tr key={i} className="hover:bg-slate-50 dark:hover:bg-robo-700/50 transition-colors">
|
|
<td className="p-4 font-mono text-sm text-slate-600 dark:text-slate-300 border-r border-slate-100 dark:border-robo-700/50 align-top">
|
|
{item.feature}
|
|
</td>
|
|
<td className="p-4 text-sm text-slate-700 dark:text-slate-300 leading-relaxed align-top">
|
|
{item.story}
|
|
</td>
|
|
<td className="p-4 align-top">
|
|
<div className="inline-block px-3 py-1.5 rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-900/30 text-blue-800 dark:text-blue-300 font-bold text-sm">
|
|
{item.headline}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div className="p-4 rounded-lg border flex items-center justify-between transition-colors
|
|
bg-white border-slate-200
|
|
dark:bg-robo-800 dark:border-robo-700
|
|
">
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={goBack}
|
|
disabled={state.isLoading}
|
|
className="text-sm font-bold text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-white flex items-center gap-2 px-4 py-2"
|
|
>
|
|
<ArrowLeft size={16}/> {labels.backBtn}
|
|
</button>
|
|
<span className="text-sm text-slate-600 dark:text-slate-400 self-center border-l pl-2 border-slate-300 dark:border-slate-600">{labels.complete}</span>
|
|
</div>
|
|
<button
|
|
onClick={() => window.location.reload()}
|
|
className="text-sm font-medium hover:underline text-slate-500 hover:text-slate-800 dark:text-slate-300 dark:hover:text-white"
|
|
>
|
|
{labels.restart}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<Layout
|
|
currentPhase={state.currentPhase}
|
|
maxAllowedPhase={getMaxAllowedPhase()}
|
|
onPhaseSelect={handlePhaseSelect}
|
|
theme={theme}
|
|
toggleTheme={toggleTheme}
|
|
language={language}
|
|
setLanguage={setLanguage}
|
|
labels={labels}
|
|
>
|
|
{error && (
|
|
<div className="mb-6 p-4 rounded-lg flex items-center gap-3 transition-colors
|
|
bg-red-50 border border-red-200 text-red-700
|
|
dark:bg-red-500/10 dark:border-red-500/50 dark:text-red-200
|
|
">
|
|
<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()}
|
|
{state.currentPhase === Phase.LandingPage && renderPhase7()}
|
|
{state.currentPhase === Phase.BusinessCase && renderPhase8()}
|
|
{state.currentPhase === Phase.TechTranslator && renderPhase9()}
|
|
</Layout>
|
|
);
|
|
};
|
|
|
|
export default App; |