feat(gtm-architect): Integrate GTM Architect app with Python backend, DB persistence, and Docker stack
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -7,6 +7,7 @@ const path = require('path');
|
||||
|
||||
const app = express();
|
||||
const PORT = 3002;
|
||||
const VERSION = "1.1.0-DEBUG (Timeout 600s)";
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
@@ -27,14 +28,32 @@ const runPythonScript = (args, res) => {
|
||||
pythonProcess.stderr.on('data', (data) => { pythonError += data.toString(); });
|
||||
|
||||
pythonProcess.on('close', (code) => {
|
||||
console.log(`[${new Date().toISOString()}] Python process exited with code ${code}`);
|
||||
if (pythonOutput.length > 0) {
|
||||
console.log(`[${new Date().toISOString()}] Stdout (first 500 chars): ${pythonOutput.substring(0, 500)}...`);
|
||||
} else {
|
||||
console.log(`[${new Date().toISOString()}] Stdout is empty.`);
|
||||
}
|
||||
if (pythonError.length > 0) {
|
||||
console.log(`[${new Date().toISOString()}] Stderr (first 500 chars): ${pythonError.substring(0, 500)}...`);
|
||||
}
|
||||
|
||||
if (code !== 0) {
|
||||
console.error(`Python error (Code ${code}): ${pythonError}`);
|
||||
return res.status(500).json({ error: 'Backend error', details: pythonError });
|
||||
return res.status(500).json({ error: 'Backend error', details: pythonError, version: VERSION });
|
||||
}
|
||||
try {
|
||||
res.json(JSON.parse(pythonOutput));
|
||||
res.header('X-Server-Timeout', '600000');
|
||||
const responseData = JSON.parse(pythonOutput);
|
||||
// Add version info to the response if it's an object
|
||||
if (typeof responseData === 'object' && responseData !== null) {
|
||||
responseData._backend_version = VERSION;
|
||||
}
|
||||
res.json(responseData);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'Invalid JSON', raw: pythonOutput });
|
||||
console.error(`JSON Parse Error: ${e.message}`);
|
||||
console.error(`Raw Output was: ${pythonOutput}`);
|
||||
res.status(500).json({ error: 'Invalid JSON', raw: pythonOutput, details: e.message, version: VERSION });
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -104,7 +123,12 @@ const initializeDatabase = () => {
|
||||
proc.on('close', (code) => console.log(`[${new Date().toISOString()}] DB init finished (Code ${code})`));
|
||||
};
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`B2B Marketing Assistant API Bridge running on port ${PORT}`);
|
||||
const server = app.listen(PORT, () => {
|
||||
console.log(`B2B Marketing Assistant API Bridge running on port ${PORT} (Version: ${VERSION})`);
|
||||
initializeDatabase();
|
||||
});
|
||||
|
||||
// Set timeout to 10 minutes (600s) to handle long AI generation steps
|
||||
server.setTimeout(600000);
|
||||
server.keepAliveTimeout = 610000;
|
||||
server.headersTimeout = 620000;
|
||||
|
||||
Reference in New Issue
Block a user