feat: introduce new PDF layouts and modules, enhance shared UI components, and add wording guidelines.

This commit is contained in:
2026-02-03 19:25:07 +01:00
parent 788c7aa7df
commit 3e70b00abc
20 changed files with 1057 additions and 1050 deletions

View File

@@ -89,7 +89,19 @@ async function main() {
}
}
// 2. AI Prompting
// 2. Distill Crawl Context (Context Filtering)
let distilledCrawl = '';
if (crawlContext) {
const cachedDistilled = await cache.get<string>(`distilled_${targetUrl}`);
if (cachedDistilled && !clearCache) {
distilledCrawl = cachedDistilled;
} else {
distilledCrawl = await distillCrawlContext(crawlContext, OPENROUTER_KEY);
await cache.set(`distilled_${targetUrl}`, distilledCrawl, 86400);
}
}
// 3. AI Prompting
console.log('🤖 Consultating Gemini 3 Flash...');
const cachedAi = !clearCache ? await cache.get<any>(finalCacheKey) : null;
let formState: any;
@@ -108,7 +120,7 @@ async function main() {
console.log('📦 Using cached AI response.');
formState = cachedAi;
} else {
const result = await getAiEstimation(briefing, crawlContext, comments, OPENROUTER_KEY, principles, techStandards, tone);
const result = await getAiEstimation(briefing, distilledCrawl, comments, OPENROUTER_KEY, principles, techStandards, tone);
formState = result.state;
usage = result.usage;
await cache.set(finalCacheKey, formState);
@@ -146,6 +158,32 @@ async function main() {
}
}
async function distillCrawlContext(rawCrawl: string, apiKey: string): Promise<string> {
if (!rawCrawl || rawCrawl.trim().length === 0) return "Keine Crawl-Daten vorhanden.";
console.log(' ↳ Distilling Crawl Context (Noise Filtering)...');
const systemPrompt = `
You are a context distiller. Your goal is to strip away HTML noise, legal footers, and generic fluff from a website crawl.
Extract the "Company DNA" in 5-8 bullet points (GERMAN).
### FOCUS ON:
1. Core Business / Services.
2. Unique Selling Points (USPs).
3. Target Audience (if clear).
4. Tech Stack or industry-specific equipment mentioned.
5. Brand tone (e.g. "industrial", "friendly", "technical").
### OUTPUT:
Return ONLY the bullet points. No intro/outro.
`;
const resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'google/gemini-3-flash-preview',
messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: `RAW_CRAWL:\n${rawCrawl.substring(0, 30000)}` }],
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
return resp.data.choices[0].message.content;
}
async function performCrawl(url: string): Promise<string> {
const pages: { url: string, content: string, type: string }[] = [];
const origin = new URL(url).origin;
@@ -205,7 +243,7 @@ async function performCrawl(url: string): Promise<string> {
return summary + pages.map(p => `--- PAGE: ${p.url} ---\n${p.content}`).join('\n\n');
}
async function getAiEstimation(briefing: string, crawlContext: string, comments: string | null, apiKey: string, principles: string, techStandards: string, tone: string) {
async function getAiEstimation(briefing: string, distilledCrawl: string, comments: string | null, apiKey: string, principles: string, techStandards: string, tone: string) {
let usage = { prompt: 0, completion: 0, cost: 0 };
const addUsage = (data: any) => {
if (data?.usage) {
@@ -222,22 +260,25 @@ async function getAiEstimation(briefing: string, crawlContext: string, comments:
}
};
// 1. PASS 1: Fact Extraction
console.log(' ↳ Pass 1: Fact Extraction...');
// 1. PASS 1: Fact Extraction (Briefing Sensor)
console.log(' ↳ Pass 1: Fact Extraction (Briefing Sensor)...');
const pass1SystemPrompt = `
You are a precision extraction engine. Analyze the briefing and extract ONLY the raw facts.
You are a precision sensor. Analyze the BRIEFING and extract ONLY the raw facts.
Tone: Literal, non-interpretive.
Output language: GERMAN (Strict).
### MISSION:
Focus 100% on the BRIEFING text provided by the user. Use the DISTILLED_CRAWL only as background context for terms or company details. If there is a conflict, the BRIEFING is the absolute source of truth.
### OBJECTIVES:
- Extract companyName (Strictly the name, no descriptors).
- Extract companyAddress (Full address if found).
- Extract personName (Primary contact if found).
- Extract **websiteTopic**: This MUST be a single, short branch name (e.g., "Kabeltiefbau", "Logistik", "Anwaltskanzlei"). ABSOLUTELY NO SENTENCES. If the briefing says "Group-Homepage for X", extract ONLY "X".
- Extract **websiteTopic**: This MUST be a single, short branch name (e.g., "Kabeltiefbau", "Logistik", "Anwaltskanzlei"). ABSOLUTELY NO SENTENCES.
- Map to internal IDs for selectedPages, features, functions, apiSystems, assets.
- Identify if isRelaunch is true (briefing mentions existing site or URL).
- For all textual values (deadline, websiteTopic, targetAudience etc.): USE GERMAN.
- **multilang**: ONLY if the briefing mentions multiple target languages (e.g., DE/EN). If only one language is mentioned, do NOT use multilang.
- **multilang**: ONLY if the briefing mentions multiple target languages (e.g., DE/EN).
- **maps**: If "Google Maps" or location maps are mentioned or implicit (Contact page).
- **CRITICAL**: Do NOT include "social" in apiSystems unless the user explicitly wants to SYNC/POST content to social media. "Existing social media links" are NOT apiSystems.
- **CRITICAL**: "Video Player", "Cookie Banner", "Animations" are NOT features. They are visual/base. Do NOT map them to features.
@@ -267,7 +308,7 @@ Output language: GERMAN (Strict).
"employeeCount": string
}
`;
const pass1UserPrompt = `BRIEFING:\n${briefing}\n\nCOMMENTS:\n${comments}\n\nCRAWL:\n${crawlContext}`;
const pass1UserPrompt = `BRIEFING (TRUTH SOURCE):\n${briefing}\n\nCOMMENTS:\n${comments}\n\nDISTILLED_CRAWL (CONTEXT ONLY):\n${distilledCrawl}`;
const p1Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'google/gemini-3-flash-preview',
messages: [{ role: 'system', content: pass1SystemPrompt }, { role: 'user', content: pass1UserPrompt }],
@@ -308,8 +349,8 @@ ${JSON.stringify(facts, null, 2)}
addUsage(p2Resp.data);
const details = JSON.parse(p2Resp.data.choices[0].message.content);
// 3. PASS 3: Strategic Content
console.log(' ↳ Pass 3: Strategic Content...');
// 3. PASS 3: Strategic Content (Bespoke Strategy)
console.log(' ↳ Pass 3: Strategic Content (Bespoke Strategy)...');
const pass3SystemPrompt = `
You are a high-end Digital Architect. Analyze the BRIEFING.
ABSOLUTE RULE: OUTPUT MUST BE 100% GERMAN.
@@ -319,14 +360,16 @@ ${tone}
### OBJECTIVE:
1. **briefingSummary**: Summarize the project's essence for the CUSTOMER.
- FOLLOW PRINCIPLE 1 & 5: Clear, direct, no marketing fluff, no "partnership talk".
- Focus purely on the CUSTOMER'S goal: What are they building, why does it matter to their business, and what is the outcome?
- FOLLOW PRINCIPLE 1 & 5: Clear, direct, no marketing fluff.
- **MIRROR TEST**: Capture unique customer "hooks" or personality (e.g., if they mention an Instagram account or a specific hobby/preference that influences the project vibe).
- Focus 100% on the BRIEFING (TRUTH SOURCE). Ignore the CRAWL context for this narrative.
- Keep it 2-3 professional, direct sentences.
2. **designVision**: A solid, grounded, and high-quality description of the look & feel.
- FOLLOW PRINCIPLE 1 & 3: Fact-based, professional, high density of information.
- **BESPOKE ELEMENTS**: If the client mentions specific layout ideas (e.g., "Timeline", "Grid", "Industrial Tiles"), incorporate these as strategic decisions.
- **NO ARROGANCE**: Eliminate all "high-end", "world-class", "dominance" language. Be humble and precise.
- **SIMPLE & CLEAR**: Use simple German. No buzzwords. "Solid Industrial Design" instead of "Technocratic Sovereignty".
- 3-4 sentences of deep analysis.
- 3-5 sentences of deep analysis.
### OUTPUT FORMAT (Strict JSON):
{
@@ -336,7 +379,7 @@ ${tone}
`;
const p3Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'google/gemini-3-flash-preview',
messages: [{ role: 'system', content: pass3SystemPrompt }, { role: 'user', content: briefing }],
messages: [{ role: 'system', content: pass3SystemPrompt }, { role: 'user', content: `BRIEFING (TRUTH SOURCE):\n${briefing}` }],
response_format: { type: 'json_object' }
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
addUsage(p3Resp.data);
@@ -365,7 +408,7 @@ ${JSON.stringify({ facts, strategy }, null, 2)}
`;
const p4Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'google/gemini-3-flash-preview',
messages: [{ role: 'system', content: pass4SystemPrompt }, { role: 'user', content: briefing }],
messages: [{ role: 'system', content: pass4SystemPrompt }, { role: 'user', content: `BRIEFING (TRUTH SOURCE):\n${briefing}\n\nDISTILLED_CRAWL (CONTEXT):\n${distilledCrawl}` }],
response_format: { type: 'json_object' }
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
addUsage(p4Resp.data);
@@ -388,26 +431,29 @@ Each position in the quote must be perfectly justified and detailed.
- **Schnittstellen (API)**: REAL Data Syncs (CRM, ERP). DO NOT include Tracking, Google Maps, or simple Video embedding here. Basic embedding is "Basis Website Setup".
- **Sorglos-Betrieb (Hosting)**: Hosting & Maintenance.
### RULES FOR positionDescriptions:
### RULES FOR positionDescriptions (STRICT):
1. **ZERO GENERALIZATION**: Do NOT say "Verschiedene Funktionen".
2. **ITEMIZED SYNTHESIS**: Mention EVERY component selected in Pass 1.
3. **BREVITY & DENSITY**: Max 1-2 short sentences. Focus on TASKS not RESULTS.
4. **STYLE**: Direct, engineering-grade, no fluff.
5. **LANGUAGE**: 100% GERMAN.
6. **SPECIFIC - PAGES**: For "Individuelle Seiten", list the pages as a comma-separated list (e.g. "Umfasst: Startseite, Über uns, Leistungen, Kontakt, Impressum").
7. **SPECIFIC - API**: Video Uploads, Google Maps, and Tracking are NOT APIs. If video/maps are standard embedding, do NOT put them in "Schnittstellen".
8. **SPECIFIC - HOSTING**: Always append: "Inkl. 20GB Speicher. Auto-Erweiterung +10€/10GB."
9. **SPECIFIC - LOGIC**: Describe the ACTUAL logic.
- BAD: "Erweiterte Formulare", "Logikfunktionen"
- GOOD: "Anfrage-Strecken mit Validierung", "Filterung nach Kategorie", "Mehrsprachigkeits-Routing".
2. **ITEMIZED SYNTHESIS**: Mention EVERY component selected in Pass 1.
3. **HARD SPECIFICS**: Preserve technical details from the briefing (e.g., "110 kV", "HDD-Bohrtechnik", specific industry standards).
4. **BREVITY & DENSITY**: Max 1-2 short sentences. Focus on TASKS not RESULTS.
5. **STYLE**: Direct, engineering-grade, no fluff.
6. **LANGUAGE**: 100% GERMAN.
7. **SPECIFIC - PAGES**: For "Individuelle Seiten", list the pages as a comma-separated list.
8. **SPECIFIC - API**: Video Uploads, Google Maps, and Tracking are NOT APIs.
9. **SPECIFIC - HOSTING**: Always append: "Inkl. 20GB Speicher. Auto-Erweiterung +10€/10GB."
10. **SPECIFIC - LOGIC**: Describe the ACTUAL logic. NEVER use generic terms like "Erweiterte Formulare" or "Individuelle Formular-Logik".
11. **STRICT KEYS**: Keys MUST be EXACTLY: "Basis Website Setup", "Individuelle Seiten", "System-Module", "Logik-Funktionen", "Schnittstellen (API)", "Inhaltsverwaltung (CMS)", "Sorglos-Betrieb (Hosting)".
### FORBIDDEN PHRASES (STRICT BLOCKLIST):
- "Erweiterte Formulare" (INSTEAD USE: "Komplexe Anfrage-Logik" or "Valide Formular-Systeme")
- "Verschiedene Funktionen"
- "Allgemeine Logik"
- "Optimierte Darstellung"
### EXAMPLES (FEW-SHOT):
- **BAD**: "Individuelle Seiten für die Unternehmensdarstellung."
- **GOOD**: "Erstellung der Seiten: Startseite (Video-Hero), Über uns (Timeline), Leistungen (110kV Montage), Kontakt."
- **BAD**: "Anbindung von externen Systemen."
- **GOOD**: "Native Integration von Google Maps zur Standortermittlung inkl. individueller Marker-Logik."
10. **NO "MARKETING LINGO"**: Never say "avoids branding" or "maximizes performance". Say "Implements HTML5 Video Player". ALWAYS DESCRIBE THE TASK.
### FORBIDDEN PHRASES:
- "Erweiterte Formulare", "Verschiedene Funktionen", "Allgemeine Logik", "Optimierte Darstellung", "Individuelle Formular-Logik".
11. **NO "MARKETING LINGO"**: Never say "avoids branding" or "maximizes performance". Say "Implements HTML5 Video Player". ALWAYS DESCRIBE THE TASK.
### DATA CONTEXT:
${JSON.stringify({ facts, details, strategy, ia }, null, 2)}
@@ -425,48 +471,60 @@ ${JSON.stringify({ facts, details, strategy, ia }, null, 2)}
addUsage(p5Resp.data);
const positionsData = JSON.parse(p5Resp.data.choices[0].message.content);
// 6. PASS 6: Reflection & Hardening
console.log(' ↳ Pass 6: Reflection & Nuance Check...');
// 6. PASS 6: The Industrial Critic
console.log(' ↳ Pass 6: The Industrial Critic (Quality Gate)...');
const pass6SystemPrompt = `
You are a senior supervisor. Compare the CURRENT_STATE against the RAW_BRIEFING.
Your goal is to catch missed nuances, specific customer wishes, and technical details.
You are the "Industrial Critic". Your goal is to catch quality regressions and ensure the document is bespoke, technical, and professional.
Analyze the CURRENT_STATE against the BRIEFING_TRUTH.
### CHECKLIST:
1. **SPECIFICS**: Did we miss names, technical terms (kV, HDD, etc.), or specific vendor refs?
2. **CONSISTENCY**: Do the positionDescriptions match the counts of features/functions in Pass 1?
3. **DEADLINE**: Is there a specific month? (e.g. April/Mai). If yes, set "deadline" field.
4. **LANGUAGE**: ABSOLUTE RULE: EVERYTHING MUST BE GERMAN.
5. **CONFLICT CHECK**: If 'languagesList' has only 1 item, REMOVE 'multilang' from 'functions'.
6. Refactor 'dontKnows' into a 'gridDontKnows' object for missing technical facts.
### CRITICAL ERROR CHECKLIST (FAIL IF FOUND):
1. **Placeholder Leakage**: Catch "null", "undefined", or generic strings like "Verschiedene Funktionen", "Erweiterte Formulare", "Individuelle Formular-Logik".
2. **Detail Loss**: The user mentioned specific terms (e.g., "110 kV", "HDD", "Timeline", "Instagram"). Are they present in the positionDescriptions or sitemap? If not, ADD THEM.
3. **Consistency**: Ensure the count of pages in "Individuelle Seiten" matches the sitemap pages.
4. **Deadlines**: Ensure relative dates (e.g., "April / Mai") are resolved to the year 2026.
5. **Tone Drift**: Remove any marketing "fluff" or "sales-y" language. Maintain the "Industrial Design" persona.
### CURRENT_STATE:
### MISSION:
Return updated fields ONLY. Specifically focus on hardening 'positionDescriptions', 'sitemap', and 'briefingSummary'.
### DATA CONTEXT:
${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)}
`;
const p6Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'google/gemini-3-flash-preview',
messages: [{ role: 'system', content: pass6SystemPrompt }, { role: 'user', content: `RAW_BRIEFING:\n${briefing}\n\nEnhance the state. Return ONLY the delta or the corrected fields.` }],
messages: [{ role: 'system', content: pass6SystemPrompt }, { role: 'user', content: `BRIEFING_TRUTH:\n${briefing}` }],
response_format: { type: 'json_object' }
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
addUsage(p6Resp.data);
const reflection = JSON.parse(p6Resp.data.choices[0].message.content);
let finalState = {
// 6. Reflection Merge Utility
const mergeReflection = (state: any, reflection: any) => {
let result = { ...state };
const unwrap = (obj: any): any => {
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj;
if (obj["0"]) return unwrap(obj["0"]);
if (obj.state) return unwrap(obj.state);
if (obj.facts) return unwrap(obj.facts);
if (obj.strategy) return unwrap(obj.strategy);
if (obj.ia) return unwrap(obj.ia);
if (obj.positionsData) return unwrap(obj.positionsData);
return obj;
};
const cleanedReflection = unwrap(reflection);
return { ...result, ...cleanedReflection };
};
let finalState = mergeReflection({
...initialState,
...facts,
...strategy,
...ia,
...positionsData,
...reflection,
statusQuo: facts.isRelaunch ? 'Relaunch' : 'Neuentwicklung'
};
...positionsData
}, reflection);
// Flatten if AI nested everything under "0", "state" or "state.0"
if (finalState["0"]) finalState = { ...finalState, ...finalState["0"] };
if ((finalState as any).state) {
const nestedState = (finalState as any).state;
finalState = { ...finalState, ...nestedState };
if (nestedState["0"]) finalState = { ...finalState, ...nestedState["0"] };
}
finalState.statusQuo = facts.isRelaunch ? 'Relaunch' : 'Neuentwicklung';
// Normalization Layer: Map hallucinated German keys back to internal keys
const normalizationMap: Record<string, string> = {

View File

@@ -15,8 +15,8 @@ const __dirname = path.dirname(__filename);
async function main() {
const args = process.argv.slice(2);
const isInteractive = args.includes('--interactive') || args.includes('-I');
const isEstimationOnly = args.includes('--estimation') || args.includes('-E');
const inputPath = args.find((_, i) => args[i - 1] === '--input' || args[i - 1] === '-i');
const outputPath = args.find((_, i) => args[i - 1] === '--output' || args[i - 1] === '-o');
let state = { ...initialState };
@@ -39,7 +39,7 @@ async function main() {
const monthlyPrice = calculateMonthly(state);
const totalPagesCount = (state.selectedPages?.length || 0) + (state.otherPages?.length || 0) + (state.otherPagesCount || 0);
const finalOutputPath = outputPath || generateDefaultPath(state);
const finalOutputPath = generateDefaultPath(state);
const outputDir = path.dirname(finalOutputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
@@ -66,7 +66,9 @@ async function main() {
createElement(CombinedQuotePDF as any, {
estimationProps,
techDetails: getTechDetails(),
principles: getPrinciples()
principles: getPrinciples(),
mode: isEstimationOnly ? 'estimation' : 'full',
showAgbs: !isEstimationOnly // AGBS only for full quotes
}) as any,
finalOutputPath
);
@@ -144,8 +146,10 @@ function generateDefaultPath(state: any) {
const now = new Date();
const month = now.toISOString().slice(0, 7);
const day = now.toISOString().slice(0, 10);
// Add seconds and minutes for 100% unique names without collision
const time = now.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' }).replace(/:/g, '-');
const company = (state.companyName || state.name || 'Unknown').replace(/[^a-z0-9]/gi, '_');
return path.join(process.cwd(), 'out', 'estimations', month, `${day}_${company}_${state.projectType}.pdf`);
return path.join(process.cwd(), 'out', 'estimations', month, `${day}_${time}_${company}_${state.projectType}.pdf`);
}
main().catch(err => {