feat(gtm-architect): Integrate GTM Architect app with Python backend, DB persistence, and Docker stack

This commit is contained in:
2025-12-31 12:38:28 +00:00
parent 08c5c5537d
commit f88a13f38e
12 changed files with 830 additions and 1070 deletions

View File

@@ -173,6 +173,106 @@ const App: React.FC = () => {
setError(null);
};
// --- IMPORT MARKDOWN ---
const handleImportMarkdown = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target?.result as string;
if (!content) return;
try {
const newAnalysisData: Partial<AnalysisData> = {};
// Helper to parse table from a section
const parseSection = (sectionTitle: string): string[][] => {
const sectionRegex = new RegExp(`## ${sectionTitle}[\\s\\S]*?(?=## |$)`, 'i');
const match = content.match(sectionRegex);
if (!match) return [];
const lines = match[0].split('\n');
const rows: string[][] = [];
let inTable = false;
for (const line of lines) {
if (line.trim().startsWith('|') && !line.includes('---')) {
const cells = line.split('|').map(c => c.trim()).filter(c => c);
if (cells.length > 0) {
if (!inTable) {
// Skip header row if we are just starting (assuming we have default headers or don't need them for raw data)
// Actually, we usually want data rows. The first row in MD table is header.
// Let's rely on standard markdown table structure: Header | Separator | Data
inTable = true;
} else {
rows.push(cells);
}
}
}
}
// Remove the header row if captured (simple heuristic: first row is usually header)
if (rows.length > 0) rows.shift();
return rows;
};
// Mapping MD titles to keys
// Note: flexible matching for DE/EN titles
newAnalysisData.offer = {
summary: [],
headers: t.stepTitles.offer.includes('Schritt') ? ["Produkt/Lösung", "Beschreibung", "Kernfunktionen", "Differenzierung", "Quelle"] : ["Product/Solution", "Description", "Core Features", "Differentiation", "Source"],
rows: parseSection("Schritt 1")
};
if (newAnalysisData.offer.rows.length === 0) newAnalysisData.offer.rows = parseSection("Step 1");
newAnalysisData.targetGroups = {
summary: [],
headers: t.stepTitles.targetGroups.includes('Schritt') ? ["Zielbranche", "Merkmale", "Region", "Quelle"] : ["Target Industry", "Characteristics", "Region", "Source"],
rows: parseSection("Schritt 2")
};
if (newAnalysisData.targetGroups.rows.length === 0) newAnalysisData.targetGroups.rows = parseSection("Step 2");
newAnalysisData.personas = {
summary: [],
headers: [],
rows: parseSection("Schritt 3")
};
if (newAnalysisData.personas.rows.length === 0) newAnalysisData.personas.rows = parseSection("Step 3");
newAnalysisData.painPoints = {
summary: [],
headers: [],
rows: parseSection("Schritt 4")
};
if (newAnalysisData.painPoints.rows.length === 0) newAnalysisData.painPoints.rows = parseSection("Step 4");
newAnalysisData.gains = {
summary: [],
headers: [],
rows: parseSection("Schritt 5")
};
if (newAnalysisData.gains.rows.length === 0) newAnalysisData.gains.rows = parseSection("Step 5");
newAnalysisData.messages = {
summary: [],
headers: [],
rows: parseSection("Schritt 6")
};
if (newAnalysisData.messages.rows.length === 0) newAnalysisData.messages.rows = parseSection("Step 6");
setAnalysisData(newAnalysisData);
setGenerationStep(6); // Jump to end
setProjectName(file.name.replace('.md', ' (Imported)'));
setProjectId(null); // Treat as new project
} catch (err) {
console.error("Parse error", err);
setError("Fehler beim Importieren der Datei.");
}
};
reader.readAsText(file);
};
const handleStartGeneration = useCallback(async () => {
if (!inputData.companyUrl) {
@@ -357,7 +457,14 @@ const App: React.FC = () => {
const step = analysisData[stepKey] as AnalysisStep | undefined;
if (!step) return null;
// Allow manual addition for Offer (Step 1) and Target Groups (Step 2)
const canAdd = ['offer', 'targetGroups'].includes(stepKey);
const canDelete = ['offer', 'targetGroups', 'personas'].includes(stepKey);
const handleManualAdd = (newRow: string[]) => {
const currentRows = step.rows || [];
handleDataChange(stepKey, { ...step, rows: [...currentRows, newRow] });
};
return (
<StepDisplay
@@ -367,8 +474,8 @@ const App: React.FC = () => {
headers={step.headers}
rows={step.rows}
onDataChange={(newRows) => handleDataChange(stepKey, { ...step, rows: newRows })}
canAddRows={false} // Disabled enrich functionality
onEnrichRow={undefined}
canAddRows={canAdd}
onEnrichRow={canAdd ? handleManualAdd : undefined}
isEnriching={false}
canDeleteRows={canDelete}
t={t}
@@ -456,6 +563,17 @@ const App: React.FC = () => {
</p>
<div className="mt-6 flex justify-center gap-3">
<label className="flex items-center px-4 py-2 bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 rounded-lg text-sm font-bold transition-all shadow-sm border border-slate-200 dark:border-slate-700 cursor-pointer">
<FolderOpen className="mr-2 h-4 w-4 text-orange-500" />
{inputData.language === 'de' ? 'MD Laden' : 'Load MD'}
<input
type="file"
accept=".md"
onChange={handleImportMarkdown}
className="hidden"
/>
</label>
<button
onClick={() => setShowHistory(true)}
className="flex items-center px-4 py-2 bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 rounded-lg text-sm font-bold transition-all shadow-sm border border-slate-200 dark:border-slate-700"