From c5ad6108eedb5854e56f6f5454bacd53a3cd6d45 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Thu, 5 Feb 2026 01:53:12 +0100 Subject: [PATCH] feat: Rename "Betrieb & Hosting" to "Sorglos-Paket" and refine PDF pricing module layout and text. --- docs/PRICING.md | 16 +- scripts/ai-estimate.ts | 486 +++++++++++++----- .../components/PriceCalculation.tsx | 2 +- .../pdf/modules/BrandingModules.tsx | 10 +- src/components/pdf/modules/CommonModules.tsx | 70 ++- src/logic/pricing/calculator.ts | 4 +- src/logic/pricing/constants.ts | 14 +- .../default/SDK_CRAWLER_STATISTICS_0.json | 83 +-- .../default/SDK_SESSION_POOL_STATE.json | 94 +++- 9 files changed, 502 insertions(+), 277 deletions(-) diff --git a/docs/PRICING.md b/docs/PRICING.md index df0acee..d7a7ca0 100644 --- a/docs/PRICING.md +++ b/docs/PRICING.md @@ -4,7 +4,7 @@ Preise Basis -6.000 € einmalig +4.000 € einmalig Die Grundlage für jede Website: • Projekt-Setup & Infrastruktur @@ -23,7 +23,7 @@ Enthält keine Seiten, Inhalte oder Funktionen. Seite -800 € / Seite +600 € / Seite Individuell gestaltete Seite – mit Layout, Struktur, Textaufteilung, responsivem Design. @@ -32,7 +32,7 @@ mit Layout, Struktur, Textaufteilung, responsivem Design. Feature (System) -2.000 € / Feature +1.500 € / Feature Ein in sich geschlossenes System mit Datenstruktur, Darstellung und Pflegefähigkeit. @@ -50,7 +50,7 @@ Ein Feature erzeugt ein Datenmodell, Übersichten & Detailseiten. Funktion (Logik) -1.000 € / Funktion +800 € / Funktion Funktionen liefern Logik und Interaktion, z. B.: • Kontaktformular @@ -73,7 +73,7 @@ Jede Funktion ist ein klar umrissener Logikbaustein. Visuelle Inszenierung -2.000 € / Abschnitt +1.500 € / Abschnitt Erweiterte Gestaltung: • Hero-Story @@ -100,7 +100,7 @@ Dargestellte, interaktive UI-Erlebnisse: Neuer Datensatz -400 € / Stück +200 € / Stück Beispiele: • Produkt @@ -126,7 +126,7 @@ Datensatz anpassen Hosting & Betrieb -12 Monate = 1440 € +12 Monate = 3.000 € Sichert: • Webhosting & Verfügbarkeit @@ -195,7 +195,7 @@ Für: API-Schnittstelle / Daten-Sync -1.000 € / Zielsystem +800 € / Zielsystem Synchronisation zu externem System (Push): • Produkt-Sync diff --git a/scripts/ai-estimate.ts b/scripts/ai-estimate.ts index e874648..f6549eb 100644 --- a/scripts/ai-estimate.ts +++ b/scripts/ai-estimate.ts @@ -7,7 +7,8 @@ import { execSync } from 'node:child_process'; import axios from 'axios'; import { FileCacheAdapter } from '../src/utils/cache/file-adapter.js'; -import { initialState } from '../src/logic/pricing/constants.js'; +import { initialState, PRICING } from '../src/logic/pricing/constants.js'; +import { calculateTotals } from '../src/logic/pricing/calculator.js'; async function main() { const OPENROUTER_KEY = process.env.OPENROUTER_KEY; @@ -19,6 +20,7 @@ async function main() { let briefing = ''; let targetUrl: string | null = null; let comments: string | null = null; + let budget: string | null = null; let cacheKey: string | null = null; let jsonStatePath: string | null = null; @@ -32,6 +34,8 @@ async function main() { targetUrl = args[++i]; } else if (arg === '--comments' || arg === '--notes') { comments = args[++i]; + } else if (arg === '--budget') { + budget = args[++i]; } else if (arg === '--cache-key') { cacheKey = args[++i]; } else if (arg === '--json') { @@ -77,7 +81,7 @@ async function main() { } const cache = new FileCacheAdapter({ prefix: 'ai_est_' }); - const finalCacheKey = cacheKey || `${briefing}_${targetUrl}_${comments}`; + const finalCacheKey = cacheKey || `${briefing}_${targetUrl}_${comments}_${budget}`; // 1. Crawl if URL provided let crawlContext = ''; @@ -103,6 +107,11 @@ async function main() { distilledCrawl = await distillCrawlContext(crawlContext, OPENROUTER_KEY); await cache.set(`distilled_${targetUrl}`, distilledCrawl, 86400); } + } else if (targetUrl) { + distilledCrawl = `WARNING: The crawl of ${targetUrl} failed (ENOTFOUND or timeout). + The AI must NOT hallucinate details about the current website. + Focus ONLY on the BRIEFING provided. If details are missing, mark them as 'unknown'.`; + console.warn('⚠️ Crawl failed. AI will be notified to avoid hallucinations.'); } // 3. AI Prompting @@ -124,7 +133,7 @@ async function main() { console.log('📦 Using cached AI response.'); formState = cachedAi; } else { - const result = await getAiEstimation(briefing, distilledCrawl, comments, OPENROUTER_KEY, principles, techStandards, tone); + const result = await getAiEstimation(briefing, distilledCrawl, comments, budget, OPENROUTER_KEY, principles, techStandards, tone); formState = result.state; usage = result.usage; await cache.set(finalCacheKey, formState); @@ -251,14 +260,24 @@ async function performCrawl(url: string): Promise { const cleanJson = (str: string) => { // Remove markdown code blocks if present let cleaned = str.replace(/```json\n?|```/g, '').trim(); + // Remove potential control characters that break JSON.parse - cleaned = cleaned.replace(/[\u0000-\u001F\u007F-\u009F]/g, " "); - // Remove trailing commas before closing braces/brackets + // We keep \n \r \t for now as they might be escaped or need handling + cleaned = cleaned.replace(/[\u0000-\u0009\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, " "); + + // Specific fix for Gemini: raw newlines inside strings + // This is tricky. We'll try to escape newlines that are NOT followed by a quote and colon (property start) + // or a closing brace/bracket. This is heuristic. + // A better way is to replace all raw newlines that are preceded by a non-backslash with \n + // but only if they are inside double quotes. + + // Simplest robust approach: Remove trailing commas and hope response_format does its job. cleaned = cleaned.replace(/,\s*([\]}])/g, '$1'); + return cleaned; }; -const getAiEstimation = async (briefing: string, distilledCrawl: string, comments: string | null, apiKey: string, principles: string, techStandards: string, tone: string) => { +const getAiEstimation = async (briefing: string, distilledCrawl: string, comments: string | null, budget: 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) { @@ -281,90 +300,150 @@ const getAiEstimation = async (briefing: string, distilledCrawl: string, comment You are a precision sensor. Analyze the BRIEFING and extract ONLY the raw facts. Tone: Literal, non-interpretive. Output language: GERMAN (Strict). +Output format: ROOT LEVEL JSON (No wrapper keys like '0' or 'data'). ### 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**: The full legal and brand name (e.g., "E-TIB GmbH"). Use signatures and crawl data. -- Extract **personName**: The name of the primary human contact (e.g., "Danny Joseph"). **CRITICAL**: Check email signatures and "Mit freundlichen Grüßen" blocks. DO NOT use "Sie", "Firma" or generic terms if a name exists. -- Extract **existingWebsite**: The primary URL mentioned in the briefing or signature (e.g., "www.e-tib.com"). -- Extract **websiteTopic**: A short descriptor of the CORE BUSINESS (e.g., "Kabeltiefbau"). MAX 3 WORDS. -- **isRelaunch**: Set to TRUE if the briefing mentions an existing website, a URL, or if the company is an established entity (e.g. "Gruppe", "GmbH seit 20XX"). Assume a presence exists that needs a modern "Zentrale Webpräsenz". -- **CRITICAL LOGIC**: If a URL is mentioned, isRelaunch MUST be TRUE. -- For all textual values (deadline, websiteTopic, targetAudience etc.): USE GERMAN. -- **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. +### PRICING REFERENCE (FOR CALCULATION): +- Base Project (Infrastructure + 12 Months Hosting): 5.440 € (MANDATORY START) +- Additional Pages: 600 € / stk +- System-Modules (Features): 1.500 € / stk +- Logic-Functions: 800 € / stk +- API Integrations: 800 € / stk +- CMS Setup: 1.500 € (optional) +- Visual Staging/Interactions: 1.500 € - 2.000 € -### CATEGORY MAPPING (IDs ONLY): -- **selectedPages**: [Home, About, Services, Contact, Landing, Legal] -- **features**: [blog_news, products, jobs, refs, events] -- **functions**: [search, filter, pdf, forms, members, calendar, multilang, chat] -- **apiSystems**: [crm_erp, payment, marketing, ecommerce, maps, social, analytics] -- **assets**: [existing_website, logo, styleguide, content_concept, media, icons, illustrations, fonts] +${budget ? `### BUDGET LOGIC (ULTRA-STRICT): +1. **Mental Calculation**: Start with 7.000 €. Add items based on the reference above. +2. **Hard Ceiling**: If total > ${budget}, you MUST discard lower priority items. +3. **Priority**: High-End Design and Core Pages > Features. +4. **Restriction**: For ${budget}, do NOT exceed 2 features and 4 extra pages. +5. THE TOTAL COST CALCULATED BY THESE RULES MUST BE <= ${budget}. +6. Do NOT mention the budget in any string fields.` : ''} -### OUTPUT FORMAT (Strict JSON): +- ** features **: Items from the FEATURE_REFERENCE. + - ** ABSOLUTE CONSERVATIVE RULE **: Only use features if the briefing implies *dynamic complexity* (CMS, filtering, search, database). + - Simple keywords like 'Karriere', 'Referenzen', 'Messen' or lists of items MUST be treated as simple pages. Add them to 'otherPages' instead. + - If in doubt, categorizing as a PAGE is the mandatory default. +- ** otherPages **: Any specific pages mentioned (e.g. 'Historie', 'Team', 'Partner') that are not in the standard list. Use this for static lists of jobs or references too. +- ** companyName **: The full legal and brand name (e.g., "E-TIB GmbH"). Use signatures and crawl data. +- ** personName **: The name of the primary human contact (e.g., "Danny Joseph"). ** CRITICAL **: Check email signatures and "Mit freundlichen Grüßen" blocks. +- ** email **: The email address of the contact person if found in the briefing / signature. +- ** existingWebsite **: The primary URL mentioned in the briefing or signature (e.g., "www.e-tib.com"). +- ** websiteTopic **: A short descriptor of the CORE BUSINESS (e.g., "Kabeltiefbau"). MAX 3 WORDS. +- ** isRelaunch **: Set to TRUE if the briefing mentions an existing website, a URL, or if the company is an established entity. +- ** CRITICAL LOGIC **: If a URL is mentioned, isRelaunch MUST be TRUE. +- For all textual values: USE GERMAN. +- ** multilang **: ONLY if the briefing mentions multiple target languages. +- ** maps **: If "Google Maps" or location maps are mentioned or implicit. +- ** CRITICAL **: Do NOT include "social" in apiSystems unless the user explicitly wants to SYNC / POST content. + +### CATEGORY MAPPING(IDs ONLY): +- ** selectedPages **: [Home, About, Services, Contact, Landing, Legal] + - ** features **: [blog_news, products, jobs, refs, events] + - ** functions **: [search, filter, pdf, forms, members, calendar, multilang, chat] + - ** apiSystems **: [crm_erp, payment, marketing, ecommerce, maps, social, analytics] + - ** assets **: [existing_website, logo, styleguide, content_concept, media, icons, illustrations, fonts] + +### OUTPUT FORMAT(Strict JSON - ROOT LEVEL): { - "companyName": string, - "companyAddress": string, - "personName": string, - "existingWebsite": string, - "websiteTopic": string, - "isRelaunch": boolean, - "selectedPages": string[], - "features": string[], - "functions": string[], - "apiSystems": string[], - "assets": string[], - "deadline": string (GERMAN), - "targetAudience": "B2B" | "B2C" | "Internal" | string (GERMAN), - "expectedAdjustments": "low" | "medium" | "high" | string (GERMAN), - "employeeCount": "ca. 10+" | "ca. 50+" | "ca. 100+" | "ca. 250+" | "ca. 500+" | "ca. 1000+" + "companyName": string, + "companyAddress": string, + "personName": string, + "email": string, + "existingWebsite": string, + "websiteTopic": string, + "isRelaunch": boolean, + "selectedPages": string[], + "features": string[], + "functions": string[], + "apiSystems": string[], + "assets": string[], + "deadline": string(GERMAN), + "targetAudience": "B2B" | "B2C" | "Internal" | string(GERMAN), + "expectedAdjustments": "low" | "medium" | "high" | string(GERMAN), + "employeeCount": "ca. 10+" | "ca. 50+" | "ca. 100+" | "ca. 250+" | "ca. 500+" | "ca. 1000+" } `; - const pass1UserPrompt = `BRIEFING (TRUTH SOURCE):\n${briefing}\n\nCOMMENTS:\n${comments}\n\nDISTILLED_CRAWL (CONTEXT ONLY):\n${distilledCrawl}`; + 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 }], response_format: { type: 'json_object' } - }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); + }, { headers: { 'Authorization': `Bearer ${apiKey} `, 'Content-Type': 'application/json' } }); if (!p1Resp.data.choices?.[0]?.message?.content) { console.error('❌ Pass 1 failed. Response:', JSON.stringify(p1Resp.data, null, 2)); throw new Error('Pass 1: No content in response'); } const facts = JSON.parse(cleanJson(p1Resp.data.choices[0].message.content)); + // 1.5. PASS 1.5: The Feature Auditor (Skeptical Review) + console.log(' ↳ Pass 1.5: The Feature Auditor (Skeptical Review)...'); + const pass15SystemPrompt = ` +You are a "Strict Cost Controller". Your mission is to prevent over-billing. +Review the extracted FEATURES and the BRIEFING. + +### RULE OF THUMB: +- A "Feature" (1.500 €) is ONLY justified for complex, dynamic systems (logic, database, CMS-driven management, advanced filtering). +- Simple lists, information sections, or static descriptions (e.g., "Messen", "Team", "Historie", "Jobs" as mere text) are ALWAYS "Pages" (600 €). +- If the briefing doesn't explicitly mention "Management System", "Filterable Database", or "Client Login", it is likely a PAGE. + +### MISSION: +Analyze each feature in the list. Decide if it should stay a "Feature" or be downgraded to the "otherPages" array. + +### OUTPUT FORMAT: +Return only the corrected 'features' and 'otherPages' arrays. +{ + "features": string[], + "otherPages": string[] +} +`; + const p15Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', { + model: 'google/gemini-3-flash-preview', + messages: [ + { role: 'system', content: pass15SystemPrompt }, + { role: 'user', content: `EXTRACTED_FEATURES: ${JSON.stringify(facts.features)} \nBRIEFING: \n${briefing}` } + ], + response_format: { type: 'json_object' } + }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); + addUsage(p15Resp.data); + const auditResult = JSON.parse(cleanJson(p15Resp.data.choices[0].message.content)); + + // Apply Audit: Downgrade features to otherPages + facts.features = auditResult.features || []; + facts.otherPages = Array.from(new Set([...(facts.otherPages || []), ...(auditResult.otherPages || [])])); + // 2. PASS 2: Feature Deep-Dive console.log(' ↳ Pass 2: Feature Deep-Dive...'); const pass2SystemPrompt = ` -You are a detail-oriented Solution Architect. -For EVERY item selected in Pass 1 (pages, features, functions, apiSystems), write a specific justification and technical scope. +You are a detail - oriented Solution Architect. +For EVERY item selected in Pass 1(pages, features, functions, apiSystems), write a specific justification and technical scope. ### RULES: -1. **CONCRETE & SPECIFIC**: Do NOT say "Implementation of X". Say "Displaying X with Y filters". -2. **NO EFFECTS**: Do not mention "fade-ins", "animations" or "visual styling". Focus on FUNCTION. -3. **ABSOLUTE RULE**: EVERYTHING MUST BE GERMAN. -4. **TRANSPARENCY**: Explain exactly what the USER gets. -5. **API NOTE**: For 'media' or 'video', explicitly state "Upload & Integration" (NO STREAMING). +1. ** CONCRETE & SPECIFIC **: Do NOT say "Implementation of X". Say "Displaying X with Y filters". +2. ** JUSTIFICATION (CRITICAL) **: For every entry in 'featureDetails', explicitly explain WHY this is a complex system (1.500 €) and not just a static page (600 €). If it's just a list of items, it's a PAGE. +3. ** NO EFFECTS **: Do not mention "fade-ins", "animations" or "visual styling". Focus on FUNCTION. +4. ** ABSOLUTE RULE **: EVERYTHING MUST BE GERMAN. +5. ** TRANSPARENCY **: Explain exactly what the USER gets. +6. ** API NOTE **: For 'media' or 'video', explicitly state "Upload & Integration" (NO STREAMING). ### INPUT (from Pass 1): ${JSON.stringify(facts, null, 2)} -### OUTPUT FORMAT (Strict JSON): +### OUTPUT FORMAT(Strict JSON): { - "pageDetails": { "Home": string, ... }, - "featureDetails": { "blog_news": string, ... }, - "functionDetails": { "search": string, ... }, - "apiDetails": { "crm_erp": string, ... } + "pageDetails": { "Home": string, ... }, + "featureDetails": { "blog_news": string, ... }, + "functionDetails": { "search": string, ... }, + "apiDetails": { "crm_erp": string, ... } } `; const p2Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', { model: 'google/gemini-3-flash-preview', messages: [{ role: 'system', content: pass2SystemPrompt }, { role: 'user', content: briefing }], response_format: { type: 'json_object' } - }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); + }, { headers: { 'Authorization': `Bearer ${apiKey} `, 'Content-Type': 'application/json' } }); addUsage(p2Resp.data); if (!p2Resp.data.choices?.[0]?.message?.content) { console.error('❌ Pass 2 failed. Response:', JSON.stringify(p2Resp.data, null, 2)); @@ -375,39 +454,39 @@ ${JSON.stringify(facts, null, 2)} // 3. PASS 3: Strategic Content (Bespoke Strategy) console.log(' ↳ Pass 3: Strategic Content (Bespoke Strategy)...'); const pass3SystemPrompt = ` -You are a high-end Digital Architect. Your goal is to make the CUSTOMER feel 100% understood. +You are a high - end Digital Architect.Your goal is to make the CUSTOMER feel 100 % understood. Analyze the BRIEFING and the EXISTING WEBSITE context. -### TONE & COMMUNICATION PRINCIPLES (STRICT): +### TONE & COMMUNICATION PRINCIPLES(STRICT): ${tone} ### OBJECTIVE: -3. **briefingSummary**: Ein sachlicher, tiefgehender Überblick der Unternehmenslage. - - **STIL**: Keine Ich-Form. Keine Marketing-Floskeln. Nutze präzise Fachbegriffe. Sei prägnant und effizient (ca. 70% der vorherigen Länge). - - **FORM**: EXAKT ZWEI ABSÄTZE. Insgesamt ca. 6 Sätze. - - **INHALT**: Welcher technologische Sprung ist notwendig? Was ist der Status Quo? (Bezug zur URL/Briefing). - - **ABSOLUTE REGEL**: Keine Halluzinationen über fehlende Präsenzen bei Relaunches. - - **DATENSCHUTZ**: KEINERLEI namentliche Nennungen von Personen (z. B. "Danny Joseph") in diesen Texten. -4. **designVision**: Ein abstraktes, strategisches Konzept. - - **STIL**: Rein konzeptionell. Keine Umsetzungsschritte. Keinerlei "To-dos". Keine Ich-Form. Sei prägnant. - - **FORM**: EXAKT ZWEI ABSÄTZE. Insgesamt ca. 4 Sätze. - - **DATENSCHUTZ**: KEINERLEI namentliche Nennungen von Personen in diesen Texten. - - **FOKUS**: Welche strategische Wirkung soll erzielt werden? (Z. B. "Industrielle Souveränität"). +3. ** briefingSummary **: Ein sachlicher, tiefgehender Überblick der Unternehmenslage. + - ** STIL **: Keine Ich - Form.Keine Marketing - Floskeln.Nutze präzise Fachbegriffe.Sei prägnant und effizient(ca. 70 % der vorherigen Länge). + - ** FORM **: EXAKT ZWEI ABSÄTZE.Insgesamt ca. 6 Sätze. + - ** INHALT **: Welcher technologische Sprung ist notwendig ? Was ist der Status Quo ? (Bezug zur URL / Briefing). + - ** ABSOLUTE REGEL **: Keine Halluzinationen über fehlende Präsenzen bei Relaunches. + - ** DATENSCHUTZ **: KEINERLEI namentliche Nennungen von Personen(z.B. "Danny Joseph") in diesen Texten. +4. ** designVision **: Ein abstraktes, strategisches Konzept. + - ** STIL **: Rein konzeptionell.Keine Umsetzungsschritte.Keinerlei "To-dos".Keine Ich - Form.Sei prägnant. + - ** FORM **: EXAKT ZWEI ABSÄTZE.Insgesamt ca. 4 Sätze. + - ** DATENSCHUTZ **: KEINERLEI namentliche Nennungen von Personen in diesen Texten. + - ** FOKUS **: Welche strategische Wirkung soll erzielt werden ? (Z.B. "Industrielle Souveränität"). -### OUTPUT FORMAT (Strict JSON): +### OUTPUT FORMAT(Strict JSON): { - "briefingSummary": string, - "designVision": string + "briefingSummary": string, + "designVision": string } `; 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 (TRUTH SOURCE):\n${briefing}\n\nEXISTING WEBSITE (CONTEXT):\n${distilledCrawl}\n\nEXTRACTED FACTS:\n${JSON.stringify(facts, null, 2)}` } + { role: 'user', content: `BRIEFING(TRUTH SOURCE): \n${briefing} \n\nEXISTING WEBSITE(CONTEXT): \n${distilledCrawl} \n\nEXTRACTED FACTS: \n${JSON.stringify(facts, null, 2)} ` } ], response_format: { type: 'json_object' } - }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); + }, { headers: { 'Authorization': `Bearer ${apiKey} `, 'Content-Type': 'application/json' } }); addUsage(p3Resp.data); if (!p3Resp.data.choices?.[0]?.message?.content) { console.error('❌ Pass 3 failed. Response:', JSON.stringify(p3Resp.data, null, 2)); @@ -418,29 +497,30 @@ ${tone} // 4. PASS 4: Information Architecture (Sitemap) console.log(' ↳ Pass 4: Information Architecture...'); const pass4SystemPrompt = ` -You are a Senior UX Architect. Design a hierarchical sitemap following the 'Industrial Logic' principle. +You are a Senior UX Architect.Design a hierarchical sitemap following the 'Industrial Logic' principle. EVERYTHING MUST BE IN GERMAN. ### SITEMAP RULES: -1. **HIERARCHY**: Build a logical tree. Group by category (e.g., "Kern-Präsenz", "Lösungen", "Vertrauen", "Rechtliches"). -2. **INTENT**: Each page MUST have a title and a brief functional conversion intent (desc). -3. **COMPREHENSIVENESS**: Ensure all 'selectedPages' and 'features' from Pass 1 are represented. -4. **LANGUAGE**: STRICT GERMAN TITLES. Do NOT use "Home", "About", "Services". Use "Startseite", "Über uns", "Leistungen". +1. ** HIERARCHY **: Build a logical tree.Group by category(e.g., "Kern-Präsenz", "Lösungen", "Vertrauen", "Rechtliches"). +2. ** INTENT **: Each page MUST have a title and a brief functional conversion intent(desc). +3. ** COMPREHENSIVENESS **: Ensure all 'selectedPages' and 'features' from Pass 1 are represented. +4. ** LANGUAGE **: STRICT GERMAN TITLES.Do NOT use "Home", "About", "Services".Use "Startseite", "Über uns", "Leistungen". +5. ** NO IMPLEMENTATION NOTES **: Do NOT add implementation details in parentheses to titles (e.g. NO "Startseite (Hero-Video)", NO "About (Timeline)"). Keep titles clean and abstract. ### DATA CONTEXT: ${JSON.stringify({ facts, strategy }, null, 2)} -### OUTPUT FORMAT (Strict JSON): +### OUTPUT FORMAT(Strict JSON): { - "websiteTopic": string, - "sitemap": [ { "category": string, "pages": [ { "title": string, "desc": string } ] } ] + "websiteTopic": string, + "sitemap": [{ "category": string, "pages": [{ "title": string, "desc": string }] }] } `; 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 (TRUTH SOURCE):\n${briefing}\n\nDISTILLED_CRAWL (CONTEXT):\n${distilledCrawl}` }], + 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' } }); + }, { headers: { 'Authorization': `Bearer ${apiKey} `, 'Content-Type': 'application/json' } }); addUsage(p4Resp.data); if (!p4Resp.data.choices?.[0]?.message?.content) { console.error('❌ Pass 4 failed. Response:', JSON.stringify(p4Resp.data, null, 2)); @@ -450,57 +530,75 @@ ${JSON.stringify({ facts, strategy }, null, 2)} // 5. PASS 5: Position Synthesis & Pricing Transparency console.log(' ↳ Pass 5: Position Synthesis...'); + + // Determine which positions are actually relevant to avoid hallucinations + const requiredPositions = [ + "1. Das technische Fundament", + facts.selectedPages.length + facts.otherPages.length > 0 ? "2. Individuelle Seiten" : null, + facts.features.length > 0 ? "3. System-Module (Features)" : null, + facts.functions.length > 0 ? "4. Logik-Funktionen" : null, + facts.apiSystems.length > 0 ? "5. Schnittstellen (API)" : null, + facts.cmsSetup ? "6. Inhaltsverwaltung (CMS)" : null, + "7. Inszenierung & Interaktion", // Always include for high-end strategy + facts.multilang ? "8. Mehrsprachigkeit" : null, + "9. Inhaltliche Initial-Pflege", + "10. Sorglos-Paket (Betrieb & Pflege)" + ].filter(Boolean); + const pass5SystemPrompt = ` You are a Senior Solution Architect. Your goal is ABSOLUTE TRANSPARENCY and professionalism. Each position in the quote must be perfectly justified and detailed using an objective, technical tone. -### POSITION TITLES (STRICT - MUST MATCH EXACTLY): -"1. Das technische Fundament", "2. Individuelle Seiten", "3. System-Module (Features)", "4. Logik-Funktionen", "5. Schnittstellen (API)", "6. Inhaltsverwaltung (CMS)", "7. Inszenierung & Interaktion", "8. Mehrsprachigkeit", "9. Inhaltliche Initial-Pflege", "10. Laufender Betrieb & Hosting". +### REQUIRED POSITION TITLES (STRICT - ONLY DESCRIBE THESE): +${requiredPositions.map(p => `"${p}"`).join(", ")} ### MAPPING RULES (STRICT): -- **1. Das technische Fundament**: Infrastructure, Hosting setup, SEO-Basics, Analytics, Environments. -- **2. Individuelle Seiten**: Layout/structure for specific pages (Home, About, etc.). -- **3. System-Module (Features)**: Functional systems like Blog, News, Products, Jobs, References. -- **4. Logik-Funktionen**: Logic modules like Search, Filter, Forms, PDF-Export. -- **5. Schnittstellen (API)**: Data Syncs with CRM, ERP, Payment systems. -- **6. Inhaltsverwaltung (CMS)**: Setup and mapping for CMS. -- **7. Inszenierung & Interaktion**: Hero-stories, visual effects, configurators. -- **8. Mehrsprachigkeit**: Architecture scaling for multiple languages. -- **9. Inhaltliche Initial-Pflege**: Manual data entry/cleanup. -- **10. Laufender Betrieb & Hosting**: Ongoing maintenance, updates, 24/7 monitoring. +- ** 1. Das technische Fundament **: Infrastructure, Hosting setup, SEO-Basics, Analytics, Environments. +- ** 2. Individuelle Seiten **: Layout / structure for specific pages. ** RULE **: If quantity is high (e.g. > 10), lead with "Umsetzung von [QTY] individuellen Einzelseiten...". +- ** 3. System-Module (Features) **: Functional systems like Blog, News, Products, Jobs, References. ** RULE **: Describe exactly 1 thing if qty is 1. If qty is 0, DO NOT DESCRIBE THIS. +- ** 4. Logik-Funktionen **: Logic modules like Search, Filter, Forms, PDF-Export. +- ** 5. Schnittstellen (API) **: Data Syncs with CRM, ERP, Payment systems. +- ** 6. Inhaltsverwaltung (CMS) **: Setup and mapping for CMS. +- ** 7. Inszenierung & Interaktion **: Hero-stories, visual effects, configurators. +- ** 8. Mehrsprachigkeit **: Architecture scaling for multiple languages. +- ** 9. Inhaltliche Initial-Pflege **: Manual data entry / cleanup. +- ** 10. Sorglos-Paket (Betrieb & Pflege) **: ** RULE **: Describe as "1 Jahr Sicherung des technischen Betriebs, Instandhaltung, Sicherheits-Updates und Inhalts-Aktualisierungen gemäß AGB Punkt 7a." -### RULES FOR positionDescriptions (STRICT): -1. **ABSOLUTE RULE: NO FIRST PERSON**: NEVER use "Ich", "Mein", "Wir" or "Unser". Lead with nouns or passive verbs. -2. **PROFESSIONAL TONE**: Use "Erstellung von...", "Anbindung der...", "Implementierung technischer...", "Bereitstellung von...". -3. **CONCISE & ITEM-BASED**: Use technical, high-density sentences. Name specific industry terms from context. -4. **ITEMIZED SYNTHESIS**: Mention EVERY component selected in Pass 1. -5. **HARD SPECIFICS**: If the briefing mentions "Glasfaser-Trassen" or "Schwerlast-Logistik", IT MUST BE IN THE DESCRIPTION. -6. **INDUSTRIAL AMBITION**: Describe it as a high-end technical solution. Avoid "schöne Website" or marketing fluff. -7. **PAGES**: For "2. Individuelle Seiten", list the pages (e.g., "Startseite (Hero-Video), Leistungen, Kontakt"). -8. **LOGIC**: Describe the ACTUAL logic (e.g., "Volltextsuche mit Auto-Complete", not "eine Suche"). -9. **KEYS**: Return EXACTLY the keys defined in "POSITION TITLES". -10. **NO AGB**: NEVER mention "AGB" or "Geschäftsbedingungen". +### RULES FOR positionDescriptions(STRICT): +1. ** ABSOLUTE RULE: NO FIRST PERSON **: NEVER use "Ich", "Mein", "Wir" or "Unser". Lead with nouns or passive verbs. +2. ** QUANTITY PARITY (ULTRA-STRICT) **: The description MUST list EXACTLY the number of items matching the 'qty' for that position. If qty is 3, describe exactly 3 items. If qty is 1, describe exactly 1 item. Do NOT "stuff" additional features into one description. +3. ** LOGIC GUARD (CMS) **: If 'cmsSetup' is false in the DATA CONTEXT, you MUST NOT mention "CMS", "Modul", "Management System" or "Inhaltsverwaltung". Use "Statische Seite" or "Darstellung". +4. ** STATIC vs DYNAMIC **: If no complex logic was extracted in Pass 2 for a feature, describe it as a technical layout/page, not as a system. +5. ** PROFESSIONAL TONE **: Use "Erstellung von...", "Anbindung der...", "Implementierung technischer...", "Bereitstellung von...". +6. ** CONCISE & ITEM-BASED **: Use technical, high-density sentences. Name specific industry terms from context. +7. ** ITEMIZED SYNTHESIS **: Mention EVERY component selected in Pass 1. +8. ** HARD SPECIFICS **: If the briefing mentions "Glasfaser-Trassen" or "Schwerlast-Logistik", IT MUST BE IN THE DESCRIPTION. +9. ** INDUSTRIAL AMBITION **: Describe it as a high-end technical solution. Avoid "schöne Website" or marketing fluff. +10. ** PAGES **: For "2. Individuelle Seiten", list the pages. ** ABSOLUTE RULE **: Do NOT add implementation details or technical notes in parentheses (e.g. NO "(Matrix-Struktur)", NO "(Timeline-Modul)"). Use clean titles like "Startseite, Über uns, Leistungen". +11. ** LOGIC **: Describe the ACTUAL logic (e.g., "Volltextsuche mit Auto-Complete", not "eine Suche"). +12. ** KEYS **: Return EXACTLY the keys defined in "POSITION TITLES". +13. ** NO AGB **: NEVER mention "AGB" or "Geschäftsbedingungen". -### EXAMPLES (PASSIVE & TECHNICAL): -- **GOOD**: "Konfiguration der CMS-Infrastruktur zur unabhängigen Verwaltung von Produkt-Katalogen und News-Beiträgen." -- **GOOD**: "Implementierung einer Volltextsuche inkl. Kategorisierungs-Logik für effizientes Auffinden von Projektreferenzen." -- **GOOD**: "Native API-Anbindung an das ERP-System zur Echtzeit-Synchronisation von Bestandsdaten." -- **BAD**: "Ich richte dir das CMS ein." -- **BAD**: "Ich programmiere eine tolle Suche für deine Seite." +### EXAMPLES(PASSIVE & TECHNICAL): +- ** GOOD **: "Konfiguration der CMS-Infrastruktur zur unabhängigen Verwaltung von Produkt-Katalogen und News-Beiträgen." + - ** GOOD **: "Implementierung einer Volltextsuche inkl. Kategorisierungs-Logik für effizientes Auffinden von Projektreferenzen." + - ** GOOD **: "Native API-Anbindung an das ERP-System zur Echtzeit-Synchronisation von Bestandsdaten." + - ** BAD **: "Ich richte dir das CMS ein." + - ** BAD **: "Ich programmiere eine tolle Suche für deine Seite." ### DATA CONTEXT: ${JSON.stringify({ facts, details, strategy, ia }, null, 2)} -### OUTPUT FORMAT (Strict JSON): +### OUTPUT FORMAT(Strict JSON): { - "positionDescriptions": { "1. Das technische Fundament": string, ... } + "positionDescriptions": { "1. Das technische Fundament": string, ... } } `; const p5Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', { model: 'google/gemini-3-flash-preview', messages: [{ role: 'system', content: pass5SystemPrompt }, { role: 'user', content: briefing }], response_format: { type: 'json_object' } - }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); + }, { headers: { 'Authorization': `Bearer ${apiKey} `, 'Content-Type': 'application/json' } }); addUsage(p5Resp.data); if (!p5Resp.data.choices?.[0]?.message?.content) { console.error('❌ Pass 5 failed. Response:', JSON.stringify(p5Resp.data, null, 2)); @@ -511,31 +609,32 @@ ${JSON.stringify({ facts, details, strategy, ia }, null, 2)} // 6. PASS 6: The Industrial Critic console.log(' ↳ Pass 6: The Industrial Critic (Quality Gate)...'); const pass6SystemPrompt = ` -You are the "Industrial Critic". Your goal is to catch quality regressions and ensure the document is bespoke, technical, and professional. +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. -### CRITICAL ERROR CHECKLIST (FAIL IF FOUND): -1. **Hallucination Leakage**: FAIL if names of people (e.g., "Frieder Helmich"), specific software versions, or invented details are used unless they appear EXACTLY in the BRIEFING. - - **CRITICAL**: Forbid "Sie", "Ansprechpartner" or "Unternehmen" for personName if a name IS in the briefing. If none is in briefing, use empty string. -2. **Logic Conflict**: FAIL if isRelaunch is true but briefingSummary claims no website exists. +### CRITICAL ERROR CHECKLIST(FAIL IF FOUND): +1. ** Hallucination Leakage **: FAIL if names of people(e.g., "Frieder Helmich"), specific software versions, or invented details are used unless they appear EXACTLY in the BRIEFING. + - ** CRITICAL **: Forbid "Sie", "Ansprechpartner" or "Unternehmen" for personName if a name IS in the briefing.If none is in briefing, use empty string. +2. ** Logic Conflict **: FAIL if isRelaunch is true but briefingSummary claims no website exists. - FAIL if the description in positionDescriptions mentions more items than extracted in facts. -3. **Implementation Fluff**: FAIL if tech-stack details are mentioned (React, etc.). Focus on Concept & Result. -4. **Genericism Check (CRITICAL)**: FAIL if any text sounds like it could apply to ANY company. It MUST mention specific industry details (e.g., "Kabeltiefbau", "Infrastruktur-Zentrum") from the Briefing or Crawl. -6. **Namen-Verbot (STRICT)**: FAIL if any personal names (e.g. "Danny Joseph", "Joseph", etc.) appear in 'briefingSummary' or 'designVision'. Use abstract terms like "Unternehmensführung" or "Management" if necessary. -7. **AGB BAN**: FAIL if "Allgemeine Geschäftsbedingungen" or "AGB" appear anywhere. -8. **Length Check**: Briefing (ca. 6 Sätze) und Vision (ca. 4 Sätze). Kürze Texte, die zu ausschweifend sind, auf das Wesentliche. +3. ** Implementation Fluff **: FAIL if tech - stack details are mentioned(React, etc.).Focus on Concept & Result. +4. ** Genericism Check(CRITICAL) **: FAIL if any text sounds like it could apply to ANY company.It MUST mention specific industry details(e.g., "Kabeltiefbau", "Infrastruktur-Zentrum") from the Briefing or Crawl. +6. ** Namen-Verbot (STRICT) **: FAIL if any personal names (e.g. "Danny Joseph", "Joseph", etc.) appear in 'briefingSummary' or 'designVision'. Use abstract terms like "Unternehmensführung" or "Management" if necessary. +7. ** LOGIC GUARD (CMS) **: If 'cmsSetup' is false in the DATA CONTEXT, FAIL if any 'positionDescriptions' or 'briefingSummary' mentions "CMS", "Content Management System" or "Inhaltsverwaltung". +8. ** AGB BAN **: FAIL if "Allgemeine Geschäftsbedingungen" or "AGB" appear anywhere. +9. ** Length Check **: Briefing (ca. 6 Sätze) und Vision (ca. 4 Sätze). Kürze Texte, die zu ausschweifend sind, auf das Wesentliche. ### MISSION: -Return updated fields ONLY. Specifically focus on hardening 'positionDescriptions', 'sitemap', 'briefingSummary', and 'designVision'. +Return updated fields ONLY.Specifically focus on hardening 'positionDescriptions', 'sitemap', 'briefingSummary', and 'designVision'. ### 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: `BRIEFING_TRUTH:\n${briefing}` }], + 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' } }); + }, { headers: { 'Authorization': `Bearer ${apiKey} `, 'Content-Type': 'application/json' } }); addUsage(p6Resp.data); if (!p6Resp.data.choices?.[0]?.message?.content) { console.error('❌ Pass 6 failed. Response:', JSON.stringify(p6Resp.data, null, 2)); @@ -548,12 +647,10 @@ ${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)} let result = { ...state }; const unwrap = (obj: any): any => { if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj; + // Always unwrap "0" if it exists, regardless of other keys (AI often nests) 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); + if (obj.state && Object.keys(obj).length === 1) return unwrap(obj.state); + if (obj.facts && Object.keys(obj).length === 1) return unwrap(obj.facts); return obj; }; @@ -576,6 +673,10 @@ ${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)} finalState.statusQuo = facts.isRelaunch ? 'Relaunch' : 'Neuentwicklung'; + // Recipient Mapping + if (finalState.personName) finalState.name = finalState.personName; + if (finalState.email) finalState.email = finalState.email; + // Normalization Layer: Map hallucinated German keys back to internal keys const normalizationMap: Record = { "Briefing-Zusammenfassung": "briefingSummary", @@ -596,6 +697,27 @@ ${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)} } }); + // Final Logic Guard: Strictly strip CMS from ALL descriptions and fields if not enabled + if (!finalState.cmsSetup) { + const stripCMS = (obj: any): any => { + if (typeof obj === 'string') { + return obj.replace(/CMS|Content-Management-System|Inhaltsverwaltung/gi, 'Plattform-Struktur'); + } + if (Array.isArray(obj)) { + return obj.map(stripCMS); + } + if (obj !== null && typeof obj === 'object') { + const newObj: any = {}; + Object.entries(obj).forEach(([k, v]) => { + newObj[k] = stripCMS(v); + }); + return newObj; + } + return obj; + }; + finalState = stripCMS(finalState); + } + // Sitemap Normalization (German keys to internal) if (Array.isArray(finalState.sitemap)) { finalState.sitemap = finalState.sitemap.map((cat: any) => ({ @@ -607,14 +729,41 @@ ${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)} })); } - // Position Descriptions Normalization + // Position Descriptions Normalization (Strict Title Mapping + Index-based Fallback) if (finalState.positionDescriptions) { const normalized: Record = {}; - Object.entries(finalState.positionDescriptions).forEach(([key, value]) => { - const normalizedKey = key === 'titel' || key === 'Title' ? 'title' : key; + const rawPositions = finalState.positionDescriptions; + + // 1. Initial cleanup + Object.entries(rawPositions).forEach(([key, value]) => { const normalizedValue = typeof value === 'object' ? (value as any).beschreibung || (value as any).description || JSON.stringify(value) : value; - normalized[normalizedKey] = normalizedValue as string; + normalized[key] = normalizedValue as string; }); + + // 2. Index-based matching (Map "10. Foo" to "10. Bar") + const standardTitles = [ + "1. Das technische Fundament", + "2. Individuelle Seiten", + "3. System-Module (Features)", + "4. Logik-Funktionen", + "5. Schnittstellen (API)", + "6. Inhaltsverwaltung (CMS)", + "7. Inszenierung & Interaktion", + "8. Mehrsprachigkeit", + "9. Inhaltliche Initial-Pflege", + "10. Sorglos-Paket (Betrieb & Pflege)" + ]; + + standardTitles.forEach(std => { + const prefix = std.split('.')[0] + '.'; // e.g., "10." + // Find any key in the AI output that starts with this number + const matchingKey = Object.keys(normalized).find(k => k.trim().startsWith(prefix)); + if (matchingKey && matchingKey !== std) { + normalized[std] = normalized[matchingKey]; + // Keep the old key too just in case, but prioritize the standard one + } + }); + finalState.positionDescriptions = normalized; } @@ -639,6 +788,69 @@ ${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)} } } + // Final Post-Reflection Budget Sync (Hard Pruning if still over) + if (budget) { + const targetValue = parseInt(budget.replace(/[^0-9]/g, '')); + if (!isNaN(targetValue)) { + console.log(`⚖️ Final Budget Audit(${targetValue} € target)...`); + let currentTotals = calculateTotals(finalState, PRICING); + + // Step-by-step pruning if too expensive + if (currentTotals.totalPrice > targetValue) { + console.log(`⚠️ Budget exceeded(${currentTotals.totalPrice} €).Pruning scope to fit ${targetValue} €...`); + + // 1. Remove optional "other" stuff + finalState.otherFeatures = []; + finalState.otherFunctions = []; + finalState.otherTech = []; + + // 2. Remove non-critical functions + const funcPriority = ['search', 'filter', 'calendar', 'multilang']; + for (const f of funcPriority) { + if (currentTotals.totalPrice <= targetValue) break; + if (finalState.functions.includes(f)) { + finalState.functions = finalState.functions.filter((x: string) => x !== f); + currentTotals = calculateTotals(finalState, PRICING); + } + } + + // 3. Remove least critical features if still over + const featurePriority = ['events', 'blog_news', 'products']; + for (const p of featurePriority) { + if (currentTotals.totalPrice <= targetValue) break; + if (finalState.features.includes(p)) { + finalState.features = finalState.features.filter((f: string) => f !== p); + currentTotals = calculateTotals(finalState, PRICING); + } + } + + // 4. Reduce page count (Selected Pages AND Sitemap) + while (currentTotals.totalPrice > targetValue && (finalState.selectedPages.length > 4 || currentTotals.totalPagesCount > 5)) { + if (finalState.selectedPages.length > 4) { + finalState.selectedPages.pop(); + } + + // Prune Sitemap to match + if (finalState.sitemap && Array.isArray(finalState.sitemap)) { + const lastCat = finalState.sitemap[finalState.sitemap.length - 1]; + if (lastCat && lastCat.pages && lastCat.pages.length > 0) { + lastCat.pages.pop(); + if (lastCat.pages.length === 0) finalState.sitemap.pop(); + } + } + currentTotals = calculateTotals(finalState, PRICING); + } + + // 5. Final fallback: Remove second feature if still over + if (currentTotals.totalPrice > targetValue && finalState.features.length > 1) { + finalState.features.pop(); + currentTotals = calculateTotals(finalState, PRICING); + } + } + console.log(`✅ Final budget audit complete: ${currentTotals.totalPrice} €`); + } + } + return { state: finalState, usage }; } diff --git a/src/components/ContactForm/components/PriceCalculation.tsx b/src/components/ContactForm/components/PriceCalculation.tsx index 4abb7eb..51a2827 100644 --- a/src/components/ContactForm/components/PriceCalculation.tsx +++ b/src/components/ContactForm/components/PriceCalculation.tsx @@ -104,7 +104,7 @@ export function PriceCalculation({
- Betrieb & Hosting + Sorglos-Paket {monthlyPrice.toLocaleString()} € / Monat
diff --git a/src/components/pdf/modules/BrandingModules.tsx b/src/components/pdf/modules/BrandingModules.tsx index 6402c23..31718c0 100644 --- a/src/components/pdf/modules/BrandingModules.tsx +++ b/src/components/pdf/modules/BrandingModules.tsx @@ -54,9 +54,9 @@ export const AboutModule = () => ( - Qualitäts-Zusage + Infrastruktur & Souveränität - Es werden keine instabilen Prototypen geliefert, sondern produktionsreife Software, die skalierbar bleibt und mit dem Unternehmen wächst. Direkt. Sauber. Ohne Ballast. + Es wird keine instabile Prototyp-Software geliefert, sondern produktionsreife Systeme, die technisch skalierbar bleiben. Die Codebasis folgt modernen Standards – bei wachsenden Ansprüchen oder dem Wechsel zu einer Agentur kann der Quellcode jederzeit nahtlos übernommen und weitergeführt werden. @@ -65,8 +65,8 @@ export const AboutModule = () => ( export const CrossSellModule = ({ state }: any) => { const isWebsite = state.projectType === 'website'; - const title = isWebsite ? "Routine-Automatisierung" : "Websites & Ökosysteme"; - const subtitle = isWebsite ? "Maßgeschneiderte Software zur Prozessoptimierung" : "Technische Infrastruktur ohne Kompromisse"; + const title = isWebsite ? "Weitere Potenziale" : "Websites & Ökosysteme"; + const subtitle = isWebsite ? "Automatisierung und Prozessoptimierung" : "Technische Infrastruktur ohne Kompromisse"; return ( <> @@ -77,7 +77,7 @@ export const CrossSellModule = ({ state }: any) => { {isWebsite ? ( <> - Manuelle Alltags-Aufgaben sind teuer und fehleranfällig. In einer separaten Beauftragung können maßgeschneiderte Systeme entwickelt werden, die Routine-Prozesse automatisiert im Hintergrund verarbeiten. + Über die klassische Webpräsenz hinaus werden maßgeschneiderte Lösungen zur Automatisierung von Routine-Prozessen angeboten. Dies ermöglicht eine signifikante Effizienzsteigerung im Tagesgeschäft. Keine Abos. Keine komplexen neuen Systeme. Gezielte Zeitersparnis. Individuelle Analyse diff --git a/src/components/pdf/modules/CommonModules.tsx b/src/components/pdf/modules/CommonModules.tsx index e97d7ab..80b9218 100644 --- a/src/components/pdf/modules/CommonModules.tsx +++ b/src/components/pdf/modules/CommonModules.tsx @@ -5,11 +5,11 @@ import { View as PDFView, Text as PDFText, StyleSheet, Image as PDFImage } from import { DocumentTitle, Divider, COLORS, FONT_SIZES } from '../SharedUI'; const styles = StyleSheet.create({ - section: { marginBottom: 24 }, - pricingGrid: { marginTop: 24 }, - pricingRow: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: COLORS.DIVIDER, paddingVertical: 10, alignItems: 'flex-start' }, - pricingTitle: { width: '30%', fontSize: FONT_SIZES.BODY, fontWeight: 'bold', color: COLORS.CHARCOAL }, - pricingDesc: { width: '55%', fontSize: FONT_SIZES.SUB, color: COLORS.TEXT_DIM, lineHeight: 1.4 }, + section: { marginBottom: 16 }, + pricingGrid: { marginTop: 12 }, + pricingRow: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: COLORS.DIVIDER, paddingVertical: 12, alignItems: 'flex-start' }, + pricingTitle: { width: '30%', fontSize: FONT_SIZES.BODY, fontWeight: 'bold', color: COLORS.CHARCOAL, paddingRight: 15 }, + pricingDesc: { width: '55%', fontSize: FONT_SIZES.SUB, color: COLORS.TEXT_DIM, lineHeight: 1.5, paddingRight: 10 }, pricingTag: { width: '15%', fontSize: FONT_SIZES.BODY, fontWeight: 'bold', textAlign: 'right', color: COLORS.CHARCOAL }, configLabel: { fontSize: FONT_SIZES.BLUEPRINT, color: COLORS.TEXT_LIGHT, textTransform: 'uppercase', marginBottom: 8 }, }); @@ -18,12 +18,11 @@ export const techPageModule = ({ techDetails, headerIcon }: any) => ( <> - Entwicklung von Websites als moderne, performante Websysteme nach industriellen Standards. {techDetails?.map((item: any, i: number) => ( - {item.t} - {item.d} + {item.t} + {item.d} ))} @@ -34,62 +33,57 @@ export const techPageModule = ({ techDetails, headerIcon }: any) => ( export const TransparenzModule = ({ pricing }: any) => ( <> - - Festpreise statt Stundenabrechnung - Maximale Planungssicherheit durch ein modulares Festpreis-System. Die Abrechnung erfolgt nach Ergebnissen statt nach reinem Zeitaufwand. Versteckte Kosten sind durch die modulare Struktur ausgeschlossen. - - - 1. Das technische Fundament - Einrichtung der technischen Infrastruktur, Hosting-Bereitstellung, SEO-Basics sowie Bereitstellung von Test-, Staging- und produktiven Live-Umgebungen. + 1. Fundament + Setup, Infrastruktur, Hosting, SEO-Basics, Staging & Live-Umgebungen. {pricing.BASE_WEBSITE?.toLocaleString('de-DE')} € - 2. Individuelle Seiten - Entwicklung individueller Layouts und Strukturen pro Seite. Optimierung für alle Endgeräte (Responsive Design) und Browser-Kompatibilität. + 2. Seiten + Layout & Umsetzung individueller Seiten. Responsive Design / Cross-Browser. {pricing.PAGE?.toLocaleString('de-DE')} € / Stk - 3. System-Module (Features) - Implementierung abgeschlossener technischer Systeme (z. B. Blog, News, Produkte). Definition notwendiger Datenfelder und Pflege-Oberflächen. + 3. Features + Abgeschlossene Systeme (z. B. Blog, Jobs, Produkte) inkl. Datenstruktur. {pricing.FEATURE?.toLocaleString('de-DE')} € / Stk - 4. Logik-Funktionen - Programmierung technischer Logik-Einheiten wie Filter, Suchen oder Kontakt-Schnittstellen zur fehlerfreien Datenverarbeitung. + 4. Funktionen + Logik-Einheiten wie Filter, Suchen oder Kontakt-Schnittstellen. {pricing.FUNCTION?.toLocaleString('de-DE')} € / Stk - 5. Schnittstellen (API) - Anbindung externer Drittsysteme (CRM, ERP, Payment-Provider) zur automatisierten Datensynchronisation und Prozessoptimierung. + 5. Schnittstellen + Anbindung externer Systeme (CRM, ERP, Payment) zur Synchronisation. ab {pricing.API_INTEGRATION?.toLocaleString('de-DE')} € / Stk - 6. Inhaltsverwaltung (CMS) - Bereitstellung und Konfiguration eines Content Management Systems (CMS) inkl. technischer Anbindung aller Module zur unabhängigen Datenpflege. - {pricing.CMS_SETUP?.toLocaleString('de-DE')} € + 6. CMS Setup + Konfiguration Headless CMS zur unabhängigen Datenpflege aller Module. + {pricing.CMS_SETUP?.toLocaleString('de-DE')} € - 7. Inszenierung & Interaktion - Umsetzung komplexer Interaktions-Mechanismen, Konfiguratoren oder aufwendiger visueller Storytelling-Elemente zur Steigerung der Conversion. + 7. Inszenierung + Interaktions-Mechanismen, Konfiguratoren oder visuelles Storytelling. ab {pricing.VISUAL_STAGING?.toLocaleString('de-DE')} € - 8. Mehrsprachigkeit - Skalierung der System-Architektur auf zusätzliche Sprachen. Inklusive struktureller Anpassungen und Logik für Sprachumschaltung. + 8. Sprachen + Skalierung der System-Architektur auf zusätzliche Sprachversionen. +20% / Sprache - 9. Inhaltliche Initial-Pflege - Manuelle Übernahme und Aufbereitung von Datensätzen in das Zielsystem zur Sicherstellung einer initialen Inhaltsabdeckung. + 9. Initial-Pflege + Manuelle Aufbereitung & Übernahme von Datensätzen in das Zielsystem. {pricing.NEW_DATASET?.toLocaleString('de-DE')} € / Stk - 10. Laufender Betrieb & Hosting - Laufender Betrieb, Hosting, Sicherheits-Updates sowie monatliche Backups und Monitoring zur Sicherstellung der Systemverfügbarkeit. - {pricing.HOSTING_MONTHLY?.toLocaleString('de-DE')} € / Monat + 10. Sorglos-Paket + Betrieb, Hosting, Updates & Monitoring gemäß AGB Punkt 7a. + Inklusive 1 Jahr @@ -99,11 +93,11 @@ export const TransparenzModule = ({ pricing }: any) => ( export const PrinciplesModule = ({ principles }: any) => ( <> - + {principles?.map((item: any, i: number) => ( - {item.t} - {item.d} + {item.t} + {item.d} ))} diff --git a/src/logic/pricing/calculator.ts b/src/logic/pricing/calculator.ts index 7e69c46..c550691 100644 --- a/src/logic/pricing/calculator.ts +++ b/src/logic/pricing/calculator.ts @@ -183,8 +183,8 @@ export function calculatePositions(state: FormState, pricing: any): Position[] { const monthlyRate = pricing.HOSTING_MONTHLY + (state.storageExpansion * pricing.STORAGE_EXPANSION_MONTHLY); positions.push({ pos: pos++, - title: '10. Laufender Betrieb & Hosting', - desc: `Bereitstellung der Infrastruktur, technische Instandhaltung, Sicherheits-Updates und Backup-Management gemäß AGB Punkt 7a. Inklusive 12 Monate Service.`, + title: '10. Sorglos-Paket (Betrieb & Pflege)', + desc: `1 Jahr Sicherung des technischen Betriebs, Instandhaltung, Sicherheits-Updates und Inhalts-Aktualisierungen gemäß AGB Punkt 7a.`, qty: 1, price: monthlyRate * 12 }); diff --git a/src/logic/pricing/constants.ts b/src/logic/pricing/constants.ts index 47869b5..9a1bd73 100644 --- a/src/logic/pricing/constants.ts +++ b/src/logic/pricing/constants.ts @@ -1,16 +1,16 @@ import { FormState } from './types'; export const PRICING = { - BASE_WEBSITE: 6000, - PAGE: 800, - FEATURE: 2000, - FUNCTION: 1000, - NEW_DATASET: 400, - HOSTING_MONTHLY: 120, + BASE_WEBSITE: 4000, + PAGE: 600, + FEATURE: 1500, + FUNCTION: 800, + NEW_DATASET: 200, + HOSTING_MONTHLY: 250, STORAGE_EXPANSION_MONTHLY: 10, CMS_SETUP: 1500, CMS_CONNECTION_PER_FEATURE: 800, - API_INTEGRATION: 1000, + API_INTEGRATION: 800, APP_HOURLY: 120, VISUAL_STAGING: 2000, COMPLEX_INTERACTION: 1500, diff --git a/storage/key_value_stores/default/SDK_CRAWLER_STATISTICS_0.json b/storage/key_value_stores/default/SDK_CRAWLER_STATISTICS_0.json index 74f9e30..c8f1fd5 100644 --- a/storage/key_value_stores/default/SDK_CRAWLER_STATISTICS_0.json +++ b/storage/key_value_stores/default/SDK_CRAWLER_STATISTICS_0.json @@ -1,68 +1,27 @@ { - "requestsFinished": 0, - "requestsFailed": 1, - "requestsRetries": 1, - "requestsFailedPerMinute": 279, - "requestsFinishedPerMinute": 0, - "requestMinDurationMillis": null, - "requestMaxDurationMillis": 0, - "requestTotalFailedDurationMillis": 2, - "requestTotalFinishedDurationMillis": 0, - "crawlerStartedAt": "2026-02-04T17:39:09.193Z", - "crawlerFinishedAt": "2026-02-04T17:39:09.396Z", - "statsPersistedAt": "2026-02-04T17:39:09.396Z", - "crawlerRuntimeMillis": 215, - "crawlerLastStartTimestamp": 1770226749181, + "requestsFinished": 7, + "requestsFailed": 0, + "requestsRetries": 0, + "requestsFailedPerMinute": 0, + "requestsFinishedPerMinute": 596, + "requestMinDurationMillis": 72, + "requestMaxDurationMillis": 503, + "requestTotalFailedDurationMillis": 0, + "requestTotalFinishedDurationMillis": 1167, + "crawlerStartedAt": "2026-02-05T00:44:22.537Z", + "crawlerFinishedAt": "2026-02-05T00:44:23.230Z", + "statsPersistedAt": "2026-02-05T00:44:23.230Z", + "crawlerRuntimeMillis": 705, + "crawlerLastStartTimestamp": 1770252262525, "requestRetryHistogram": [ - null, - null, - null, - 1 + 7 ], "statsId": 0, - "requestAvgFailedDurationMillis": 2, - "requestAvgFinishedDurationMillis": null, - "requestTotalDurationMillis": 2, - "requestsTotal": 1, + "requestAvgFailedDurationMillis": null, + "requestAvgFinishedDurationMillis": 167, + "requestTotalDurationMillis": 1167, + "requestsTotal": 7, "requestsWithStatusCode": {}, - "errors": { - "file:///Users/marcmintel/Projects/mintel.me/node_modules/got/dist/source/core/index.js:198:21": { - "ENOTFOUND": { - "RequestError": { - "getaddrinfo ENOTFOUND etib-et.com": { - "count": 1 - } - } - } - }, - "node:dns:120:26": { - "ENOTFOUND": { - "Error": { - "getaddrinfo ENOTFOUND etib-et.com": { - "count": 1 - } - } - } - } - }, - "retryErrors": { - "file:///Users/marcmintel/Projects/mintel.me/node_modules/got/dist/source/core/index.js:198:21": { - "ENOTFOUND": { - "RequestError": { - "getaddrinfo ENOTFOUND etib-et.com": { - "count": 3 - } - } - } - }, - "node:dns:120:26": { - "ENOTFOUND": { - "Error": { - "getaddrinfo ENOTFOUND etib-et.com": { - "count": 3 - } - } - } - } - } + "errors": {}, + "retryErrors": {} } \ No newline at end of file diff --git a/storage/key_value_stores/default/SDK_SESSION_POOL_STATE.json b/storage/key_value_stores/default/SDK_SESSION_POOL_STATE.json index cdeff86..769857f 100644 --- a/storage/key_value_stores/default/SDK_SESSION_POOL_STATE.json +++ b/storage/key_value_stores/default/SDK_SESSION_POOL_STATE.json @@ -1,9 +1,9 @@ { - "usableSessionsCount": 4, + "usableSessionsCount": 7, "retiredSessionsCount": 0, "sessions": [ { - "id": "session_qS9LzMPYLU", + "id": "session_MKcXTj8jBY", "cookieJar": { "version": "tough-cookie@6.0.0", "storeType": "MemoryCookieStore", @@ -16,14 +16,14 @@ "userData": {}, "maxErrorScore": 3, "errorScoreDecrement": 0.5, - "expiresAt": "2026-02-04T18:29:09.266Z", - "createdAt": "2026-02-04T17:39:09.266Z", + "expiresAt": "2026-02-05T01:34:22.574Z", + "createdAt": "2026-02-05T00:44:22.574Z", "usageCount": 1, "maxUsageCount": 50, - "errorScore": 1 + "errorScore": 0 }, { - "id": "session_rQXoQ3YoSI", + "id": "session_CzBvH8k5d6", "cookieJar": { "version": "tough-cookie@6.0.0", "storeType": "MemoryCookieStore", @@ -36,14 +36,14 @@ "userData": {}, "maxErrorScore": 3, "errorScoreDecrement": 0.5, - "expiresAt": "2026-02-04T18:29:09.369Z", - "createdAt": "2026-02-04T17:39:09.369Z", + "expiresAt": "2026-02-05T01:34:23.083Z", + "createdAt": "2026-02-05T00:44:23.083Z", "usageCount": 1, "maxUsageCount": 50, - "errorScore": 1 + "errorScore": 0 }, { - "id": "session_VQKYMg9QOd", + "id": "session_6tYd3j1pzA", "cookieJar": { "version": "tough-cookie@6.0.0", "storeType": "MemoryCookieStore", @@ -56,14 +56,14 @@ "userData": {}, "maxErrorScore": 3, "errorScoreDecrement": 0.5, - "expiresAt": "2026-02-04T18:29:09.376Z", - "createdAt": "2026-02-04T17:39:09.376Z", + "expiresAt": "2026-02-05T01:34:23.085Z", + "createdAt": "2026-02-05T00:44:23.085Z", "usageCount": 1, "maxUsageCount": 50, - "errorScore": 1 + "errorScore": 0 }, { - "id": "session_Jf7MUM5INz", + "id": "session_MahMPRKWfS", "cookieJar": { "version": "tough-cookie@6.0.0", "storeType": "MemoryCookieStore", @@ -76,11 +76,71 @@ "userData": {}, "maxErrorScore": 3, "errorScoreDecrement": 0.5, - "expiresAt": "2026-02-04T18:29:09.380Z", - "createdAt": "2026-02-04T17:39:09.380Z", + "expiresAt": "2026-02-05T01:34:23.086Z", + "createdAt": "2026-02-05T00:44:23.086Z", "usageCount": 1, "maxUsageCount": 50, - "errorScore": 1 + "errorScore": 0 + }, + { + "id": "session_POCFcWGlXP", + "cookieJar": { + "version": "tough-cookie@6.0.0", + "storeType": "MemoryCookieStore", + "rejectPublicSuffixes": true, + "enableLooseMode": false, + "allowSpecialUseDomain": true, + "prefixSecurity": "silent", + "cookies": [] + }, + "userData": {}, + "maxErrorScore": 3, + "errorScoreDecrement": 0.5, + "expiresAt": "2026-02-05T01:34:23.087Z", + "createdAt": "2026-02-05T00:44:23.087Z", + "usageCount": 1, + "maxUsageCount": 50, + "errorScore": 0 + }, + { + "id": "session_zra0Syci00", + "cookieJar": { + "version": "tough-cookie@6.0.0", + "storeType": "MemoryCookieStore", + "rejectPublicSuffixes": true, + "enableLooseMode": false, + "allowSpecialUseDomain": true, + "prefixSecurity": "silent", + "cookies": [] + }, + "userData": {}, + "maxErrorScore": 3, + "errorScoreDecrement": 0.5, + "expiresAt": "2026-02-05T01:34:23.088Z", + "createdAt": "2026-02-05T00:44:23.088Z", + "usageCount": 1, + "maxUsageCount": 50, + "errorScore": 0 + }, + { + "id": "session_JbCC8UIBHk", + "cookieJar": { + "version": "tough-cookie@6.0.0", + "storeType": "MemoryCookieStore", + "rejectPublicSuffixes": true, + "enableLooseMode": false, + "allowSpecialUseDomain": true, + "prefixSecurity": "silent", + "cookies": [] + }, + "userData": {}, + "maxErrorScore": 3, + "errorScoreDecrement": 0.5, + "expiresAt": "2026-02-05T01:34:23.092Z", + "createdAt": "2026-02-05T00:44:23.092Z", + "usageCount": 1, + "maxUsageCount": 50, + "errorScore": 0 } ] } \ No newline at end of file