- market_intel_orchestrator.py: Updated generate_outreach_campaign to identify all relevant roles, generate top 5, and return remaining as suggestions. Added specific_role mode. - types.ts: Added OutreachResponse interface. - geminiService.ts: Updated to handle new response structure and specificRole parameter. - StepOutreach.tsx: Added sidebar section for Suggested Roles with on-demand generation buttons.
254 lines
8.9 KiB
TypeScript
254 lines
8.9 KiB
TypeScript
import { LeadStatus, AnalysisResult, Competitor, Language, Tier, EmailDraft, SearchStrategy, SearchSignal, OutreachResponse } from "../types";
|
|
|
|
// URL Konfiguration:
|
|
// Im Production-Build (Docker/Nginx) nutzen wir den relativen Pfad '/api', da Nginx als Reverse Proxy fungiert.
|
|
// Im Development-Modus (lokal) greifen wir direkt auf den Port 3001 zu.
|
|
const API_BASE_URL = (import.meta as any).env.PROD
|
|
? 'api'
|
|
: `http://${window.location.hostname}:3001/api`;
|
|
|
|
// Helper to extract JSON (kann ggf. entfernt werden, wenn das Backend immer sauberes JSON liefert)
|
|
const extractJson = (text: string): any => {
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch (e) {
|
|
const jsonMatch = text.match(/```json\s*([\s\S]*?)\s*```/);
|
|
if (jsonMatch && jsonMatch[1]) {
|
|
try { return JSON.parse(jsonMatch[1]); } catch (e2) {}
|
|
}
|
|
const arrayMatch = text.match(/\\[\s\S]*\\s*\]/);
|
|
if (arrayMatch) {
|
|
try { return JSON.parse(arrayMatch[0]); } catch (e3) {}
|
|
}
|
|
const objectMatch = text.match(/\{\s*[\s\S]*\s*\}/);
|
|
if (objectMatch) {
|
|
try { return JSON.parse(objectMatch[0]); } catch (e4) {}
|
|
}
|
|
throw new Error("Could not parse JSON response");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* NEU: Ruft unser Python-Backend über die Node.js API-Brücke auf.
|
|
*/
|
|
export const generateSearchStrategy = async (
|
|
referenceUrl: string,
|
|
contextContent: string,
|
|
language: Language
|
|
): Promise<SearchStrategy> => {
|
|
// API-Key wird jetzt vom Backend verwaltet
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/generate-search-strategy`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ referenceUrl, contextContent, language }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(`Backend-Fehler: ${errorData.error || response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
return {
|
|
productContext: (data.summaryOfOffer || "Market Analysis") + " [Build: " + new Date().toLocaleString() + "]",
|
|
idealCustomerProfile: data.idealCustomerProfile || "Companies similar to reference",
|
|
searchStrategyICP: data.searchStrategyICP || "N/A (Not in API response)",
|
|
digitalSignals: data.digitalSignals || "N/A (Not in API response)",
|
|
targetPages: data.targetPages || "N/A (Not in API response)",
|
|
signals: data.signals || []
|
|
};
|
|
} catch (error) {
|
|
console.error("Strategy generation failed via API Bridge", error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// Helper to generate IDs
|
|
const generateId = () => Math.random().toString(36).substr(2, 9);
|
|
|
|
export const identifyCompetitors = async (
|
|
referenceUrl: string,
|
|
targetMarket: string,
|
|
contextContent: string,
|
|
referenceCity?: string,
|
|
referenceCountry?: string,
|
|
summaryOfOffer?: string
|
|
): Promise<{ localCompetitors: Competitor[], nationalCompetitors: Competitor[], internationalCompetitors: Competitor[] }> => {
|
|
console.log("Frontend: identifyCompetitors API-Aufruf gestartet.");
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/identify-competitors`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ referenceUrl, targetMarket, contextContent, referenceCity, referenceCountry, summaryOfOffer }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
console.error(`Frontend: Backend-Fehler bei identifyCompetitors: ${errorData.error || response.statusText}`);
|
|
throw new Error(`Backend-Fehler: ${errorData.error || response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log("Frontend: identifyCompetitors API-Aufruf erfolgreich. Empfangene Daten:", data);
|
|
|
|
const addIds = (comps: any[]) => (comps || []).map(c => ({ ...c, id: c.id || generateId() }));
|
|
|
|
return {
|
|
localCompetitors: addIds(data.localCompetitors),
|
|
nationalCompetitors: addIds(data.nationalCompetitors),
|
|
internationalCompetitors: addIds(data.internationalCompetitors)
|
|
};
|
|
} catch (error) {
|
|
console.error("Frontend: Konkurrenten-Identifikation über API Bridge fehlgeschlagen", error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* UPDATED: Dynamic Analysis based on Strategy (muss noch im Python-Backend implementiert werden)
|
|
*/
|
|
export const analyzeCompanyWithStrategy = async (
|
|
companyName: string,
|
|
strategy: SearchStrategy,
|
|
language: Language
|
|
): Promise<AnalysisResult> => {
|
|
console.log(`Frontend: Starte Audit für ${companyName}...`);
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/analyze-company`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
companyName,
|
|
strategy,
|
|
targetMarket: language === 'de' ? 'Germany' : 'USA' // Einfache Ableitung, kann verfeinert werden
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(`Backend-Fehler: ${errorData.error || response.statusText}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
console.log(`Frontend: Audit für ${companyName} erfolgreich.`);
|
|
return result as AnalysisResult;
|
|
|
|
} catch (error) {
|
|
console.error(`Frontend: Audit fehlgeschlagen für ${companyName}`, error);
|
|
|
|
// Fallback-Analyse erstellen, damit die UI nicht abstürzt
|
|
const fallbackAnalysis: Record<string, { value: string; proof: string }> = {};
|
|
if (strategy && strategy.signals) {
|
|
strategy.signals.forEach(s => {
|
|
fallbackAnalysis[s.id] = { value: "N/A (Error)", proof: "Audit failed" };
|
|
});
|
|
}
|
|
|
|
// Fallback-Objekt bei Fehler, damit der Prozess nicht komplett stoppt
|
|
return {
|
|
companyName,
|
|
status: LeadStatus.UNKNOWN,
|
|
revenue: "Error",
|
|
employees: "Error",
|
|
tier: Tier.TIER_3,
|
|
dataSource: "Error",
|
|
dynamicAnalysis: fallbackAnalysis,
|
|
recommendation: "Fehler bei der Analyse: " + (error as Error).message,
|
|
processingChecks: { wiki: false, revenue: false, signalsChecked: false }
|
|
};
|
|
}
|
|
};
|
|
|
|
export const generateOutreachCampaign = async (
|
|
companyData: AnalysisResult,
|
|
knowledgeBase: string,
|
|
language: Language,
|
|
referenceUrl: string,
|
|
specificRole?: string
|
|
): Promise<OutreachResponse> => {
|
|
console.log(`Frontend: Starte Outreach-Generierung für ${companyData.companyName} (Role: ${specificRole || "All"})...`);
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/generate-outreach`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
companyData,
|
|
knowledgeBase,
|
|
referenceUrl,
|
|
specific_role: specificRole
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(`Backend-Fehler: ${errorData.error || response.statusText}`);
|
|
}
|
|
|
|
const result = await response.json(); // Expected: { campaigns: [...], available_roles: [...] } or single object
|
|
console.log(`Frontend: Outreach-Generierung erfolgreich.`, result);
|
|
|
|
let rawCampaigns: any[] = [];
|
|
let availableRoles: string[] = [];
|
|
|
|
// Handle Mode A (Batch) vs Mode B (Single) response structures
|
|
if (result.campaigns && Array.isArray(result.campaigns)) {
|
|
rawCampaigns = result.campaigns;
|
|
availableRoles = result.available_roles || [];
|
|
} else if (result.target_role && result.emails) {
|
|
// Single campaign response (Mode B)
|
|
rawCampaigns = [result];
|
|
} else if (Array.isArray(result)) {
|
|
// Legacy fallback
|
|
rawCampaigns = result;
|
|
}
|
|
|
|
const processedCampaigns: EmailDraft[] = rawCampaigns.map((item: any) => {
|
|
let fullBody = "";
|
|
const firstSubject = item.emails?.[0]?.subject || "No Subject";
|
|
|
|
if (item.emails && Array.isArray(item.emails)) {
|
|
item.emails.forEach((mail: any, idx: number) => {
|
|
fullBody += `### Email ${idx + 1}: ${mail.subject}\n\n`;
|
|
fullBody += `${mail.body}\n\n`;
|
|
if (idx < item.emails.length - 1) fullBody += `\n---\n\n`;
|
|
});
|
|
} else {
|
|
fullBody = item.body || "No content generated.";
|
|
}
|
|
|
|
return {
|
|
persona: item.target_role || "Unknown Role",
|
|
subject: firstSubject,
|
|
body: fullBody,
|
|
keyPoints: item.rationale ? [item.rationale] : []
|
|
};
|
|
});
|
|
|
|
return {
|
|
campaigns: processedCampaigns,
|
|
available_roles: availableRoles
|
|
};
|
|
|
|
} catch (error) {
|
|
console.error(`Frontend: Outreach-Generierung fehlgeschlagen für ${companyData.companyName}`, error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
export const translateEmailDrafts = async (drafts: EmailDraft[], targetLanguage: Language): Promise<EmailDraft[]> => {
|
|
// Dieser Teil muss noch im Python-Backend oder direkt im Frontend implementiert werden
|
|
console.warn("translateEmailDrafts ist noch nicht im Python-Backend implementiert.");
|
|
return drafts;
|
|
} |