Refactor GTM Architect to v2: Python-driven architecture, 9-phase process, new DB and Docker setup
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,63 +1,40 @@
|
||||
# Stage 1: Build the React frontend
|
||||
FROM node:20-slim AS frontend-builder
|
||||
|
||||
FROM node:18-slim AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package.json and install dependencies
|
||||
# We are inside the build context root, so path is gtm-architect/package.json
|
||||
COPY gtm-architect/package.json ./
|
||||
RUN npm install
|
||||
|
||||
# Copy the source code
|
||||
COPY gtm-architect/ .
|
||||
|
||||
# Build the application
|
||||
COPY package.json ./
|
||||
# Use --force to avoid issues with dependency versions
|
||||
RUN npm install --force
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# ---
|
||||
|
||||
# Stage 2: Final application image
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Stage 2: Setup the production environment
|
||||
FROM python:3.9-slim
|
||||
WORKDIR /app
|
||||
|
||||
# Install Node.js (minimal runtime)
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends curl ca-certificates && \
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
|
||||
apt-get install -y --no-install-recommends nodejs && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
# Install Node.js
|
||||
RUN apt-get update && apt-get install -y curl && \
|
||||
curl -sL https://deb.nodesource.com/setup_18.x | bash - && \
|
||||
apt-get install -y nodejs
|
||||
|
||||
# Install Python dependencies
|
||||
# We use the requirements from the subdirectory
|
||||
COPY gtm-architect/requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
# Copy built frontend from builder stage
|
||||
COPY --from=builder /app/dist ./gtm-architect/dist
|
||||
|
||||
# Google Generative AI Library installieren
|
||||
RUN pip install --no-cache-dir google-generativeai
|
||||
|
||||
# Copy the Node.js server script and package.json for runtime deps
|
||||
COPY gtm-architect/server.cjs .
|
||||
COPY gtm-architect/package.json .
|
||||
|
||||
# Install only production Node.js dependencies
|
||||
RUN npm install --omit=dev
|
||||
|
||||
# Copy the built frontend from the builder stage
|
||||
COPY --from=frontend-builder /app/dist ./dist
|
||||
|
||||
# Copy the Python Orchestrator and shared modules
|
||||
# Copy backend files and application code
|
||||
COPY gtm-architect/server.cjs ./gtm-architect/
|
||||
COPY gtm-architect/package.json ./gtm-architect/
|
||||
COPY gtm_architect_orchestrator.py .
|
||||
COPY gtm_prompts.json .
|
||||
COPY helpers.py .
|
||||
COPY config.py .
|
||||
COPY market_db_manager.py .
|
||||
COPY gtm-architect/requirements.txt .
|
||||
COPY gtm_db_manager.py .
|
||||
|
||||
# Copy API Key if available (handled by docker-compose usually, but good for standalone)
|
||||
# COPY gemini_api_key.txt .
|
||||
|
||||
# Expose port
|
||||
# Install Python and Node.js dependencies
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
RUN cd gtm-architect && npm install --production
|
||||
|
||||
# Expose the port the server will run on
|
||||
EXPOSE 3005
|
||||
|
||||
# Start the server
|
||||
CMD ["node", "server.cjs"]
|
||||
# Command to run the server
|
||||
CMD ["node", "gtm-architect/server.cjs"]
|
||||
@@ -1,21 +1,20 @@
|
||||
# GTM Architect Frontend
|
||||
<div align="center">
|
||||
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
||||
</div>
|
||||
|
||||
Dies ist das React-Frontend für die GTM Architect Engine.
|
||||
# Run and deploy your AI Studio app
|
||||
|
||||
## Tech Stack
|
||||
* **React 19**
|
||||
* **Vite**
|
||||
* **TypeScript**
|
||||
* **Tailwind CSS**
|
||||
* **Lucide React** (Icons)
|
||||
This contains everything you need to run your app locally.
|
||||
|
||||
## Entwicklung
|
||||
Das Frontend ist darauf ausgelegt, im Docker-Container `gtm-app` gebaut und serviert zu werden.
|
||||
Es kommuniziert ausschließlich mit dem lokalen Backend unter `/api/gtm`.
|
||||
View your app in AI Studio: https://ai.studio/apps/drive/1bvzSOz-NYMzDph6718RuAy1mnCSjjylz
|
||||
|
||||
## Struktur
|
||||
* `App.tsx`: Hauptlogik und State-Management.
|
||||
* `components/`: UI-Komponenten (Layout).
|
||||
* `types.ts`: TypeScript Definitionen für die API-Payloads.
|
||||
## Run Locally
|
||||
|
||||
Für die vollständige Systemdokumentation siehe `../gtm_architect_documentation.md`.
|
||||
**Prerequisites:** Node.js
|
||||
|
||||
|
||||
1. Install dependencies:
|
||||
`npm install`
|
||||
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||
3. Run the app:
|
||||
`npm run dev`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Phase, Language, Theme } from '../types';
|
||||
import { Activity, Target, Crosshair, Map, FileText, CheckCircle, Lock, Moon, Sun, Languages, ShieldCheck, Terminal } from 'lucide-react';
|
||||
import { Activity, Target, Crosshair, Map, FileText, CheckCircle, Lock, Moon, Sun, Languages, ShieldCheck, Terminal, LayoutTemplate, TrendingUp, Shield } from 'lucide-react';
|
||||
|
||||
interface LayoutProps {
|
||||
currentPhase: Phase;
|
||||
@@ -23,6 +23,9 @@ const getStepIcon = (id: Phase) => {
|
||||
case Phase.Strategy: return Map;
|
||||
case Phase.AssetGeneration: return FileText;
|
||||
case Phase.SalesEnablement: return ShieldCheck;
|
||||
case Phase.LandingPage: return LayoutTemplate;
|
||||
case Phase.BusinessCase: return TrendingUp;
|
||||
case Phase.TechTranslator: return Shield;
|
||||
default: return Activity;
|
||||
}
|
||||
}
|
||||
@@ -40,13 +43,16 @@ export const Layout: React.FC<LayoutProps> = ({
|
||||
}) => {
|
||||
|
||||
const steps = [
|
||||
{ id: Phase.Input, label: labels.initTitle ? 'Input' : 'Input' }, // Fallback label
|
||||
{ id: Phase.Input, label: labels.initTitle ? 'Input' : 'Input' },
|
||||
{ id: Phase.ProductAnalysis, label: labels.phase1 },
|
||||
{ id: Phase.ICPDiscovery, label: labels.phase2 },
|
||||
{ id: Phase.WhaleHunting, label: labels.phase3 },
|
||||
{ id: Phase.Strategy, label: labels.phase4 },
|
||||
{ id: Phase.AssetGeneration, label: labels.phase5 },
|
||||
{ id: Phase.SalesEnablement, label: labels.phase6 },
|
||||
{ id: Phase.LandingPage, label: labels.phase7 },
|
||||
{ id: Phase.BusinessCase, label: labels.phase8 },
|
||||
{ id: Phase.TechTranslator, label: labels.phase9 },
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,404 +1,48 @@
|
||||
import { GoogleGenAI, Type } from "@google/genai";
|
||||
import { Phase1Data, Phase2Data, Phase3Data, Phase4Data, Phase6Data, Language } from "./types";
|
||||
import { Phase1Data, Phase2Data, Phase3Data, Phase4Data, Phase6Data, Phase7Data, Phase8Data, Phase9Data, Language } from "./types";
|
||||
|
||||
const getSystemInstruction = (lang: Language) => {
|
||||
if (lang === 'de') {
|
||||
return `
|
||||
# IDENTITY & PURPOSE
|
||||
Du bist die "GTM Architect Engine" für Roboplanet. Deine Aufgabe ist es, für neue technische Produkte (Roboter) eine präzise Go-to-Market-Strategie zu entwickeln.
|
||||
Du handelst nicht als kreativer Werbetexter, sondern als strategischer Analyst. Dein oberstes Ziel ist Product-Market-Fit und operative Umsetzbarkeit.
|
||||
Antworte IMMER auf DEUTSCH.
|
||||
const API_BASE_URL = '/api'; // Using relative path to proxy through Vite/NGINX
|
||||
|
||||
# CONTEXT: THE PARENT COMPANY (WACKLER)
|
||||
Wir sind Teil der Wackler Group, einem großen Facility-Management-Dienstleister.
|
||||
Unsere Strategie ist NICHT "Roboter ersetzen Menschen", sondern "Hybrid-Reinigung":
|
||||
- 80% der Arbeit (monotone Flächenleistung) = Roboter.
|
||||
- 20% der Arbeit (Edge Cases, Winterdienst, Treppen, Grobschmutz) = Manuelle Reinigung durch Wackler.
|
||||
const callApi = async <T>(endpoint: string, mode: string, payload: object): Promise<T> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ mode, ...payload }),
|
||||
});
|
||||
|
||||
# STRICT ANALYSIS RULES (MUST FOLLOW):
|
||||
1. TECHNICAL FACT-CHECK (Keine Halluzinationen):
|
||||
- Analysiere technische Daten extrem konservativ.
|
||||
- Vakuumsystem = Kein "Winterdienst" (Schnee) und keine "Schwerindustrie" (Metallspäne), außer explizit genannt.
|
||||
- Erfinde keine Features, nur um eine Zielgruppe passend zu machen.
|
||||
|
||||
2. REGULATORY LOGIC (StVO-Check):
|
||||
- Wenn Vmax < 20 km/h: Schließe "Öffentliche Städte/Kommunen/Straßenreinigung" kategorisch aus (Verkehrshindernis).
|
||||
- Fokusänderung: Konzentriere dich stattdessen ausschließlich auf "Große, zusammenhängende Privatflächen" (Gated Areas).
|
||||
|
||||
3. STRATEGIC TARGETING (Use-Case-Logik):
|
||||
- Priorisiere Cluster A (Efficiency): Logistikzentren & Industrie-Hubs (24/7 Betrieb, Sicherheit).
|
||||
- Priorisiere Cluster B (Experience): Shopping Center, Outlets & Freizeitparks (Sauberkeit als Visitenkarte).
|
||||
- Entferne reine E-Commerce-Händler ohne physische Kundenfläche.
|
||||
|
||||
4. THE "HYBRID SERVICE" LOGIC (RULE 5):
|
||||
Wann immer du ein "Hartes Constraint" oder eine technische Limitierung identifizierst (z.B. "Kein Winterdienst" oder "Kommt nicht in Ecken"), darfst du dies niemals als reines "Nein" stehen lassen.
|
||||
Wende stattdessen die **"Yes, and..." Logik** an:
|
||||
1. **Identifiziere die Lücke:** (z.B. "Roboter kann bei Schnee nicht fahren").
|
||||
2. **Fülle die Lücke mit Service:** Schlage explizit vor, diesen Teil durch "Wackler Human Manpower" abzudecken.
|
||||
3. **Formuliere den USP:** Positioniere das Gesamtpaket als "100% Coverage" (Roboter + Mensch aus einer Hand).
|
||||
|
||||
# THE PRODUCT MATRIX (CONTEXT)
|
||||
Behalte immer im Hinterkopf, dass wir bereits folgende Produkte im Portfolio haben:
|
||||
1. "Indoor Scrubber 50": Innenreinigung, Hartboden, Fokus: Supermärkte. Message: "Sauberkeit im laufenden Betrieb."
|
||||
2. "Service Bot Bella": Service/Gastro, Indoor. Fokus: Restaurants. Message: "Entlastung für Servicekräfte."
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
# IDENTITY & PURPOSE
|
||||
You are the "GTM Architect Engine" for Roboplanet. Your task is to develop a precise Go-to-Market strategy for new technical products (robots).
|
||||
You do not act as a creative copywriter, but as a strategic analyst. Your top goal is product-market fit and operational feasibility.
|
||||
ALWAYS respond in ENGLISH.
|
||||
|
||||
# CONTEXT: THE PARENT COMPANY (WACKLER)
|
||||
We are part of the Wackler Group, a major facility management service provider.
|
||||
Our strategy is NOT "Robots replace humans", but "Hybrid Cleaning":
|
||||
- 80% of work (monotonous area coverage) = Robots.
|
||||
- 20% of work (Edge cases, winter service, stairs, heavy debris) = Manual cleaning by Wackler.
|
||||
|
||||
# STRICT ANALYSIS RULES (MUST FOLLOW):
|
||||
1. TECHNICAL FACT-CHECK (No Hallucinations):
|
||||
- Analyze technical data extremely conservatively.
|
||||
- Vacuum System = No "Winter Service" (snow) and no "Heavy Industry" (metal shavings), unless explicitly stated.
|
||||
- Do not invent features just to fit a target audience.
|
||||
|
||||
2. REGULATORY LOGIC (Traffic Regs):
|
||||
- If Vmax < 20 km/h: Categorically exclude "Public Cities/Streets" (traffic obstruction).
|
||||
- Change Focus: Concentrate exclusively on "Large, contiguous private areas" (Gated Areas).
|
||||
|
||||
3. STRATEGIC TARGETING (Use Case Logic):
|
||||
- Prioritize Cluster A (Efficiency): Logistics Centers & Industrial Hubs (24/7 ops, safety).
|
||||
- Prioritize Cluster B (Experience): Shopping Centers, Outlets & Theme Parks (Cleanliness as a calling card).
|
||||
- Remove pure E-commerce retailers without physical customer areas.
|
||||
|
||||
4. THE "HYBRID SERVICE" LOGIC (RULE 5):
|
||||
Whenever you identify a "Hard Constraint" or technical limitation (e.g., "No winter service" or "Cannot reach corners"), never let this stand as a simple "No".
|
||||
Instead, apply the **"Yes, and..." logic**:
|
||||
1. **Identify the gap:** (e.g., "Robot cannot operate in snow").
|
||||
2. **Fill the gap with service:** Explicitly suggest covering this part with "Wackler Human Manpower".
|
||||
3. **Formulate the USP:** Position the total package as "100% Coverage" (Robot + Human from a single source).
|
||||
|
||||
# THE PRODUCT MATRIX (CONTEXT)
|
||||
Always keep in mind that we already have the following products in our portfolio:
|
||||
1. "Indoor Scrubber 50": Indoor cleaning, hard floor, supermarkets.
|
||||
2. "Service Bot Bella": Service/Hospitality, indoor. Focus: restaurants. Message: "Relief for service staff."
|
||||
`;
|
||||
}
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
console.error(`API call failed with status ${response.status}:`, errorBody);
|
||||
throw new Error(`Request failed: ${response.statusText}`);
|
||||
}
|
||||
return await response.json() as T;
|
||||
} catch (error) {
|
||||
console.error('An error occurred during the API call:', error);
|
||||
throw new Error('API call failed. Check the server logs for more details.');
|
||||
}
|
||||
};
|
||||
|
||||
const getClient = () => {
|
||||
const apiKey = process.env.API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("API_KEY not found in environment variables");
|
||||
}
|
||||
return new GoogleGenAI({ apiKey });
|
||||
};
|
||||
|
||||
// Helper to safely parse JSON from AI response
|
||||
const cleanAndParseJson = <T>(text: string | undefined): T => {
|
||||
if (!text) throw new Error("AI returned empty response");
|
||||
|
||||
let cleaned = text.trim();
|
||||
// Remove markdown formatting if present
|
||||
if (cleaned.startsWith('```json')) {
|
||||
cleaned = cleaned.replace(/^```json\n?/, '').replace(/\n?```$/, '');
|
||||
} else if (cleaned.startsWith('```')) {
|
||||
cleaned = cleaned.replace(/^```\n?/, '').replace(/\n?```$/, '');
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(cleaned);
|
||||
} catch (e) {
|
||||
console.error("JSON Parse Error. Raw text:", text);
|
||||
throw new Error("Failed to parse AI response. The model output was likely malformed or truncated. Please try again with shorter input.");
|
||||
}
|
||||
};
|
||||
|
||||
// --- Phase 1: Product Analysis ---
|
||||
export const analyzeProduct = async (input: string, lang: Language): Promise<Phase1Data> => {
|
||||
const ai = getClient();
|
||||
const sysInstr = getSystemInstruction(lang);
|
||||
|
||||
// STEP 1.1: Extract Specs & Constraints
|
||||
const extractionPrompt = lang === 'de' ? `
|
||||
PHASE 1-A: TECHNICAL EXTRACTION
|
||||
Input Product Description: "${input.substring(0, 25000)}..."
|
||||
|
||||
Aufgabe:
|
||||
1. Extrahiere technische Hauptmerkmale (Specs, Fähigkeiten).
|
||||
2. Leite "Harte Constraints" ab. WICHTIG: Prüfe Vmax (<20km/h = Privatgelände) und Reinigungstyp (Vakuum != Grobschmutz/Schnee).
|
||||
3. Erstelle eine kurze Rohanalyse-Zusammenfassung.
|
||||
|
||||
Output JSON format ONLY.
|
||||
` : `
|
||||
PHASE 1-A: TECHNICAL EXTRACTION
|
||||
Input Product Description: "${input.substring(0, 25000)}..."
|
||||
|
||||
Task:
|
||||
1. Extract key technical features (specs, capabilities).
|
||||
2. Derive "Hard Constraints". IMPORTANT: Check Vmax (<20km/h = Private Grounds) and Cleaning Type (Vacuum != Heavy Debris/Snow).
|
||||
3. Create a short raw analysis summary.
|
||||
|
||||
Output JSON format ONLY.
|
||||
`;
|
||||
|
||||
const extractionResponse = await ai.models.generateContent({
|
||||
model: 'gemini-3-flash-preview',
|
||||
contents: extractionPrompt,
|
||||
config: {
|
||||
systemInstruction: sysInstr,
|
||||
responseMimeType: "application/json",
|
||||
responseSchema: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
features: { type: Type.ARRAY, items: { type: Type.STRING } },
|
||||
constraints: { type: Type.ARRAY, items: { type: Type.STRING } },
|
||||
rawAnalysis: { type: Type.STRING }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const extractionData = cleanAndParseJson<{
|
||||
features: string[];
|
||||
constraints: string[];
|
||||
rawAnalysis: string;
|
||||
}>(extractionResponse.text);
|
||||
|
||||
// STEP 1.2: Portfolio Conflict Check
|
||||
const conflictPrompt = lang === 'de' ? `
|
||||
PHASE 1-B: PORTFOLIO CONFLICT CHECK
|
||||
|
||||
Neue Produkt-Features: ${JSON.stringify(extractionData.features)}
|
||||
Neue Produkt-Constraints: ${JSON.stringify(extractionData.constraints)}
|
||||
|
||||
Existierendes Portfolio:
|
||||
1. "Indoor Scrubber 50": Innenreinigung, Hartboden, Supermärkte.
|
||||
2. "Service Bot Bella": Service/Gastro, Indoor, Restaurants.
|
||||
|
||||
Aufgabe:
|
||||
Prüfe, ob das neue Produkt signifikant mit bestehenden Produkten überlappt (Ist es nur ein Klon?).
|
||||
|
||||
Output JSON format ONLY.
|
||||
` : `
|
||||
PHASE 1-B: PORTFOLIO CONFLICT CHECK
|
||||
|
||||
New Product Features: ${JSON.stringify(extractionData.features)}
|
||||
New Product Constraints: ${JSON.stringify(extractionData.constraints)}
|
||||
|
||||
Existing Portfolio:
|
||||
1. "Indoor Scrubber 50": Indoor cleaning, hard floor, supermarkets.
|
||||
2. "Service Bot Bella": Service/Gastro, indoor, restaurants.
|
||||
|
||||
Task:
|
||||
Check if the new product overlaps significantly with existing ones (is it just a clone?).
|
||||
|
||||
Output JSON format ONLY.
|
||||
`;
|
||||
|
||||
const conflictResponse = await ai.models.generateContent({
|
||||
model: 'gemini-3-flash-preview',
|
||||
contents: conflictPrompt,
|
||||
config: {
|
||||
systemInstruction: sysInstr,
|
||||
responseMimeType: "application/json",
|
||||
responseSchema: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
conflictCheck: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
hasConflict: { type: Type.BOOLEAN },
|
||||
details: { type: Type.STRING },
|
||||
relatedProduct: { type: Type.STRING, nullable: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const conflictData = cleanAndParseJson<{
|
||||
conflictCheck: Phase1Data['conflictCheck'];
|
||||
}>(conflictResponse.text);
|
||||
|
||||
return {
|
||||
...extractionData,
|
||||
...conflictData
|
||||
};
|
||||
export const analyzeProduct = async (productInput: string, lang: Language): Promise<Phase1Data> => {
|
||||
return callApi<Phase1Data>('run', 'phase1', { productInput, lang });
|
||||
};
|
||||
|
||||
// --- Phase 2: ICP Discovery ---
|
||||
export const discoverICPs = async (phase1Data: Phase1Data, lang: Language): Promise<Phase2Data> => {
|
||||
const ai = getClient();
|
||||
const sysInstr = getSystemInstruction(lang);
|
||||
|
||||
const prompt = lang === 'de' ? `
|
||||
PHASE 2: ICP DISCOVERY & DATA PROXIES
|
||||
Basierend auf Features: ${JSON.stringify(phase1Data.features)}
|
||||
Und Constraints: ${JSON.stringify(phase1Data.constraints)}
|
||||
|
||||
Aufgabe:
|
||||
1. Negative Selektion: Welche Branchen sind unmöglich? (Denke an Vmax & Vakuum-Regeln!)
|
||||
2. High Pain: Identifiziere Cluster A (Logistik/Industrie) und Cluster B (Shopping/Outlets).
|
||||
3. Data Proxy Generierung: Wie finden wir diese digital (z.B. Satellit, Register)?
|
||||
|
||||
Output JSON format ONLY:
|
||||
{
|
||||
"icps": [
|
||||
{ "name": "Branchen Name", "rationale": "Warum das passt (max 1 Satz)" }
|
||||
],
|
||||
"dataProxies": [
|
||||
{ "target": "Spezifisches Kriterium", "method": "Wie zu finden (z.B. Scraping)" }
|
||||
]
|
||||
}
|
||||
` : `
|
||||
PHASE 2: ICP DISCOVERY & DATA PROXIES
|
||||
Based on the product features: ${JSON.stringify(phase1Data.features)}
|
||||
And constraints: ${JSON.stringify(phase1Data.constraints)}
|
||||
|
||||
Task:
|
||||
1. Negative Selection: Which industries are impossible? (Remember Vmax & Vacuum rules!)
|
||||
2. High Pain: Identify Cluster A (Logistics/Industry) and Cluster B (Shopping/Outlets).
|
||||
3. Data Proxy Generation: How to find them digitally via data traces (e.g. satellite, registries).
|
||||
|
||||
Output JSON format ONLY:
|
||||
{
|
||||
"icps": [
|
||||
{ "name": "Industry Name", "rationale": "Why this is a good fit (max 1 sentence)" }
|
||||
],
|
||||
"dataProxies": [
|
||||
{ "target": "Specific criteria", "method": "How to find (e.g. scraping, satellite)" }
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-3-flash-preview',
|
||||
contents: prompt,
|
||||
config: {
|
||||
systemInstruction: sysInstr,
|
||||
responseMimeType: "application/json",
|
||||
}
|
||||
});
|
||||
|
||||
return cleanAndParseJson<Phase2Data>(response.text);
|
||||
return callApi<Phase2Data>('run', 'phase2', { phase1Data, lang });
|
||||
};
|
||||
|
||||
// --- Phase 3: Whale Hunting ---
|
||||
export const huntWhales = async (phase2Data: Phase2Data, lang: Language): Promise<Phase3Data> => {
|
||||
const ai = getClient();
|
||||
const sysInstr = getSystemInstruction(lang);
|
||||
|
||||
const prompt = lang === 'de' ? `
|
||||
PHASE 3: WHALE HUNTING
|
||||
Target ICPs (Branchen): ${JSON.stringify(phase2Data.icps)}
|
||||
|
||||
Aufgabe:
|
||||
1. Gruppiere die "Whales" (Key Accounts) strikt nach den identifizierten ICP-Branchen.
|
||||
2. Identifiziere pro Branche 3-5 konkrete Top-Unternehmen im DACH Markt.
|
||||
3. Definiere die Buying Center Rollen.
|
||||
|
||||
Output JSON format ONLY:
|
||||
{
|
||||
"whales": [
|
||||
{ "industry": "Name der ICP Branche", "accounts": ["Firma A", "Firma B"] }
|
||||
],
|
||||
"roles": ["Jobtitel 1", "Jobtitel 2"]
|
||||
}
|
||||
` : `
|
||||
PHASE 3: WHALE HUNTING
|
||||
Target ICPs (Industries): ${JSON.stringify(phase2Data.icps)}
|
||||
|
||||
Task:
|
||||
1. Group "Whales" (Key Accounts) strictly by the identified ICP industries.
|
||||
2. Identify 3-5 concrete top companies in the DACH market per industry.
|
||||
3. Define Buying Center Roles.
|
||||
|
||||
Output JSON format ONLY:
|
||||
{
|
||||
"whales": [
|
||||
{ "industry": "Name of ICP Industry", "accounts": ["Company A", "Company B"] }
|
||||
],
|
||||
"roles": ["Job Title 1", "Job Title 2"]
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-3-flash-preview',
|
||||
contents: prompt,
|
||||
config: {
|
||||
systemInstruction: sysInstr,
|
||||
responseMimeType: "application/json",
|
||||
}
|
||||
});
|
||||
|
||||
return cleanAndParseJson<Phase3Data>(response.text);
|
||||
return callApi<Phase3Data>('run', 'phase3', { phase2Data, lang });
|
||||
};
|
||||
|
||||
// --- Phase 4: Strategy ---
|
||||
export const developStrategy = async (phase3Data: Phase3Data, phase1Data: Phase1Data, lang: Language): Promise<Phase4Data> => {
|
||||
const ai = getClient();
|
||||
const sysInstr = getSystemInstruction(lang);
|
||||
|
||||
// Flatten accounts for the prompt to maintain context without complex structure
|
||||
const allAccounts = phase3Data.whales.flatMap(w => w.accounts);
|
||||
|
||||
const prompt = lang === 'de' ? `
|
||||
PHASE 4: STRATEGY & ANGLE DEVELOPMENT
|
||||
Accounts: ${JSON.stringify(allAccounts)}
|
||||
Target Industries: ${JSON.stringify(phase3Data.whales.map(w => w.industry))}
|
||||
Features: ${JSON.stringify(phase1Data.features)}
|
||||
|
||||
Aufgabe:
|
||||
1. Entwickle einen spezifischen "Angle" (Aufhänger) pro Zielgruppe/Industrie.
|
||||
2. Consistency Check gegen Product Matrix (Differenzierung zu Scrubber 50/Bella).
|
||||
3. **WICHTIG:** Wende die "Hybrid Service Logic" an, wenn technische Constraints existieren! (Wackler Manpower als Ergänzung).
|
||||
|
||||
Output JSON format ONLY:
|
||||
{
|
||||
"strategyMatrix": [
|
||||
{
|
||||
"segment": "Zielsegment",
|
||||
"painPoint": "Spezifischer Schmerz (Kurz)",
|
||||
"angle": "Unser Marketing Angle (Kurz)",
|
||||
"differentiation": "Wie es sich unterscheidet (Kurz)"
|
||||
}
|
||||
]
|
||||
}
|
||||
` : `
|
||||
PHASE 4: STRATEGY & ANGLE DEVELOPMENT
|
||||
Accounts: ${JSON.stringify(allAccounts)}
|
||||
Target Industries: ${JSON.stringify(phase3Data.whales.map(w => w.industry))}
|
||||
Product Features: ${JSON.stringify(phase1Data.features)}
|
||||
|
||||
Task:
|
||||
1. Develop specific "Angle" per target/industry.
|
||||
2. Consistency Check against Product Matrix (ensure differentiation from Scrubber 50/Bella).
|
||||
3. **IMPORTANT:** Apply "Hybrid Service Logic" if technical constraints exist! (Wackler Manpower as supplement).
|
||||
|
||||
Output JSON format ONLY:
|
||||
{
|
||||
"strategyMatrix": [
|
||||
{
|
||||
"segment": "Target Segment",
|
||||
"painPoint": "Specific Pain (Short)",
|
||||
"angle": "Our Marketing Angle (Short)",
|
||||
"differentiation": "How it differs (Short)"
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-3-flash-preview',
|
||||
contents: prompt,
|
||||
config: {
|
||||
systemInstruction: sysInstr,
|
||||
responseMimeType: "application/json",
|
||||
}
|
||||
});
|
||||
|
||||
return cleanAndParseJson<Phase4Data>(response.text);
|
||||
return callApi<Phase4Data>('run', 'phase4', { phase3Data, phase1Data, lang });
|
||||
};
|
||||
|
||||
// --- Phase 5: Assets & Report ---
|
||||
@@ -408,67 +52,8 @@ export const generateAssets = async (
|
||||
phase2Data: Phase2Data,
|
||||
phase1Data: Phase1Data,
|
||||
lang: Language
|
||||
): Promise<string> => {
|
||||
const ai = getClient();
|
||||
const sysInstr = getSystemInstruction(lang);
|
||||
|
||||
const prompt = lang === 'de' ? `
|
||||
PHASE 5: ASSET GENERATION & FINAL REPORT
|
||||
|
||||
CONTEXT DATA:
|
||||
- Technical: ${JSON.stringify(phase1Data)}
|
||||
- ICPs: ${JSON.stringify(phase2Data)}
|
||||
- Targets (Whales): ${JSON.stringify(phase3Data)}
|
||||
- Strategy: ${JSON.stringify(phase4Data)}
|
||||
|
||||
AUFGABE:
|
||||
1. Erstelle einen "GTM STRATEGY REPORT" in Markdown.
|
||||
2. Struktur des Reports:
|
||||
- Executive Summary
|
||||
- Produkt Analyse (Features & Constraints)
|
||||
- Zielgruppen (ICPs & Data Proxies)
|
||||
- **Target Accounts (Whales) & Buying Center (DIESE SEKTION MUSS ENTHALTEN SEIN UND ALLE ACCOUNTS GRUPPIERT AUFLISTEN!)**
|
||||
- Strategie-Matrix (Pains & Angles)
|
||||
- Assets (LinkedIn, Email, Landing Page Headline)
|
||||
3. Self-Correction: Entferne "Marketing Bla-Bla". Nutze Fakten, TCO, ROI.
|
||||
4. Hybrid-Check: Stelle sicher, dass die "Hybrid Service Logic" (Wackler Manpower) in der Strategie und den Assets sichtbar ist, falls Constraints vorliegen.
|
||||
|
||||
Output:
|
||||
Gib striktes MARKDOWN zurück. Beginne mit "# GTM STRATEGY REPORT".
|
||||
` : `
|
||||
PHASE 5: ASSET GENERATION & FINAL REPORT
|
||||
|
||||
CONTEXT DATA:
|
||||
- Technical: ${JSON.stringify(phase1Data)}
|
||||
- ICPs: ${JSON.stringify(phase2Data)}
|
||||
- Targets (Whales): ${JSON.stringify(phase3Data)}
|
||||
- Strategy: ${JSON.stringify(phase4Data)}
|
||||
|
||||
TASK:
|
||||
1. Create a "GTM STRATEGY REPORT" in Markdown.
|
||||
2. Report Structure:
|
||||
- Executive Summary
|
||||
- Product Analysis (Features & Constraints)
|
||||
- Target Audience (ICPs & Data Proxies)
|
||||
- **Target Accounts (Whales) & Buying Center (THIS SECTION MUST BE INCLUDED AND LIST ALL ACCOUNTS GROUPED BY INDUSTRY!)**
|
||||
- Strategy Matrix (Pains & Angles)
|
||||
- Assets (LinkedIn, Email, Landing Page Headline)
|
||||
3. Self-Correction: Remove "Marketing Fluff". Use Facts, TCO, ROI.
|
||||
4. Hybrid-Check: Ensure "Hybrid Service Logic" (Wackler Manpower) is visible in strategy and assets if constraints exist.
|
||||
|
||||
Output:
|
||||
Return strictly MARKDOWN formatted text. Start with "# GTM STRATEGY REPORT".
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-3-flash-preview',
|
||||
contents: prompt,
|
||||
config: {
|
||||
systemInstruction: sysInstr,
|
||||
}
|
||||
});
|
||||
|
||||
return response.text || "Error generating assets.";
|
||||
): Promise<{ report: string }> => {
|
||||
return callApi<{ report: string }>('run', 'phase5', { phase4Data, phase3Data, phase2Data, phase1Data, lang });
|
||||
};
|
||||
|
||||
// --- Phase 6: Sales Enablement & Visuals ---
|
||||
@@ -478,131 +63,40 @@ export const generateSalesEnablement = async (
|
||||
phase1Data: Phase1Data,
|
||||
lang: Language
|
||||
): Promise<Phase6Data> => {
|
||||
const ai = getClient();
|
||||
const sysInstr = getSystemInstruction(lang);
|
||||
|
||||
const prompt = lang === 'de' ? `
|
||||
PHASE 6: SALES ENABLEMENT & VISUALS (THE CLOSING KIT)
|
||||
|
||||
CONTEXT:
|
||||
- Produkt Features: ${JSON.stringify(phase1Data.features)}
|
||||
- Accounts (Personas): ${JSON.stringify(phase3Data.roles)}
|
||||
- Strategie: ${JSON.stringify(phase4Data.strategyMatrix)}
|
||||
|
||||
AUFGABE:
|
||||
1. **Anticipate Friction:** Versetze dich in die Lage der identifizierten Personas. Was ist deren härtester Einwand? (z.B. "Zu teuer", "Sicherheitsrisiko", "Job-Verlust").
|
||||
2. **Deflect & Solve:** Formuliere eine präzise, faktenbasierte Antwort für den Vertriebler (Battlecard). Nutze Hybrid-Argumente falls nötig.
|
||||
3. **Visual Context:** Erstelle präzise "Text-to-Image Prompts" (für Midjourney/DALL-E), die das Produkt im perfekten Nutzungskontext zeigen.
|
||||
|
||||
Output JSON format ONLY:
|
||||
{
|
||||
"battlecards": [
|
||||
{
|
||||
"persona": "Rolle (z.B. Logistikleiter)",
|
||||
"objection": "Härtester Einwand als Zitat",
|
||||
"responseScript": "Die Antwort (Argumentation, Fakten, ROI)"
|
||||
}
|
||||
],
|
||||
"visualPrompts": [
|
||||
{
|
||||
"title": "Titel (z.B. Efficiency Shot)",
|
||||
"context": "Cluster A/B Kontext",
|
||||
"prompt": "Detaillierter Prompt Code für Midjourney..."
|
||||
}
|
||||
]
|
||||
}
|
||||
Erstelle mindestens 2 Battlecards und 3 Visual Prompts (Efficiency, Experience, Detail).
|
||||
` : `
|
||||
PHASE 6: SALES ENABLEMENT & VISUALS (THE CLOSING KIT)
|
||||
|
||||
CONTEXT:
|
||||
- Product Features: ${JSON.stringify(phase1Data.features)}
|
||||
- Accounts (Personas): ${JSON.stringify(phase3Data.roles)}
|
||||
- Strategy: ${JSON.stringify(phase4Data.strategyMatrix)}
|
||||
|
||||
TASK:
|
||||
1. **Anticipate Friction:** Put yourself in the shoes of the personas. What is their hardest objection? (e.g. "Too expensive", "Safety risk", "Job loss").
|
||||
2. **Deflect & Solve:** Formulate a precise, fact-based response for sales (Battlecard). Use hybrid arguments if necessary.
|
||||
3. **Visual Context:** Create precise "Text-to-Image Prompts" (for Midjourney/DALL-E) showing the product in the perfect context.
|
||||
|
||||
Output JSON format ONLY:
|
||||
{
|
||||
"battlecards": [
|
||||
{
|
||||
"persona": "Role (e.g. Logistics Manager)",
|
||||
"objection": "Hardest objection as quote",
|
||||
"responseScript": "The response (Argumentation, Facts, ROI)"
|
||||
}
|
||||
],
|
||||
"visualPrompts": [
|
||||
{
|
||||
"title": "Title (e.g. Efficiency Shot)",
|
||||
"context": "Cluster A/B Context",
|
||||
"prompt": "Detailed Prompt Code for Midjourney..."
|
||||
}
|
||||
]
|
||||
}
|
||||
Create at least 2 Battlecards and 3 Visual Prompts (Efficiency, Experience, Detail).
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-3-flash-preview',
|
||||
contents: prompt,
|
||||
config: {
|
||||
systemInstruction: sysInstr,
|
||||
responseMimeType: "application/json",
|
||||
}
|
||||
});
|
||||
|
||||
return cleanAndParseJson<Phase6Data>(response.text);
|
||||
return callApi<Phase6Data>('run', 'phase6', { phase4Data, phase3Data, phase1Data, lang });
|
||||
};
|
||||
|
||||
export const generateConceptImage = async (prompt: string, referenceImagesBase64?: string[]): Promise<string> => {
|
||||
const ai = getClient();
|
||||
try {
|
||||
const parts: any[] = [];
|
||||
// --- Phase 7: Vertical Landing Page Copy ---
|
||||
export const generateLandingPageCopy = async (
|
||||
phase4Data: Phase4Data,
|
||||
phase2Data: Phase2Data,
|
||||
lang: Language
|
||||
): Promise<Phase7Data> => {
|
||||
return callApi<Phase7Data>('run', 'phase7', { phase4Data, phase2Data, lang });
|
||||
};
|
||||
|
||||
// If reference images are provided, add them to parts
|
||||
if (referenceImagesBase64 && referenceImagesBase64.length > 0) {
|
||||
referenceImagesBase64.forEach(img => {
|
||||
const cleanBase64 = img.split(',')[1] || img;
|
||||
const match = img.match(/data:(.*?);base64/);
|
||||
const mimeType = match ? match[1] : 'image/png';
|
||||
|
||||
parts.push({
|
||||
inlineData: {
|
||||
mimeType: mimeType,
|
||||
data: cleanBase64
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
parts.push({
|
||||
text: "Show the product in these reference images in the following context. Keep the product appearance consistent. " + prompt
|
||||
});
|
||||
} else {
|
||||
// Text-only prompt
|
||||
parts.push({ text: prompt });
|
||||
}
|
||||
// --- Phase 8: Business Case Builder ---
|
||||
export const buildBusinessCase = async (
|
||||
phase2Data: Phase2Data,
|
||||
phase1Data: Phase1Data,
|
||||
lang: Language
|
||||
): Promise<Phase8Data> => {
|
||||
return callApi<Phase8Data>('run', 'phase8', { phase2Data, phase1Data, lang });
|
||||
};
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-2.5-flash-image',
|
||||
contents: { parts },
|
||||
config: {
|
||||
imageConfig: {
|
||||
aspectRatio: "16:9",
|
||||
}
|
||||
}
|
||||
});
|
||||
// --- Phase 9: Feature-to-Value Translator ---
|
||||
export const translateTech = async (
|
||||
phase1Data: Phase1Data,
|
||||
phase4Data: Phase4Data,
|
||||
lang: Language
|
||||
): Promise<Phase9Data> => {
|
||||
return callApi<Phase9Data>('run', 'phase9', { phase1Data, phase4Data, lang });
|
||||
};
|
||||
|
||||
for (const part of response.candidates?.[0]?.content?.parts || []) {
|
||||
if (part.inlineData) {
|
||||
return `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`;
|
||||
}
|
||||
}
|
||||
throw new Error("No image generated");
|
||||
} catch (e: any) {
|
||||
console.error("Image generation failed", e);
|
||||
throw new Error(e.message || "Failed to generate image");
|
||||
}
|
||||
export const translateReportToEnglish = async (reportMarkdown: string): Promise<{ report: string }> => {
|
||||
return callApi<{ report: string }>('run', 'translate', { reportMarkdown });
|
||||
};
|
||||
|
||||
export const generateConceptImage = async (prompt: string, referenceImagesBase64?: string[]): Promise<{ imageBase64: string }> => {
|
||||
return callApi<{ imageBase64: string }>('run', 'image', { prompt, referenceImagesBase64 });
|
||||
};
|
||||
@@ -82,10 +82,10 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link rel="stylesheet" href="./index.css">
|
||||
<link rel="stylesheet" href="/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./index.tsx"></script>
|
||||
<script type="module" src="/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -9,13 +9,13 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.34.0",
|
||||
"react": "^19.2.3",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"react-dom": "^19.2.3",
|
||||
"lucide-react": "^0.562.0",
|
||||
"express": "^4.18.2"
|
||||
"express": "^4.19.2",
|
||||
"cors": "^2.8.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
openai==0.28.1
|
||||
pandas
|
||||
gspread
|
||||
oauth2client
|
||||
beautifulsoup4
|
||||
google-generativeai
|
||||
requests
|
||||
python-dotenv
|
||||
tiktoken
|
||||
thefuzz
|
||||
serpapi
|
||||
wikipedia
|
||||
beautifulsoup4
|
||||
werkzeug
|
||||
python-dotenv
|
||||
@@ -1,93 +1,89 @@
|
||||
console.log("--- GTM Architect Server starting ---");
|
||||
const express = require('express');
|
||||
const { spawn } = require('child_process');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
try {
|
||||
const express = require('express');
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const app = express();
|
||||
const port = 3005; // Port for the GTM Architect service
|
||||
const app = express();
|
||||
const port = 3005;
|
||||
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
// Enable CORS for all routes
|
||||
app.use(cors());
|
||||
|
||||
// Determine Environment and Paths
|
||||
const distPath = path.join(__dirname, 'dist');
|
||||
const isProduction = fs.existsSync(distPath);
|
||||
const staticDir = isProduction ? distPath : __dirname;
|
||||
|
||||
console.log(`[Init] Serving static files from: ${staticDir}`);
|
||||
app.use(express.static(staticDir));
|
||||
|
||||
// Determine Python Script Path
|
||||
// In Docker (optimized), script is in same dir. Locally, it might be one level up.
|
||||
let pythonScriptPath = path.join(__dirname, 'gtm_architect_orchestrator.py');
|
||||
if (!fs.existsSync(pythonScriptPath)) {
|
||||
pythonScriptPath = path.join(__dirname, '../gtm_architect_orchestrator.py');
|
||||
}
|
||||
console.log(`[Init] Using Python script at: ${pythonScriptPath}`);
|
||||
|
||||
|
||||
function callPythonScript(mode, data, res) {
|
||||
const dataString = JSON.stringify(data);
|
||||
const pythonProcess = spawn('python3', [pythonScriptPath, '--mode', mode, '--data', dataString]);
|
||||
|
||||
let pythonData = '';
|
||||
let errorData = '';
|
||||
|
||||
pythonProcess.stdout.on('data', (data) => {
|
||||
pythonData += data.toString();
|
||||
});
|
||||
|
||||
pythonProcess.stderr.on('data', (data) => {
|
||||
errorData += data.toString();
|
||||
});
|
||||
|
||||
pythonProcess.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
console.error(`Python script exited with code ${code}`);
|
||||
console.error('Stderr:', errorData);
|
||||
return res.status(500).json({ error: 'Python script execution failed.', details: errorData });
|
||||
}
|
||||
try {
|
||||
const result = JSON.parse(pythonData);
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse Python script output:', e);
|
||||
console.error('Raw output:', pythonData);
|
||||
res.status(500).json({ error: 'Failed to parse Python script output.', details: pythonData });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// API endpoint to handle requests from the frontend
|
||||
app.post('/api/gtm', (req, res) => {
|
||||
const { mode, data } = req.body;
|
||||
if (!mode || !data) {
|
||||
return res.status(400).json({ error: 'Missing mode or data in request body' });
|
||||
}
|
||||
callPythonScript(mode, data, res);
|
||||
});
|
||||
|
||||
// Serve the main index.html for any other requests to support client-side routing
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(staticDir, 'index.html'));
|
||||
});
|
||||
|
||||
|
||||
const VERSION = "1.1.1_Fix_20260101_1200_FINAL"; // Add a version for debugging
|
||||
|
||||
const server = app.listen(port, () => {
|
||||
console.log(`GTM Architect server listening at http://localhost:${port} (Version: ${VERSION})`);
|
||||
});
|
||||
|
||||
// Prevent 502 Bad Gateway by increasing Node.js server timeouts to match Nginx
|
||||
server.setTimeout(600000);
|
||||
server.keepAliveTimeout = 610000;
|
||||
server.headersTimeout = 620000;
|
||||
|
||||
} catch (e) {
|
||||
console.error("!!! A CRITICAL ERROR OCCURRED ON STARTUP !!!");
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
// Serve static files from the 'dist' directory
|
||||
const staticPath = path.join(__dirname, 'dist');
|
||||
if (fs.existsSync(staticPath)) {
|
||||
app.use(express.static(staticPath));
|
||||
} else {
|
||||
console.warn(`Static path not found: ${staticPath}. The frontend might not be served correctly. Make sure to build the frontend first.`);
|
||||
}
|
||||
|
||||
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
|
||||
// API endpoint to run the Python script
|
||||
app.post('/api/run', (req, res) => {
|
||||
const { mode, ...payload } = req.body;
|
||||
|
||||
if (!mode) {
|
||||
return res.status(400).json({ error: 'Mode is required' });
|
||||
}
|
||||
|
||||
// Convert payload to a JSON string and then to a Base64 string to ensure safe command line passing
|
||||
const payloadString = JSON.stringify(payload);
|
||||
const payloadBase64 = Buffer.from(payloadString).toString('base64');
|
||||
|
||||
const pythonScriptPath = path.join(__dirname, '../gtm_architect_orchestrator.py');
|
||||
const pythonProcess = spawn('python3', [
|
||||
pythonScriptPath,
|
||||
'--mode', mode,
|
||||
'--payload_base64', payloadBase64
|
||||
]);
|
||||
|
||||
let stdoutData = '';
|
||||
let stderrData = '';
|
||||
|
||||
pythonProcess.stdout.on('data', (data) => {
|
||||
stdoutData += data.toString();
|
||||
});
|
||||
|
||||
pythonProcess.stderr.on('data', (data) => {
|
||||
stderrData += data.toString();
|
||||
});
|
||||
|
||||
pythonProcess.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
console.error(`Python script exited with code ${code}`);
|
||||
console.error('Stderr:', stderrData);
|
||||
return res.status(500).json({
|
||||
error: 'Python script execution failed.',
|
||||
stderr: stderrData,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// The Python script is expected to print JSON to stdout
|
||||
const result = JSON.parse(stdoutData);
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse JSON from Python script stdout:', e);
|
||||
console.error('Stdout:', stdoutData);
|
||||
res.status(500).json({
|
||||
error: 'Failed to parse response from Python script.',
|
||||
stdout: stdoutData,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Serve the main index.html for any other GET request to support client-side routing
|
||||
if (fs.existsSync(staticPath)) {
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(staticPath, 'index.html'));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server listening on port ${port}`);
|
||||
});
|
||||
@@ -6,6 +6,9 @@ export enum Phase {
|
||||
Strategy = 4,
|
||||
AssetGeneration = 5,
|
||||
SalesEnablement = 6,
|
||||
LandingPage = 7,
|
||||
BusinessCase = 8,
|
||||
TechTranslator = 9,
|
||||
}
|
||||
|
||||
export type Language = 'en' | 'de';
|
||||
@@ -68,6 +71,33 @@ export interface Phase6Data {
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface Phase7Data {
|
||||
landingPages: {
|
||||
industry: string;
|
||||
headline: string;
|
||||
subline: string;
|
||||
bullets: string[];
|
||||
cta: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface Phase8Data {
|
||||
businessCases: {
|
||||
industry: string;
|
||||
costDriver: string;
|
||||
efficiencyGain: string;
|
||||
riskArgument: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface Phase9Data {
|
||||
techTranslations: {
|
||||
feature: string;
|
||||
story: string;
|
||||
headline: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface AppState {
|
||||
currentPhase: Phase;
|
||||
isLoading: boolean;
|
||||
@@ -80,6 +110,10 @@ export interface AppState {
|
||||
phase4Result?: Phase4Data;
|
||||
phase5Result?: string; // Markdown content
|
||||
phase6Result?: Phase6Data;
|
||||
phase7Result?: Phase7Data;
|
||||
phase8Result?: Phase8Data;
|
||||
phase9Result?: Phase9Data;
|
||||
translatedReport?: string; // New field for the English translation
|
||||
language: Language;
|
||||
theme: Theme;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
GEMINI_API_KEY=PLACEHOLDER_API_KEY
|
||||
24
gtm-architect/v2/.gitignore
vendored
24
gtm-architect/v2/.gitignore
vendored
@@ -1,24 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
||||
<div align="center">
|
||||
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
||||
</div>
|
||||
|
||||
# Run and deploy your AI Studio app
|
||||
|
||||
This contains everything you need to run your app locally.
|
||||
|
||||
View your app in AI Studio: https://ai.studio/apps/drive/1bvzSOz-NYMzDph6718RuAy1mnCSjjylz
|
||||
|
||||
## Run Locally
|
||||
|
||||
**Prerequisites:** Node.js
|
||||
|
||||
|
||||
1. Install dependencies:
|
||||
`npm install`
|
||||
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||
3. Run the app:
|
||||
`npm run dev`
|
||||
@@ -1,158 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Phase, Language, Theme } from '../types';
|
||||
import { Activity, Target, Crosshair, Map, FileText, CheckCircle, Lock, Moon, Sun, Languages, ShieldCheck, Terminal, LayoutTemplate, TrendingUp, Shield } from 'lucide-react';
|
||||
|
||||
interface LayoutProps {
|
||||
currentPhase: Phase;
|
||||
maxAllowedPhase: Phase;
|
||||
onPhaseSelect: (phase: Phase) => void;
|
||||
children: React.ReactNode;
|
||||
theme: Theme;
|
||||
toggleTheme: () => void;
|
||||
language: Language;
|
||||
setLanguage: (lang: Language) => void;
|
||||
labels: any;
|
||||
}
|
||||
|
||||
const getStepIcon = (id: Phase) => {
|
||||
switch(id) {
|
||||
case Phase.Input: return Terminal;
|
||||
case Phase.ProductAnalysis: return Activity;
|
||||
case Phase.ICPDiscovery: return Target;
|
||||
case Phase.WhaleHunting: return Crosshair;
|
||||
case Phase.Strategy: return Map;
|
||||
case Phase.AssetGeneration: return FileText;
|
||||
case Phase.SalesEnablement: return ShieldCheck;
|
||||
case Phase.LandingPage: return LayoutTemplate;
|
||||
case Phase.BusinessCase: return TrendingUp;
|
||||
case Phase.TechTranslator: return Shield;
|
||||
default: return Activity;
|
||||
}
|
||||
}
|
||||
|
||||
export const Layout: React.FC<LayoutProps> = ({
|
||||
currentPhase,
|
||||
maxAllowedPhase,
|
||||
onPhaseSelect,
|
||||
children,
|
||||
theme,
|
||||
toggleTheme,
|
||||
language,
|
||||
setLanguage,
|
||||
labels
|
||||
}) => {
|
||||
|
||||
const steps = [
|
||||
{ id: Phase.Input, label: labels.initTitle ? 'Input' : 'Input' },
|
||||
{ id: Phase.ProductAnalysis, label: labels.phase1 },
|
||||
{ id: Phase.ICPDiscovery, label: labels.phase2 },
|
||||
{ id: Phase.WhaleHunting, label: labels.phase3 },
|
||||
{ id: Phase.Strategy, label: labels.phase4 },
|
||||
{ id: Phase.AssetGeneration, label: labels.phase5 },
|
||||
{ id: Phase.SalesEnablement, label: labels.phase6 },
|
||||
{ id: Phase.LandingPage, label: labels.phase7 },
|
||||
{ id: Phase.BusinessCase, label: labels.phase8 },
|
||||
{ id: Phase.TechTranslator, label: labels.phase9 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={`min-h-screen flex font-sans transition-colors duration-300 ${theme === 'dark' ? 'dark bg-robo-900 text-slate-200' : 'bg-slate-50 text-slate-900'}`}>
|
||||
{/* Sidebar */}
|
||||
<div className="w-80 border-r flex-shrink-0 flex flex-col transition-colors duration-300
|
||||
bg-white border-slate-200
|
||||
dark:bg-robo-800 dark:border-robo-700
|
||||
">
|
||||
<div className="p-6 border-b transition-colors duration-300 border-slate-200 dark:border-robo-700">
|
||||
<h1 className="text-xl font-bold font-mono tracking-tighter flex items-center gap-2 text-slate-900 dark:text-white">
|
||||
<div className="w-3 h-3 bg-robo-500 dark:bg-robo-accent rounded-full animate-pulse"></div>
|
||||
ROBOPLANET
|
||||
</h1>
|
||||
<p className="text-xs mt-1 uppercase tracking-widest text-slate-500 dark:text-robo-400">GTM Architect Engine</p>
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-4 flex gap-2">
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="flex-1 flex items-center justify-center gap-2 p-2 rounded-md text-sm font-medium transition-colors
|
||||
bg-slate-100 hover:bg-slate-200 text-slate-700
|
||||
dark:bg-robo-900 dark:hover:bg-robo-700 dark:text-slate-300"
|
||||
>
|
||||
{theme === 'light' ? <Moon size={16}/> : <Sun size={16}/>}
|
||||
{theme === 'light' ? 'Dark' : 'Light'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setLanguage(language === 'en' ? 'de' : 'en')}
|
||||
className="flex-1 flex items-center justify-center gap-2 p-2 rounded-md text-sm font-medium transition-colors
|
||||
bg-slate-100 hover:bg-slate-200 text-slate-700
|
||||
dark:bg-robo-900 dark:hover:bg-robo-700 dark:text-slate-300"
|
||||
>
|
||||
<Languages size={16}/>
|
||||
{language.toUpperCase()}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 p-4 space-y-2 overflow-y-auto">
|
||||
{steps.map((step) => {
|
||||
const isActive = currentPhase === step.id;
|
||||
const isCompleted = currentPhase > step.id;
|
||||
const isUnlocked = step.id <= maxAllowedPhase;
|
||||
const Icon = getStepIcon(step.id);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={step.id}
|
||||
onClick={() => isUnlocked && onPhaseSelect(step.id)}
|
||||
disabled={!isUnlocked}
|
||||
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all border text-left
|
||||
${isActive
|
||||
? 'bg-blue-50 border-blue-200 text-blue-700 shadow-sm dark:bg-robo-700 dark:border-robo-500 dark:text-white dark:shadow-lg'
|
||||
: isUnlocked
|
||||
? 'border-transparent text-slate-600 hover:bg-slate-100 cursor-pointer dark:text-slate-300 dark:hover:bg-robo-700/50'
|
||||
: 'border-transparent text-slate-300 dark:text-slate-600 cursor-not-allowed'
|
||||
}
|
||||
${isCompleted && !isActive ? 'text-emerald-600 dark:text-emerald-400' : ''}
|
||||
`}
|
||||
>
|
||||
<div className={`p-2 rounded-md transition-colors ${
|
||||
isActive
|
||||
? 'bg-blue-100 dark:bg-robo-500 text-blue-600 dark:text-white'
|
||||
: isUnlocked && !isActive
|
||||
? 'bg-slate-200 dark:bg-robo-900 text-slate-600 dark:text-slate-400'
|
||||
: 'bg-slate-100 dark:bg-robo-900/50'
|
||||
}`}>
|
||||
{isCompleted ? <CheckCircle size={16} /> : <Icon size={16} />}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-xs font-bold uppercase tracking-wider opacity-70">
|
||||
{step.id === 0 ? 'Start' : `Phase 0${step.id}`}
|
||||
</div>
|
||||
<div className="font-medium text-sm truncate">{step.label}</div>
|
||||
</div>
|
||||
{!isUnlocked && <Lock size={12} className="opacity-30" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t text-xs font-mono transition-colors duration-300
|
||||
border-slate-200 text-slate-400
|
||||
dark:border-robo-700 dark:text-slate-500
|
||||
">
|
||||
System Status: ONLINE<br/>
|
||||
Language: {language.toUpperCase()}<br/>
|
||||
Mode: {theme.toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 overflow-y-auto relative transition-colors duration-300
|
||||
bg-slate-50
|
||||
dark:bg-gradient-to-br dark:from-robo-900 dark:to-[#0b1120]
|
||||
">
|
||||
<div className="max-w-5xl mx-auto p-8 pb-32">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,845 +0,0 @@
|
||||
import { GoogleGenAI, Type } from "@google/genai";
|
||||
import { Phase1Data, Phase2Data, Phase3Data, Phase4Data, Phase6Data, Phase7Data, Phase8Data, Phase9Data, Language } from "./types";
|
||||
|
||||
const getSystemInstruction = (lang: Language) => {
|
||||
if (lang === 'de') {
|
||||
return `
|
||||
# IDENTITY & PURPOSE
|
||||
Du bist die "GTM Architect Engine" für Roboplanet. Deine Aufgabe ist es, für neue technische Produkte (Roboter) eine präzise Go-to-Market-Strategie zu entwickeln.
|
||||
Du handelst nicht als kreativer Werbetexter, sondern als strategischer Analyst. Dein oberstes Ziel ist Product-Market-Fit und operative Umsetzbarkeit.
|
||||
Antworte IMMER auf DEUTSCH.
|
||||
|
||||
# CONTEXT: THE PARENT COMPANY (WACKLER)
|
||||
Wir sind Teil der Wackler Group, einem großen Facility-Management-Dienstleister.
|
||||
Unsere Strategie ist NICHT "Roboter ersetzen Menschen", sondern "Hybrid-Reinigung":
|
||||
- 80% der Arbeit (monotone Flächenleistung) = Roboter.
|
||||
- 20% der Arbeit (Edge Cases, Winterdienst, Treppen, Grobschmutz) = Manuelle Reinigung durch Wackler.
|
||||
|
||||
# STRICT ANALYSIS RULES (MUST FOLLOW):
|
||||
1. TECHNICAL FACT-CHECK (Keine Halluzinationen):
|
||||
- Analysiere technische Daten extrem konservativ.
|
||||
- Vakuumsystem = Kein "Winterdienst" (Schnee) und keine "Schwerindustrie" (Metallspäne), außer explizit genannt.
|
||||
- Erfinde keine Features, nur um eine Zielgruppe passend zu machen.
|
||||
|
||||
2. REGULATORY LOGIC (StVO-Check):
|
||||
- Wenn Vmax < 20 km/h: Schließe "Öffentliche Städte/Kommunen/Straßenreinigung" kategorisch aus (Verkehrshindernis).
|
||||
- Fokusänderung: Konzentriere dich stattdessen ausschließlich auf "Große, zusammenhängende Privatflächen" (Gated Areas).
|
||||
|
||||
3. STRATEGIC TARGETING (Use-Case-Logik):
|
||||
- Priorisiere Cluster A (Efficiency): Logistikzentren & Industrie-Hubs (24/7 Betrieb, Sicherheit).
|
||||
- Priorisiere Cluster B (Experience): Shopping Center, Outlets & Freizeitparks (Sauberkeit als Visitenkarte).
|
||||
- Entferne reine E-Commerce-Händler ohne physische Kundenfläche.
|
||||
|
||||
4. THE "HYBRID SERVICE" LOGIC (RULE 5):
|
||||
Wann immer du ein "Hartes Constraint" oder eine technische Limitierung identifizierst (z.B. "Kein Winterdienst" oder "Kommt nicht in Ecken"), darfst du dies niemals als reines "Nein" stehen lassen.
|
||||
Wende stattdessen die **"Yes, and..." Logik** an:
|
||||
1. **Identifiziere die Lücke:** (z.B. "Roboter kann bei Schnee nicht fahren").
|
||||
2. **Fülle die Lücke mit Service:** Schlage explizit vor, diesen Teil durch "Wackler Human Manpower" abzudecken.
|
||||
3. **Formuliere den USP:** Positioniere das Gesamtpaket als "100% Coverage" (Roboter + Mensch aus einer Hand).
|
||||
|
||||
# THE PRODUCT MATRIX (CONTEXT)
|
||||
Behalte immer im Hinterkopf, dass wir bereits folgende Produkte im Portfolio haben:
|
||||
1. "Indoor Scrubber 50": Innenreinigung, Hartboden, Fokus: Supermärkte. Message: "Sauberkeit im laufenden Betrieb."
|
||||
2. "Service Bot Bella": Service/Gastro, Indoor. Fokus: Restaurants. Message: "Entlastung für Servicekräfte."
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
# IDENTITY & PURPOSE
|
||||
You are the "GTM Architect Engine" for Roboplanet. Your task is to develop a precise Go-to-Market strategy for new technical products (robots).
|
||||
You do not act as a creative copywriter, but as a strategic analyst. Your top goal is product-market fit and operational feasibility.
|
||||
ALWAYS respond in ENGLISH.
|
||||
|
||||
# CONTEXT: THE PARENT COMPANY (WACKLER)
|
||||
We are part of the Wackler Group, a major facility management service provider.
|
||||
Our strategy is NOT "Robots replace humans", but "Hybrid Cleaning":
|
||||
- 80% of work (monotonous area coverage) = Robots.
|
||||
- 20% of work (Edge cases, winter service, stairs, heavy debris) = Manual cleaning by Wackler.
|
||||
|
||||
# STRICT ANALYSIS RULES (MUST FOLLOW):
|
||||
1. TECHNICAL FACT-CHECK (No Hallucinations):
|
||||
- Analyze technical data extremely conservatively.
|
||||
- Vacuum System = No "Winter Service" (snow) and no "Heavy Industry" (metal shavings), unless explicitly stated.
|
||||
- Do not invent features just to fit a target audience.
|
||||
|
||||
2. REGULATORY LOGIC (Traffic Regs):
|
||||
- If Vmax < 20 km/h: Categorically exclude "Public Cities/Streets" (traffic obstruction).
|
||||
- Change Focus: Concentrate exclusively on "Large, contiguous private areas" (Gated Areas).
|
||||
|
||||
3. STRATEGIC TARGETING (Use Case Logic):
|
||||
- Prioritize Cluster A (Efficiency): Logistics Centers & Industrial Hubs (24/7 ops, safety).
|
||||
- Prioritize Cluster B (Experience): Shopping Centers, Outlets & Theme Parks (Cleanliness as a calling card).
|
||||
- Remove pure E-commerce retailers without physical customer areas.
|
||||
|
||||
4. THE "HYBRID SERVICE" LOGIC (RULE 5):
|
||||
Whenever you identify a "Hard Constraint" or technical limitation (e.g., "No winter service" or "Cannot reach corners"), never let this stand as a simple "No".
|
||||
Instead, apply the **"Yes, and..." logic**:
|
||||
1. **Identify the gap:** (e.g., "Robot cannot operate in snow").
|
||||
2. **Fill the gap with service:** Explicitly suggest covering this part with "Wackler Human Manpower".
|
||||
3. **Formulate the USP:** Position the total package as "100% Coverage" (Robot + Human from a single source).
|
||||
|
||||
# THE PRODUCT MATRIX (CONTEXT)
|
||||
Always keep in mind that we already have the following products in our portfolio:
|
||||
1. "Indoor Scrubber 50": Indoor cleaning, hard floor, supermarkets.
|
||||
2. "Service Bot Bella": Service/Hospitality, indoor. Focus: restaurants. Message: "Relief for service staff."
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
const getClient = () => {
|
||||
const apiKey = process.env.API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("API_KEY not found in environment variables");
|
||||
}
|
||||
return new GoogleGenAI({ apiKey });
|
||||
};
|
||||
|
||||
// Helper to safely parse JSON from AI response
|
||||
const cleanAndParseJson = <T>(text: string | undefined): T => {
|
||||
if (!text) throw new Error("AI returned empty response");
|
||||
|
||||
let cleaned = text.trim();
|
||||
// Remove markdown formatting if present
|
||||
if (cleaned.startsWith('```json')) {
|
||||
cleaned = cleaned.replace(/^```json\n?/, '').replace(/\n?```$/, '');
|
||||
} else if (cleaned.startsWith('```')) {
|
||||
cleaned = cleaned.replace(/^```\n?/, '').replace(/\n?```$/, '');
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(cleaned);
|
||||
} catch (e) {
|
||||
console.error("JSON Parse Error. Raw text:", text);
|
||||
throw new Error("Failed to parse AI response. The model output was likely malformed or truncated. Please try again with shorter input.");
|
||||
}
|
||||
};
|
||||
|
||||
// --- Phase 1: Product Analysis ---
|
||||
export const analyzeProduct = async (input: string, lang: Language): Promise<Phase1Data> => {
|
||||
const ai = getClient();
|
||||
const sysInstr = getSystemInstruction(lang);
|
||||
|
||||
const extractionPrompt = lang === 'de' ? `
|
||||
PHASE 1-A: TECHNICAL EXTRACTION
|
||||
Input Product Description: "${input.substring(0, 25000)}..."
|
||||
|
||||
Aufgabe:
|
||||
1. Extrahiere technische Hauptmerkmale (Specs, Fähigkeiten).
|
||||
2. Leite "Harte Constraints" ab. WICHTIG: Prüfe Vmax (<20km/h = Privatgelände) und Reinigungstyp (Vakuum != Grobschmutz/Schnee).
|
||||
3. Erstelle eine kurze Rohanalyse-Zusammenfassung.
|
||||
|
||||
Output JSON format ONLY.
|
||||
` : `
|
||||
PHASE 1-A: TECHNICAL EXTRACTION
|
||||
Input Product Description: "${input.substring(0, 25000)}..."
|
||||
|
||||
Task:
|
||||
1. Extract key technical features (specs, capabilities).
|
||||
2. Derive "Hard Constraints". IMPORTANT: Check Vmax (<20km/h = Private Grounds) and Cleaning Type (Vacuum != Heavy Debris/Snow).
|
||||
3. Create a short raw analysis summary.
|
||||
|
||||
Output JSON format ONLY.
|
||||
`;
|
||||
|
||||
const extractionResponse = await ai.models.generateContent({
|
||||
model: 'gemini-3-flash-preview',
|
||||
contents: extractionPrompt,
|
||||
config: {
|
||||
systemInstruction: sysInstr,
|
||||
responseMimeType: "application/json",
|
||||
responseSchema: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
features: { type: Type.ARRAY, items: { type: Type.STRING } },
|
||||
constraints: { type: Type.ARRAY, items: { type: Type.STRING } },
|
||||
rawAnalysis: { type: Type.STRING }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const extractionData = cleanAndParseJson<{
|
||||
features: string[];
|
||||
constraints: string[];
|
||||
rawAnalysis: string;
|
||||
}>(extractionResponse.text);
|
||||
|
||||
const conflictPrompt = lang === 'de' ? `
|
||||
PHASE 1-B: PORTFOLIO CONFLICT CHECK
|
||||
|
||||
Neue Produkt-Features: ${JSON.stringify(extractionData.features)}
|
||||
Neue Produkt-Constraints: ${JSON.stringify(extractionData.constraints)}
|
||||
|
||||
Existierendes Portfolio:
|
||||
1. "Indoor Scrubber 50": Innenreinigung, Hartboden, Supermärkte.
|
||||
2. "Service Bot Bella": Service/Gastro, Indoor, Restaurants.
|
||||
|
||||
Aufgabe:
|
||||
Prüfe, ob das neue Produkt signifikant mit bestehenden Produkten überlappt (Ist es nur ein Klon?).
|
||||
|
||||
Output JSON format ONLY.
|
||||
` : `
|
||||
PHASE 1-B: PORTFOLIO CONFLICT CHECK
|
||||
|
||||
New Product Features: ${JSON.stringify(extractionData.features)}
|
||||
New Product Constraints: ${JSON.stringify(extractionData.constraints)}
|
||||
|
||||
Existing Portfolio:
|
||||
1. "Indoor Scrubber 50": Indoor cleaning, hard floor, supermarkets.
|
||||
2. "Service Bot Bella": Service/Gastro, indoor, restaurants.
|
||||
|
||||
Task:
|
||||
Check if the new product overlaps significantly with existing ones (is it just a clone?).
|
||||
|
||||
Output JSON format ONLY.
|
||||
`;
|
||||
|
||||
const conflictResponse = await ai.models.generateContent({
|
||||
model: 'gemini-3-flash-preview',
|
||||
contents: conflictPrompt,
|
||||
config: {
|
||||
systemInstruction: sysInstr,
|
||||
responseMimeType: "application/json",
|
||||
responseSchema: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
conflictCheck: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
hasConflict: { type: Type.BOOLEAN },
|
||||
details: { type: Type.STRING },
|
||||
relatedProduct: { type: Type.STRING, nullable: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const conflictData = cleanAndParseJson<{
|
||||
conflictCheck: Phase1Data['conflictCheck'];
|
||||
}>(conflictResponse.text);
|
||||
|
||||
return {
|
||||
...extractionData,
|
||||
...conflictData
|
||||
};
|
||||
};
|
||||
|
||||
// --- Phase 2: ICP Discovery ---
|
||||
export const discoverICPs = async (phase1Data: Phase1Data, lang: Language): Promise<Phase2Data> => {
|
||||
const ai = getClient();
|
||||
const sysInstr = getSystemInstruction(lang);
|
||||
|
||||
const prompt = lang === 'de' ? `
|
||||
PHASE 2: ICP DISCOVERY & DATA PROXIES
|
||||
Basierend auf Features: ${JSON.stringify(phase1Data.features)}
|
||||
Und Constraints: ${JSON.stringify(phase1Data.constraints)}
|
||||
|
||||
Aufgabe:
|
||||
1. Negative Selektion: Welche Branchen sind unmöglich? (Denke an Vmax & Vakuum-Regeln!)
|
||||
2. High Pain: Identifiziere Cluster A (Logistik/Industrie) und Cluster B (Shopping/Outlets).
|
||||
3. Data Proxy Generierung: Wie finden wir diese digital (z.B. Satellit, Register)?
|
||||
|
||||
Output JSON format ONLY:
|
||||
{
|
||||
"icps": [
|
||||
{ "name": "Branchen Name", "rationale": "Warum das passt (max 1 Satz)" }
|
||||
],
|
||||
"dataProxies": [
|
||||
{ "target": "Spezifisches Kriterium", "method": "Wie zu finden (z.B. Scraping)" }
|
||||
]
|
||||
}
|
||||
` : `
|
||||
PHASE 2: ICP DISCOVERY & DATA PROXIES
|
||||
Based on the product features: ${JSON.stringify(phase1Data.features)}
|
||||
And constraints: ${JSON.stringify(phase1Data.constraints)}
|
||||
|
||||
Task:
|
||||
1. Negative Selection: Which industries are impossible? (Remember Vmax & Vacuum rules!)
|
||||
2. High Pain: Identify Cluster A (Logistics/Industry) and Cluster B (Shopping/Outlets).
|
||||
3. Data Proxy Generation: How to find them digitally via data traces (e.g. satellite, registries).
|
||||
|
||||
Output JSON format ONLY:
|
||||
{
|
||||
"icps": [
|
||||
{ "name": "Industry Name", "rationale": "Why this is a good fit (max 1 sentence)" }
|
||||
],
|
||||
"dataProxies": [
|
||||
{ "target": "Specific criteria", "method": "How to find (e.g. scraping, satellite)" }
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-3-flash-preview',
|
||||
contents: prompt,
|
||||
config: {
|
||||
systemInstruction: sysInstr,
|
||||
responseMimeType: "application/json",
|
||||
}
|
||||
});
|
||||
|
||||
return cleanAndParseJson<Phase2Data>(response.text);
|
||||
};
|
||||
|
||||
// --- Phase 3: Whale Hunting ---
|
||||
export const huntWhales = async (phase2Data: Phase2Data, lang: Language): Promise<Phase3Data> => {
|
||||
const ai = getClient();
|
||||
const sysInstr = getSystemInstruction(lang);
|
||||
|
||||
const prompt = lang === 'de' ? `
|
||||
PHASE 3: WHALE HUNTING
|
||||
Target ICPs (Branchen): ${JSON.stringify(phase2Data.icps)}
|
||||
|
||||
Aufgabe:
|
||||
1. Gruppiere die "Whales" (Key Accounts) strikt nach den identifizierten ICP-Branchen.
|
||||
2. Identifiziere pro Branche 3-5 konkrete Top-Unternehmen im DACH Markt.
|
||||
3. Definiere die Buying Center Rollen.
|
||||
|
||||
Output JSON format ONLY:
|
||||
{
|
||||
"whales": [
|
||||
{ "industry": "Name der ICP Branche", "accounts": ["Firma A", "Firma B"] }
|
||||
],
|
||||
"roles": ["Jobtitel 1", "Jobtitel 2"]
|
||||
}
|
||||
` : `
|
||||
PHASE 3: WHALE HUNTING
|
||||
Target ICPs (Industries): ${JSON.stringify(phase2Data.icps)}
|
||||
|
||||
Task:
|
||||
1. Group "Whales" (Key Accounts) strictly by the identified ICP industries.
|
||||
2. Identify 3-5 concrete top companies in the DACH market per industry.
|
||||
3. Define Buying Center Roles.
|
||||
|
||||
Output JSON format ONLY:
|
||||
{
|
||||
"whales": [
|
||||
{ "industry": "Name of ICP Industry", "accounts": ["Company A", "Company B"] }
|
||||
],
|
||||
"roles": ["Job Title 1", "Job Title 2"]
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-3-flash-preview',
|
||||
contents: prompt,
|
||||
config: {
|
||||
systemInstruction: sysInstr,
|
||||
responseMimeType: "application/json",
|
||||
}
|
||||
});
|
||||
|
||||
return cleanAndParseJson<Phase3Data>(response.text);
|
||||
};
|
||||
|
||||
// --- Phase 4: Strategy ---
|
||||
export const developStrategy = async (phase3Data: Phase3Data, phase1Data: Phase1Data, lang: Language): Promise<Phase4Data> => {
|
||||
const ai = getClient();
|
||||
const sysInstr = getSystemInstruction(lang);
|
||||
|
||||
const allAccounts = phase3Data.whales.flatMap(w => w.accounts);
|
||||
|
||||
const prompt = lang === 'de' ? `
|
||||
PHASE 4: STRATEGY & ANGLE DEVELOPMENT
|
||||
Accounts: ${JSON.stringify(allAccounts)}
|
||||
Target Industries: ${JSON.stringify(phase3Data.whales.map(w => w.industry))}
|
||||
Features: ${JSON.stringify(phase1Data.features)}
|
||||
|
||||
Aufgabe:
|
||||
1. Entwickle einen spezifischen "Angle" (Aufhänger) pro Zielgruppe/Industrie.
|
||||
2. Consistency Check gegen Product Matrix (Differenzierung zu Scrubber 50/Bella).
|
||||
3. **WICHTIG:** Wende die "Hybrid Service Logic" an, wenn technische Constraints existieren! (Wackler Manpower als Ergänzung).
|
||||
|
||||
Output JSON format ONLY:
|
||||
{
|
||||
"strategyMatrix": [
|
||||
{
|
||||
"segment": "Zielsegment",
|
||||
"painPoint": "Spezifischer Schmerz (Kurz)",
|
||||
"angle": "Unser Marketing Angle (Kurz)",
|
||||
"differentiation": "Wie es sich unterscheidet (Kurz)"
|
||||
}
|
||||
]
|
||||
}
|
||||
` : `
|
||||
PHASE 4: STRATEGY & ANGLE DEVELOPMENT
|
||||
Accounts: ${JSON.stringify(allAccounts)}
|
||||
Target Industries: ${JSON.stringify(phase3Data.whales.map(w => w.industry))}
|
||||
Product Features: ${JSON.stringify(phase1Data.features)}
|
||||
|
||||
Task:
|
||||
1. Develop specific "Angle" per target/industry.
|
||||
2. Consistency Check against Product Matrix (ensure differentiation from Scrubber 50/Bella).
|
||||
3. **IMPORTANT:** Apply "Hybrid Service Logic" if technical constraints exist! (Wackler Manpower as supplement).
|
||||
|
||||
Output JSON format ONLY:
|
||||
{
|
||||
"strategyMatrix": [
|
||||
{
|
||||
"segment": "Target Segment",
|
||||
"painPoint": "Specific Pain (Short)",
|
||||
"angle": "Our Marketing Angle (Short)",
|
||||
"differentiation": "How it differs (Short)"
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-3-flash-preview',
|
||||
contents: prompt,
|
||||
config: {
|
||||
systemInstruction: sysInstr,
|
||||
responseMimeType: "application/json",
|
||||
}
|
||||
});
|
||||
|
||||
return cleanAndParseJson<Phase4Data>(response.text);
|
||||
};
|
||||
|
||||
// --- Phase 5: Assets & Report ---
|
||||
export const generateAssets = async (
|
||||
phase4Data: Phase4Data,
|
||||
phase3Data: Phase3Data,
|
||||
phase2Data: Phase2Data,
|
||||
phase1Data: Phase1Data,
|
||||
lang: Language
|
||||
): Promise<string> => {
|
||||
const ai = getClient();
|
||||
const sysInstr = getSystemInstruction(lang);
|
||||
|
||||
const prompt = lang === 'de' ? `
|
||||
PHASE 5: ASSET GENERATION & FINAL REPORT
|
||||
|
||||
CONTEXT DATA:
|
||||
- Technical: ${JSON.stringify(phase1Data)}
|
||||
- ICPs: ${JSON.stringify(phase2Data)}
|
||||
- Targets (Whales): ${JSON.stringify(phase3Data)}
|
||||
- Strategy: ${JSON.stringify(phase4Data)}
|
||||
|
||||
AUFGABE:
|
||||
1. Erstelle einen "GTM STRATEGY REPORT" in Markdown.
|
||||
2. Struktur des Reports:
|
||||
- Executive Summary
|
||||
- Produkt Analyse (Features & Constraints)
|
||||
- Zielgruppen (ICPs & Data Proxies)
|
||||
- Target Accounts (Whales) & Buying Center
|
||||
- Strategie-Matrix (Pains & Angles)
|
||||
- Assets (LinkedIn, Email, Landing Page Headline)
|
||||
3. Self-Correction: Entferne "Marketing Bla-Bla". Nutze Fakten, TCO, ROI.
|
||||
4. Hybrid-Check: Stelle sicher, dass die "Hybrid Service Logic" (Wackler Manpower) in der Strategie und den Assets sichtbar ist, falls Constraints vorliegen.
|
||||
|
||||
Output:
|
||||
Gib striktes MARKDOWN zurück. Beginne mit "# GTM STRATEGY REPORT".
|
||||
` : `
|
||||
PHASE 5: ASSET GENERATION & FINAL REPORT
|
||||
|
||||
CONTEXT DATA:
|
||||
- Technical: ${JSON.stringify(phase1Data)}
|
||||
- ICPs: ${JSON.stringify(phase2Data)}
|
||||
- Targets (Whales): ${JSON.stringify(phase3Data)}
|
||||
- Strategy: ${JSON.stringify(phase4Data)}
|
||||
|
||||
TASK:
|
||||
1. Create a "GTM STRATEGY REPORT" in Markdown.
|
||||
2. Report Structure:
|
||||
- Executive Summary
|
||||
- Product Analysis (Features & Constraints)
|
||||
- Target Audience (ICPs & Data Proxies)
|
||||
- Target Accounts (Whales) & Buying Center
|
||||
- Strategy Matrix (Pains & Angles)
|
||||
- Assets (LinkedIn, Email, Landing Page Headline)
|
||||
3. Self-Correction: Remove "Marketing Fluff". Use Facts, TCO, ROI.
|
||||
4. Hybrid-Check: Ensure "Hybrid Service Logic" (Wackler Manpower) is visible in strategy and assets if constraints exist.
|
||||
|
||||
Output:
|
||||
Return strictly MARKDOWN formatted text. Start with "# GTM STRATEGY REPORT".
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-3-flash-preview',
|
||||
contents: prompt,
|
||||
config: {
|
||||
systemInstruction: sysInstr,
|
||||
}
|
||||
});
|
||||
|
||||
return response.text || "Error generating assets.";
|
||||
};
|
||||
|
||||
// --- Phase 6: Sales Enablement & Visuals ---
|
||||
export const generateSalesEnablement = async (
|
||||
phase4Data: Phase4Data,
|
||||
phase3Data: Phase3Data,
|
||||
phase1Data: Phase1Data,
|
||||
lang: Language
|
||||
): Promise<Phase6Data> => {
|
||||
const ai = getClient();
|
||||
const sysInstr = getSystemInstruction(lang);
|
||||
|
||||
const prompt = lang === 'de' ? `
|
||||
PHASE 6: SALES ENABLEMENT & VISUALS
|
||||
|
||||
CONTEXT:
|
||||
- Produkt Features: ${JSON.stringify(phase1Data.features)}
|
||||
- Accounts (Personas): ${JSON.stringify(phase3Data.roles)}
|
||||
- Strategie: ${JSON.stringify(phase4Data.strategyMatrix)}
|
||||
|
||||
AUFGABE:
|
||||
1. **Anticipate Friction:** Versetze dich in die Lage der identifizierten Personas. Was ist deren härtester Einwand?
|
||||
2. **Deflect & Solve:** Formuliere eine präzise, faktenbasierte Antwort für den Vertriebler (Battlecard).
|
||||
3. **Visual Context:** Erstelle präzise "Text-to-Image Prompts" (für Midjourney/DALL-E).
|
||||
|
||||
Output JSON format ONLY:
|
||||
{
|
||||
"battlecards": [
|
||||
{
|
||||
"persona": "Rolle",
|
||||
"objection": "Einwand",
|
||||
"responseScript": "Antwort"
|
||||
}
|
||||
],
|
||||
"visualPrompts": [
|
||||
{
|
||||
"title": "Titel",
|
||||
"context": "Kontext",
|
||||
"prompt": "Prompt..."
|
||||
}
|
||||
]
|
||||
}
|
||||
` : `
|
||||
PHASE 6: SALES ENABLEMENT & VISUALS
|
||||
|
||||
CONTEXT:
|
||||
- Product Features: ${JSON.stringify(phase1Data.features)}
|
||||
- Accounts (Personas): ${JSON.stringify(phase3Data.roles)}
|
||||
- Strategy: ${JSON.stringify(phase4Data.strategyMatrix)}
|
||||
|
||||
TASK:
|
||||
1. **Anticipate Friction:** Identify hardest objections.
|
||||
2. **Deflect & Solve:** Formulate response script (Battlecard).
|
||||
3. **Visual Context:** Create "Text-to-Image Prompts".
|
||||
|
||||
Output JSON format ONLY:
|
||||
{
|
||||
"battlecards": [
|
||||
{
|
||||
"persona": "Role",
|
||||
"objection": "Objection",
|
||||
"responseScript": "Response"
|
||||
}
|
||||
],
|
||||
"visualPrompts": [
|
||||
{
|
||||
"title": "Title",
|
||||
"context": "Context",
|
||||
"prompt": "Prompt..."
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-3-flash-preview',
|
||||
contents: prompt,
|
||||
config: {
|
||||
systemInstruction: sysInstr,
|
||||
responseMimeType: "application/json",
|
||||
}
|
||||
});
|
||||
|
||||
return cleanAndParseJson<Phase6Data>(response.text);
|
||||
};
|
||||
|
||||
// --- Phase 7: Vertical Landing Page Copy ---
|
||||
export const generateLandingPageCopy = async (
|
||||
phase4Data: Phase4Data,
|
||||
phase2Data: Phase2Data,
|
||||
lang: Language
|
||||
): Promise<Phase7Data> => {
|
||||
const ai = getClient();
|
||||
const sysInstr = getSystemInstruction(lang);
|
||||
|
||||
const prompt = lang === 'de' ? `
|
||||
PHASE 7: VERTICAL LANDING PAGE COPY (Conversion Optimization)
|
||||
|
||||
ICPs: ${JSON.stringify(phase2Data.icps)}
|
||||
Strategy: ${JSON.stringify(phase4Data.strategyMatrix)}
|
||||
|
||||
AUFGABE:
|
||||
1. Identifiziere generische Aussagen und transformiere sie in spezifische Nutzenargumente für die Top 2 ICPs.
|
||||
2. Wende die "Wackler-Symbiose" an: Erwähne Service/Hygiene als Trust-Element.
|
||||
|
||||
Erstelle für die Top 2 ICPs jeweils einen Landingpage-Entwurf (Hero Section).
|
||||
|
||||
Output JSON format ONLY:
|
||||
{
|
||||
"landingPages": [
|
||||
{
|
||||
"industry": "Zielbranche",
|
||||
"headline": "Nutzenorientierte Headline (Kein Produktname)",
|
||||
"subline": "Tech Enabler + Wackler Service Garantie",
|
||||
"bullets": ["Vorteil 1", "Vorteil 2", "Vorteil 3"],
|
||||
"cta": "Branchen-spezifischer CTA"
|
||||
}
|
||||
]
|
||||
}
|
||||
` : `
|
||||
PHASE 7: VERTICAL LANDING PAGE COPY (Conversion Optimization)
|
||||
|
||||
ICPs: ${JSON.stringify(phase2Data.icps)}
|
||||
Strategy: ${JSON.stringify(phase4Data.strategyMatrix)}
|
||||
|
||||
TASK:
|
||||
1. Transform generic features into specific benefits for the Top 2 ICPs.
|
||||
2. Apply "Wackler Symbiosis": Mention service/hygiene as trust element.
|
||||
|
||||
Create Landing Page Drafts (Hero Section) for Top 2 ICPs.
|
||||
|
||||
Output JSON format ONLY:
|
||||
{
|
||||
"landingPages": [
|
||||
{
|
||||
"industry": "Target Industry",
|
||||
"headline": "Benefit-oriented Headline",
|
||||
"subline": "Tech Enabler + Wackler Service Guarantee",
|
||||
"bullets": ["Benefit 1", "Benefit 2", "Benefit 3"],
|
||||
"cta": "Industry-specific CTA"
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-3-flash-preview',
|
||||
contents: prompt,
|
||||
config: {
|
||||
systemInstruction: sysInstr,
|
||||
responseMimeType: "application/json",
|
||||
}
|
||||
});
|
||||
|
||||
return cleanAndParseJson<Phase7Data>(response.text);
|
||||
};
|
||||
|
||||
// --- Phase 8: Business Case Builder ---
|
||||
export const buildBusinessCase = async (
|
||||
phase2Data: Phase2Data,
|
||||
phase1Data: Phase1Data,
|
||||
lang: Language
|
||||
): Promise<Phase8Data> => {
|
||||
const ai = getClient();
|
||||
const sysInstr = getSystemInstruction(lang);
|
||||
|
||||
const prompt = lang === 'de' ? `
|
||||
PHASE 8: BUSINESS CASE BUILDER (The CFO Pitch)
|
||||
|
||||
Input:
|
||||
ICPs: ${JSON.stringify(phase2Data.icps)}
|
||||
Features: ${JSON.stringify(phase1Data.features)}
|
||||
|
||||
AUFGABE:
|
||||
1. Schätze Lohnkosten/Probleme der Zielgruppe (Personalmangel, Krankheitsstand).
|
||||
2. Setze den Roboter (Leasing ca. 330-600€/Monat) dagegen.
|
||||
3. Entwickle eine ROI-Logik.
|
||||
|
||||
Erstelle für jeden ICP einen "Financial Argumentation Guide".
|
||||
|
||||
Output JSON format ONLY:
|
||||
{
|
||||
"businessCases": [
|
||||
{
|
||||
"industry": "Branche",
|
||||
"costDriver": "Was kostet der Status Quo? (z.B. Laufwege)",
|
||||
"efficiencyGain": "Wie amortisiert sich der Roboter?",
|
||||
"riskArgument": "Warum Miete + Wackler Service sicherer ist als Kauf"
|
||||
}
|
||||
]
|
||||
}
|
||||
` : `
|
||||
PHASE 8: BUSINESS CASE BUILDER (The CFO Pitch)
|
||||
|
||||
Input:
|
||||
ICPs: ${JSON.stringify(phase2Data.icps)}
|
||||
Features: ${JSON.stringify(phase1Data.features)}
|
||||
|
||||
TASK:
|
||||
1. Estimate labor costs/pain points (shortage, sick leave).
|
||||
2. Compare against Robot Leasing (approx 330-600€/month).
|
||||
3. Develop ROI logic.
|
||||
|
||||
Create a "Financial Argumentation Guide" for each ICP.
|
||||
|
||||
Output JSON format ONLY:
|
||||
{
|
||||
"businessCases": [
|
||||
{
|
||||
"industry": "Industry",
|
||||
"costDriver": "Status Quo Cost",
|
||||
"efficiencyGain": "Amortization Logic",
|
||||
"riskArgument": "Why Lease + Service is safer than buying"
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-3-flash-preview',
|
||||
contents: prompt,
|
||||
config: {
|
||||
systemInstruction: sysInstr,
|
||||
responseMimeType: "application/json",
|
||||
}
|
||||
});
|
||||
|
||||
return cleanAndParseJson<Phase8Data>(response.text);
|
||||
};
|
||||
|
||||
// --- Phase 9: Feature-to-Value Translator ---
|
||||
export const translateTech = async (
|
||||
phase1Data: Phase1Data,
|
||||
phase4Data: Phase4Data,
|
||||
lang: Language
|
||||
): Promise<Phase9Data> => {
|
||||
const ai = getClient();
|
||||
const sysInstr = getSystemInstruction(lang);
|
||||
|
||||
const prompt = lang === 'de' ? `
|
||||
PHASE 9: THE "FEATURE-TO-VALUE" TRANSLATOR
|
||||
|
||||
Input Features: ${JSON.stringify(phase1Data.features)}
|
||||
Strategy Pains: ${JSON.stringify(phase4Data.strategyMatrix.map(s => s.painPoint))}
|
||||
|
||||
AUFGABE:
|
||||
1. Nimm ein technisches Feature (z.B. "LiDAR").
|
||||
2. Frage: "So what?" (Was bringt das im Alltag?).
|
||||
3. Frage nochmal: "So what?" (Was ist der emotionale oder finanzielle Vorteil?).
|
||||
* Kette: LiDAR -> Erkennt Hindernisse -> Stößt nicht an -> Keine Unfälle/Haftung -> **Sicherheit & Ruhe**.
|
||||
4. Formuliere den Benefit **ohne** Fachchinesisch. Nutze emotionale Trigger.
|
||||
|
||||
Erstelle eine Tabelle für die Website-Kommunikation:
|
||||
* **feature:** (Klein gedruckt für Techies)
|
||||
* **story:** (Die Story) Der emotionale/wirtschaftliche Nutzen in der Sprache der Zielgruppe.
|
||||
* **headline:** (Die Headline) Ein knackiger Slogan für dieses Feature.
|
||||
|
||||
Output JSON format ONLY:
|
||||
{
|
||||
"techTranslations": [
|
||||
{
|
||||
"feature": "Technisches Feature",
|
||||
"story": "Benefit Story (So what chain)",
|
||||
"headline": "Knackiger Slogan"
|
||||
}
|
||||
]
|
||||
}
|
||||
` : `
|
||||
PHASE 9: THE "FEATURE-TO-VALUE" TRANSLATOR
|
||||
|
||||
Input Features: ${JSON.stringify(phase1Data.features)}
|
||||
Strategy Pains: ${JSON.stringify(phase4Data.strategyMatrix.map(s => s.painPoint))}
|
||||
|
||||
TASK:
|
||||
1. Take a technical feature (e.g. "LiDAR").
|
||||
2. Ask: "So what?" (What does it do in daily life?).
|
||||
3. Ask again: "So what?" (What is the emotional or financial benefit?).
|
||||
* Chain: LiDAR -> See obstacles -> No collision -> No accidents/liability -> **Safety & Peace of Mind**.
|
||||
4. Formulate the benefit **without** jargon. Use emotional triggers.
|
||||
|
||||
Create a table for website communication:
|
||||
* **feature:** (Small print for techies)
|
||||
* **story:** (The Story) The emotional/economic benefit in target language.
|
||||
* **headline:** (The Headline) A punchy slogan for this feature.
|
||||
|
||||
Output JSON format ONLY:
|
||||
{
|
||||
"techTranslations": [
|
||||
{
|
||||
"feature": "Tech Feature",
|
||||
"story": "Benefit Story (So what chain)",
|
||||
"headline": "Punchy Slogan"
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-3-flash-preview',
|
||||
contents: prompt,
|
||||
config: {
|
||||
systemInstruction: sysInstr,
|
||||
responseMimeType: "application/json",
|
||||
}
|
||||
});
|
||||
|
||||
return cleanAndParseJson<Phase9Data>(response.text);
|
||||
};
|
||||
|
||||
export const translateReportToEnglish = async (reportMarkdown: string): Promise<string> => {
|
||||
const ai = getClient();
|
||||
const prompt = `
|
||||
TASK: Translate the following GTM Strategy Report into professional Business English.
|
||||
CONTEXT: It is a B2B robotic strategy document.
|
||||
CONSTRAINT: Keep the Markdown structure (#, ##, -, | tables) EXACTLY as is. Only translate the content.
|
||||
|
||||
INPUT:
|
||||
${reportMarkdown}
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-3-flash-preview',
|
||||
contents: prompt
|
||||
});
|
||||
|
||||
return response.text || reportMarkdown;
|
||||
};
|
||||
|
||||
export const generateConceptImage = async (prompt: string, referenceImagesBase64?: string[]): Promise<string> => {
|
||||
const ai = getClient();
|
||||
try {
|
||||
const parts: any[] = [];
|
||||
|
||||
// If reference images are provided, add them to parts
|
||||
if (referenceImagesBase64 && referenceImagesBase64.length > 0) {
|
||||
referenceImagesBase64.forEach(img => {
|
||||
const cleanBase64 = img.split(',')[1] || img;
|
||||
const match = img.match(/data:(.*?);base64/);
|
||||
const mimeType = match ? match[1] : 'image/png';
|
||||
|
||||
parts.push({
|
||||
inlineData: {
|
||||
mimeType: mimeType,
|
||||
data: cleanBase64
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
parts.push({
|
||||
text: "Show the product in these reference images in the following context. Keep the product appearance consistent. " + prompt
|
||||
});
|
||||
} else {
|
||||
// Text-only prompt
|
||||
parts.push({ text: prompt });
|
||||
}
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-2.5-flash-image',
|
||||
contents: { parts },
|
||||
config: {
|
||||
imageConfig: {
|
||||
aspectRatio: "16:9",
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const part of response.candidates?.[0]?.content?.parts || []) {
|
||||
if (part.inlineData) {
|
||||
return `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`;
|
||||
}
|
||||
}
|
||||
throw new Error("No image generated");
|
||||
} catch (e: any) {
|
||||
console.error("Image generation failed", e);
|
||||
throw new Error(e.message || "Failed to generate image");
|
||||
}
|
||||
};
|
||||
@@ -1,91 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Roboplanet GTM Architect</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
mono: ['JetBrains Mono', 'monospace'],
|
||||
},
|
||||
colors: {
|
||||
robo: {
|
||||
900: '#0f172a',
|
||||
800: '#1e293b',
|
||||
700: '#334155',
|
||||
500: '#3b82f6',
|
||||
400: '#60a5fa',
|
||||
accent: '#06b6d4'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
/* Default Light Mode colors */
|
||||
background-color: #f8fafc;
|
||||
color: #0f172a;
|
||||
transition: background-color 0.3s, color 0.3s;
|
||||
}
|
||||
/* Dark Mode overrides via class */
|
||||
.dark body {
|
||||
background-color: #0f172a;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1; /* slate-300 */
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8; /* slate-400 */
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-track {
|
||||
background: #1e293b;
|
||||
}
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: #475569;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background: #64748b;
|
||||
}
|
||||
</style>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"@google/genai": "https://esm.sh/@google/genai@^1.34.0",
|
||||
"react/": "https://esm.sh/react@^19.2.3/",
|
||||
"react": "https://esm.sh/react@^19.2.3",
|
||||
"react-markdown": "https://esm.sh/react-markdown@^10.1.0",
|
||||
"remark-gfm": "https://esm.sh/remark-gfm@^4.0.0",
|
||||
"react-dom/": "https://esm.sh/react-dom@^19.2.3/",
|
||||
"lucide-react": "https://esm.sh/lucide-react@^0.562.0"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link rel="stylesheet" href="/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
if (!rootElement) {
|
||||
throw new Error("Could not find root element to mount to");
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "Roboplanet GTM Architect",
|
||||
"description": "A strategic AI engine for developing Go-to-Market strategies for robotic products, ensuring product-market fit and operational feasibility.",
|
||||
"requestFramePermissions": []
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"name": "roboplanet-gtm-architect",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.34.0",
|
||||
"react": "^19.2.3",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"react-dom": "^19.2.3",
|
||||
"lucide-react": "^0.562.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
export enum Phase {
|
||||
Input = 0,
|
||||
ProductAnalysis = 1,
|
||||
ICPDiscovery = 2,
|
||||
WhaleHunting = 3,
|
||||
Strategy = 4,
|
||||
AssetGeneration = 5,
|
||||
SalesEnablement = 6,
|
||||
LandingPage = 7,
|
||||
BusinessCase = 8,
|
||||
TechTranslator = 9,
|
||||
}
|
||||
|
||||
export type Language = 'en' | 'de';
|
||||
export type Theme = 'light' | 'dark';
|
||||
|
||||
export interface Message {
|
||||
role: 'user' | 'model';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface Phase1Data {
|
||||
features: string[];
|
||||
constraints: string[];
|
||||
conflictCheck: {
|
||||
hasConflict: boolean;
|
||||
details: string;
|
||||
relatedProduct?: string;
|
||||
};
|
||||
rawAnalysis: string;
|
||||
}
|
||||
|
||||
export interface Phase2Data {
|
||||
icps: {
|
||||
name: string;
|
||||
rationale: string;
|
||||
}[];
|
||||
dataProxies: {
|
||||
target: string;
|
||||
method: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface Phase3Data {
|
||||
whales: {
|
||||
industry: string;
|
||||
accounts: string[];
|
||||
}[];
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
export interface Phase4Data {
|
||||
strategyMatrix: {
|
||||
segment: string;
|
||||
painPoint: string;
|
||||
angle: string;
|
||||
differentiation: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface Phase6Data {
|
||||
battlecards: {
|
||||
persona: string;
|
||||
objection: string;
|
||||
responseScript: string;
|
||||
}[];
|
||||
visualPrompts: {
|
||||
title: string;
|
||||
context: string;
|
||||
prompt: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface Phase7Data {
|
||||
landingPages: {
|
||||
industry: string;
|
||||
headline: string;
|
||||
subline: string;
|
||||
bullets: string[];
|
||||
cta: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface Phase8Data {
|
||||
businessCases: {
|
||||
industry: string;
|
||||
costDriver: string;
|
||||
efficiencyGain: string;
|
||||
riskArgument: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface Phase9Data {
|
||||
techTranslations: {
|
||||
feature: string;
|
||||
story: string;
|
||||
headline: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface AppState {
|
||||
currentPhase: Phase;
|
||||
isLoading: boolean;
|
||||
history: Message[];
|
||||
productInput: string;
|
||||
productImages: string[]; // Array of Base64 strings
|
||||
phase1Result?: Phase1Data;
|
||||
phase2Result?: Phase2Data;
|
||||
phase3Result?: Phase3Data;
|
||||
phase4Result?: Phase4Data;
|
||||
phase5Result?: string; // Markdown content
|
||||
phase6Result?: Phase6Data;
|
||||
phase7Result?: Phase7Data;
|
||||
phase8Result?: Phase8Data;
|
||||
phase9Result?: Phase9Data;
|
||||
translatedReport?: string; // New field for the English translation
|
||||
language: Language;
|
||||
theme: Theme;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import path from 'path';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, '.', '');
|
||||
return {
|
||||
server: {
|
||||
port: 3000,
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
plugins: [react()],
|
||||
define: {
|
||||
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -5,16 +5,12 @@ import react from '@vitejs/plugin-react';
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, '.', '');
|
||||
return {
|
||||
base: './', // Fix for subdirectory deployment (White Screen)
|
||||
base: './',
|
||||
server: {
|
||||
port: 3000,
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
plugins: [react()],
|
||||
define: {
|
||||
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
|
||||
Reference in New Issue
Block a user