feat: Implement AI-powered estimation system, add comprehensive usage guide, and redesign PDF sitemap module.
Some checks failed
Build & Deploy Mintel Blog / build-and-deploy (push) Failing after 23s

This commit is contained in:
2026-02-04 01:36:27 +01:00
parent ce421eb8d2
commit 226de83d0f
9 changed files with 261 additions and 128 deletions

View File

@@ -87,4 +87,18 @@ npm run video:preview
# Render the showcase video # Render the showcase video
npm run video:render npm run video:render
``` ```
See `.agent/workflows/video-toolkit.md` for more details. See `.agent/workflows/video-toolkit.md` for more details.
## 🤖 Industrial AI Estimation System
A multi-pass AI consultation engine for generating technical project estimations and professional PDFs.
- **Context-Aware**: Crawls existing customer websites to understand company DNA.
- **Industrial Standards**: Strictly follows "Industrial Engineering" aesthetics and technical wording.
- **Transparent**: Maps scope directly to a modular pricing model.
### Quick Start
```bash
# Generate estimation with website crawl
npm run ai-estimate -- "Project Briefing" --url https://example.com
```
See [ESTIMATION_GUIDE.md](docs/ESTIMATION_GUIDE.md) for full documentation.

83
docs/ESTIMATION_GUIDE.md Normal file
View File

@@ -0,0 +1,83 @@
# Service Estimation & AI Consultation Guide
This guide explains how to use the automated estimation system to generate professional PDF quotes for clients using AI-driven context analysis.
## 🛠 Basic Usage
The primary entry point is the `ai-estimate` script. It orchestrates a 6-pass AI consultation:
1. **Fact Extraction**: Identifying company data and project scope.
2. **Feature Deep-Dive**: Generating technical justifications for items.
3. **Strategic Content**: Creating the Briefing Analysis and Strategic Vision.
4. **Information Architecture**: Designing a hierarchical sitemap.
5. **Position Synthesis**: Mapping everything to a transparent pricing model.
6. **Industrial Critic**: Final quality gate for tone and accuracy.
### Generating an Estimation from Scratch
#### 1. With a Website URL (Recommended)
Providing a URL allows the system to crawl the existing site to understand the "Company DNA" and services.
```bash
npm run ai-estimate -- "Relaunch der Website mit Fokus auf B2B Leads" --url https://example.com
```
#### 2. From a Text File
If you have a long briefing in a `.txt` file:
```bash
npm run ai-estimate -- @briefing.txt --url https://example.com
```
#### 3. Text-Only (No URL)
If no URL is provided, the system relies entirely on your briefing text.
```bash
npm run ai-estimate -- "Neuentwicklung eines Portals für XYZ"
```
---
## 📄 Output Modes
The system can generate two types of documents:
### 1. Full Quote (Default)
Includes everything: Front Page, Briefing Analysis, Vision, Sitemap, Technical Principles, Detailed Pricing, Roadmap, and Legal Terms (AGB).
```bash
npm run ai-estimate -- "Project Briefing"
```
### 2. Estimation Only
A condensed version excluding legal terms and deep technical principles. Focuses purely on the strategic fit and the price.
```bash
npm run ai-estimate -- "Project Briefing" --estimation
```
---
## 📦 Cache & Cache Management
To save costs and time, all AI responses and crawl results are cached in the `.cache` directory.
### Regenerating with Cached Data
If you run the same command again (identical briefing and URL), the system will use the cached results and won't call the AI APIs again. This is useful if you want to tweak the PDF layout without spending tokens.
### Forcing a Refresh
To ignore the cache and get a fresh AI consultation:
```bash
npm run ai-estimate -- "Project Briefing" --clear-cache
```
### Manual Tweaking (JSON State)
Every run saves a detailed state to `out/estimations/json/[Company]_[Timestamp].json`.
If you want to manually edit the AI's results (e.g., fix a typo in the sitemap or description), you can edit this JSON file and then regenerate the PDF from it:
```bash
npm run ai-estimate -- --json out/estimations/json/Your_Project.json
```
*(Add `--estimation` if you want the condensed version).*
---
## 💡 Advanced Options
- `--comments "..."`: Add manual notes that the AI should consider (e.g., "Customer prefers a minimalist blue theme").
- `--clear-cache`: Purges all cached data for this project before starting.
- `--url [URL]`: Explicitly sets the crawl target (auto-discovered from briefing if omitted).

View File

