diff --git a/gtm-architect/App.tsx b/gtm-architect/App.tsx index ad9e343d..8e795e13 100644 --- a/gtm-architect/App.tsx +++ b/gtm-architect/App.tsx @@ -248,145 +248,152 @@ const App: React.FC = () => { } }, [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, - })); - setViewingSessions(false); // Switch back to the main view - } catch (e: any) { - setError("Failed to load session: " + e.message); - setState(s => ({ ...s, isLoading: false })); - } - }; - - const handleDeleteSession = async (projectId: string) => { - if (!window.confirm("Delete this session permanently?")) return; + // Load Sessions on Mount + useEffect(() => { + const fetchSessions = async () => { + try { + const res = await Gemini.listSessions(); + if (res && Array.isArray(res.projects)) { + setSessions(res.projects); + } else { + setSessions([]); + } + } catch (e) { + console.error("Failed to load sessions", e); + setSessions([]); + } + }; + fetchSessions(); + }, []); + + useEffect(() => { + if (!state.isLoading && !isTranslating) return; + + const messages = labels.loading; - 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) => { - 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 } - })); + 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 })); } }; - reader.readAsText(file); - }; - - const generateFullReportMarkdown = (): string => { - if (!state.phase5Result || !state.phase5Result.report) return ""; - - let fullReport = state.phase5Result.report; - + + // 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, + })); + setViewingSessions(false); // Switch back to the main view + } catch (e: any) { + setError("Failed to load session: " + e.message); + setState(s => ({ ...s, isLoading: false })); + } + }; + + const handleDeleteSession = async (projectId: string) => { + 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) => { + 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 ""; + + const sourceUrl = state.phase1Result?.specs?.metadata?.manufacturer_url; + let fullReport = sourceUrl ? `# GTM Strategy\n\n**Recherche-URL:** ${sourceUrl}\n\n---\n\n` : '# GTM Strategy\n\n'; + + fullReport += state.phase5Result.report; if (state.phase6Result) { fullReport += `\n\n# SALES ENABLEMENT & VISUALS (PHASE 6)\n\n`; fullReport += `## Kill-Critique Battlecards\n\n`; @@ -920,6 +927,7 @@ const App: React.FC = () => { sessions={sessions} onLoadSession={handleLoadSession} onDeleteSession={handleDeleteSession} + onStartNew={() => setViewingSessions(false)} /> ) : (
diff --git a/gtm-architect/components/SessionBrowser.css b/gtm-architect/components/SessionBrowser.css index 5c5a19e9..442ba5ab 100644 --- a/gtm-architect/components/SessionBrowser.css +++ b/gtm-architect/components/SessionBrowser.css @@ -1,82 +1,4 @@ -.session-browser { - padding: 20px; - background-color: #f0f2f5; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; -} - -.session-browser h2 { - text-align: center; - color: #333; - margin-bottom: 24px; -} - -.session-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 20px; -} - -.session-card { - background-color: #ffffff; - border-radius: 12px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); - overflow: hidden; - display: flex; - flex-direction: column; - transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; -} - -.session-card:hover { - transform: translateY(-5px); - box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12); -} - -.card-header { - padding: 16px; - display: flex; - align-items: center; - border-bottom: 1px solid #e8e8e8; -} - -.category-icon { - font-size: 24px; - margin-right: 12px; -} - -.card-header h3 { - margin: 0; - font-size: 1.1rem; - color: #1a1a1a; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.thumbnail-placeholder { - width: 100%; - height: 150px; - background-color: #e9ecef; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - color: #adb5bd; - font-size: 1.5rem; -} - -.thumbnail-placeholder span { - font-size: 3rem; -} - -.thumbnail-placeholder p { - margin-top: 8px; - font-size: 0.9rem; -} - -.card-content { - padding: 16px; - flex-grow: 1; /* Ensures content area expands */ -} +/* ... (existing styles) ... */ .product-description { font-size: 0.9rem; @@ -90,45 +12,26 @@ -webkit-box-orient: vertical; } +.source-url { + font-size: 0.8rem; + margin-bottom: 12px; +} + +.source-url a { + color: #007bff; + text-decoration: none; + transition: color 0.2s; +} + +.source-url a:hover { + color: #0056b3; + text-decoration: underline; +} + .last-updated { font-size: 0.8rem; color: #888; margin-top: auto; /* Pushes to the bottom if content is short */ } -.card-actions { - display: flex; - justify-content: space-between; - padding: 12px 16px; - border-top: 1px solid #e8e8e8; - background-color: #fafafa; -} - -.card-actions button { - border: none; - border-radius: 6px; - padding: 8px 16px; - font-weight: 600; - cursor: pointer; - transition: background-color 0.2s, color 0.2s; -} - -.load-btn { - background-color: #007bff; - color: white; -} - -.load-btn:hover { - background-color: #0056b3; -} - -.delete-btn { - background-color: #fceeee; - color: #d9534f; -} - -.delete-btn:hover { - background-color: #f8d7da; - color: #b0413e; -} - +/* ... (rest of the styles) ... */ \ No newline at end of file diff --git a/gtm-architect/components/SessionBrowser.tsx b/gtm-architect/components/SessionBrowser.tsx index b719bb74..3897cef6 100644 --- a/gtm-architect/components/SessionBrowser.tsx +++ b/gtm-architect/components/SessionBrowser.tsx @@ -1,56 +1,79 @@ import React from 'react'; import { ProjectHistoryItem } from './types'; import './SessionBrowser.css'; +import { Plus, FileText } from 'lucide-react'; interface SessionBrowserProps { sessions: ProjectHistoryItem[]; onLoadSession: (projectId: string) => void; onDeleteSession: (projectId: string) => void; + onStartNew: () => void; } -const SessionBrowser: React.FC = ({ sessions, onLoadSession, onDeleteSession }) => { +const SessionBrowser: React.FC = ({ sessions, onLoadSession, onDeleteSession, onStartNew }) => { const getCategoryIcon = (category: string) => { + // Return an icon based on the category, default to a generic robot + if (!category) return '🤖'; switch (category.toLowerCase()) { case 'reinigungsroboter': - return '🧹'; // Sweeping broom emoji + return '🧹'; case 'serviceroboter': - return '🛎️'; // Bellhop bell emoji + return '🛎️'; case 'transportroboter': - return '📦'; // Package emoji + return '📦'; case 'security roboter': - return '🛡️'; // Shield emoji + return '🛡️'; default: - return '🤖'; // Default robot emoji + return '🤖'; } }; return (
-

Gespeicherte Sitzungen

-
- {sessions.map((session) => ( -
-
- {getCategoryIcon(session.productCategory)} -

{session.productName}

-
- {/* Thumbnail placeholder */} -
- 🖼️ -

Thumbnail

-
-
-

{session.productDescription}

-

Zuletzt bearbeitet: {new Date(session.updated_at).toLocaleString()}

-
-
- - -
-
- ))} +
+

Gespeicherte Sitzungen

+
+ + {(!sessions || sessions.length === 0) ? ( +
+

Keine gespeicherten Sitzungen gefunden.

+ +
+ ) : ( +
+ {sessions.map((session) => ( +
+
+ {getCategoryIcon(session.productCategory)} +

{session.productName || 'Unbenannt'}

+
+
+ 🖼️ +

Thumbnail

+
+
+

{session.productDescription || 'Keine Beschreibung verfügbar.'}

+

+ + Quelle anzeigen + +

+

Zuletzt bearbeitet: {new Date(session.updated_at).toLocaleString()}

+
+
+ + +
+
+ ))} +
+ )}
); }; diff --git a/gtm-architect/types.ts b/gtm-architect/types.ts index d60b92d6..b7c12e06 100644 --- a/gtm-architect/types.ts +++ b/gtm-architect/types.ts @@ -168,5 +168,6 @@ export interface ProjectHistoryItem { productName: string; productCategory: string; productDescription: string; + sourceUrl: string; productThumbnail?: string; // Optional for now } \ No newline at end of file diff --git a/gtm_architect_documentation.md b/gtm_architect_documentation.md index 0a049e9f..9851716c 100644 --- a/gtm_architect_documentation.md +++ b/gtm_architect_documentation.md @@ -91,6 +91,11 @@ Das System verwaltet persistente Sitzungen in der SQLite-Datenbank: ## 7. Historie & Fixes (Jan 2026) +* **[UPGRADE] v2.6.1: Stability & URL Persistence** + * **Bugfix:** Behebung des "Weißen Bildschirms" durch Absicherung der Session-Liste gegen `undefined`-Werte. + * **URL Tracking:** Die Recherche-URL wird nun zwingend im Projekt gespeichert, in der Lade-Übersicht angezeigt und in den finalen Report-Export integriert. + * **UX Flow:** Direkter Wechsel zwischen "Letzte Sitzungen" und "Neue Analyse starten" jederzeit möglich. + * **[UPGRADE] v2.6: Rich Session Browser** * **Neues UI:** Die textbasierte Liste für "Letzte Sitzungen" wurde durch eine dedizierte, kartenbasierte UI (`SessionBrowser.tsx`) ersetzt. * **Angereicherte Daten:** Jede Sitzungskarte zeigt nun den Produktnamen, die Produktkategorie (mit Icon), eine Kurzbeschreibung und einen Thumbnail-Platzhalter an. diff --git a/gtm_architect_orchestrator.py b/gtm_architect_orchestrator.py index 68aac211..be5a67b0 100644 --- a/gtm_architect_orchestrator.py +++ b/gtm_architect_orchestrator.py @@ -282,6 +282,13 @@ def phase1(payload): try: specs_data = json.loads(specs_response) + + # FORCE URL PERSISTENCE: If input was a URL, ensure it's in the metadata + if product_input.strip().startswith('http'): + if 'metadata' not in specs_data: + specs_data['metadata'] = {} + specs_data['metadata']['manufacturer_url'] = product_input.strip() + data['specs'] = specs_data except json.JSONDecodeError: logging.error(f"Failed to decode JSON from Gemini response in phase1 (specs): {specs_response}") diff --git a/gtm_db_manager.py b/gtm_db_manager.py index e14809ef..b4e686ef 100644 --- a/gtm_db_manager.py +++ b/gtm_db_manager.py @@ -104,7 +104,8 @@ def get_all_projects(): updated_at, json_extract(data, '$.phases.phase1_result.specs.metadata.model_name') AS productName, json_extract(data, '$.phases.phase1_result.specs.metadata.category') AS productCategory, - json_extract(data, '$.phases.phase1_result.specs.metadata.description') AS productDescription + json_extract(data, '$.phases.phase1_result.specs.metadata.description') AS productDescription, + json_extract(data, '$.phases.phase1_result.specs.metadata.manufacturer_url') AS sourceUrl FROM gtm_projects ORDER BY updated_at DESC """ @@ -119,6 +120,8 @@ def get_all_projects(): project_dict['productCategory'] = "Uncategorized" # Default category if project_dict.get('productDescription') is None: project_dict['productDescription'] = "No description available." # Default description + if project_dict.get('sourceUrl') is None: + project_dict['sourceUrl'] = "No source URL found." # Default URL project_list.append(project_dict) return project_list finally: