feat(gtm): add session history, database loading, and markdown file import
This commit is contained in:
@@ -1,13 +1,18 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Layout } from './components/Layout';
|
import { Layout } from './components/Layout';
|
||||||
import { Phase, AppState, Phase1Data, Phase2Data, Phase3Data, Phase4Data, Phase6Data, Phase7Data, Phase8Data, Phase9Data, Language, Theme } from './types';
|
import { Phase, AppState, Phase1Data, Phase2Data, Phase3Data, Phase4Data, Phase6Data, Phase7Data, Phase8Data, Phase9Data, Language, Theme, ProjectHistoryItem } from './types';
|
||||||
import * as Gemini from './geminiService';
|
import * as Gemini from './geminiService';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
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 } from 'lucide-react';
|
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 = {
|
const TRANSLATIONS = {
|
||||||
en: {
|
en: {
|
||||||
|
// ... existing ...
|
||||||
|
historyTitle: 'Recent Sessions',
|
||||||
|
loadBtn: 'Load',
|
||||||
|
noSessions: 'No history found.',
|
||||||
|
// ... existing ...
|
||||||
phase1: 'Product & Constraints',
|
phase1: 'Product & Constraints',
|
||||||
phase2: 'ICP Discovery',
|
phase2: 'ICP Discovery',
|
||||||
phase3: 'Whale Hunting',
|
phase3: 'Whale Hunting',
|
||||||
@@ -92,6 +97,9 @@ const TRANSLATIONS = {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
de: {
|
de: {
|
||||||
|
historyTitle: 'Letzte Sitzungen',
|
||||||
|
loadBtn: 'Laden',
|
||||||
|
noSessions: 'Keine Historie gefunden.',
|
||||||
phase1: 'Produkt & Constraints',
|
phase1: 'Produkt & Constraints',
|
||||||
phase2: 'ICP Entdeckung',
|
phase2: 'ICP Entdeckung',
|
||||||
phase3: 'Whale Hunting',
|
phase3: 'Whale Hunting',
|
||||||
@@ -196,6 +204,9 @@ const App: React.FC = () => {
|
|||||||
const [loadingMessage, setLoadingMessage] = useState("");
|
const [loadingMessage, setLoadingMessage] = useState("");
|
||||||
const [isTranslating, setIsTranslating] = useState(false);
|
const [isTranslating, setIsTranslating] = useState(false);
|
||||||
|
|
||||||
|
// Session Management
|
||||||
|
const [sessions, setSessions] = useState<ProjectHistoryItem[]>([]);
|
||||||
|
|
||||||
// Local state for adding new items (Human in the Loop inputs)
|
// Local state for adding new items (Human in the Loop inputs)
|
||||||
// Phase 1
|
// Phase 1
|
||||||
const [newFeatureInput, setNewFeatureInput] = useState("");
|
const [newFeatureInput, setNewFeatureInput] = useState("");
|
||||||
@@ -235,6 +246,19 @@ const App: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [theme]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!state.isLoading && !isTranslating) return;
|
if (!state.isLoading && !isTranslating) return;
|
||||||
|
|
||||||
@@ -297,6 +321,65 @@ const App: React.FC = () => {
|
|||||||
setState(s => ({ ...s, currentPhase: phase }));
|
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 => {
|
const generateFullReportMarkdown = (): string => {
|
||||||
if (!state.phase5Result || !state.phase5Result.report) return "";
|
if (!state.phase5Result || !state.phase5Result.report) return "";
|
||||||
|
|
||||||
@@ -515,7 +598,7 @@ const App: React.FC = () => {
|
|||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const result = await Gemini.analyzeProduct(state.productInput, language);
|
const result = await Gemini.analyzeProduct(state.productInput, language);
|
||||||
setState(s => ({ ...s, currentPhase: Phase.ProductAnalysis, phase1Result: result, isLoading: false }));
|
setState(s => ({ ...s, currentPhase: Phase.ProductAnalysis, phase1Result: result, isLoading: false, projectId: result.projectId })); // Save projectId!
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(e.message || "Analysis failed");
|
setError(e.message || "Analysis failed");
|
||||||
setState(s => ({ ...s, isLoading: false }));
|
setState(s => ({ ...s, isLoading: false }));
|
||||||
@@ -825,103 +908,153 @@ const App: React.FC = () => {
|
|||||||
|
|
||||||
const renderInputPhase = () => (
|
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="flex flex-col items-center justify-center min-h-[60vh] animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||||
<div className="w-full max-w-2xl p-8 rounded-xl shadow-2xl transition-colors duration-300
|
<div className="w-full max-w-4xl grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
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">
|
{/* LEFT COLUMN: Input */}
|
||||||
<div>
|
<div className="md:col-span-2 p-8 rounded-xl shadow-2xl transition-colors duration-300
|
||||||
<label className="block text-sm font-medium text-slate-500 dark:text-slate-400 mb-2">
|
bg-white border border-slate-200
|
||||||
{labels.inputLabel}
|
dark:bg-robo-800 dark:border-robo-700
|
||||||
</label>
|
">
|
||||||
<textarea
|
<h2 className="text-2xl font-bold mb-6 flex items-center gap-2 text-slate-800 dark:text-white">
|
||||||
className="w-full h-40 rounded-lg p-4 font-mono text-sm resize-none focus:ring-2 focus:outline-none transition-colors
|
<Terminal className="text-blue-600 dark:text-robo-accent" />
|
||||||
bg-slate-50 border border-slate-300 text-slate-900 focus:ring-blue-500
|
{labels.initTitle}
|
||||||
dark:bg-robo-900 dark:border-robo-700 dark:text-slate-200 dark:focus:ring-robo-500"
|
</h2>
|
||||||
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
|
<div className="space-y-4">
|
||||||
bg-slate-50 border-slate-300
|
<div>
|
||||||
dark:bg-robo-900/50 dark:border-robo-600
|
<label className="block text-sm font-medium text-slate-500 dark:text-slate-400 mb-2">
|
||||||
">
|
{labels.inputLabel}
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2 flex items-center gap-2">
|
</label>
|
||||||
<ImageIcon size={16}/> {labels.uploadImage}
|
<textarea
|
||||||
</label>
|
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="grid grid-cols-4 gap-4 mb-4">
|
<div className="p-4 rounded-lg border border-dashed transition-colors
|
||||||
{state.productImages.map((img, idx) => (
|
bg-slate-50 border-slate-300
|
||||||
<div key={idx} className="relative group aspect-square rounded-lg overflow-hidden border border-slate-200 dark:border-robo-700 bg-white">
|
dark:bg-robo-900/50 dark:border-robo-600
|
||||||
<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">
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2 flex items-center gap-2">
|
||||||
<div className="relative flex h-3 w-3">
|
<ImageIcon size={16}/> {labels.uploadImage}
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
</label>
|
||||||
<span className="relative inline-flex rounded-full h-3 w-3 bg-emerald-500"></span>
|
|
||||||
|
<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>
|
||||||
<span className="animate-pulse">{loadingMessage}</span>
|
<div className="mt-2 h-1 w-full bg-slate-200 dark:bg-robo-800 rounded-full overflow-hidden">
|
||||||
</div>
|
<div className="h-full bg-emerald-500/50 animate-pulse w-2/3 rounded-full"></div>
|
||||||
<div className="mt-2 h-1 w-full bg-slate-200 dark:bg-robo-800 rounded-full overflow-hidden">
|
</div>
|
||||||
<div className="h-full bg-emerald-500/50 animate-pulse w-2/3 rounded-full"></div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Phase1Data, Phase2Data, Phase3Data, Phase4Data, Phase6Data, Phase7Data, Phase8Data, Phase9Data, Language } from "./types";
|
import { Phase1Data, Phase2Data, Phase3Data, Phase4Data, Phase6Data, Phase7Data, Phase8Data, Phase9Data, Language, ProjectHistoryItem } from "./types";
|
||||||
|
|
||||||
const API_BASE_URL = './api'; // Relative path to respect Nginx /gtm/ routing
|
const API_BASE_URL = './api'; // Relative path to respect Nginx /gtm/ routing
|
||||||
|
|
||||||
@@ -101,3 +101,15 @@ export const generateConceptImage = async (prompt: string, referenceImagesBase64
|
|||||||
const result = await callApi<{ imageBase64: string }>('run', 'image', { prompt, referenceImagesBase64 });
|
const result = await callApi<{ imageBase64: string }>('run', 'image', { prompt, referenceImagesBase64 });
|
||||||
return result.imageBase64;
|
return result.imageBase64;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const listSessions = async (): Promise<{ projects: ProjectHistoryItem[] }> => {
|
||||||
|
return callApi('run', 'list_history', {});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadSession = async (projectId: string): Promise<any> => {
|
||||||
|
return callApi('run', 'load_history', { projectId });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteSession = async (projectId: string): Promise<any> => {
|
||||||
|
return callApi('run', 'delete_session', { projectId });
|
||||||
|
};
|
||||||
@@ -108,7 +108,7 @@ export interface AppState {
|
|||||||
phase2Result?: Phase2Data;
|
phase2Result?: Phase2Data;
|
||||||
phase3Result?: Phase3Data;
|
phase3Result?: Phase3Data;
|
||||||
phase4Result?: Phase4Data;
|
phase4Result?: Phase4Data;
|
||||||
phase5Result?: string; // Markdown content
|
phase5Result?: { report: string }; // Markdown content wrapped in object
|
||||||
phase6Result?: Phase6Data;
|
phase6Result?: Phase6Data;
|
||||||
phase7Result?: Phase7Data;
|
phase7Result?: Phase7Data;
|
||||||
phase8Result?: Phase8Data;
|
phase8Result?: Phase8Data;
|
||||||
@@ -116,4 +116,12 @@ export interface AppState {
|
|||||||
translatedReport?: string; // New field for the English translation
|
translatedReport?: string; // New field for the English translation
|
||||||
language: Language;
|
language: Language;
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
|
projectId?: string; // Current Project ID
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectHistoryItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
}
|
}
|
||||||
@@ -131,6 +131,26 @@ def get_output_lang_instruction(lang):
|
|||||||
|
|
||||||
# --- ORCHESTRATOR PHASES ---
|
# --- ORCHESTRATOR PHASES ---
|
||||||
|
|
||||||
|
def list_history(payload):
|
||||||
|
projects = db_manager.get_all_projects()
|
||||||
|
return {"projects": projects}
|
||||||
|
|
||||||
|
def load_history(payload):
|
||||||
|
project_id = payload.get('projectId')
|
||||||
|
if not project_id:
|
||||||
|
raise ValueError("No projectId provided for loading history.")
|
||||||
|
|
||||||
|
data = db_manager.get_project_data(project_id)
|
||||||
|
if not data:
|
||||||
|
raise ValueError(f"Project {project_id} not found.")
|
||||||
|
return data
|
||||||
|
|
||||||
|
def delete_session(payload):
|
||||||
|
project_id = payload.get('projectId')
|
||||||
|
if not project_id:
|
||||||
|
raise ValueError("No projectId provided for deletion.")
|
||||||
|
return db_manager.delete_project(project_id)
|
||||||
|
|
||||||
def phase1(payload):
|
def phase1(payload):
|
||||||
product_input = payload.get('productInput', '')
|
product_input = payload.get('productInput', '')
|
||||||
lang = payload.get('lang', 'de')
|
lang = payload.get('lang', 'de')
|
||||||
@@ -148,6 +168,20 @@ def phase1(payload):
|
|||||||
analysis_content = product_input
|
analysis_content = product_input
|
||||||
logging.info("Input is raw text. Analyzing directly.")
|
logging.info("Input is raw text. Analyzing directly.")
|
||||||
|
|
||||||
|
# AUTOMATISCHE PROJEKTERSTELLUNG
|
||||||
|
if not project_id:
|
||||||
|
# Generiere Namen aus Input
|
||||||
|
raw_name = product_input.strip()
|
||||||
|
if raw_name.startswith('http'):
|
||||||
|
name = f"Web Analysis: {raw_name[:30]}..."
|
||||||
|
else:
|
||||||
|
name = (raw_name[:30] + "...") if len(raw_name) > 30 else raw_name
|
||||||
|
|
||||||
|
logging.info(f"Creating new project: {name}")
|
||||||
|
new_proj = db_manager.create_project(name)
|
||||||
|
project_id = new_proj['id']
|
||||||
|
logging.info(f"New Project ID: {project_id}")
|
||||||
|
|
||||||
sys_instr = get_system_instruction(lang)
|
sys_instr = get_system_instruction(lang)
|
||||||
lang_instr = get_output_lang_instruction(lang)
|
lang_instr = get_output_lang_instruction(lang)
|
||||||
|
|
||||||
@@ -170,12 +204,16 @@ def phase1(payload):
|
|||||||
try:
|
try:
|
||||||
data = json.loads(response)
|
data = json.loads(response)
|
||||||
db_manager.save_gtm_result(project_id, 'phase1_result', json.dumps(data))
|
db_manager.save_gtm_result(project_id, 'phase1_result', json.dumps(data))
|
||||||
|
|
||||||
|
# WICHTIG: ID zurückgeben, damit Frontend sie speichert
|
||||||
|
data['projectId'] = project_id
|
||||||
return data
|
return data
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
logging.error(f"Failed to decode JSON from Gemini response in phase1: {response}")
|
logging.error(f"Failed to decode JSON from Gemini response in phase1: {response}")
|
||||||
error_response = {
|
error_response = {
|
||||||
"error": "Die Antwort des KI-Modells war kein gültiges JSON. Das passiert manchmal bei hoher Auslastung. Bitte versuchen Sie es in Kürze erneut.",
|
"error": "Die Antwort des KI-Modells war kein gültiges JSON. Das passiert manchmal bei hoher Auslastung. Bitte versuchen Sie es in Kürze erneut.",
|
||||||
"details": response
|
"details": response,
|
||||||
|
"projectId": project_id # Auch bei Fehler ID zurückgeben? Besser nicht, da noch nichts gespeichert.
|
||||||
}
|
}
|
||||||
return error_response
|
return error_response
|
||||||
|
|
||||||
@@ -471,6 +509,9 @@ def main():
|
|||||||
"phase9": phase9,
|
"phase9": phase9,
|
||||||
"translate": translate,
|
"translate": translate,
|
||||||
"image": image,
|
"image": image,
|
||||||
|
"list_history": list_history,
|
||||||
|
"load_history": load_history,
|
||||||
|
"delete_session": delete_session,
|
||||||
}
|
}
|
||||||
|
|
||||||
mode_function = modes.get(args.mode)
|
mode_function = modes.get(args.mode)
|
||||||
|
|||||||
Reference in New Issue
Block a user