@@ -23,6 +23,8 @@ async function main() {
let jsonStatePath: string | null = null; let jsonStatePath: string | null = null;
const isEstimation = process.argv.includes('--estimation') || process.argv.includes('-E');
const args = process.argv.slice(2); const args = process.argv.slice(2);
for (let i = 0; i < args.length; i++) { for (let i = 0; i < args.length; i++) {
const arg = args[i]; const arg = args[i];
@@ -34,6 +36,8 @@ async function main() {
cacheKey = args[++i]; cacheKey = args[++i];
} else if (arg === '--json') { } else if (arg === '--json') {
jsonStatePath = args[++i]; jsonStatePath = args[++i];
} else if (arg === '--estimation' || arg === '-E') {
// Handled above
} else if (!arg.startsWith('--')) { } else if (!arg.startsWith('--')) {
briefing = arg; briefing = arg;
} }
@@ -140,7 +144,8 @@ async function main() {
console.log(`📦 Saved detailed state to: ${finalJsonPath}`); console.log(`📦 Saved detailed state to: ${finalJsonPath}`);
console.log('📄 Generating PDF estimation...'); console.log('📄 Generating PDF estimation...');
try { try {
execSync(`npx tsx ./scripts/generate-quote.ts --input ${tempJsonPath}`, { stdio: 'inherit' }); const genArgs = isEstimation ? '--estimation' : '';
execSync(`npx tsx ./scripts/generate-quote.ts --input ${tempJsonPath} ${genArgs}`, { stdio: 'inherit' });
} finally { } finally {
// await fs.unlink(tempJsonPath); // await fs.unlink(tempJsonPath);
} }
@@ -243,7 +248,17 @@ async function performCrawl(url: string): Promise<string> {
return summary + pages.map(p => `--- PAGE: ${p.url} ---\n${p.content}`).join('\n\n'); return summary + pages.map(p => `--- PAGE: ${p.url} ---\n${p.content}`).join('\n\n');
} }
async function getAiEstimation(briefing: string, distilledCrawl: string, comments: string | null, apiKey: string, principles: string, techStandards: string, tone: string) { 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
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) => {
let usage = { prompt: 0, completion: 0, cost: 0 }; let usage = { prompt: 0, completion: 0, cost: 0 };
const addUsage = (data: any) => { const addUsage = (data: any) => {
if (data?.usage) { if (data?.usage) {
@@ -316,7 +331,7 @@ Focus 100% on the BRIEFING text provided by the user. Use the DISTILLED_CRAWL on
response_format: { type: 'json_object' } response_format: { type: 'json_object' }
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
addUsage(p1Resp.data); addUsage(p1Resp.data);
const facts = JSON.parse(p1Resp.data.choices[0].message.content); const facts = JSON.parse(cleanJson(p1Resp.data.choices[0].message.content));
// 2. PASS 2: Feature Deep-Dive // 2. PASS 2: Feature Deep-Dive
console.log(' ↳ Pass 2: Feature Deep-Dive...'); console.log(' ↳ Pass 2: Feature Deep-Dive...');
@@ -348,7 +363,7 @@ ${JSON.stringify(facts, null, 2)}
response_format: { type: 'json_object' } response_format: { type: 'json_object' }
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
addUsage(p2Resp.data); addUsage(p2Resp.data);
const details = JSON.parse(p2Resp.data.choices[0].message.content); const details = JSON.parse(cleanJson(p2Resp.data.choices[0].message.content));
// 3. PASS 3: Strategic Content (Bespoke Strategy) // 3. PASS 3: Strategic Content (Bespoke Strategy)
console.log(' ↳ Pass 3: Strategic Content (Bespoke Strategy)...'); console.log(' ↳ Pass 3: Strategic Content (Bespoke Strategy)...');
@@ -360,19 +375,17 @@ Analyze the BRIEFING and the EXISTING WEBSITE context.
${tone} ${tone}
### OBJECTIVE: ### OBJECTIVE:
1. **briefingSummary**: A deep, respectful summary of the status quo and the target state. 3. **briefingSummary**: Ein sachlicher, tiefgehender Überblick der Unternehmenslage.
- **LENGTH**: EXACTLY TWO PARAGRAPHS. Minimum 8 sentences total. - **STIL**: Keine Ich-Form. Keine Marketing-Floskeln. Nutze präzise Fachbegriffe. Sei prägnant und effizient (ca. 70% der vorherigen Länge).
- **MIRROR TEST**: Acknowledge the EXISTING website specifically. Why does the new project make sense NOW? - **FORM**: EXAKT ZWEI ABSÄTZE. Insgesamt ca. 6 Sätze.
- **ABSOLUTE RULE**: DO NOT claim "keine digitale Repräsentation", "erstmals abgebildet", "Erstplatzierung" or "kommunikative Lücke" regarding existence if Pass 1 identified this as a RELAUNCH (isRelaunch=true). EXPLICITLY acknowledge the existing context and the NEED FOR EVOLUTION/MODERNIZATION. - **INHALT**: Welcher technologische Sprung ist notwendig? Was ist der Status Quo? (Bezug zur URL/Briefing).
- **RESPECT**: Explicitly incorporate the customer's expressed wishes. - **ABSOLUTE REGEL**: Keine Halluzinationen über fehlende Präsenzen bei Relaunches.
- **ABSOLUTE RULE**: DO NOT INVENT DETAILS. Do not mention specific people (e.g., "Frieder Helmich"), software versions, or internal details NOT present in the briefing. - **DATENSCHUTZ**: KEINERLEI namentliche Nennungen von Personen (z. B. "Danny Joseph") in diesen Texten.
- **TONE**: Natural Ich-Form. Clear, direct, zero marketing fluff. 4. **designVision**: Ein abstraktes, strategisches Konzept.
2. **designVision**: A high-density, professional vision of the future execution. - **STIL**: Rein konzeptionell. Keine Umsetzungsschritte. Keinerlei "To-dos". Sei prägnant.
- **LENGTH**: EXACTLY TWO PARAGRAPHS. Minimum 6 sentences total. - **FORM**: EXAKT ZWEI ABSÄTZE. Insgesamt ca. 4 Sätze.
- **INVESTMENT VALUE**: Plant a clear picture of a stable, high-quality system. - **DATENSCHUTZ**: KEINERLEI namentliche Nennungen von Personen in diesen Texten.
- **TECHNICAL PRECISION**: Focus on execution (Typografie, Visual Logic, Performance). - **FOKUS**: Welche strategische Wirkung soll erzielt werden? (Z. B. "Industrielle Souveränität").
- **NO FLUFF**: Do NOT focus on "Full-Screen Hero Video" as the main thing. Focus on the FIRM's essence and how we translate it into a professional tool.
- **ABSOLUTE RULE**: NO HALLUCINATIONS. Stay general yet precise. No "Verzicht auf Stockmaterial" unless explicitly stated.
### OUTPUT FORMAT (Strict JSON): ### OUTPUT FORMAT (Strict JSON):
{ {
@@ -389,7 +402,7 @@ ${tone}
response_format: { type: 'json_object' } response_format: { type: 'json_object' }
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
addUsage(p3Resp.data); addUsage(p3Resp.data);
const strategy = JSON.parse(p3Resp.data.choices[0].message.content); const strategy = JSON.parse(cleanJson(p3Resp.data.choices[0].message.content));
// 4. PASS 4: Information Architecture (Sitemap) // 4. PASS 4: Information Architecture (Sitemap)
console.log(' ↳ Pass 4: Information Architecture...'); console.log(' ↳ Pass 4: Information Architecture...');
@@ -418,7 +431,7 @@ ${JSON.stringify({ facts, strategy }, null, 2)}
response_format: { type: 'json_object' } response_format: { type: 'json_object' }
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
addUsage(p4Resp.data); addUsage(p4Resp.data);
const ia = JSON.parse(p4Resp.data.choices[0].message.content); const ia = JSON.parse(cleanJson(p4Resp.data.choices[0].message.content));
// 5. PASS 5: Position Synthesis & Pricing Transparency // 5. PASS 5: Position Synthesis & Pricing Transparency
console.log(' ↳ Pass 5: Position Synthesis...'); console.log(' ↳ Pass 5: Position Synthesis...');
@@ -443,14 +456,15 @@ Each position in the quote must be perfectly justified and detailed.
### RULES FOR positionDescriptions (STRICT): ### RULES FOR positionDescriptions (STRICT):
1. **NO "ICH-FORM"**: Do NOT use "Ich" or "Mein". Lead with the action or component. 1. **NO "ICH-FORM"**: Do NOT use "Ich" or "Mein". Lead with the action or component.
2. **CONCISE & ITEM-BASED**: Use short, technical sentences. Focus on WHAT is delivered. 2. **CONCISE & ITEM-BASED**: Use short, technical sentences. Focus on WHAT is delivered.
3. **ZERO GENERALIZATION**: Do NOT say "Verschiedene Funktionen". 3. **ZERO GENERALIZATION**: Do NOT say "Verschiedene Funktionen" or "Optimierte Darstellung". Name the things.
4. **ITEMIZED SYNTHESIS**: Mention EVERY component selected in Pass 1. 4. **ITEMIZED SYNTHESIS**: Mention EVERY component selected in Pass 1.
5. **HARD SPECIFICS**: Preserve technical details from the briefing (e.g., "110 kV", "HDD-Bohrtechnik"). 5. **HARD SPECIFICS**: Preserve technical details from the briefing and CURRENT WEBSITE (distilledCrawl). If they mention "HDD-Bohrtechnik" or "110kV Kabelanlagen", IT MUST BE IN THE DESCRIPTION.
6. **STYLE**: Direct, engineering-grade, 100% GERMAN. 6. **INDUSTRIAL AMBITION**: Describe it as a high-end technical solution, not a cheap website.
7. **SPECIFIC - PAGES**: For "Individuelle Seiten", list the pages as a comma-separated list. 7. **SPECIFIC - PAGES**: For "Individuelle Seiten", list the pages as a comma-separated list.
8. **SPECIFIC - HOSTING**: Always append: "Inkl. 20GB Speicher." 8. **SPECIFIC - HOSTING**: Always append: "Inkl. 20GB Speicher."
9. **SPECIFIC - LOGIC**: Describe the ACTUAL logic. NEVER use generic terms. 9. **SPECIFIC - LOGIC**: Describe the ACTUAL logic. NEVER use generic terms.
10. **STRICT KEYS**: Keys MUST be EXACTLY the ones defined in POSITION TITLES. 10. **STRICT KEYS**: Keys MUST be EXACTLY the ones defined in POSITION TITLES.
11. **AGB BAN (CRITICAL)**: NEVER mention "Allgemeine Geschäftsbedingungen" or "AGGs" in any text. They are NOT part of this offer.
### EXAMPLES (FEW-SHOT): ### EXAMPLES (FEW-SHOT):
- **BAD**: "Ich entwickle die Seiten: Startseite, Leistungen, Kontakt." - **BAD**: "Ich entwickle die Seiten: Startseite, Leistungen, Kontakt."
@@ -481,7 +495,7 @@ ${JSON.stringify({ facts, details, strategy, ia }, null, 2)}
response_format: { type: 'json_object' } response_format: { type: 'json_object' }
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
addUsage(p5Resp.data); addUsage(p5Resp.data);
const positionsData = JSON.parse(p5Resp.data.choices[0].message.content); const positionsData = JSON.parse(cleanJson(p5Resp.data.choices[0].message.content));
// 6. PASS 6: The Industrial Critic // 6. PASS 6: The Industrial Critic
console.log(' ↳ Pass 6: The Industrial Critic (Quality Gate)...'); console.log(' ↳ Pass 6: The Industrial Critic (Quality Gate)...');
@@ -492,10 +506,13 @@ Analyze the CURRENT_STATE against the BRIEFING_TRUTH.
### CRITICAL ERROR CHECKLIST (FAIL IF FOUND): ### 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. 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. - **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 or uses phrases like "Da aktuell keine digitale Repräsentation vorliegt", "erstmals abgebildet", "Erstplatzierung" or "Lücke schließen" (regarding existence). 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. - FAIL if the description in positionDescriptions mentions more items than extracted in facts.
3. **Implementation Fluff**: FAIL if "React", "Next.js", "TypeScript", "Tailwind" or other tech-stack details are mentioned. Focus on Concept & Result. 3. **Implementation Fluff**: FAIL if tech-stack details are mentioned (React, etc.). Focus on Concept & Result.
4. **Length Check**: Briefing and Vision MUST be significantly long (EXACTLY 2 paragraphs each, minimum 8 sentences for briefing, 6 for vision). 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.
### MISSION: ### 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'.
@@ -509,7 +526,7 @@ ${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)}
response_format: { type: 'json_object' } response_format: { type: 'json_object' }
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
addUsage(p6Resp.data); addUsage(p6Resp.data);
const reflection = JSON.parse(p6Resp.data.choices[0].message.content); const reflection = JSON.parse(cleanJson(p6Resp.data.choices[0].message.content));
// 6. Reflection Merge Utility // 6. Reflection Merge Utility
const mergeReflection = (state: any, reflection: any) => { const mergeReflection = (state: any, reflection: any) => {

View File

@@ -16,13 +16,13 @@ export const COLORS = {
}; };
export const FONT_SIZES = { export const FONT_SIZES = {
H1: 24, H1: 28,
H2: 18, H2: 20,
H3: 12, H3: 14,
BODY: 9, BODY: 11,
TINY: 7, TINY: 9,
SUB: 8, SUB: 10,
BLUEPRINT: 5 BLUEPRINT: 8
}; };
// Register a more technical font if possible, or use Helvetica with varying weights // Register a more technical font if possible, or use Helvetica with varying weights
@@ -94,8 +94,7 @@ export const pdfStyles = StyleSheet.create({
fontWeight: 'bold', fontWeight: 'bold',
marginBottom: 4, marginBottom: 4,
color: COLORS.CHARCOAL, color: COLORS.CHARCOAL,
textTransform: 'uppercase', letterSpacing: 0.5,
letterSpacing: 1,
}, },
subTitle: { subTitle: {
fontSize: FONT_SIZES.BODY, fontSize: FONT_SIZES.BODY,

View File

@@ -6,14 +6,11 @@ import { IndustrialListItem, IndustrialCard, Divider, COLORS, FONT_SIZES } from
const styles = StyleSheet.create({ const styles = StyleSheet.create({
industrialTitle: { fontSize: FONT_SIZES.H1, fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 6, letterSpacing: -1 }, industrialTitle: { fontSize: FONT_SIZES.H1, fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 6, letterSpacing: -1 },
industrialSubtitle: { fontSize: FONT_SIZES.SUB, fontWeight: 'bold', color: COLORS.TEXT_LIGHT, textTransform: 'uppercase', marginBottom: 16, letterSpacing: 2 }, industrialSubtitle: { fontSize: FONT_SIZES.SUB, fontWeight: 'bold', color: COLORS.TEXT_LIGHT, marginBottom: 16, letterSpacing: 0.5 },
industrialTextLead: { fontSize: FONT_SIZES.H3, color: COLORS.TEXT_MAIN, lineHeight: 1.6, marginBottom: 12 }, industrialTextLead: { fontSize: FONT_SIZES.H3, color: COLORS.TEXT_MAIN, lineHeight: 1.6, marginBottom: 16 },
industrialText: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_DIM, lineHeight: 1.6, marginBottom: 8 }, industrialText: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_DIM, lineHeight: 1.6, marginBottom: 12 },
industrialGrid2: { flexDirection: 'row', gap: 32 }, industrialGrid2: { flexDirection: 'row' },
industrialCol: { width: '48%' }, industrialCol: { width: '46%' },
darkBox: { marginTop: 32, padding: 24, backgroundColor: COLORS.CHARCOAL, color: COLORS.WHITE },
darkTitle: { fontSize: FONT_SIZES.H2, fontWeight: 'bold', color: COLORS.WHITE, marginBottom: 8 },
darkText: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_LIGHT, lineHeight: 1.6 },
industrialBulletBox: { industrialBulletBox: {
width: 6, width: 6,
height: 6, height: 6,
@@ -26,30 +23,41 @@ const styles = StyleSheet.create({
export const AboutModule = () => ( export const AboutModule = () => (
<> <>
<PDFText style={styles.industrialTitle}>Über mich</PDFText> <PDFText style={styles.industrialTitle}>Über mich</PDFText>
<PDFText style={styles.industrialSubtitle}>Direkt. Sauber. Verantwortlich.</PDFText> <PDFText style={styles.industrialSubtitle}>Senior Software Developer & Technischer Partner</PDFText>
<Divider style={{ marginVertical: 16, backgroundColor: COLORS.GRID }} /> <Divider style={{ marginVertical: 16, backgroundColor: COLORS.GRID }} />
<PDFView style={[styles.industrialGrid2, { marginTop: 20 }]}>
<PDFView style={styles.industrialCol}> <PDFView style={{ marginTop: 24 }}>
<PDFText style={styles.industrialTextLead}>Seit 15 Jahren entstehen unter meiner Leitung Websysteme für Agenturen, Konzerne und Startups. Ich arbeite bewusst alleine, um die volle Verantwortung für jedes Projekt zu tragen.</PDFText> <PDFText style={styles.industrialTextLead}>
<PDFText style={styles.industrialText}>Technik scheitert selten an Bits und Bytes, sondern meist an unklaren Zuständigkeiten. Als Ihr direkter Ansprechpartner treffe ich die notwendigen Entscheidungen und löse technische Probleme ohne Umwege.</PDFText> Ich begleite mittelständische Unternehmen und Agenturen bei der Realisierung anspruchsvoller Web-Projekte. Als Senior Software Developer mit über 15 Jahren Erfahrung biete ich das gesamte technische Spektrum von der Architektur bis zum fertigen Produkt.
<PDFView style={{ marginTop: 16 }}> </PDFText>
<PDFText style={[styles.industrialText, { fontWeight: 'bold' }]}>Mein Standard:</PDFText>
<IndustrialListItem><PDFText style={styles.industrialText}>Code-Lieferung ohne technische Altlasten.</PDFText></IndustrialListItem> <PDFView style={[styles.industrialGrid2, { marginTop: 20 }]}>
<IndustrialListItem><PDFText style={styles.industrialText}>Entwicklung von Systemen, die autark operieren.</PDFText></IndustrialListItem> <PDFView style={[styles.industrialCol, { marginRight: '8%' }]}>
<IndustrialListItem><PDFText style={styles.industrialText}>Verzicht auf Overhead und Kommunikationsverluste.</PDFText></IndustrialListItem> <PDFText style={[styles.industrialText, { fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 8 }]}>Erfahrung & Substanz</PDFText>
<PDFText style={styles.industrialText}>
Mein Weg führte mich durch alle Ebenen der Webentwicklung: vom Junior zum Teamleiter, von der kleinen Kreativagentur bis zur Softwareentwicklung für internationale Konzerne.
</PDFText>
<PDFText style={styles.industrialText}>
Ich kenne die Prozesse hinter großen Systemen ebenso gut wie die Agilität, die im Mittelstand gefordert wird. Dieses Wissen nutze ich heute, um Lösungen zu bauen, die technologisch auf Augenhöhe mit den Großen sind, aber ohne deren bürokratischen Overhead auskommen.
</PDFText>
</PDFView>
<PDFView style={styles.industrialCol}>
<PDFText style={[styles.industrialText, { fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 8 }]}>Fokus Einzelentwicklung</PDFText>
<PDFText style={styles.industrialText}>
Ich arbeite bewusst als Einzelentwickler. Für Sie bedeutet das: maximale Geschwindigkeit und volle Verantwortung. Es gibt keine Informationsverluste zwischen Projektmanagern, Vertrieblern und Entwicklern.
</PDFText>
<PDFText style={styles.industrialText}>
Sie haben einen direkten technischen Sparringspartner, der Ihren Code von der ersten bis zur letzten Zeile kennt und dafür einsteht. Diese Unmittelbarkeit garantiert Ergebnisse, die technisch sauber und wirtschaftlich sinnvoll sind.
</PDFText>
</PDFView> </PDFView>
</PDFView> </PDFView>
<PDFView style={styles.industrialCol}>
<IndustrialCard title="ZUSTÄNDIGKEIT"> <PDFView style={{ marginTop: 32, paddingVertical: 16, borderTopWidth: 1, borderTopColor: COLORS.GRID }}>
<IndustrialListItem><PDFText style={styles.industrialText}>Ich bin der einzige Entwickler.</PDFText></IndustrialListItem> <PDFText style={[styles.industrialText, { fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 4 }]}>Meine Zusage</PDFText>
<IndustrialListItem><PDFText style={styles.industrialText}>Ich antworte direkt auf technische Fragen.</PDFText></IndustrialListItem> <PDFText style={styles.industrialText}>
<IndustrialListItem><PDFText style={styles.industrialText}>Ich garantiere die Umsetzung.</PDFText></IndustrialListItem> Ich baue keine Prototypen, die beim ersten Nutzeransturm zusammenbrechen. Ich liefere produktionsreife Software, die wartbar bleibt und mit Ihrem Unternehmen mitwächst. Direkt. Sauber. Ohne Ballast.
</IndustrialCard> </PDFText>
<IndustrialCard title="KEIN BALLAST" style={{ backgroundColor: COLORS.WHITE, borderColor: COLORS.DIVIDER }}>
<PDFText style={[styles.industrialText, { color: COLORS.TEXT_LIGHT }]}>Keine Projektmanager.</PDFText>
<PDFText style={[styles.industrialText, { color: COLORS.TEXT_LIGHT }]}>Keine Vertriebler.</PDFText>
<PDFText style={[styles.industrialText, { color: COLORS.TEXT_LIGHT }]}>Kein Ticket-Chaos.</PDFText>
</IndustrialCard>
</PDFView> </PDFView>
</PDFView> </PDFView>
</> </>
@@ -57,34 +65,34 @@ export const AboutModule = () => (
export const CrossSellModule = ({ state }: any) => { export const CrossSellModule = ({ state }: any) => {
const isWebsite = state.projectType === 'website'; const isWebsite = state.projectType === 'website';
const title = isWebsite ? "Digitale Routine" : "Websites & Ökosysteme"; const title = isWebsite ? "Routine-Automatisierung" : "Websites & Ökosysteme";
const subtitle = isWebsite ? "Automatisierung statt Fleißarbeit" : "Technische Infrastruktur ohne Kompromisse"; const subtitle = isWebsite ? "Maßgeschneiderte Software zur Prozessoptimierung" : "Technische Infrastruktur ohne Kompromisse";
return ( return (
<> <>
<PDFText style={styles.industrialTitle}>{title}</PDFText> <PDFText style={styles.industrialTitle}>{title}</PDFText>
<PDFText style={styles.industrialSubtitle}>{subtitle}</PDFText> <PDFText style={styles.industrialSubtitle}>{subtitle}</PDFText>
<Divider style={{ marginVertical: 16, backgroundColor: COLORS.GRID }} /> <Divider style={{ marginVertical: 16, backgroundColor: COLORS.GRID }} />
<PDFView style={[styles.industrialGrid2, { marginTop: 12 }]} > <PDFView style={[styles.industrialGrid2, { marginTop: 16 }]} >
{isWebsite ? ( {isWebsite ? (
<> <>
<PDFView style={styles.industrialCol}> <PDFView style={[styles.industrialCol, { marginRight: '8%' }]}>
<PDFText style={styles.industrialTextLead}>Manuelle Abläufe binden Kapazitäten. Durch maßgeschneiderte Software ersetze ich fehleranfällige Prozesse und eliminiere Zeitfresser in Ihrem Unternehmen.</PDFText> <PDFText style={styles.industrialTextLead}>Manuelle Alltags-Aufgaben sind teuer und fehleranfällig. In einer separaten Beauftragung entwickle ich maßgeschneiderte Helfer, die Ihre Routine-Prozesse lautlos im Hintergrund automatisieren.</PDFText>
<PDFText style={[styles.industrialText, { fontWeight: 'bold' }]}>Ziel ist der Gewinn wertvoller Arbeitszeit. Digitalisierung ordnet das Chaos.</PDFText> <PDFText style={[styles.industrialText, { fontWeight: 'bold' }]}>Keine Abos. Keine komplexen neuen Systeme. Nur gezielte Zeitersparnis.</PDFText>
<PDFView style={styles.darkBox}> <PDFView style={{ marginTop: 24, padding: 16, backgroundColor: '#f8fafc', borderLeftWidth: 2, borderLeftColor: COLORS.GRID }}>
<PDFText style={styles.darkTitle}>Individuelle Prüfung</PDFText> <PDFText style={[styles.industrialText, { fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 4 }]}>Individuelle Prüfung</PDFText>
<PDFText style={styles.darkText}>Ich analysiere Ihren spezifischen Prozess auf technisches Automatisierungspotenzial. Das Ergebnis liefert Klarheit darüber, ob eine Umsetzung wirtschaftlich sinnvoll ist.</PDFText> <PDFText style={styles.industrialText}>Ich analysiere Ihren spezifischen Prozess auf technisches Automatisierungspotenzial. Das Ergebnis liefert Klarheit darüber, ob eine Umsetzung wirtschaftlich sinnvoll ist.</PDFText>
</PDFView> </PDFView>
</PDFView> </PDFView>
<PDFView style={styles.industrialCol}> <PDFView style={styles.industrialCol}>
<IndustrialCard title="PDF-GENERATOREN"> <IndustrialCard title="DOKUMENT-AUTOMATION">
<PDFText style={styles.industrialText}>Ich automatisiere Angebote und Berichte. Ich reduziere den Zeitaufwand auf 5 Minuten. Ich garantiere fehlerfreie Exporte.</PDFText> <PDFText style={styles.industrialText}>Erstellung von PDF-Angeboten, Berichten oder Protokollen in Sekunden statt Stunden.</PDFText>
</IndustrialCard> </IndustrialCard>
<IndustrialCard title="EXCEL-AUTOMATISIERUNG"> <IndustrialCard title="EXCEL-LOGIK">
<PDFText style={styles.industrialText}>Ich synchronisiere Ihre Datenbestände. Ich schaffe Echtzeit-Transparenz ohne manuelle Listenpflege.</PDFText> <PDFText style={styles.industrialText}>Intelligente Tabellen und automatisierte Auswertungen Ihrer Bestandsdaten.</PDFText>
</IndustrialCard> </IndustrialCard>
<IndustrialCard title="DATEN-SCANN"> <IndustrialCard title="KI-ASSISTENZ">
<PDFText style={styles.industrialText}>Ich implementiere Schrifterkennung für analoge Dokumente. Ich übertrage Daten direkt in Ihre Systeme.</PDFText> <PDFText style={styles.industrialText}>Effizientes Einlesen und Verarbeiten von analogen Dokumenten oder handschriftlichen Notizen.</PDFText>
</IndustrialCard> </IndustrialCard>
</PDFView> </PDFView>
</> </>

View File

@@ -7,10 +7,6 @@ import { DocumentTitle, COLORS, FONT_SIZES } from '../SharedUI';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
section: { marginBottom: 24 }, section: { marginBottom: 24 },
sectionTitle: { fontSize: FONT_SIZES.BODY + 1, fontWeight: 'bold', marginBottom: 8, color: COLORS.CHARCOAL }, sectionTitle: { fontSize: FONT_SIZES.BODY + 1, fontWeight: 'bold', marginBottom: 8, color: COLORS.CHARCOAL },
configGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 6, marginTop: 6 },
configItem: { width: '24%', marginBottom: 4 },
configLabel: { fontSize: FONT_SIZES.BLUEPRINT, color: COLORS.TEXT_LIGHT, textTransform: 'uppercase', marginBottom: 2 },
configValue: { fontSize: FONT_SIZES.TINY, color: COLORS.CHARCOAL, fontWeight: 'bold' },
visionText: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_MAIN, lineHeight: 1.8, textAlign: 'justify' }, visionText: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_MAIN, lineHeight: 1.8, textAlign: 'justify' },
}); });
@@ -20,22 +16,13 @@ export const BriefingModule = ({ state }: any) => (
{state.briefingSummary && ( {state.briefingSummary && (
<PDFView style={styles.section}> <PDFView style={styles.section}>
<PDFText style={styles.sectionTitle}>Briefing Analyse</PDFText> <PDFText style={styles.sectionTitle}>Briefing Analyse</PDFText>
<PDFText style={{ fontSize: 8, color: '#334155', lineHeight: 1.6, textAlign: 'justify' }}>{state.briefingSummary}</PDFText> <PDFText style={{ fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_MAIN, lineHeight: 1.6, textAlign: 'justify' }}>{state.briefingSummary}</PDFText>
</PDFView> </PDFView>
)} )}
<PDFView style={styles.section}>
<PDFText style={styles.sectionTitle}>Kern-Informationen</PDFText>
<PDFView style={styles.configGrid}>
<PDFView style={styles.configItem}><PDFText style={styles.configLabel}>Kontakt</PDFText><PDFText style={styles.configValue}>{state.personName || "—"}</PDFText></PDFView>
<PDFView style={styles.configItem}><PDFText style={styles.configLabel}>Projekttyp</PDFText><PDFText style={styles.configValue}>{state.isRelaunch ? 'Website Evolution' : (state.statusQuo || 'Neukonzeption')}</PDFText></PDFView>
<PDFView style={styles.configItem}><PDFText style={styles.configLabel}>Mitarbeiter</PDFText><PDFText style={styles.configValue}>{state.employeeCount || "—"}</PDFText></PDFView>
<PDFView style={styles.configItem}><PDFText style={styles.configLabel}>Zeitplan</PDFText><PDFText style={styles.configValue}>{state.deadline || 'Flexibel'}</PDFText></PDFView>
</PDFView>
</PDFView>
{state.designVision && ( {state.designVision && (
<PDFView style={[styles.section, { padding: 16, borderLeftWidth: 2, borderLeftColor: COLORS.DIVIDER, backgroundColor: COLORS.GRID }]}> <PDFView style={[styles.section, { padding: 12, borderLeftWidth: 2, borderLeftColor: COLORS.DIVIDER, backgroundColor: COLORS.GRID }]}>
<PDFText style={[styles.sectionTitle, { color: COLORS.CHARCOAL }]}>Strategische Vision</PDFText> <PDFText style={[styles.sectionTitle, { color: COLORS.CHARCOAL, marginBottom: 4 }]}>Strategische Vision</PDFText>
<PDFText style={styles.visionText}>{state.designVision}</PDFText> <PDFText style={[styles.visionText, { lineHeight: 1.6 }]}>{state.designVision}</PDFText>
</PDFView> </PDFView>
)} )}
</> </>

View File

@@ -97,3 +97,4 @@ export const PrinciplesModule = ({ principles }: any) => (
</PDFView> </PDFView>
</> </>
); );

View File

@@ -74,7 +74,7 @@ export const FrontPageModule = ({ state, headerIcon, date }: any) => {
{headerIcon ? <PDFImage src={headerIcon} style={{ width: 40, height: 40 }} /> : <PDFText style={styles.brandIconText}>M</PDFText>} {headerIcon ? <PDFImage src={headerIcon} style={{ width: 40, height: 40 }} /> : <PDFText style={styles.brandIconText}>M</PDFText>}
</PDFView> </PDFView>
<PDFText style={[styles.titleProjectName, { fontSize }]}>{fullTitle}</PDFText> <PDFText style={[styles.titleProjectName, { fontSize }]}>{fullTitle}</PDFText>
<PDFView style={styles.titleDivider} /> <PDFView style={{ marginBottom: 40 }} />
<PDFText style={styles.titleDate}>{date} | Marc Mintel</PDFText> <PDFText style={styles.titleDate}>{date} | Marc Mintel</PDFText>
</PDFView> </PDFView>
); );

View File

@@ -5,43 +5,67 @@ import { View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/rendere
import { DocumentTitle, Divider, COLORS, FONT_SIZES } from '../SharedUI'; import { DocumentTitle, Divider, COLORS, FONT_SIZES } from '../SharedUI';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
section: { marginBottom: 24 }, section: { marginBottom: 32 },
sitemapTree: { marginTop: 20 }, intro: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_DIM, lineHeight: 1.6, marginBottom: 24, textAlign: 'justify' },
sitemapRootNode: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 }, sitemapTree: { marginTop: 8 },
sitemapRootDot: { width: 6, height: 6, borderRadius: 3, backgroundColor: COLORS.CHARCOAL, marginRight: 10 }, rootNode: {
sitemapRootTitle: { fontSize: FONT_SIZES.H3, fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: 1, color: COLORS.CHARCOAL }, padding: 12,
sitemapMainLine: { position: 'absolute', left: 2, top: 20, bottom: 0, width: 0.5, backgroundColor: COLORS.DIVIDER }, backgroundColor: COLORS.CHARCOAL,
sitemapBranch: { marginLeft: 20, marginBottom: 12, position: 'relative' }, marginBottom: 20,
sitemapNode: { flexDirection: 'row', alignItems: 'center', marginBottom: 4 }, borderLeftWidth: 3,
sitemapRootIcon: { width: 4, height: 4, backgroundColor: COLORS.CHARCOAL, marginRight: 8 }, borderLeftColor: COLORS.TEXT_LIGHT
sitemapBranchTitle: { fontSize: FONT_SIZES.BODY, fontWeight: 'bold', color: COLORS.TEXT_MAIN }, },
sitemapLeaf: { marginLeft: 12, borderLeftWidth: 0.5, borderLeftColor: COLORS.DIVIDER, paddingLeft: 12, marginTop: 4 }, rootTitle: { fontSize: FONT_SIZES.H3, fontWeight: 'bold', color: COLORS.WHITE, letterSpacing: 0.5 },
sitemapLeafNode: { flexDirection: 'row', alignItems: 'flex-start', marginBottom: 6 }, categorySection: { marginBottom: 20 },
sitemapLeafPointer: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_LIGHT, marginRight: 6 }, categoryHeader: {
sitemapLeafTitle: { fontSize: FONT_SIZES.BODY, fontWeight: 'bold', color: COLORS.TEXT_MAIN }, flexDirection: 'row',
sitemapLeafDesc: { fontSize: FONT_SIZES.TINY, color: COLORS.TEXT_DIM, lineHeight: 1.3, marginTop: 1 }, alignItems: 'center',
paddingBottom: 6,
borderBottomWidth: 1,
borderBottomColor: COLORS.BLUEPRINT,
marginBottom: 10
},
categoryIcon: { width: 8, height: 8, backgroundColor: COLORS.CHARCOAL, marginRight: 10 },
categoryTitle: { fontSize: FONT_SIZES.BODY, fontWeight: 'bold', color: COLORS.CHARCOAL, textTransform: 'uppercase', letterSpacing: 1 },
pagesGrid: { flexDirection: 'row', flexWrap: 'wrap' },
pageCard: {
width: '48%',
marginRight: '2%',
marginBottom: 12,
padding: 10,
borderWidth: 1,
borderColor: COLORS.GRID,
backgroundColor: '#fafafa'
},
pageTitle: { fontSize: FONT_SIZES.BODY, fontWeight: 'bold', color: COLORS.TEXT_MAIN, marginBottom: 2 },
pageDesc: { fontSize: FONT_SIZES.TINY, color: COLORS.TEXT_DIM, lineHeight: 1.4 },
}); });
export const SitemapModule = ({ state }: any) => ( export const SitemapModule = ({ state }: any) => (
<> <>
<DocumentTitle title="Seitenstruktur" /> <DocumentTitle title="Informationsarchitektur" />
<PDFView style={styles.section}> <PDFView style={styles.section}>
<PDFText style={{ fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_DIM, lineHeight: 1.6, marginBottom: 16 }}>Die folgende Struktur bildet das Fundament für die Benutzerführung und Informationsarchitektur Ihres Projekts.</PDFText> <PDFText style={styles.intro}>
<Divider style={{ marginBottom: 20 }} /> Die folgende Struktur definiert die logische Hierarchie und Benutzerführung. Sie dient als Bauplan für die technische Umsetzung und stellt sicher, dass alle relevanten Geschäftsbereiche intuitiv auffindbar sind.
</PDFText>
<PDFView style={styles.sitemapTree}> <PDFView style={styles.sitemapTree}>
<PDFView style={styles.sitemapRootNode}><PDFView style={styles.sitemapRootDot} /><PDFText style={styles.sitemapRootTitle}>{state.websiteTopic || 'Digitales Ökosystem'}</PDFText></PDFView> <PDFView style={styles.rootNode}>
<PDFView style={styles.sitemapMainLine} /> <PDFText style={styles.rootTitle}>{state.websiteTopic || 'Digitales Ökosystem'}</PDFText>
</PDFView>
{state.sitemap?.map((cat: any, i: number) => ( {state.sitemap?.map((cat: any, i: number) => (
<PDFView key={i} style={styles.sitemapBranch}> <PDFView key={i} style={styles.categorySection} wrap={false}>
<PDFView style={styles.sitemapNode}><PDFView style={styles.sitemapRootIcon} /><PDFText style={styles.sitemapBranchTitle}>{cat.category}</PDFText></PDFView> <PDFView style={styles.categoryHeader}>
<PDFView style={styles.sitemapLeaf}> <PDFView style={styles.categoryIcon} />
<PDFText style={styles.categoryTitle}>{cat.category}</PDFText>
</PDFView>
<PDFView style={styles.pagesGrid}>
{cat.pages.map((p: any, j: number) => ( {cat.pages.map((p: any, j: number) => (
<PDFView key={j} style={styles.sitemapLeafNode}> <PDFView key={j} style={[styles.pageCard, j % 2 === 1 ? { marginRight: 0 } : {}]}>
<PDFText style={styles.sitemapLeafPointer}></PDFText> <PDFText style={styles.pageTitle}>{p.title}</PDFText>
<PDFView style={{ flex: 1 }}> {p.desc && <PDFText style={styles.pageDesc}>{p.desc}</PDFText>}
<PDFText style={styles.sitemapLeafTitle}>{p.title}</PDFText>
{p.desc && <PDFText style={styles.sitemapLeafDesc}>{p.desc}</PDFText>}
</PDFView>
</PDFView> </PDFView>
))} ))}
</PDFView> </PDFView>