From 7be63b6d14d0feacb94d46a0f602ce571c419ab9 Mon Sep 17 00:00:00 2001 From: Floke Date: Sun, 4 Jan 2026 19:53:21 +0000 Subject: [PATCH] feat(gtm): add responsive collapsible sidebar for mobile --- gtm-architect/App.tsx | 497 +++++++++++++++------------- gtm-architect/components/Layout.tsx | 56 +++- gtm-architect/geminiService.ts | 5 +- gtm_architect_orchestrator.py | 41 +-- 4 files changed, 326 insertions(+), 273 deletions(-) diff --git a/gtm-architect/App.tsx b/gtm-architect/App.tsx index 1f282a27..1619418b 100644 --- a/gtm-architect/App.tsx +++ b/gtm-architect/App.tsx @@ -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) => { + 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 | React.TouchEvent) => { + 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 | React.TouchEvent) => { + 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\