feat(gtm): add responsive collapsible sidebar for mobile

This commit is contained in:
2026-01-04 19:53:21 +00:00
parent 52bad1fc89
commit 7be63b6d14
4 changed files with 326 additions and 273 deletions

View File

@@ -8,9 +8,11 @@ import { AlertTriangle, ArrowRight, ArrowLeft, Check, Database, Globe, Search, S
const TRANSLATIONS = {
en: {
// ... existing ...
historyTitle: 'Recent Sessions',
loadBtn: 'Load',
noSessions: 'No history found.',
// ... existing ...
phase1: 'Product & Constraints',
phase2: 'ICP Discovery',
phase3: 'Whale Hunting',
@@ -25,7 +27,7 @@ const TRANSLATIONS = {
inputPlaceholder: 'Paste raw technical data, brochure text, or product URL here...',
startBtn: 'Start Analysis',
backBtn: 'Back',
processing: 'Processing...',
processing: 'Processing...',
features: 'Detected Features',
constraints: 'Hard Constraints',
conflictTitle: 'Portfolio Conflict Detected',
@@ -53,7 +55,7 @@ const TRANSLATIONS = {
downloadAsset: 'Download Asset',
translateBtn: 'Translate Report to English',
downloadEn: 'Download English Report (.md)',
translating: 'Translating...',
translating: 'Translating...',
toPhase6: 'Proceed to Sales Enablement (Phase 6)',
toPhase7: 'Proceed to Landing Pages (Phase 7)',
toPhase8: 'Proceed to Business Case (Phase 8)',
@@ -72,18 +74,19 @@ const TRANSLATIONS = {
editImage: 'Edit & Iterate',
regenerateSketch: 'Regenerate with Sketch',
cancelEdit: 'Cancel Edit',
generating: 'Generating...',
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...',
// New placeholders
addName: 'Name...',
addRationale: 'Rationale...',
addTarget: 'Target criteria...',
addMethod: 'Method...',
addAccount: 'Account name...',
addRole: 'Job title...',
addRole: 'Job title...',
loading: [
"Initializing quantum analysis engines...",
"Parsing raw technical specifications...",
@@ -108,10 +111,10 @@ const TRANSLATIONS = {
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...',
inputPlaceholder: 'Füge hier rohe technische Daten, Broschürentexte oder Produkt-URL ein...',
startBtn: 'Analyse starten',
backBtn: 'Zurück',
processing: 'Verarbeite...',
processing: 'Verarbeite...',
features: 'Erkannte Features',
constraints: 'Harte Constraints',
conflictTitle: 'Portfolio-Konflikt erkannt',
@@ -134,12 +137,12 @@ const TRANSLATIONS = {
genAssets: 'Generierter Strategie-Report & Assets',
complete: 'Prozess abgeschlossen. Strategie fixiert.',
restart: 'Neue Session starten',
addPlaceholder: 'Punkt hinzufügen...',
addPlaceholder: 'Punkt hinzufügen...',
download: 'Gesamtbericht herunterladen (.md)',
downloadAsset: 'Bild speichern',
translateBtn: 'Bericht ins Englische übersetzen',
downloadEn: 'Englischen Bericht herunterladen (.md)',
translating: 'Übersetze...',
translating: 'Übersetze...',
toPhase6: 'Weiter zu Sales Enablement (Phase 6)',
toPhase7: 'Weiter zu Landing Pages (Phase 7)',
toPhase8: 'Weiter zu Business Case (Phase 8)',
@@ -158,18 +161,19 @@ const TRANSLATIONS = {
editImage: 'Zeichnen & Iterieren',
regenerateSketch: 'Mit Skizze neu generieren',
cancelEdit: 'Abbrechen',
generating: 'Generiere...',
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...',
// New placeholders
addName: 'Name...',
addRationale: 'Begründung...',
addTarget: 'Zielkriterium...',
addMethod: 'Suchmethode...',
addAccount: 'Firmenname...',
addRole: 'Jobtitel...',
loading: [
"Initialisiere Quanten-Analyse-Engines...",
"Analysiere technische Spezifikationen...",
@@ -187,7 +191,7 @@ const App: React.FC = () => {
isLoading: false,
history: [],
productInput: '',
productImages: [],
productImages: [], // Now an array
language: 'light', // temporary init, fix in useEffect
theme: 'light' // Default to light
});
@@ -324,64 +328,21 @@ const App: React.FC = () => {
const data = await Gemini.loadSession(projectId);
const phases = data.phases || {};
console.log("Raw Loaded Phases from DB:", phases);
// Helper to safely parse potentially MULTIPLE times stringified JSON
const parsePhaseData = (phaseData: any) => {
if (!phaseData) return undefined;
let parsed = phaseData;
try {
// Versuch 1
if (typeof parsed === 'string') parsed = JSON.parse(parsed);
// Versuch 2 (falls double encoded)
if (typeof parsed === 'string') parsed = JSON.parse(parsed);
// Versuch 3 (extrem)
if (typeof parsed === 'string') parsed = JSON.parse(parsed);
} catch (e) {
console.warn("Failed to parse phase data", e, phaseData);
return null;
}
return parsed;
};
const p1 = parsePhaseData(phases.phase1_result);
const p2 = parsePhaseData(phases.phase2_result);
const p3 = parsePhaseData(phases.phase3_result);
const p4 = parsePhaseData(phases.phase4_result);
const p5 = parsePhaseData(phases.phase5_result);
const p6 = parsePhaseData(phases.phase6_result);
const p7 = parsePhaseData(phases.phase7_result);
const p8 = parsePhaseData(phases.phase8_result);
const p9 = parsePhaseData(phases.phase9_result);
console.log("Parsed Phase 2:", p2); // Debug
// Determine the highest available phase to jump to
let targetPhase = Phase.ProductAnalysis;
if (p9) targetPhase = Phase.TechTranslator;
else if (p8) targetPhase = Phase.BusinessCase;
else if (p7) targetPhase = Phase.LandingPage;
else if (p6) targetPhase = Phase.SalesEnablement;
else if (p5) targetPhase = Phase.AssetGeneration;
else if (p4) targetPhase = Phase.Strategy;
else if (p3) targetPhase = Phase.WhaleHunting;
else if (p2) targetPhase = Phase.ICPDiscovery;
setState(s => ({
...s,
isLoading: false,
currentPhase: targetPhase,
currentPhase: Phase.ProductAnalysis,
projectId: projectId,
productInput: p1?.rawAnalysis || "",
phase1Result: p1,
phase2Result: p2,
phase3Result: p3,
phase4Result: p4,
phase5Result: p5,
phase6Result: p6,
phase7Result: p7,
phase8Result: p8,
phase9Result: p9,
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);
@@ -419,8 +380,6 @@ const App: React.FC = () => {
reader.readAsText(file);
};
// --- Asset Generation Helpers ---
const generateFullReportMarkdown = (): string => {
if (!state.phase5Result || !state.phase5Result.report) return "";
@@ -438,11 +397,7 @@ const App: React.FC = () => {
state.phase6Result.visualPrompts.forEach(prompt => {
fullReport += `### ${prompt.title}\n`;
fullReport += `*Context: ${prompt.context}*\n\n`;
fullReport += `\
\
${prompt.prompt}\n\
\
`;
fullReport += `\`\`\`\n${prompt.prompt}\n\`\`\`\n\n`;
});
}
@@ -512,7 +467,7 @@ ${prompt.prompt}\n\
const element = document.createElement("a");
element.href = dataUrl;
element.download = `roboplanet-visual-${index + 1}.png`;
document.body.appendChild(element);
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
};
@@ -525,170 +480,256 @@ ${prompt.prompt}\n\
// --- 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, state.projectId);
setGeneratedImages(prev => ({ ...prev, [index]: imageUrl }));
// If we were editing, close edit mode
if (editingIndex === index) {
setEditingIndex(null);
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);
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];
}
} catch (e: any) {
console.error("Failed to generate image", e);
setError("Image generation failed: " + e.message);
} finally {
setGeneratingImages(prev => ({ ...prev, [index]: false }));
}
};
// ... (Upload handlers remain same) ...
// --- 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 }));
} catch (e: any) {
setError(e.message || "Analysis failed");
setState(s => ({ ...s, isLoading: false }));
};
const handleRegenerateWithSketch = (index: number, prompt: string) => {
const canvas = canvasRef.current;
if (canvas) {
const sketchData = canvas.toDataURL('image/png');
handleGenerateImage(prompt, index, sketchData);
}
};
const handlePhase2Submit = async () => {
if (!state.phase1Result || !state.projectId) return;
};
// --- 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.discoverICPs(state.phase1Result, language, state.projectId);
setState(s => ({ ...s, currentPhase: Phase.ICPDiscovery, phase2Result: result, isLoading: false }));
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 }));
setError(e.message);
setState(s => ({ ...s, isLoading: false }));
}
};
const handlePhase3Submit = async () => {
if (!state.phase2Result || !state.projectId) return;
};
const handlePhase7Submit = async () => {
if (!state.phase4Result || !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 }));
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 }));
setError(e.message);
setState(s => ({ ...s, isLoading: false }));
}
};
const handlePhase4Submit = async () => {
if (!state.phase3Result || !state.phase1Result || !state.projectId) return;
};
const handlePhase8Submit = async () => {
if (!state.phase2Result || !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 }));
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 }));
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;
};
const handlePhase9Submit = async () => {
if (!state.phase1Result || !state.phase4Result || !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 }));
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 }));
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 = () => {
@@ -924,7 +965,7 @@ ${prompt.prompt}\n\
<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/*"
accept="image/*"
multiple
onChange={handleImageUpload}
className="hidden"
@@ -1160,9 +1201,7 @@ ${prompt.prompt}\n\
{state.isLoading ? (
<span className="flex items-center gap-2"><Loader2 className="animate-spin"/> {labels.processing}</span>
) : (
<>
{labels.confirmICP} <ArrowRight size={18} />
</>
<> {labels.confirmICP} <ArrowRight size={18} /></>
)}
</button>
</div>
@@ -2057,4 +2096,4 @@ ${prompt.prompt}\n\
);
};
export default App;
export default App;

View File

@@ -1,6 +1,6 @@
import React from 'react';
import React, { useState } from 'react';
import { Phase, Language, Theme } from '../types';
import { Activity, Target, Crosshair, Map, FileText, CheckCircle, Lock, Moon, Sun, Languages, ShieldCheck, Terminal, LayoutTemplate, TrendingUp, Shield } from 'lucide-react';
import { Activity, Target, Crosshair, Map, FileText, CheckCircle, Lock, Moon, Sun, Languages, ShieldCheck, Terminal, LayoutTemplate, TrendingUp, Shield, Menu, X } from 'lucide-react';
interface LayoutProps {
currentPhase: Phase;
@@ -41,6 +41,7 @@ export const Layout: React.FC<LayoutProps> = ({
setLanguage,
labels
}) => {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const steps = [
{ id: Phase.Input, label: labels.initTitle ? 'Input' : 'Input' },
@@ -57,17 +58,33 @@ export const Layout: React.FC<LayoutProps> = ({
return (
<div className={`min-h-screen flex font-sans transition-colors duration-300 ${theme === 'dark' ? 'dark bg-robo-900 text-slate-200' : 'bg-slate-50 text-slate-900'}`}>
{/* Mobile Backdrop */}
{isSidebarOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 md:hidden backdrop-blur-sm"
onClick={() => setIsSidebarOpen(false)}
/>
)}
{/* Sidebar */}
<div className="w-80 border-r flex-shrink-0 flex flex-col transition-colors duration-300
bg-white border-slate-200
dark:bg-robo-800 dark:border-robo-700
">
<div className="p-6 border-b transition-colors duration-300 border-slate-200 dark:border-robo-700">
<h1 className="text-xl font-bold font-mono tracking-tighter flex items-center gap-2 text-slate-900 dark:text-white">
<div className="w-3 h-3 bg-robo-500 dark:bg-robo-accent rounded-full animate-pulse"></div>
ROBOPLANET
</h1>
<p className="text-xs mt-1 uppercase tracking-widest text-slate-500 dark:text-robo-400">GTM Architect Engine</p>
<div className={`
fixed inset-y-0 left-0 z-50 w-80 border-r flex-shrink-0 flex flex-col transition-all duration-300 transform
bg-white border-slate-200 dark:bg-robo-800 dark:border-robo-700
${isSidebarOpen ? 'translate-x-0' : '-translate-x-full'}
md:relative md:translate-x-0
`}>
<div className="p-6 border-b transition-colors duration-300 border-slate-200 dark:border-robo-700 flex justify-between items-center">
<div>
<h1 className="text-xl font-bold font-mono tracking-tighter flex items-center gap-2 text-slate-900 dark:text-white">
<div className="w-3 h-3 bg-robo-500 dark:bg-robo-accent rounded-full animate-pulse"></div>
ROBOPLANET
</h1>
<p className="text-xs mt-1 uppercase tracking-widest text-slate-500 dark:text-robo-400">GTM Architect Engine</p>
</div>
<button onClick={() => setIsSidebarOpen(false)} className="md:hidden p-2 text-slate-500">
<X size={20} />
</button>
</div>
<div className="px-4 py-4 flex gap-2">
@@ -101,7 +118,12 @@ export const Layout: React.FC<LayoutProps> = ({
return (
<button
key={step.id}
onClick={() => isUnlocked && onPhaseSelect(step.id)}
onClick={() => {
if (isUnlocked) {
onPhaseSelect(step.id);
setIsSidebarOpen(false);
}
}}
disabled={!isUnlocked}
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all border text-left
${isActive
@@ -149,6 +171,14 @@ export const Layout: React.FC<LayoutProps> = ({
bg-slate-50
dark:bg-gradient-to-br dark:from-robo-900 dark:to-[#0b1120]
">
{/* Mobile Header Toggle */}
<header className="md:hidden flex items-center p-4 border-b bg-white dark:bg-robo-800 border-slate-200 dark:border-robo-700">
<button onClick={() => setIsSidebarOpen(true)} className="p-2 text-slate-600 dark:text-slate-300">
<Menu size={24} />
</button>
<h1 className="ml-2 font-bold font-mono tracking-tighter text-slate-900 dark:text-white">ROBOPLANET</h1>
</header>
<div className="max-w-5xl mx-auto p-8 pb-32">
{children}
</div>

View File

@@ -102,8 +102,9 @@ export const translateReportToEnglish = async (reportMarkdown: string): Promise<
return callApi<{ report: string }>('run', 'translate', { reportMarkdown });
};
export const generateConceptImage = async (prompt: string, referenceImagesBase64?: string[], projectId?: string): Promise<string> => {
return (await callApi<{ imageBase64: string }>('run', 'image', { prompt, referenceImagesBase64, projectId })).imageBase64;
export const generateConceptImage = async (prompt: string, referenceImagesBase64?: string[]): Promise<string> => {
const result = await callApi<{ imageBase64: string }>('run', 'image', { prompt, referenceImagesBase64 });
return result.imageBase64;
};
export const listSessions = async (): Promise<{ projects: ProjectHistoryItem[] }> => {