mein-solar: full feature set

- Schema: companyType, topics, salesScore, salesReason, offerPackage, approved, approvedAt, SearchHistory table
- /api/search-history: GET (by mode) + POST (save query)
- /api/ai-search: stadtwerke/industrie/custom prompts with history dedup
- /api/enrich-leads: website scraping + GPT-4o-mini enrichment (fire-and-forget after each job)
- /api/generate-email: personalized outreach via GPT-4o
- Suche page: 3 mode tabs (Stadtwerke/Industrie/Freie Suche), Alle-Bundesländer queue button, AiSearchModal gets searchMode + history
- Leadspeicher: Bewertung dots column, Paket badge column, Freigeben toggle button, email generator in SidePanel, approved-only export option
- Leads API: approvedOnly + companyType filters, new fields in PATCH

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Timo Uttenweiler
2026-04-08 21:06:07 +02:00
parent e5172cbdc5
commit 54e0d22f9c
14 changed files with 866 additions and 47 deletions

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
const SYSTEM_PROMPT = `Du bist ein spezialisierter Assistent für B2B-Lead-Generierung im deutschsprachigen Raum.
const SYSTEM_PROMPT_DEFAULT = `Du bist ein spezialisierter Assistent für B2B-Lead-Generierung im deutschsprachigen Raum.
## Was passiert mit deiner Ausgabe
@@ -36,45 +36,110 @@ Nutze mehrere Queries wenn:
Maximal 4 Queries. Keine Duplikate (gleiche query + gleiche region).
## Beispiele
Eingabe: "Dachdecker in Bayern"
Ausgabe:
[
{ "query": "Dachdecker", "region": "München" },
{ "query": "Dachdecker", "region": "Nürnberg" }
]
Eingabe: "Steuerberater in ganz Deutschland"
Ausgabe:
[
{ "query": "Steuerberater", "region": "Bayern" },
{ "query": "Steuerberater", "region": "NRW" },
{ "query": "Steuerberater", "region": "Hamburg" },
{ "query": "Steuerberater", "region": "Berlin" }
]
Eingabe: "Solaranlagen Installateure und Elektriker in Stuttgart"
Ausgabe:
[
{ "query": "Solaranlage", "region": "Stuttgart" },
{ "query": "Elektriker", "region": "Stuttgart" }
]
Eingabe: "Metallbaubetriebe in Süddeutschland"
Ausgabe:
[
{ "query": "Metallbau", "region": "Bayern" },
{ "query": "Metallbau", "region": "Baden-Württemberg" }
]
## Ausgabeformat
Antworte ausschließlich mit einem JSON-Array. Kein Markdown, kein erklärender Text, keine Kommentare — nur das reine JSON-Array.`;
const SYSTEM_PROMPT_STADTWERKE = `Du bist ein spezialisierter Assistent für B2B-Lead-Generierung im deutschen Energiesektor.
## Kontext
Das Tool sucht deutsche Stadtwerke und kommunale Energieversorger als potenzielle Kunden für ein Solarunternehmen (mein-solar.com). Stadtwerke betreiben lokale Energie-, Wasser- und Verkehrsinfrastruktur und sind starke Abnehmer für Photovoltaik-Großanlagen, Batteriespeicher und Ladeinfrastruktur.
## Was passiert mit deiner Ausgabe
Die Queries werden an die Google Maps Places API und Google SERP übergeben. Gesucht werden Stadtwerke-Websites, aus denen dann Entscheidungsträger-E-Mails extrahiert werden.
## Deine Aufgabe
Generiere 24 Suchanfragen die Stadtwerke, Gemeindewerke und kommunale Energieversorger in Deutschland finden. Decke verschiedene Bundesländer oder Regionen ab.
## Suchbegriffe die funktionieren
- "Stadtwerke" — findet kommunale Versorger
- "Gemeindewerk" — kleinere Gemeindebetriebe
- "Stadtwerk" — Variante
- "kommunaler Energieversorger" — für SERP
- Niemals: LinkedIn, XING, Verzeichnisse, Social Media
## Priorisierung
"Stadtwerke" schlägt immer "Gemeindewerk". Große Bundesländer (NRW, Bayern, BW) zuerst wenn keine Region angegeben.
## Bereits verwendete Queries (diese NICHT nochmal verwenden — generiere neue Regionen/Varianten):
[HISTORY_PLACEHOLDER]
## Beispiel
Eingabe: "Stadtwerke in Norddeutschland"
Ausgabe:
[
{ "query": "Stadtwerke", "region": "Hamburg" },
{ "query": "Stadtwerke", "region": "Schleswig-Holstein" },
{ "query": "Gemeindewerk", "region": "Niedersachsen" }
]
## Ausgabeformat
Nur reines JSON-Array, kein Markdown, keine Erklärungen.`;
const SYSTEM_PROMPT_INDUSTRIE = `Du bist ein spezialisierter Assistent für B2B-Lead-Generierung im deutschen Industriesektor.
## Kontext
Das Tool sucht deutsche Industriebetriebe als Kunden für ein Solarunternehmen (mein-solar.com), das PV-Großanlagen (5 kW bis 10+ MW), Batteriespeicher und Ladeinfrastruktur anbietet. Zielkunden sind Firmen mit hohem Energieverbrauch, großen Dachflächen oder Freiflächen, und Interesse an Kostensenkung durch Eigenerzeugung.
## Was passiert mit deiner Ausgabe
Die Queries werden an Google Maps und SERP übergeben um Unternehmenswebsites zu finden, aus denen Entscheidungsträger-E-Mails extrahiert werden.
## Deine Aufgabe
Generiere 24 Suchanfragen die energieintensive Industriebetriebe, Produktionsunternehmen, Logistiker oder Gewerbeparks finden — ideale Kandidaten für Solar-Großanlagen.
## Gut geeignete Branchen
Produktion/Fertigung, Logistik/Lager, Lebensmittelindustrie, Metallverarbeitung, Automobilzulieferer, Landwirtschaft (Agri-PV), Gewerbeparks, Einzelhandel (große Flächen)
## Suchbegriffe die funktionieren
Kurze branchenbezogene Begriffe die bei Google Maps echte Betriebe finden. Keine Adjektive.
## Priorisierung
Wähle immer den geläufigsten Begriff (z.B. "Logistikzentrum" vor "Lagerhaus"). Branchen mit hohem Energieverbrauch und Dachfläche zuerst.
## Bereits verwendete Queries (diese NICHT nochmal verwenden):
[HISTORY_PLACEHOLDER]
## Beispiel
Eingabe: "Industriebetriebe in Bayern mit großen Dachflächen"
Ausgabe:
[
{ "query": "Produktionsbetrieb", "region": "München" },
{ "query": "Logistikzentrum", "region": "Augsburg" },
{ "query": "Lebensmittelproduktion", "region": "Bayern" }
]
## Ausgabeformat
Nur reines JSON-Array, kein Markdown, keine Erklärungen.`;
function buildSystemPrompt(
searchMode: string,
history: Array<{ query: string; region: string }>
): string {
const historyText =
history.length > 0
? history
.slice(0, 20)
.map(h => `- "${h.query}" in "${h.region}"`)
.join("\n")
: "Keine bisherigen Suchen.";
if (searchMode === "stadtwerke") {
return SYSTEM_PROMPT_STADTWERKE.replace("[HISTORY_PLACEHOLDER]", historyText);
}
if (searchMode === "industrie") {
return SYSTEM_PROMPT_INDUSTRIE.replace("[HISTORY_PLACEHOLDER]", historyText);
}
return SYSTEM_PROMPT_DEFAULT;
}
export async function POST(req: NextRequest) {
try {
const { description } = await req.json() as { description: string };
const body = await req.json() as {
description: string;
searchMode?: string;
history?: Array<{ query: string; region: string }>;
};
const { description, searchMode = "custom", history = [] } = body;
if (!description?.trim()) {
return NextResponse.json({ error: "Beschreibung fehlt" }, { status: 400 });
@@ -85,20 +150,22 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: "OpenRouter API Key nicht konfiguriert" }, { status: 500 });
}
const systemPrompt = buildSystemPrompt(searchMode, history);
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
"HTTP-Referer": "https://onvyaleads.app",
"X-Title": "OnyvaLeads",
"HTTP-Referer": "https://mein-solar.com",
"X-Title": "MeinSolar Leads",
},
body: JSON.stringify({
model: "openai/gpt-4o-mini",
temperature: 0.4,
max_tokens: 512,
messages: [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "system", content: systemPrompt },
{ role: "user", content: description.trim() },
],
}),
@@ -134,6 +201,18 @@ export async function POST(req: NextRequest) {
count: 50,
}));
// Save to SearchHistory (fire and forget)
if (searchMode && searchMode !== "custom" && queries.length > 0) {
const baseUrl = req.url.replace(/\/api\/ai-search.*/, "");
for (const q of queries) {
fetch(`${baseUrl}/api/search-history`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: q.query, region: q.region, searchMode }),
}).catch(err => console.error("[ai-search] search-history save error:", err));
}
}
return NextResponse.json({ queries });
} catch (err) {
console.error("[ai-search] error:", err);