{"workflow":{"id":12177,"name":"Estimate 4D/5D construction costs from Revit BIM models with DDC CWICR","views":190,"recentViews":0,"totalViews":190,"createdAt":"2025-12-26T12:58:00.409Z","description":"A **professional BIM-to-cost pipeline** that extracts data from **Revit models** (2015–2026), classifies elements with AI, decomposes them into construction works, and generates **detailed cost estimates** using the open-source **DDC CWICR** database. Produces **HTML reports** and **Excel exports** with full resource breakdown.\n\n## Who's it for\n\n- **BIM Managers** automating quantity takeoff and cost estimation\n- **Cost Engineers** integrating 5D workflows into design pipelines\n- **Construction Companies** standardizing estimates from Revit models\n- **General Contractors** doing rapid budget checks during design\n- **MEP Engineers** pricing mechanical/electrical/plumbing systems\n- **Developers** building custom BIM-to-cost integrations\n\n## What it does\n\n1. **Extracts** BIM data from Revit model via converter (RvtExporter)\n2. **Classifies** building vs non-building elements using AI\n3. **Detects** project type (Residential/Commercial/Industrial)\n4. **Generates** construction phases and assigns element types\n5. **Decomposes** each BIM type into detailed work items\n6. **Searches** DDC CWICR vector database for matching rates\n7. **Calculates** costs with unit mapping and resource breakdown\n8. **Validates** work completeness and checks for gaps\n9. **Generates** professional HTML report + Excel file\n\n## How it works\n\n```\n┌─────────────────────────────────────────────────────────────────────────────┐\n│                          REVIT MODEL (.rvt)                                  │\n│                     Revit 2015–2026 supported                                │\n└─────────────────────────────────────────────────────────────────────────────┘\n                                     ↓\n┌─────────────────────────────────────────────────────────────────────────────┐\n│  BLOCK 1: CONVERSION                                                         │\n│  RvtExporter.exe → Excel with BIM element schedules                          │\n└─────────────────────────────────────────────────────────────────────────────┘\n                                     ↓\n┌─────────────────────────────────────────────────────────────────────────────┐\n│  BLOCK 2: DATA LOADING & CLASSIFICATION                                      │\n│  • Filter 3D View elements only                                              │\n│  • AI analyzes headers → aggregation rules (sum/mean/last)                   │\n│  • AI classifies building vs non-building elements                           │\n│  • Hard exclude: Grids, Levels, Annotations, Views, etc.                     │\n└─────────────────────────────────────────────────────────────────────────────┘\n                                     ↓\n┌─────────────────────────────────────────────────────────────────────────────┐\n│  BLOCK 3: PROJECT ANALYSIS (Stages 0–3)                                      │\n│  STAGE 0: Collect filtered BIM data                                          │\n│  STAGE 1: AI detects project type                                            │\n│  STAGE 2: AI generates construction phases                                   │\n│  STAGE 3: AI assigns element types to phases                                 │\n└─────────────────────────────────────────────────────────────────────────────┘\n                                     ↓\n┌─────────────────────────────────────────────────────────────────────────────┐\n│  BLOCK 4: WORK DECOMPOSITION (Stage 4)                                       │\n│  Loop through each BIM type:                                                 │\n│  • AI decomposes type into work items                                        │\n│  • Example: Window → Demolition, Installation, Sealing, Hardware             │\n│  • Prepares search queries for pricing                                       │\n└─────────────────────────────────────────────────────────────────────────────┘\n                                     ↓\n┌─────────────────────────────────────────────────────────────────────────────┐\n│  BLOCK 5: PRICING & CALCULATION (Stages 5–7)                                 │\n│  STAGE 5: Vector search in Qdrant (text-embedding-3-large, 3072 dim)         │\n│  STAGE 6: Map BIM units → Rate units (m² → 100 m²)                           │\n│  STAGE 7: Calculate costs (Qty × Unit Price)                                 │\n└─────────────────────────────────────────────────────────────────────────────┘\n                                     ↓\n┌─────────────────────────────────────────────────────────────────────────────┐\n│  BLOCK 6: VALIDATION & AGGREGATION                                           │\n│  STAGE 7.5: AI validates work completeness                                   │\n│  STAGE 8: Aggregate costs by phases                                          │\n└─────────────────────────────────────────────────────────────────────────────┘\n                                     ↓\n┌─────────────────────────────────────────────────────────────────────────────┐\n│  BLOCK 7: REPORT GENERATION (Stage 9)                                        │\n│  • Professional HTML report with expandable rows                             │\n│  • Excel-compatible XLS file                                                 │\n│  • Auto-opens in browser                                                     │\n└─────────────────────────────────────────────────────────────────────────────┘\n```\n\n## Pipeline Stages\n\n| Stage | Name | Description |\n|-------|------|-------------|\n| 0 | Collect | Gather filtered BIM data |\n| 1 | Project Type | AI detects Residential/Commercial/Industrial |\n| 2 | Phases | AI generates construction phases |\n| 3 | Assignment | AI assigns element types to phases |\n| 4 | Decomposition | AI breaks types into work items |\n| 5 | Vector Search | Query Qdrant for pricing rates |\n| 6 | Unit Mapping | Convert BIM units to rate units |\n| 7 | Calculation | Compute costs (Qty × Price) |\n| 7.5 | Validation | AI checks completeness, finds gaps |\n| 8 | Aggregation | Sum costs by phases |\n| 9 | Reports | Generate HTML + XLS outputs |\n\n## Prerequisites\n\n| Component | Requirement |\n|-----------|-------------|\n| **n8n** | v1.30+ with Execute Command node |\n| **Revit Exporter** | RvtExporter.exe (provided separately) |\n| **OpenAI API** | For embeddings + LLM tasks |\n| **Qdrant** | Vector DB with DDC CWICR collections |\n| **DDC CWICR Data** | [GitHub](https://github.com/datadrivenconstruction/OpenConstructionEstimate-DDC-CWICR) |\n| **Windows** | For Revit converter execution |\n\n## Setup\n\n### 1. Configure File Paths\n\nIn **Setup - Define file paths** node:\n```json\n{\n  \"path_to_converter\": \"C:\\\\path\\\\to\\\\RvtExporter.exe\",\n  \"project_file\": \"C:\\\\path\\\\to\\\\your_project.rvt\",\n  \"group_by\": \"Type Name\",\n  \"language_code\": \"DE\"\n}\n```\n\n### 2. Select Language & Region\n\n| Code | Language | City | Currency |\n|------|----------|------|----------|\n| **AR** | Arabic | Dubai | AED |\n| **ZH** | Chinese | Shanghai | CNY |\n| **DE** | German | Berlin | EUR |\n| **EN** | English | Toronto | CAD |\n| **ES** | Spanish | Barcelona | EUR |\n| **FR** | French | Paris | EUR |\n| **HI** | Hindi | Mumbai | INR |\n| **PT** | Portuguese | São Paulo | BRL |\n| **RU** | Russian | St. Petersburg | RUB |\n\n### 3. Configure AI Model\n\nConnect your preferred LLM in the model nodes:\n\n| Provider | Model | Notes |\n|----------|-------|-------|\n| **OpenAI** | GPT-4o | Default, recommended |\n| **Anthropic** | Claude Opus 4 | High quality |\n| **Google** | Gemini 2.5 Pro | Good for large contexts |\n| **xAI** | Grok 4 | Fast inference |\n| **DeepSeek** | DeepSeek Chat | Cost-effective |\n| **OpenRouter** | Various | Multi-model access |\n\n### 4. Set Up Qdrant\n\nEnsure DDC CWICR collections are loaded:\n```\nDE_BERLIN_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR\nENG_TORONTO_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR\nRU_STPETERSBURG_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR\n...\n```\n\n### 5. Configure OpenAI Credentials\n\nSet up OpenAI API credential for:\n- Embeddings (text-embedding-3-large, 3072 dimensions)\n- LLM calls (if using OpenAI as primary model)\n\n## Features\n\n| Feature | Description |\n|---------|-------------|\n| 🏗️ **Revit Integration** | Direct extraction from .rvt files (2015–2026) |\n| 🤖 **Multi-LLM Support** | OpenAI, Claude, Gemini, Grok, DeepSeek |\n| 🔍 **Smart Classification** | AI separates building from non-building elements |\n| 📊 **Work Decomposition** | Breaks BIM types into detailed work items |\n| 🎯 **Vector Search** | Semantic matching via Qdrant + OpenAI embeddings |\n| 🧮 **Unit Mapping** | Automatic conversion (m² → 100 m², pcs → sets) |\n| ✅ **AI Validation** | Checks for missing works and duplications |\n| 📈 **Phase Aggregation** | Costs grouped by construction phases |\n| 📄 **HTML Report** | Professional report with quality indicators |\n| 📑 **Excel Export** | XLS file with formulas and links |\n| 🌍 **9 Languages** | Full localization + regional pricing |\n\n## Hard Exclude Categories\n\nThe pipeline automatically excludes non-physical elements:\n\n- Levels, Grids, Reference Planes\n- Annotations, Dimensions, Text Notes\n- Tags, Views, Sheets, Schedules\n- Legends, Viewports, Section Boxes\n- Scope Boxes, Match Lines\n- Model Groups, Detail Groups\n- Entourage (RPC people, cars, plants)\n\n## Example Output\n\n**Input:** Residential building Revit model (45 element types)\n\n**Processing:**\n- Project type detected: Residential Multi-Family\n- Phases generated: Foundations → Structure → Envelope → MEP → Finishes\n- Types assigned: 45 types → 5 phases\n- Works decomposed: 45 types → 280 work items\n- Rates found: 245/280 (87.5%)\n\n**Output Files:**\n```\nproject_2024-12-08.html  → Professional HTML report\nproject_2024-12-08.xls   → Excel with full breakdown\n```\n\n**HTML Report Features:**\n- KPI summary (total cost, items, phases)\n- Expandable phase sections\n- Quality indicators (● green/yellow/red)\n- Resource breakdown per work item\n- Clickable rate codes\n- Responsive design\n\n## Output Structure\n\n```\n📊 Cost Estimate: Residential Building\n├── 📁 Phase 1: Foundations\n│   ├── Foundation walls — 125.5 m³ — €12,450\n│   ├── Concrete footings — 45.2 m³ — €8,340\n│   └── Waterproofing — 280 m² — €4,200\n├── 📁 Phase 2: Structure\n│   ├── Concrete columns — 18 pcs — €9,720\n│   ├── Floor slabs — 450 m² — €67,500\n│   └── Stairs — 3 flights — €8,100\n├── 📁 Phase 3: Envelope\n│   ├── Exterior walls — 680 m² — €95,200\n│   ├── Windows — 42 pcs — €25,200\n│   └── Roof system — 225 m² — €33,750\n└── 💰 TOTAL: €485,240\n```\n\n## Notes & Tips\n\n- **First run:** Conversion takes 1–3 minutes depending on model size\n- **Cached conversion:** Subsequent runs skip conversion if Excel exists\n- **Testing mode:** Limit to 10 types for faster debugging\n- **Rate accuracy:** Depends on DDC CWICR coverage for your region\n- **Custom phases:** AI adapts phases based on project type\n- **Missing rates:** Flagged with red indicator in report\n\n## Extending the Pipeline\n\n- **Add custom rates:** Extend Qdrant collection with your pricing\n- **Chain to PM tools:** Connect to OpenProject, Monday, Asana\n- **Email reports:** Add email node after report generation\n- **Cloud storage:** Upload to Google Drive, OneDrive, S3\n- **Webhook trigger:** Replace manual trigger for API access\n\n## Categories\n\n`AI` · `Data Transformation` · `Document Ops` · `Files & Storage`\n\n## Tags\n\n`bim`, `revit`, `cost-estimation`, `5d-bim`, `4d-bim`, `qdrant`, `vector-search`, `openai`, `construction`, `quantity-takeoff`, `html-report`, `multilingual`\n\n---\n\n## Author\n\n**DataDrivenConstruction.io**  \n[https://DataDrivenConstruction.io](https://DataDrivenConstruction.io)  \n[info@datadrivenconstruction.io](mailto:info@datadrivenconstruction.io)\n\n## Consulting & Training\n\nWe help AEC firms implement:\n- BIM-to-cost automation pipelines\n- 4D/5D integration workflows\n- Custom Revit data extractors\n- AI-powered estimation systems\n- Vector database deployment for construction data\n\n**Contact us** to adapt this pipeline to your Revit templates and regional pricing.\n\n## Resources\n\n- **DDC CWICR Database:** [GitHub](https://github.com/datadrivenconstruction/OpenConstructionEstimate-DDC-CWICR)\n- **Qdrant Documentation:** [qdrant.tech/documentation](https://qdrant.tech/documentation/)\n- **OpenAI Embeddings:** [platform.openai.com](https://platform.openai.com/docs/guides/embeddings)\n- **n8n Execute Command:** [docs.n8n.io](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.executecommand/)\n\n---\n\n⭐ **Star us on GitHub!** [github.com/datadrivenconstruction/DDC-CWICR](https://github.com/datadrivenconstruction/OpenConstructionEstimate-DDC-CWICR)","workflow":{"id":"FXzf12SMkIB4vyr5bcckX","meta":{"instanceId":"faa70e11b7175129a74fd834d3451fdc1862589b16d68ded03f91ca7b1ecca12"},"name":"V3.1_CAD_(BIM)_Cost_Estimation_Pipeline_with_DDC_CWICR ES copy","tags":[],"nodes":[{"id":"7b6d90b8-5909-412b-8bdf-694bfc1790c9","name":"When clicking 'Execute workflow'","type":"n8n-nodes-base.manualTrigger","position":[240,624],"parameters":{},"typeVersion":1},{"id":"564abc51-4148-4619-99d8-90a1d0b42f0c","name":"Setup - Define file paths1","type":"n8n-nodes-base.set","position":[528,624],"parameters":{"options":{},"assignments":{"assignments":[{"id":"path-id","name":"path_to_converter","type":"string","value":"C:\\Users\\Artem Boiko\\Desktop\\DS Workshop\\Anwendungen\\NAMENSKÜRZEL-122025_DDC_Workshop_Effizienz-Booster-für-Bauingenieure\\DDC_CONVERTER\\DDC_CONVERTER_REVIT\\RvtExporter.exe"},{"id":"project-file-id","name":"project_file","type":"string","value":"C:\\Users\\Artem Boiko\\Desktop\\DS Workshop\\Anwendungen\\NAMENSKÜRZEL-122025_DDC_Workshop_Effizienz-Booster-für-Bauingenieure\\6_PW_n8n_Revit_QTO\\Data\\2023 racbasicsampleproject.rvt"},{"id":"group-by-id","name":"group_by","type":"string","value":"Type Name"},{"id":"user-description-id","name":"user_project_description","type":"string","value":""},{"id":"language-code-id","name":"language_code","type":"string","value":"DE"}]}},"typeVersion":3.4},{"id":"1823dd10-c1ec-4463-997b-99f1b6a21319","name":"Configure Language & Vector DB","type":"n8n-nodes-base.code","position":[752,624],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════\n// Language & Vector Database Configuration\n// ═══════════════════════════════════════════════════════════\n\nconst input = $input.first().json;\nconst languageCode = (input.language_code || 'DE').toUpperCase();\n\n// Language configurations\nconst languageConfig = {\n  'DE': {\n    city: 'Berlin',\n    vectorDb: 'DE_BERLIN_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n    language: 'German',\n    languageNative: 'Deutsch',\n    currency: 'EUR',\n    currencySymbol: '€',\n    locale: 'de-DE',\n    systemPromptLang: 'Antworte auf Deutsch.',\n    searchLang: 'German'\n  },\n  'EN': {\n    city: 'Toronto',\n    vectorDb: 'ENG_TORONTO_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n    language: 'English',\n    languageNative: 'English',\n    currency: 'CAD',\n    currencySymbol: '$',\n    locale: 'en-CA',\n    systemPromptLang: 'Respond in English.',\n    searchLang: 'English'\n  },\n  'FR': {\n    city: 'Paris',\n    vectorDb: 'FR_PARIS_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n    language: 'French',\n    languageNative: 'Français',\n    currency: 'EUR',\n    currencySymbol: '€',\n    locale: 'fr-FR',\n    systemPromptLang: 'Répondez en français.',\n    searchLang: 'French'\n  },\n  'ES': {\n    city: 'Barcelona',\n    vectorDb: 'ES_BARCELONA_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n    language: 'Spanish',\n    languageNative: 'Español',\n    currency: 'EUR',\n    currencySymbol: '€',\n    locale: 'es-ES',\n    systemPromptLang: 'Responde en español.',\n    searchLang: 'Spanish'\n  },\n  'HI': {\n    city: 'Mumbai',\n    vectorDb: 'HI_MUMBAI_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n    language: 'Hindi',\n    languageNative: 'हिन्दी',\n    currency: 'INR',\n    currencySymbol: '₹',\n    locale: 'hi-IN',\n    systemPromptLang: 'हिंदी में जवाब दें।',\n    searchLang: 'Hindi'\n  },\n  'PT': {\n    city: 'São Paulo',\n    vectorDb: 'PT_SAOPAULO_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n    language: 'Portuguese',\n    languageNative: 'Português',\n    currency: 'BRL',\n    currencySymbol: 'R$',\n    locale: 'pt-BR',\n    systemPromptLang: 'Responda em português.',\n    searchLang: 'Portuguese'\n  },\n  'RU': {\n    city: 'St. Petersburg',\n    vectorDb: 'RU_STPETERSBURG_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n    language: 'Russian',\n    languageNative: 'Русский',\n    currency: 'RUB',\n    currencySymbol: '₽',\n    locale: 'ru-RU',\n    systemPromptLang: 'Отвечай на русском языке.',\n    searchLang: 'Russian'\n  },\n  'ZH': {\n    city: 'Shanghai',\n    vectorDb: 'ZH_SHANGHAI_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n    language: 'Chinese',\n    languageNative: '中文',\n    currency: 'CNY',\n    currencySymbol: '¥',\n    locale: 'zh-CN',\n    systemPromptLang: '请用中文回答。',\n    searchLang: 'Chinese'\n  },\n  'AR': {\n    city: 'Dubai',\n    vectorDb: 'AR_DUBAI_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n    language: 'Arabic',\n    languageNative: 'العربية',\n    currency: 'AED',\n    currencySymbol: 'د.إ',\n    locale: 'ar-AE',\n    systemPromptLang: 'أجب باللغة العربية.',\n    searchLang: 'Arabic'\n  }\n};\n\n// Get config for selected language (default to DE)\nconst config = languageConfig[languageCode] || languageConfig['DE'];\n\n// Universal pricing standards reference\nconst pricingStandards = 'GESN, FER, NRR, ESN, ENiR, SHNK, REKN, SNiR, BNbD, Định Mức';\n\nreturn {\n  json: {\n    // Pass through original values\n    path_to_converter: input.path_to_converter,\n    project_file: input.project_file,\n    group_by: input.group_by,\n    user_project_description: input.user_project_description,\n    \n    // Language configuration\n    language_code: languageCode,\n    language_config: config,\n    qdrant_collection: config.vectorDb,\n    pricing_standards: pricingStandards,\n    \n    // Convenience fields\n    city: config.city,\n    language: config.language,\n    language_native: config.languageNative,\n    currency: config.currency,\n    currency_symbol: config.currencySymbol,\n    locale: config.locale,\n    system_prompt_lang: config.systemPromptLang,\n    search_lang: config.searchLang,\n    \n    // Pricing level info for report header\n    pricing_level: config.city,\n    pricing_level_full: `${config.city}, ${config.language}`\n  }\n};"},"typeVersion":2},{"id":"c6c247ac-60e9-428f-9618-dc3ea93461ee","name":"Non-3D View Elements Output1","type":"n8n-nodes-base.set","position":[1040,992],"parameters":{"options":{},"assignments":{"assignments":[{"id":"message","name":"message","type":"string","value":"Elements not visible in 3D view"},{"id":"filtered_count","name":"filtered_count","type":"number","value":"={{ $input.all().length }}"}]}},"typeVersion":3.4},{"id":"10a25b62-845b-4b40-91fd-e719f47f6681","name":"Find Category Fields","type":"n8n-nodes-base.code","position":[576,1472],"parameters":{"jsCode":"\nconst items = $input.all();\nif (items.length === 0) {\n  return [{json: {error: 'No grouped data found'}}];\n}\n\nconst headers = Object.keys(items[0].json);\n\nconst categoryPatterns = [\n  { pattern: /^category$/i, type: 'Category' },\n  { pattern: /^ifc[\\s_-]?type$/i, type: 'IFC' },\n  { pattern: /^host[\\s_-]?category$/i, type: 'Host' },\n  { pattern: /^ifc[\\s_-]?export[\\s_-]?as$/i, type: 'Export' },\n  { pattern: /^layer$/i, type: 'Layer' }\n];\n\nlet categoryField = null;\nlet categoryFieldType = 'None';\n\nfor (const header of headers) {\n  for (const {pattern, type} of categoryPatterns) {\n    if (pattern.test(header)) {\n      categoryField = header;\n      categoryFieldType = type;\n      break;\n    }\n  }\n  if (categoryField) break;\n}\n\nconst volumetricPatterns = /volume|area|length|count|quantity|thickness|perimeter|depth|size|dimension|weight|mass/i;\nconst volumetricFields = headers.filter(header => volumetricPatterns.test(header));\n\nconst categoryValues = new Set();\nif (categoryField) {\n  items.forEach(item => {\n    const value = item.json[categoryField];\n    if (value && value !== '' && value !== null) {\n      categoryValues.add(value);\n    }\n  });\n}\n\nreturn [{\n  json: {\n    categoryField: categoryField,\n    categoryFieldType: categoryFieldType,\n    categoryValues: Array.from(categoryValues),\n    volumetricFields: volumetricFields,\n    groupedData: items.map(item => item.json),\n    totalGroups: items.length\n  }\n}];"},"typeVersion":2},{"id":"ea9bc69a-9357-4c7a-8e3f-256588005880","name":"Apply Classification to Groups","type":"n8n-nodes-base.code","position":[1200,1472],"parameters":{"jsCode":"\n// ═══════════════════════════════════════════════════════════════════════════════\n// APPLY CLASSIFICATION TO GROUPS - v6.1 with HARD EXCLUDE\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst categoryInfo = $node['Find Category Fields'].json;\nconst groupedData = categoryInfo.groupedData;\nconst categoryField = categoryInfo.categoryField;\nconst volumetricFields = categoryInfo.volumetricFields || [];\n\n// HARD EXCLUDE - категории которые НИКОГДА не должны быть в смете\nconst HARD_EXCLUDE_PATTERNS = [\n  /^levels?$/i, /^ost_levels$/i,\n  /^grids?$/i, /^ost_grids$/i,\n  /^entourage$/i, /^ost_entourage$/i,\n  /model\\s*groups?/i, /^ost_iosmodelgroups$/i,\n  /^detail\\s*groups?$/i,\n  /^reference\\s*planes?$/i,\n  /^scope\\s*box/i,\n  /^match\\s*lines?$/i,\n  /^section\\s*box/i,\n  /^annotations?$/i,\n  /^dimensions?$/i,\n  /^text\\s*notes?$/i,\n  /^tags?$/i,\n  /^views?$/i,\n  /^sheets?$/i,\n  /^schedules?$/i,\n  /^legends?$/i,\n  /^viewports?$/i,\n  /^rpc\\s/i  // RPC people/cars\n];\n\nfunction isHardExcluded(categoryValue, typeName) {\n  if (!categoryValue && !typeName) return false;\n  const catLower = (categoryValue || '').toLowerCase().trim();\n  const typeLower = (typeName || '').toLowerCase().trim();\n  \n  // Check category\n  if (HARD_EXCLUDE_PATTERNS.some(pattern => pattern.test(catLower))) {\n    return true;\n  }\n  \n  // Also check type name for entourage items (Alex, Wine Bottles, etc.)\n  if (catLower.includes('entourage') || typeLower.includes('rpc ')) {\n    return true;\n  }\n  \n  return false;\n}\n\nlet classifications = {};\nlet buildingCategories = [];\nlet drawingCategories = [];\n\ntry {\n  const aiResponse = $input.first().json;\n  const content = aiResponse.content || aiResponse.message || aiResponse.response || '';\n  \n  const jsonMatch = content.match(/\\{[\\s\\S]*\\}/);\n  if (jsonMatch) {\n    const parsed = JSON.parse(jsonMatch[0]);\n    classifications = parsed.classifications || {};\n    buildingCategories = parsed.building_categories || [];\n    drawingCategories = parsed.drawing_categories || [];\n  }\n} catch (error) {\n  console.error('Error parsing AI classification:', error.message);\n}\n\nreturn groupedData.map(group => {\n  let isBuildingElement = false;\n  let reason = '';\n  let confidence = 0;\n  \n  const categoryValue = categoryField ? (group[categoryField] || '') : '';\n  const typeName = group['Type Name'] || group.type_name || '';\n  \n  // ШАГ 1: HARD EXCLUDE\n  if (isHardExcluded(categoryValue, typeName)) {\n    console.log(`HARD EXCLUDE: ${categoryValue} / ${typeName}`);\n    return {\n      json: {\n        ...group,\n        is_building_element: false,\n        element_confidence: 100,\n        element_reason: 'Hard exclude - non-physical element'\n      }\n    };\n  }\n  \n  // ШАГ 2: Оригинальная логика\n  if (categoryField && categoryValue) {\n    if (classifications[categoryValue] !== undefined) {\n      isBuildingElement = classifications[categoryValue];\n      confidence = 95;\n      reason = 'AI classified';\n    } else {\n      const lowerCategory = categoryValue.toLowerCase();\n      const drawingKeywords = /annotation|drawing|text|dimension|tag|view|sheet|grid|section|elevation/i;\n      const buildingKeywords = /wall|floor|roof|column|beam|door|window|stair|pipe|duct|equipment|fixture|furniture|generic|speciality|planting|mass|ceiling|casework|curtain|railing|lighting|plumbing|mechanical|electrical/i;\n      \n      if (drawingKeywords.test(lowerCategory)) {\n        isBuildingElement = false;\n        confidence = 85;\n        reason = 'Drawing keywords';\n      } else if (buildingKeywords.test(lowerCategory)) {\n        isBuildingElement = true;\n        confidence = 85;\n        reason = 'Building keywords';\n      } else {\n        isBuildingElement = true;\n        confidence = 70;\n        reason = 'Default';\n      }\n    }\n  } else {\n    let hasVolumetricData = false;\n    for (const field of volumetricFields) {\n      const value = parseFloat(group[field]);\n      if (!isNaN(value) && value > 0) {\n        hasVolumetricData = true;\n        break;\n      }\n    }\n    isBuildingElement = hasVolumetricData;\n    confidence = hasVolumetricData ? 80 : 60;\n    reason = hasVolumetricData ? 'Has volumetric' : 'No volumetric';\n  }\n  \n  return {\n    json: {\n      ...group,\n      is_building_element: isBuildingElement,\n      element_confidence: confidence,\n      element_reason: reason\n    }\n  };\n});\n"},"typeVersion":2},{"id":"8806d190-86f8-4ba3-a966-d9079ddc3ccf","name":"Non-Building Elements Output1","type":"n8n-nodes-base.set","position":[1552,1328],"parameters":{"options":{},"assignments":{"assignments":[{"id":"set-no-building","name":"result","type":"string","value":"Non-building elements excluded from cost estimation"}]}},"typeVersion":3.4},{"id":"03f51fd1-08dc-480c-9452-ffb1f6f13c73","name":"Is Building Element1","type":"n8n-nodes-base.if","position":[1376,1472],"parameters":{"options":{},"conditions":{"options":{"version":1,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"condition-building","operator":{"type":"boolean","operation":"equals"},"leftValue":"={{ $json.is_building_element }}","rightValue":true}]}},"typeVersion":2},{"id":"01596437-c3fe-4f20-bbb0-530043bfb883","name":"Sticky Note5","type":"n8n-nodes-base.stickyNote","position":[2064,1056],"parameters":{"color":3,"width":208,"height":272,"content":"## Only 10 Groups\n\n\n\n\n\n\n\n\n\n\n\n\nLimits output to first 10 element groups for faster testing and debugging. "},"typeVersion":1},{"id":"4bfe5ddc-253b-4fdc-b09a-83996a1f4a9c","name":"Group Data with AI Rules1","type":"n8n-nodes-base.code","position":[1904,1104],"parameters":{"jsCode":"const input = $input.first().json;\nconst aggregationRules = input.aggregationRules;\nconst headerMapping = input.headerMapping;\nconst rawData = input.rawData;\nconst groupByParamOriginal = input.groupByParam;\n\nconst groupByParam = headerMapping[groupByParamOriginal] || groupByParamOriginal;\n\nconsole.log(`Grouping ${rawData.length} items by: ${groupByParam}`);\n\nconst cleanedData = rawData.map(item => {\n  const cleaned = {};\n  Object.entries(item).forEach(([key, value]) => {\n    const newKey = headerMapping[key] || key;\n    cleaned[newKey] = value;\n  });\n  return cleaned;\n});\n\nconst grouped = {};\n\ncleanedData.forEach(item => {\n  const groupKey = item[groupByParam];\n  \n  if (!groupKey || groupKey === '' || groupKey === null) return;\n  \n  if (!grouped[groupKey]) {\n    grouped[groupKey] = {\n      _count: 0,\n      _values: {}\n    };\n    \n    Object.keys(aggregationRules).forEach(param => {\n      if (param !== groupByParam) {\n        grouped[groupKey]._values[param] = [];\n      }\n    });\n  }\n  \n  grouped[groupKey]._count++;\n  \n  Object.entries(item).forEach(([key, value]) => {\n    if (key === groupByParam) return;\n    \n    if (value !== null && value !== undefined && value !== '' && grouped[groupKey]._values[key]) {\n      grouped[groupKey]._values[key].push(value);\n    }\n  });\n});\n\nconst result = [];\n\nObject.entries(grouped).forEach(([groupKey, groupData]) => {\n  const aggregated = {\n    [groupByParam]: groupKey,\n    'Element Count': groupData._count\n  };\n  \n  Object.entries(groupData._values).forEach(([param, values]) => {\n    const rule = aggregationRules[param] || 'last';\n    \n    if (values.length === 0) {\n      aggregated[param] = null;\n      return;\n    }\n    \n    switch(rule) {\n      case 'sum':\n        const numericValues = values.map(v => {\n          const num = parseFloat(String(v).replace(',', '.'));\n          return isNaN(num) ? 0 : num;\n        });\n        aggregated[param] = numericValues.reduce((a, b) => a + b, 0);\n        \n        if (aggregated[param] % 1 !== 0) {\n          aggregated[param] = Math.round(aggregated[param] * 100) / 100;\n        }\n        break;\n        \n      case 'mean':\n      case 'average':\n        const avgValues = values.map(v => {\n          const num = parseFloat(String(v).replace(',', '.'));\n          return isNaN(num) ? null : num;\n        }).filter(v => v !== null);\n        \n        if (avgValues.length > 0) {\n          const avg = avgValues.reduce((a, b) => a + b, 0) / avgValues.length;\n          aggregated[param] = Math.round(avg * 100) / 100;\n        } else {\n          aggregated[param] = values[values.length - 1];\n        }\n        break;\n        \n      case 'last':\n        aggregated[param] = values[values.length - 1];\n        break;\n        \n      case 'first':\n      default:\n        aggregated[param] = values[0];\n        break;\n    }\n  });\n  \n  result.push({ json: aggregated });\n});\n\nresult.sort((a, b) => {\n  const aVal = a.json[groupByParam];\n  const bVal = b.json[groupByParam];\n  if (aVal < bVal) return -1;\n  if (aVal > bVal) return 1;\n  return 0;\n});\n\nconsole.log(`\\nGrouping complete:`);\nconsole.log(`- Input items: ${cleanedData.length}`);\nconsole.log(`- Output groups: ${result.length}`);\nconsole.log(`- Parameters processed: ${Object.keys(aggregationRules).length}`);\n\nconst rulesSummary = { sum: [], mean: [], last: [], first: [] };\nObject.entries(aggregationRules).forEach(([param, rule]) => {\n  if (rulesSummary[rule]) rulesSummary[rule].push(param);\n});\n\nconsole.log('\\nAggregation summary:');\nif (rulesSummary.sum.length > 0) {\n  console.log(`- SUM (${rulesSummary.sum.length}): ${rulesSummary.sum.slice(0, 5).join(', ')}${rulesSummary.sum.length > 5 ? '...' : ''}`);\n}\nif (rulesSummary.mean.length > 0) {\n  console.log(`- MEAN (${rulesSummary.mean.length}): ${rulesSummary.mean.slice(0, 5).join(', ')}${rulesSummary.mean.length > 5 ? '...' : ''}`);\n}\nif (rulesSummary.last.length > 0) {\n  console.log(`- LAST (${rulesSummary.last.length}): ${rulesSummary.last.slice(0, 5).join(', ')}${rulesSummary.last.length > 5 ? '...' : ''}`);\n}\n\nreturn result;"},"typeVersion":2},{"id":"0963e1e8-5985-45d5-8793-a525bdb17613","name":"Extract Headers and Data","type":"n8n-nodes-base.code","position":[1136,1104],"parameters":{"jsCode":"\n const items = $input.all();\n if (items.length === 0) {\n  throw new Error('No data found in Excel file');\n }\n\n\n const allHeaders = new Set();\n items.forEach(item => {\n  Object.keys(item.json).forEach(key => allHeaders.add(key));\n });\n\n\n const headers = Array.from(allHeaders);\n const cleanedHeaders = headers.map(header => {\n  return header.replace(/:\\s*(string|double|int|float|boolean|number)\\s*$/i, '').trim();\n });\n\n\n const headerMapping = {};\n headers.forEach((oldHeader, index) => {\n  headerMapping[oldHeader] = cleanedHeaders[index];\n });\n\n\n const sampleValues = {};\n cleanedHeaders.forEach((header, index) => {\n  const originalHeader = headers[index];\n  for (const item of items) {\n    const value = item.json[originalHeader];\n    if (value !== null && value !== undefined && value !== '') {\n      sampleValues[header] = value;\n      break;\n    }\n  }\n  if (!sampleValues[header]) {\n    sampleValues[header] = null;\n  }\n });\n\n console.log(`Found ${headers.length} unique headers across ${items.length} items`);\n\n\n return [{\n  json: {\n    headers: cleanedHeaders,\n    originalHeaders: headers,\n    headerMapping: headerMapping,\n    sampleValues: sampleValues,\n    totalRows: items.length,\n    totalHeaders: headers.length,\n    \n    rawData: items.map(item => item.json)\n  }\n }];"},"typeVersion":2},{"id":"2821f087-9a2d-44af-a9fc-013313157b5a","name":"Read Excel File1","type":"n8n-nodes-base.readBinaryFile","position":[544,1120],"parameters":{"filePath":"={{ $json.path_to_file }}"},"typeVersion":1},{"id":"fd8e1e04-83de-4c38-9e1c-64e47ebc98c7","name":"Parse Excel1","type":"n8n-nodes-base.spreadsheetFile","position":[688,1120],"parameters":{"options":{"headerRow":true,"sheetName":"={{ $node['Set Parameters1'].json.sheet_name }}","includeEmptyCells":false},"fileFormat":"xlsx"},"typeVersion":2},{"id":"b7d2a68f-c3c7-4314-a6de-9c556f0dc2f6","name":"Create - Excel filename1","type":"n8n-nodes-base.set","position":[1040,528],"parameters":{"options":{},"assignments":{"assignments":[{"id":"xlsx-filename-id","name":"xlsx_filename","type":"string","value":"={{ $json[\"project_file\"].slice(0, $json[\"project_file\"].lastIndexOf(\".\")) + \"_\" + $json[\"project_file\"].split(\".\").pop() + \".xlsx\" }}"},{"id":"path-to-converter-pass","name":"path_to_converter","type":"string","value":"={{ $json[\"path_to_converter\"] }}"},{"id":"project-file-pass","name":"project_file","type":"string","value":"={{ $json[\"project_file\"] }}"},{"id":"lang-config-pass","name":"language_config","type":"object","value":"={{ $json[\"language_config\"] }}"},{"id":"qdrant-collection-pass","name":"qdrant_collection","type":"string","value":"={{ $json[\"qdrant_collection\"] }}"},{"id":"pricing-standards-pass","name":"pricing_standards","type":"string","value":"={{ $json[\"pricing_standards\"] }}"},{"id":"currency-pass","name":"currency","type":"string","value":"={{ $json[\"currency\"] }}"},{"id":"language-pass","name":"language","type":"string","value":"={{ $json[\"language\"] }}"},{"id":"locale-pass","name":"locale","type":"string","value":"={{ $json[\"locale\"] }}"},{"id":"system-prompt-lang-pass","name":"system_prompt_lang","type":"string","value":"={{ $json[\"system_prompt_lang\"] }}"}]}},"typeVersion":3.4},{"id":"8df36e4f-de0a-4699-9675-cf53fe51d031","name":"Check - Does Excel file exist?1","type":"n8n-nodes-base.readBinaryFile","position":[1248,528],"parameters":{"filePath":"={{ $json[\"xlsx_filename\"] }}"},"typeVersion":1,"continueOnFail":true,"alwaysOutputData":true},{"id":"74909e53-96fd-4b53-8358-614fdefff67e","name":"If - File exists?1","type":"n8n-nodes-base.if","position":[1408,528],"parameters":{"options":{},"conditions":{"options":{"version":1,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"e7fb1577-e753-43f5-9f5a-4d5285aeb96e","operator":{"type":"boolean","operation":"equals"},"leftValue":"={{ $binary.data ? true : false }}","rightValue":"={{ true }}"}]}},"typeVersion":2},{"id":"9333fc8b-3fae-45f4-99e3-8b6cc771358d","name":"Extract - Run converter1","type":"n8n-nodes-base.executeCommand","position":[1168,784],"parameters":{"command":"=\"{{$node[\"Configure Language & Vector DB\"].json[\"path_to_converter\"]}}\" \"{{$node[\"Configure Language & Vector DB\"].json[\"project_file\"]}}\""},"typeVersion":1,"continueOnFail":true},{"id":"5938642d-5d10-4ca2-95c0-23ccf1870ec9","name":"Info - Skip conversion1","type":"n8n-nodes-base.set","position":[1584,496],"parameters":{"options":{},"assignments":{"assignments":[{"id":"status-id","name":"status","type":"string","value":"File already exists - skipping conversion"},{"id":"xlsx-filename-id","name":"xlsx_filename","type":"string","value":"={{ $node[\"Create - Excel filename1\"].json[\"xlsx_filename\"] }}"}]}},"typeVersion":3.4},{"id":"91c1c1f8-027b-47e0-90e7-cb195bb88590","name":"Check - Did extraction succeed?1","type":"n8n-nodes-base.if","position":[1376,784],"parameters":{"options":{},"conditions":{"options":{"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"condition1","operator":{"type":"object","operation":"exists","rightType":"any"},"leftValue":"={{ $node[\"Extract - Run converter1\"].json.error }}","rightValue":""}]}},"typeVersion":2},{"id":"9f626e7b-e60e-4af7-bd87-355e5a961f95","name":"Error - Show what went wrong1","type":"n8n-nodes-base.set","position":[1584,656],"parameters":{"options":{},"assignments":{"assignments":[{"id":"error-message-id","name":"error_message","type":"string","value":"=Extraction failed: {{ $node[\"Extract - Run converter1\"].json.error || \"Unknown error\" }}"},{"id":"error-code-id","name":"error_code","type":"number","value":"={{ $node[\"Extract - Run converter1\"].json.code || -1 }}"},{"id":"xlsx-filename-error","name":"xlsx_filename","type":"string","value":"={{ $node[\"Create - Excel filename1\"].json[\"xlsx_filename\"] }}"}]}},"typeVersion":3.4},{"id":"96473e72-2069-4e84-a577-9b59b526d947","name":"Set xlsx_filename after success1","type":"n8n-nodes-base.set","position":[1744,800],"parameters":{"options":{},"assignments":{"assignments":[{"id":"xlsx-filename-success","name":"xlsx_filename","type":"string","value":"={{ $node[\"Create - Excel filename1\"].json[\"xlsx_filename\"] }}"}]}},"typeVersion":3.4},{"id":"d242257c-6836-40be-9990-6a941c2f0589","name":"Merge - Continue workflow1","type":"n8n-nodes-base.merge","position":[1904,576],"parameters":{},"typeVersion":3},{"id":"02a7cd9b-a60b-4f63-90f0-5bf6895e3736","name":"Set Parameters1","type":"n8n-nodes-base.set","position":[2080,800],"parameters":{"options":{},"assignments":{"assignments":[{"id":"path-id","name":"path_to_file","type":"string","value":"={{ $json.xlsx_filename }}"}]}},"typeVersion":3.4},{"id":"d865851b-2601-4c7a-af30-d92dc5ad927a","name":"Process AI Response1","type":"n8n-nodes-base.code","position":[1728,1104],"parameters":{"jsCode":"const aiResponse = $input.first().json;\nconst headerData = $node['Extract Headers and Data'].json;\n\nlet aiRules = {};\ntry {\n  // Извлекаем текст из поля text (приоритет) или других возможных полей\n  const content = aiResponse.text || aiResponse.content || aiResponse.message || aiResponse.response || '';\n  console.log('AI Response received, length:', content.length);\n  \n  if (!content) {\n    console.warn('No content found in AI response');\n  } else {\n    // Пытаемся распарсить JSON из текста\n    // Убираем markdown code blocks если есть\n    let cleanContent = content.trim();\n    cleanContent = cleanContent.replace(/^\\s*/i, '').replace(/^```\\s*/i, '').replace(/\\s*```$/i, '').trim();\n    \n    // Ищем JSON объект в тексте\n    const jsonMatch = cleanContent.match(/\\{[\\s\\S]*\\}/);\n    if (jsonMatch) {\n      const parsed = JSON.parse(jsonMatch[0]);\n      \n      // Проверяем разные возможные структуры ответа\n      if (parsed.aggregation_rules) {\n        aiRules = parsed.aggregation_rules;\n      } else if (parsed.parameter_aggregation) {\n        aiRules = parsed.parameter_aggregation;\n      } else if (typeof parsed === 'object' && !Array.isArray(parsed)) {\n        // Если сам объект уже является правилами (как в примере входа)\n        aiRules = parsed;\n      }\n      \n      console.log(`AI provided ${Object.keys(aiRules).length} rules`);\n    } else {\n      console.warn('No JSON object found in AI response');\n    }\n  }\n} catch (error) {\n  console.error('Error parsing AI response:', error.message);\n  console.error('Response content:', aiResponse.text || aiResponse.content || 'N/A');\n}\n\n// Создаем финальные правила, объединяя AI правила с дефолтными\nconst finalRules = {};\nheaderData.headers.forEach(header => {\n  // Проверяем точное совпадение\n  if (aiRules[header]) {\n    finalRules[header] = aiRules[header];\n  } else {\n    // Проверяем case-insensitive совпадение\n    const headerLower = header.toLowerCase();\n    const matchingKey = Object.keys(aiRules).find(key => key.toLowerCase() === headerLower);\n    \n    if (matchingKey) {\n      finalRules[header] = aiRules[matchingKey];\n    } else {\n      // Применяем дефолтные правила на основе паттернов\n      const lowerHeader = header.toLowerCase();\n      \n      if (lowerHeader.match(/volume|area|length|width|height|count|quantity|thickness|perimeter|depth|size|dimension|weight|mass|total|amount|number/)) {\n        finalRules[header] = 'sum';\n      } else if (lowerHeader.match(/price|rate|cost|coefficient|factor|percent|ratio|efficiency|avg|average|mean/)) {\n        finalRules[header] = 'mean';\n      } else {\n        finalRules[header] = 'last';\n      }\n    }\n  }\n});\n\nconst groupByParam = $node['Configure Language & Vector DB'].json.group_by;\n\nconsole.log(`\\nAggregation rules summary:`);\nconsole.log(`- Total headers: ${headerData.headers.length}`);\nconsole.log(`- AI rules: ${Object.keys(aiRules).length}`);\nconsole.log(`- Default rules: ${headerData.headers.length - Object.keys(aiRules).length}`);\nconsole.log(`- Group by: ${groupByParam}`);\n\nreturn [{\n  json: {\n    aggregationRules: finalRules,\n    headerMapping: headerData.headerMapping,\n    headers: headerData.headers,\n    originalHeaders: headerData.originalHeaders,\n    rawData: headerData.rawData,\n    groupByParam: groupByParam,\n    totalRows: headerData.totalRows\n  }\n}];"},"typeVersion":2},{"id":"2171308c-bb09-4e75-841c-59e8391deb88","name":"On the standard 3D View","type":"n8n-nodes-base.if","position":[864,1120],"parameters":{"conditions":{"boolean":[{"value1":"={{$json['On the standard 3D View : Boolean']}}","value2":true}]}},"typeVersion":1},{"id":"ed80580f-23d9-46cc-bb01-f580fad59941","name":"STAGE 0 - Collect BIM Data","type":"n8n-nodes-base.code","position":[1696,1456],"parameters":{"jsCode":"// STAGE 0: Collect and prepare BIM data\n// Task: Collect ALL data including uncategorized parameters\n// FIXED: Deduplicate types by type_name and aggregate quantities\n\nconst items = $input.all();\nconst firstItem = items[0]?.json || {};\n\n// Get project settings from language config node\nconst langConfig = $('Configure Language & Vector DB').first().json;\nconst projectDescription = langConfig.user_project_description || '';\nconst qdrantCollection = langConfig.qdrant_collection;\nconst pricingStandards = langConfig.pricing_standards;\nconst language = langConfig.language;\nconst currency = langConfig.currency;\nconst locale = langConfig.locale;\nconst systemPromptLang = langConfig.system_prompt_lang;\nconst searchLang = langConfig.search_lang;\n\n// Collect list of all available parameters ONCE\nconst availableParameters = Object.keys(firstItem);\n\n// Patterns for parameter type detection\nconst patterns = {\n  volumetric: /volume|area|length|width|height|thickness|perimeter|quantity|size|weight|mass|count/i,\n  material: /material|finish|coating|type.*material/i,\n  identifier: /^id$|name|mark|number|code|type\\s*name/i,\n  // Parameters NOT needed for pricing\n  system: /guid|uniqueid|workset|phase|level|design.*option|edited|created|^id$/i\n};\n\n// Classify parameters\nconst parametersByType = {\n  volumetric: availableParameters.filter(p => patterns.volumetric.test(p)),\n  material: availableParameters.filter(p => patterns.material.test(p)),\n  identifier: availableParameters.filter(p => patterns.identifier.test(p)),\n  system: availableParameters.filter(p => patterns.system.test(p)),\n  other: availableParameters.filter(p => \n    !patterns.volumetric.test(p) && \n    !patterns.material.test(p) && \n    !patterns.identifier.test(p) &&\n    !patterns.system.test(p)\n  )\n};\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// FIXED: Use Map to deduplicate types by type_name and aggregate data\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst MAX_TYPES = 1000;\nconst typeMap = new Map();\n\nitems.forEach((item) => {\n  const json = item.json;\n  \n  // Get type name - this is the key for grouping\n  const typeName = json['Type Name'] || json.type_name || 'Unknown';\n  const category = json['Category'] || json.category || '';\n  \n  // Create composite key: type_name + category (same name in different categories = different types)\n  const typeKey = `${typeName}|||${category}`;\n  \n  if (!typeMap.has(typeKey)) {\n    // First occurrence - create new entry\n    typeMap.set(typeKey, {\n      type_name: typeName,\n      category: category,\n      element_count: 0,\n      quantities: {},\n      additional_parameters: {},\n      material_parameters: {},\n      raw_name: typeName,\n      _items_count: 0  // Track how many items were merged\n    });\n  }\n  \n  const typeData = typeMap.get(typeKey);\n  typeData._items_count++;\n  \n  // Aggregate element_count\n  typeData.element_count += parseInt(json['Element Count'] || json.element_count || 1);\n  \n  // Aggregate volumetric quantities (SUM)\n  parametersByType.volumetric.forEach(param => {\n    const value = parseFloat(json[param]);\n    if (!isNaN(value) && value !== 0) {\n      typeData.quantities[param] = (typeData.quantities[param] || 0) + value;\n    }\n  });\n  \n  // For additional parameters - take first non-empty value (FIRST wins)\n  parametersByType.other.forEach(param => {\n    const value = json[param];\n    if (value !== null && value !== undefined && value !== '' && !typeData.additional_parameters[param]) {\n      typeData.additional_parameters[param] = value;\n    }\n  });\n  \n  // For material parameters - take first non-empty value (FIRST wins)\n  parametersByType.material.forEach(param => {\n    const value = json[param];\n    if (value !== null && value !== undefined && value !== '' && !typeData.material_parameters[param]) {\n      typeData.material_parameters[param] = value;\n    }\n  });\n});\n\n// Convert Map to array and add indices\nconst rawTypes = Array.from(typeMap.values())\n  .slice(0, MAX_TYPES)\n  .map((type, index) => ({\n    type_index: index,\n    type_name: type.type_name,\n    category: type.category,\n    element_count: type.element_count,\n    quantities: type.quantities,\n    additional_parameters: type.additional_parameters,\n    material_parameters: type.material_parameters,\n    raw_name: type.raw_name,\n    _merged_from: type._items_count  // Info: how many BIM elements were merged into this type\n  }));\n\n// Log deduplication results\nconsole.log(`═══ STAGE 0: BIM Data Collection ═══`);\nconsole.log(`Input items: ${items.length}`);\nconsole.log(`Unique types after deduplication: ${rawTypes.length}`);\nconsole.log(`Truncated to MAX_TYPES: ${typeMap.size > MAX_TYPES}`);\n\nreturn [{\n  json: {\n    // Project metadata\n    project_description: projectDescription,\n    qdrant_collection: qdrantCollection,\n    pricing_standards: pricingStandards,\n    \n    // Language settings\n    language: language,\n    currency: currency,\n    locale: locale,\n    system_prompt_lang: systemPromptLang,\n    search_lang: searchLang,\n    \n    // Available parameters info (for LLM context)\n    parameters_info: {\n      total_count: availableParameters.length,\n      volumetric: parametersByType.volumetric,\n      material: parametersByType.material,\n      other: parametersByType.other\n    },\n    \n    // Raw types data (DEDUPLICATED)\n    raw_types: rawTypes,\n    types_count: rawTypes.length,\n    total_in_bim: items.length,\n    unique_types_found: typeMap.size,\n    was_truncated: typeMap.size > MAX_TYPES,\n    \n    stage: 'STAGE_0_DATA_COLLECTED'\n  }\n}];\n"},"typeVersion":2},{"id":"2894525e-321f-4e0d-9d82-831a6807044e","name":"Parse Stage 1 - Project Type","type":"n8n-nodes-base.code","position":[480,1776],"parameters":{"jsCode":"// Parse Stage 1 - Project Type Detection\nconst response = $input.first().json;\nconst stage0Data = $('STAGE 0 - Collect BIM Data').first().json;\n\nlet parsed;\ntry {\n  // Ответ гарантированно в поле text\n  const content = response.text || '';\n  \n  if (content) {\n    // Парсим JSON напрямую (markdown обёртки нет)\n    parsed = JSON.parse(content);\n  } else if (response.project_type) {\n    // Fallback: если уже объект\n    parsed = response;\n  } else {\n    throw new Error('No content in text field');\n  }\n  \n} catch (e) {\n  console.log('Parse error:', e.message);\n  parsed = {\n    project_type: { detected: 'unknown', confidence: 0, reasoning: 'Parse error: ' + e.message },\n    project_scale: 'unknown',\n    main_categories: []\n  };\n}\n\nreturn {\n  json: {\n    // Stage 1 result\n    project_type: parsed.project_type,\n    project_scale: parsed.project_scale,\n    main_categories: parsed.main_categories || [],\n    project_notes: parsed.notes || '',\n    \n    // Pass through Stage 0 data\n    raw_types: stage0Data.raw_types,\n    types_count: stage0Data.types_count,\n    parameters_info: stage0Data.parameters_info,\n    project_description: stage0Data.project_description,\n    qdrant_collection: stage0Data.qdrant_collection,\n    pricing_standards: stage0Data.pricing_standards,\n    language: stage0Data.language,\n    currency: stage0Data.currency,\n    locale: stage0Data.locale,\n    system_prompt_lang: stage0Data.system_prompt_lang,\n    \n    stage: 'STAGE_1_PROJECT_DETECTED'\n  }\n};"},"typeVersion":2},{"id":"abeaf708-7a2d-47c1-bd6b-67a47ad8e719","name":"Parse Stage 2 - Phases","type":"n8n-nodes-base.code","position":[1088,1776],"parameters":{"jsCode":"// Parse Stage 2 - Construction Phases\nconst response = $input.first().json;\nconst prevData = $('Parse Stage 1 - Project Type').first().json;\n\nlet parsed;\ntry {\n  // Ответ гарантированно в поле text\n  const content = response.text || '';\n  \n  if (content) {\n    // Парсим JSON напрямую (markdown обёртки нет)\n    parsed = JSON.parse(content);\n  } else {\n    throw new Error('No content in text field');\n  }\n  \n} catch (e) {\n  console.log('Parse error:', e.message);\n  parsed = {\n    construction_phases: [],\n    phase_logic: 'Parse error: ' + e.message\n  };\n}\n\nreturn {\n  json: {\n    // Stage 2 result\n    construction_phases: parsed.construction_phases || [],\n    phase_logic: parsed.phase_logic || parsed.summary?.reasoning || '',\n    total_phases: (parsed.construction_phases || []).length,\n    \n    // Project data from previous stages\n    project_type: prevData.project_type,\n    project_scale: prevData.project_scale,\n    main_categories: prevData.main_categories,\n    raw_types: prevData.raw_types,\n    types_count: prevData.types_count,\n    parameters_info: prevData.parameters_info,\n    project_description: prevData.project_description,\n    qdrant_collection: prevData.qdrant_collection,\n    pricing_standards: prevData.pricing_standards,\n    language: prevData.language,\n    currency: prevData.currency,\n    locale: prevData.locale,\n    system_prompt_lang: prevData.system_prompt_lang,\n    \n    stage: 'STAGE_2_PHASES_GENERATED'\n  }\n};"},"typeVersion":2},{"id":"d4c78a1e-2e69-460c-80dc-9717cfc437b4","name":"Parse Stage 3 - Final Structure","type":"n8n-nodes-base.code","position":[1904,1776],"parameters":{"jsCode":"// Parse Stage 3 - Types Assignment - FIXED v2\n// CRITICAL FIX: Use ALL types from BIM, not just LLM response\n\nconst response = $input.first().json;\nconst prevData = $('Parse Stage 2 - Phases').first().json;\nconst originalRawTypes = prevData.raw_types || [];\n\nconsole.log(`Parse Stage 3: ${originalRawTypes.length} types from BIM`);\n\nlet llmAssignments = {};\nlet parsed = null;\n\ntry {\n  let content = '';\n  if (typeof response.message?.content === 'string') content = response.message.content;\n  else if (typeof response.content === 'string') content = response.content;\n  else if (typeof response.text === 'string') content = response.text;\n  else if (response.types_with_phases) parsed = response;\n  \n  if (!parsed && content) {\n    content = content.replace(/^```json\\s*/i, '').replace(/^```\\s*/i, '').replace(/\\s*```$/i, '').trim();\n    const jsonMatch = content.match(/\\{[\\s\\S]*\\}/);\n    if (jsonMatch) parsed = JSON.parse(jsonMatch[0]);\n  }\n  \n  if (parsed?.types_with_phases) {\n    parsed.types_with_phases.forEach(t => {\n      if (t.type_index !== undefined) llmAssignments[t.type_index] = t;\n      if (t.type_name) llmAssignments[t.type_name] = t;\n    });\n    console.log(`LLM assigned ${parsed.types_with_phases.length} types`);\n  }\n} catch (e) {\n  console.log(`Parse error: ${e.message}`);\n}\n\n// KEY FIX: Start from ALL original types\nconst typesWithPhases = originalRawTypes.map((original, idx) => {\n  const llmData = llmAssignments[idx] || llmAssignments[original.type_index] || llmAssignments[original.type_name] || {};\n  \n  return {\n    type_index: original.type_index ?? idx,\n    type_name: original.type_name || 'Unknown',\n    category: original.category || '',\n    element_count: original.element_count || 1,\n    assigned_phase_id: llmData.assigned_phase_id || 1,\n    sequence_in_phase: llmData.sequence_in_phase || idx + 1,\n    detected_materials: llmData.detected_materials || [],\n    primary_quantity: llmData.primary_quantity || { parameter: 'Area', unit: 'm²' },\n    quantities: original.quantities || {},\n    additional_parameters: original.additional_parameters || {},\n    material_parameters: original.material_parameters || {},\n    raw_name: original.raw_name || original.type_name || ''\n  };\n});\n\nconst phases = prevData.construction_phases || [];\nconst enrichedPhases = phases.map(phase => {\n  const phaseTypes = typesWithPhases.filter(t => t.assigned_phase_id === phase.phase_id)\n    .sort((a, b) => (a.sequence_in_phase || 0) - (b.sequence_in_phase || 0));\n  return { ...phase, assigned_types: phaseTypes, types_count: phaseTypes.length };\n});\n\nconsole.log(`OUTPUT: ${typesWithPhases.length} types → ${enrichedPhases.length} phases`);\n\nreturn { json: {\n  phases_with_types: enrichedPhases,\n  types_with_phases: typesWithPhases,\n  unassigned_types: [],\n  assignment_summary: { total_types: typesWithPhases.length },\n  project_context: {\n    project_type: prevData.project_type?.detected || 'unknown',\n    project_scale: prevData.project_scale || 'unknown',\n    total_phases: enrichedPhases.length,\n    total_types: typesWithPhases.length\n  },\n  parameters_info: prevData.parameters_info,\n  qdrant_collection: prevData.qdrant_collection,\n  pricing_standards: prevData.pricing_standards,\n  language: prevData.language,\n  currency: prevData.currency,\n  locale: prevData.locale,\n  system_prompt_lang: prevData.system_prompt_lang,\n  stage: 'STAGE_3_TYPES_ASSIGNED'\n}};"},"typeVersion":2},{"id":"7862d526-b3ab-45c6-9a05-a68b8c79ec48","name":"Prepare Types for Decomposition","type":"n8n-nodes-base.code","position":[2064,1776],"parameters":{"jsCode":"// Prepare Types for Decomposition Loop\n// Prepare each type for sending to STAGE 4 (work decomposition)\n// INCLUDING all uncategorized parameters\n\nconst data = $input.first().json;\nconst typesWithPhases = data.types_with_phases || [];\nconst phasesWithTypes = data.phases_with_types || [];\nconst projectContext = data.project_context || {};\n\n// Create lookup for fast phase search\nconst phaseLookup = {};\nphasesWithTypes.forEach(phase => {\n  phaseLookup[phase.phase_id] = {\n    phase_id: phase.phase_id,\n    phase_code: phase.phase_code,\n    phase_name_ru: phase.phase_name_ru,\n    phase_name_en: phase.phase_name_en\n  };\n});\n\n// Prepare items for loop\nconst typeItems = typesWithPhases.map((type, index) => {\n  const phase = phaseLookup[type.assigned_phase_id] || null;\n  \n  // Combine ALL parameters into one object for LLM\n  const allQuantities = {\n    ...type.quantities,\n    ...type.additional_parameters,\n  };\n  \n  return {\n    json: {\n      // Indexing for loop\n      type_index: index,\n      total_types: typesWithPhases.length,\n      \n      // Type data\n      type_name: type.type_name,\n      category: type.category,\n      element_count: type.element_count,\n      detected_materials: type.detected_materials || [],\n      primary_quantity: type.primary_quantity || {},\n      \n      // ALL parameters for LLM\n      quantities: allQuantities,\n      \n      // Keep separately for reference\n      categorized_quantities: type.quantities || {},\n      additional_parameters: type.additional_parameters || {},\n      material_parameters: type.material_parameters || {},\n      \n      // Phase context\n      assigned_phase: phase,\n      sequence_in_phase: type.sequence_in_phase || 1,\n      \n      // Project context (for LLM in STAGE 4)\n      project_type: projectContext.project_type,\n      project_scale: projectContext.project_scale,\n      \n      // Language & pricing settings\n      qdrant_collection: data.qdrant_collection,\n      pricing_standards: data.pricing_standards,\n      language: data.language,\n      currency: data.currency,\n      locale: data.locale,\n      system_prompt_lang: data.system_prompt_lang\n    }\n  };\n});\n\nreturn typeItems;"},"typeVersion":2},{"id":"ab3fd821-66e2-4a33-baad-ace7d4f09cd3","name":"Loop Types for Decomposition","type":"n8n-nodes-base.splitInBatches","position":[480,2496],"parameters":{"options":{"reset":false}},"typeVersion":3},{"id":"6cc8ec9a-c47c-43f2-ab76-de0156948636","name":"Parse Decomposition & Prepare Works","type":"n8n-nodes-base.code","position":[752,2496],"parameters":{"jsCode":"\n// Parse Decomposition & Prepare Works - v6.1 with dedup and param fixes\n\nconst allResponses = $input.all();\nconst allTypes = $('Prepare Types for Decomposition').all();\n\n// Domain patterns\nconst DOMAIN_PATTERNS = {\n  electrical: [/\\b(volt|kv|amp|watt|hz|mw|kw)\\b/i, /\\b(electric|elektr|электр)\\b/i, /\\b(schalt|switch|panel|transform)\\b/i],\n  industrial: [/\\b(industrial|industrie|промышл)\\b/i, /\\b(conveyor|förder|конвейер)\\b/i, /\\b(vacuum|vakuum|вакуум)\\b/i],\n  vertical_transport: [/\\b(aufzug|elevator|lift|fahrstuhl|лифт)\\b/i, /\\b(escalator|rolltreppe|эскалатор)\\b/i]\n};\n\nconst CATEGORY_GROUPS = {\n  structural: ['foundation', 'floor', 'wall', 'stair', 'roof', 'column', 'beam', 'slab', 'ceiling', 'framing'],\n  openings: ['window', 'door', 'curtain', 'fenster', 'tür', 'окн', 'двер'],\n  site: ['plant', 'tree', 'topograph', 'site', 'parking', 'landscape'],\n  mep: ['plumb', 'mechan', 'electr', 'pipe', 'duct', 'fixture', 'hvac', 'lighting', 'equipment'],\n  unit_based: ['window', 'door', 'lighting', 'plumbing', 'fixture', 'equipment', 'furniture', 'appliance']\n};\n\nconst INCOMPATIBLE = {\n  structural: ['electrical', 'industrial', 'vertical_transport'],\n  openings: ['industrial', 'vertical_transport', 'electrical'],\n  site: ['electrical', 'industrial']\n};\n\nfunction getCategoryGroup(category) {\n  const cat = (category || '').toLowerCase().replace('ost_', '');\n  for (const [group, keywords] of Object.entries(CATEGORY_GROUPS)) {\n    if (keywords.some(kw => cat.includes(kw))) return group;\n  }\n  return 'other';\n}\n\nfunction isUnitBased(category) {\n  const cat = (category || '').toLowerCase().replace('ost_', '');\n  return CATEGORY_GROUPS.unit_based.some(kw => cat.includes(kw));\n}\n\nfunction detectDomains(text) {\n  const domains = [];\n  for (const [domain, patterns] of Object.entries(DOMAIN_PATTERNS)) {\n    if (patterns.some(p => p.test(text))) domains.push(domain);\n  }\n  return domains;\n}\n\nfunction isWorkValid(workName, searchQuery, category) {\n  const text = ((workName || '') + ' ' + (searchQuery || '')).toLowerCase();\n  const catGroup = getCategoryGroup(category);\n  const domains = detectDomains(text);\n  const blocked = INCOMPATIBLE[catGroup] || [];\n  for (const domain of domains) {\n    if (blocked.includes(domain)) return false;\n  }\n  return true;\n}\n\nfunction generateGenericFallback(typeName, category, quantities) {\n  const catClean = (category || '').replace('OST_', '').replace(/([A-Z])/g, ' $1').trim();\n  const catLower = (category || '').toLowerCase();\n  const searchBase = `${typeName} ${catClean}`.trim();\n  const works = [];\n  const q = quantities || {};\n  \n  const countCategories = ['window', 'door', 'furniture', 'fixture', 'equipment', 'lighting', 'plumbing', 'speciality', 'appliance'];\n  const areaCategories = ['floor', 'wall', 'roof', 'ceiling', 'slab'];\n  const volumeCategories = ['foundation', 'column', 'footing'];\n  const linearCategories = ['railing', 'pipe', 'duct', 'cable', 'conduit', 'beam'];\n  \n  const isCountBased = countCategories.some(c => catLower.includes(c));\n  const isAreaBased = areaCategories.some(c => catLower.includes(c));\n  const isVolumeBased = volumeCategories.some(c => catLower.includes(c));\n  const isLinearBased = linearCategories.some(c => catLower.includes(c));\n  \n  if (isCountBased) {\n    works.push({ work_id: 'W001', work_name: `${catClean} - Installation`, search_query: searchBase, expected_unit: 'Stück', quantity_source: { method: 'direct', bim_parameter: 'element_count', coefficient: 1.0 }, work_sequence: 1 });\n  } else if (isVolumeBased) {\n    if (q.Volume && parseFloat(q.Volume) > 0) works.push({ work_id: 'W001', work_name: `${catClean} - Volume work`, search_query: searchBase, expected_unit: 'm³', quantity_source: { method: 'direct', bim_parameter: 'Volume', coefficient: 1.0 }, work_sequence: 1 });\n    if (q.Area && parseFloat(q.Area) > 0) works.push({ work_id: `W${String(works.length + 1).padStart(3, '0')}`, work_name: `${catClean} - Area work`, search_query: searchBase, expected_unit: 'm²', quantity_source: { method: 'direct', bim_parameter: 'Area', coefficient: 1.0 }, work_sequence: works.length + 1 });\n  } else if (isAreaBased) {\n    if (q.Area && parseFloat(q.Area) > 0) works.push({ work_id: 'W001', work_name: `${catClean} - Area work`, search_query: searchBase, expected_unit: 'm²', quantity_source: { method: 'direct', bim_parameter: 'Area', coefficient: 1.0 }, work_sequence: 1 });\n    if (q.Volume && parseFloat(q.Volume) > 0) works.push({ work_id: `W${String(works.length + 1).padStart(3, '0')}`, work_name: `${catClean} - Volume work`, search_query: searchBase, expected_unit: 'm³', quantity_source: { method: 'direct', bim_parameter: 'Volume', coefficient: 1.0 }, work_sequence: works.length + 1 });\n  } else if (isLinearBased) {\n    if (q.Length && parseFloat(q.Length) > 0) works.push({ work_id: 'W001', work_name: `${catClean} - Linear work`, search_query: searchBase, expected_unit: 'm', quantity_source: { method: 'direct', bim_parameter: 'Length', coefficient: 1.0 }, work_sequence: 1 });\n  } else {\n    if (q.Area && parseFloat(q.Area) > 0) works.push({ work_id: 'W001', work_name: `${catClean} - Area work`, search_query: searchBase, expected_unit: 'm²', quantity_source: { method: 'direct', bim_parameter: 'Area', coefficient: 1.0 }, work_sequence: 1 });\n  }\n  \n  if (works.length === 0) works.push({ work_id: 'W001', work_name: `${catClean} - Installation`, search_query: searchBase, expected_unit: 'Stück', quantity_source: { method: 'direct', bim_parameter: 'element_count', coefficient: 1.0 }, work_sequence: 1 });\n  return works;\n}\n\nfunction fixQuantityParams(workItems, category) {\n  if (!isUnitBased(category)) return workItems;\n  return workItems.map(work => {\n    const param = work.quantity_source?.bim_parameter || '';\n    if (param.toLowerCase().includes('area') || param.toLowerCase().includes('volume')) {\n      return { ...work, quantity_source: { method: 'direct', bim_parameter: 'element_count', coefficient: 1.0 }, expected_unit: 'Stück' };\n    }\n    return work;\n  });\n}\n\nfunction deduplicateWorks(workItems) {\n  const seenNames = new Set();\n  const seenQueries = new Set();\n  const unique = [];\n  for (const work of workItems) {\n    const normName = (work.work_name || '').toLowerCase().trim();\n    const normQuery = (work.search_query || '').toLowerCase().trim();\n    if (!seenNames.has(normName) && !seenQueries.has(normQuery)) {\n      seenNames.add(normName);\n      seenQueries.add(normQuery);\n      unique.push(work);\n    }\n  }\n  unique.forEach((work, idx) => { work.work_id = `W${String(idx + 1).padStart(3, '0')}`; work.work_sequence = idx + 1; });\n  return unique;\n}\n\nconst allExpandedItems = [];\n\nfor (let i = 0; i < allTypes.length; i++) {\n  const typeData = allTypes[i]?.json || {};\n  const response = allResponses[i]?.json || {};\n  const typeKey = typeData._typeKey || `${typeData.type_name}|||${typeData.category}`;\n  \n  let parsed = { work_items: [] };\n  try {\n    const content = response.text || '';\n    if (content) {\n      let clean = content.replace(/^```json\\s*/i, '').replace(/^```\\s*/i, '').replace(/\\s*```$/i, '').trim();\n      parsed = JSON.parse(clean);\n    } else if (response.work_items) {\n      parsed = response;\n    }\n  } catch (e) {}\n  \n  let workItems = parsed.work_items || [];\n  workItems = workItems.filter(w => isWorkValid(w.work_name || '', w.search_query || '', typeData.category));\n  \n  if (workItems.length === 0) {\n    workItems = generateGenericFallback(typeData.type_name, typeData.category, typeData.quantities);\n  }\n  \n  workItems = fixQuantityParams(workItems, typeData.category);\n  workItems = deduplicateWorks(workItems);\n  \n  workItems.forEach((work, idx) => {\n    allExpandedItems.push({\n      json: {\n        work_id: work.work_id || `W${String(idx + 1).padStart(3, '0')}`,\n        work_name: work.work_name || 'Work',\n        search_query: work.search_query || typeData.type_name,\n        expected_unit: work.expected_unit || 'Stück',\n        quantity_source: work.quantity_source || { method: 'direct', bim_parameter: 'element_count', coefficient: 1.0 },\n        work_sequence: work.work_sequence || idx + 1,\n        type_name: typeData.type_name,\n        type_index: typeData.type_index,\n        total_types: typeData.total_types,\n        category: typeData.category,\n        category_group: getCategoryGroup(typeData.category),\n        assigned_phase: typeData.assigned_phase,\n        _typeKey: typeKey,\n        quantities: typeData.quantities || {},\n        element_count: typeData.element_count || 1,\n        element_analysis: parsed.element_analysis || {},\n        _work_index: idx,\n        _total_works_in_type: workItems.length,\n        qdrant_collection: typeData.qdrant_collection,\n        language: typeData.language,\n        currency: typeData.currency || 'EUR',\n        locale: typeData.locale || 'de-DE',\n        system_prompt_lang: typeData.system_prompt_lang,\n        no_works: false\n      }\n    });\n  });\n}\n\nreturn allExpandedItems;\n"},"typeVersion":2},{"id":"687a5af6-0c84-4a55-874b-447f8450da78","name":"Has Work Items?","type":"n8n-nodes-base.if","position":[944,2496],"parameters":{"options":{},"conditions":{"options":{"version":1,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"has-works-condition","operator":{"type":"boolean","operation":"notEquals"},"leftValue":"={{ $json.no_works }}","rightValue":true}]}},"typeVersion":2},{"id":"e0a41c0a-e0c8-4ee4-a010-01e3421242bd","name":"Loop Work Items","type":"n8n-nodes-base.splitInBatches","position":[1232,2304],"parameters":{"options":{"reset":false}},"typeVersion":3},{"id":"3c69f7db-5584-4b54-8318-f724ee17ab41","name":"STAGE 6 - Map Rate Units to BIM","type":"n8n-nodes-base.code","position":[2112,2336],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════════\n// STAGE 6: Smart Unit Mapping v3.0 - With Automatic Unit Conversion\n// IMPROVED: Detects BIM units (mm, mm², mm³) and converts to rate units (m, m², m³)\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst item = $input.first().json;\n\nconst quantities = item.quantities || {};\nconst elementCount = item.element_count || 1;\nconst quantitySource = item.quantity_source || {};\n\n// Get rate info\nconst rateInfo = item.rateInfo || item.rate || {};\nconst rateUnit = rateInfo.rate_unit || item.expected_unit || 'm²';\nconst rateUnitLower = (rateUnit || '').toLowerCase().trim();\nconst rateUnitNormalized = rateUnitLower.replace(/\\s+/g, ' ');\n\nconsole.log(`\\n${'═'.repeat(60)}`);\nconsole.log(`STAGE 6: Smart Unit Mapping v3.0 (with unit conversion)`);\nconsole.log(`Rate unit from database: \"${rateUnit}\"`);\nconsole.log(`Available BIM quantities:`, JSON.stringify(quantities));\nconsole.log(`Element count: ${elementCount}`);\nconsole.log(`Quantity source method: ${quantitySource.method || 'not specified'}`);\nconsole.log(`${'═'.repeat(60)}`);\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// UNIT CONVERSION LOGIC\n// BIM software typically exports:\n// - Length: mm or m (Revit often uses mm internally)\n// - Area: m² (usually already converted)\n// - Volume: m³ (usually already converted)\n// ═══════════════════════════════════════════════════════════════════════════════\n\n// Thresholds to detect if value is likely in mm vs m\n// If Width > 100, it's probably mm (a 100m wide window would be unusual)\n// If Length > 50, it's probably mm\nconst UNIT_DETECTION = {\n  length: {\n    threshold: 50,        // If > 50, probably mm\n    conversion: 1000,     // mm → m\n    label: 'mm → m'\n  },\n  width: {\n    threshold: 50,\n    conversion: 1000,\n    label: 'mm → m'\n  },\n  height: {\n    threshold: 50,\n    conversion: 1000,\n    label: 'mm → m'\n  },\n  perimeter: {\n    threshold: 100,       // Perimeter > 100 is probably mm\n    conversion: 1000,\n    label: 'mm → m'\n  },\n  area: {\n    threshold: 10000,     // If > 10000, probably mm² (10000 mm² = 0.01 m²)\n    conversion: 1000000,  // mm² → m²\n    label: 'mm² → m²'\n  },\n  volume: {\n    threshold: 1000000,   // If > 1M, probably mm³\n    conversion: 1+1234567890, // mm³ → m³\n    label: 'mm³ → m³'\n  }\n};\n\n// Function to detect and convert units\nfunction detectAndConvertUnit(paramName, rawValue) {\n  const paramLower = (paramName || '').toLowerCase();\n  \n  // Find matching detection rule\n  let detection = null;\n  for (const [key, config] of Object.entries(UNIT_DETECTION)) {\n    if (paramLower.includes(key)) {\n      detection = config;\n      break;\n    }\n  }\n  \n  // If no specific rule, check for common length parameters\n  if (!detection) {\n    if (['width', 'height', 'length', 'depth', 'thickness'].some(k => paramLower.includes(k))) {\n      detection = UNIT_DETECTION.length;\n    }\n  }\n  \n  // Apply detection\n  if (detection && rawValue > detection.threshold) {\n    const converted = rawValue / detection.conversion;\n    console.log(`  🔄 Unit conversion: ${rawValue} ${detection.label} = ${converted.toFixed(4)}`);\n    return {\n      value: converted,\n      wasConverted: true,\n      conversionLabel: detection.label,\n      originalValue: rawValue,\n      conversionFactor: detection.conversion\n    };\n  }\n  \n  return {\n    value: rawValue,\n    wasConverted: false,\n    conversionLabel: null,\n    originalValue: rawValue,\n    conversionFactor: 1\n  };\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// HELPER: Get BIM parameter value with fallbacks\n// ═══════════════════════════════════════════════════════════════════════════════\n\nfunction getBimValue(paramName, quantities, elementCount, applyConversion = true) {\n  const paramLower = (paramName || '').toLowerCase().trim();\n  \n  // Direct mapping\n  const directMapping = {\n    'element_count': elementCount,\n    'count': elementCount,\n    'area': quantities['Area'] || quantities['area'] || quantities['Surface Area'] || \n            quantities['Gross Area'] || quantities['Net Area'] || 0,\n    'volume': quantities['Volume'] || quantities['volume'] || quantities['Gross Volume'] || \n              quantities['Net Volume'] || 0,\n    'length': quantities['Length'] || quantities['length'] || quantities['Nominal Length'] || \n              quantities['Curve Length'] || quantities['System Length'] || 0,\n    'width': quantities['Width'] || quantities['width'] || quantities['Nominal Width'] || 0,\n    'height': quantities['Height'] || quantities['height'] || quantities['Nominal Height'] || \n              quantities['Unconnected Height'] || 0,\n    'weight': quantities['Weight'] || quantities['weight'] || quantities['Shipping Weight'] || \n              quantities['Mass'] || 0,\n    'perimeter': quantities['Perimeter'] || quantities['perimeter'] || 0,\n    'thickness': quantities['Thickness'] || quantities['thickness'] || 0\n  };\n  \n  let rawValue = 0;\n  let foundKey = paramLower;\n  \n  // Try direct match\n  if (directMapping[paramLower] !== undefined) {\n    rawValue = directMapping[paramLower];\n  } else {\n    // Try partial match in quantities\n    for (const [key, val] of Object.entries(quantities)) {\n      if (key.toLowerCase().includes(paramLower) && val > 0) {\n        rawValue = val;\n        foundKey = key.toLowerCase();\n        break;\n      }\n    }\n  }\n  \n  // Apply unit conversion if needed\n  if (applyConversion && rawValue > 0 && paramLower !== 'element_count' && paramLower !== 'count') {\n    const conversion = detectAndConvertUnit(foundKey, rawValue);\n    return conversion;\n  }\n  \n  return {\n    value: rawValue,\n    wasConverted: false,\n    conversionLabel: null,\n    originalValue: rawValue,\n    conversionFactor: 1\n  };\n}\n\n// Simple version that just returns the value\nfunction getBimValueSimple(paramName, quantities, elementCount) {\n  const result = getBimValue(paramName, quantities, elementCount, true);\n  return result.value;\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// STEP 1: Determine unit divisor (100, 10, 1000, etc.)\n// ═══════════════════════════════════════════════════════════════════════════════\n\nlet unitDivisor = 1;\nlet divisorNote = 'Standard unit (divisor = 1)';\n\nif (rateUnitNormalized.match(/^1000\\s/i)) {\n  unitDivisor = 1000;\n  divisorNote = 'Rate per 1000 units';\n} else if (rateUnitNormalized.match(/^100\\s/i)) {\n  unitDivisor = 100;\n  divisorNote = 'Rate per 100 units';\n} else if (rateUnitNormalized.match(/^10\\s/i)) {\n  unitDivisor = 10;\n  divisorNote = 'Rate per 10 units';\n}\n\nconsole.log(`  Unit divisor: ${unitDivisor} (${divisorNote})`);\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// STEP 2: Determine unit type from rate unit\n// ═══════════════════════════════════════════════════════════════════════════════\n\nlet detectedUnitType = 'pieces';\nlet bimParameterToUse = 'element_count';\n\nconst unitPatterns = {\n  area: /m²|m2|sq\\.?\\s*m|кв\\.?\\s*м|м²|м2|quadratmeter|square/i,\n  volume: /m³|m3|cu\\.?\\s*m|куб\\.?\\s*м|м³|м3|kubikmeter|cubic/i,\n  length: /^(?:100\\s+)?(?:m|м|l\\.?m\\.?|п\\.?м\\.?|lm|lfm|laufend|meter|метр)$/i,\n  pieces: /pcs|шт|stück|stk|units?|единиц|piece/i,\n  weight: /kg|кг|kilogram|t(?:on)?|т(?:онн)?/i,\n  energy: /kwh|квт\\.?\\s*ч|kilowatt/i\n};\n\nfor (const [type, pattern] of Object.entries(unitPatterns)) {\n  if (pattern.test(rateUnitNormalized)) {\n    detectedUnitType = type;\n    break;\n  }\n}\n\nconst unitTypeToBimParam = {\n  area: 'area',\n  volume: 'volume', \n  length: 'length',\n  pieces: 'element_count',\n  weight: 'weight',\n  energy: 'element_count'\n};\n\nbimParameterToUse = unitTypeToBimParam[detectedUnitType] || 'element_count';\n\nconsole.log(`  Detected unit type: ${detectedUnitType}`);\nconsole.log(`  Default BIM parameter: ${bimParameterToUse}`);\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// STEP 3: Process quantity source (direct, formula, or derived)\n// ═══════════════════════════════════════════════════════════════════════════════\n\nlet rawValue = 0;\nlet coefficient = 1.0;\nlet coefficientNote = 'Default';\nlet formulaDisplay = '';           // Human-readable formula: \"(Width + Height) × 2\"\nlet calculationDisplay = '';       // With values: \"(1.18 + 1.17) × 2 = 4.70\"\nlet usedParameters = {};           // { \"Width\": 1.18, \"Height\": 1.17 }\nlet unitConversions = [];          // Track all conversions applied\n\nconst method = quantitySource.method || 'direct';\n\nif (method === 'formula' && quantitySource.formula) {\n  // ═══════════════════════════════════════════════════════════════════════════\n  // FORMULA METHOD: Parse and evaluate formula with BIM parameters\n  // ═══════════════════════════════════════════════════════════════════════════\n  \n  console.log(`\\n  Processing FORMULA: ${quantitySource.formula}`);\n  \n  const formula = quantitySource.formula;\n  const bimParams = quantitySource.bim_parameters || [];\n  coefficient = parseFloat(quantitySource.coefficient) || 1.0;\n  coefficientNote = quantitySource.note || 'From formula';\n  \n  // Extract parameter values with unit conversion\n  let evalFormula = formula;\n  let valuesFormula = formula;\n  \n  // Get all parameter values from bim_parameters list\n  for (const param of bimParams) {\n    const result = getBimValue(param, quantities, elementCount, true);\n    usedParameters[param] = result.value;\n    \n    if (result.wasConverted) {\n      unitConversions.push({\n        param: param,\n        original: result.originalValue,\n        converted: result.value,\n        label: result.conversionLabel\n      });\n    }\n    \n    console.log(`    ${param} = ${result.originalValue}${result.wasConverted ? ` → ${result.value.toFixed(4)} (${result.conversionLabel})` : ''}`);\n  }\n  \n  // Also try to find parameters mentioned in formula but not in bim_parameters\n  const formulaParams = formula.match(/\\b(Width|Height|Length|Area|Volume|Perimeter|Count|weight)\\b/gi) || [];\n  for (const param of formulaParams) {\n    const paramKey = param.charAt(0).toUpperCase() + param.slice(1).toLowerCase();\n    if (!usedParameters[paramKey] && !usedParameters[param]) {\n      const result = getBimValue(param, quantities, elementCount, true);\n      if (result.value > 0) {\n        usedParameters[paramKey] = result.value;\n        \n        if (result.wasConverted) {\n          unitConversions.push({\n            param: paramKey,\n            original: result.originalValue,\n            converted: result.value,\n            label: result.conversionLabel\n          });\n        }\n        \n        console.log(`    ${paramKey} = ${result.originalValue}${result.wasConverted ? ` → ${result.value.toFixed(4)} (${result.conversionLabel})` : ''} (auto-detected)`);\n      }\n    }\n  }\n  \n  // Replace parameters with values in eval formula\n  for (const [param, value] of Object.entries(usedParameters)) {\n    const regex = new RegExp(`\\\\b${param}\\\\b`, 'gi');\n    evalFormula = evalFormula.replace(regex, value.toString());\n    valuesFormula = valuesFormula.replace(regex, value.toFixed(2));\n  }\n  \n  // Clean up formula for display\n  formulaDisplay = formula\n    .replace(/\\*/g, '×')\n    .replace(/\\//g, '÷');\n  \n  // Evaluate formula\n  try {\n    // Safe eval: only allow numbers, operators, parentheses\n    const safeEval = evalFormula.replace(/[^0-9+\\-*/().]/g, '');\n    rawValue = eval(safeEval);\n    \n    calculationDisplay = `${valuesFormula.replace(/\\*/g, '×').replace(/\\//g, '÷')} = ${rawValue.toFixed(2)}`;\n    \n    console.log(`    Evaluated: ${evalFormula} = ${rawValue}`);\n  } catch (e) {\n    console.log(`    ⚠️ Formula eval error: ${e.message}`);\n    // Fallback to direct parameter\n    const fallback = getBimValue(bimParameterToUse, quantities, elementCount, true);\n    rawValue = fallback.value;\n    calculationDisplay = `${bimParameterToUse} = ${rawValue.toFixed(2)}`;\n  }\n  \n} else if (method === 'derived' && quantitySource.base_parameter) {\n  // ═══════════════════════════════════════════════════════════════════════════\n  // DERIVED METHOD: Calculate from base parameter with conversion factor\n  // ═══════════════════════════════════════════════════════════════════════════\n  \n  console.log(`\\n  Processing DERIVED from ${quantitySource.base_parameter}`);\n  \n  const baseParam = quantitySource.base_parameter;\n  const conversionFactor = parseFloat(quantitySource.conversion_factor) || 1.0;\n  const baseResult = getBimValue(baseParam, quantities, elementCount, true);\n  \n  usedParameters[baseParam] = baseResult.value;\n  rawValue = baseResult.value * conversionFactor;\n  coefficient = 1.0; // Conversion already applied\n  \n  if (baseResult.wasConverted) {\n    unitConversions.push({\n      param: baseParam,\n      original: baseResult.originalValue,\n      converted: baseResult.value,\n      label: baseResult.conversionLabel\n    });\n  }\n  \n  formulaDisplay = `${baseParam} × ${conversionFactor}`;\n  calculationDisplay = `${baseResult.value.toFixed(2)} × ${conversionFactor} = ${rawValue.toFixed(2)}`;\n  coefficientNote = quantitySource.result_description || `Derived from ${baseParam}`;\n  \n  console.log(`    ${baseParam} = ${baseResult.value}`);\n  console.log(`    × ${conversionFactor} = ${rawValue}`);\n  \n} else {\n  // ═══════════════════════════════════════════════════════════════════════════\n  // DIRECT METHOD: Use single BIM parameter\n  // ═══════════════════════════════════════════════════════════════════════════\n  \n  console.log(`\\n  Processing DIRECT parameter`);\n  \n  // Check if LLM specified a parameter\n  const llmParam = (quantitySource.bim_parameter || '').toLowerCase();\n  coefficient = parseFloat(quantitySource.coefficient) || 1.0;\n  coefficientNote = quantitySource.note || 'Direct parameter';\n  \n  // Use LLM param if it makes sense, otherwise use detected unit type\n  let paramToUse = bimParameterToUse;\n  if (llmParam) {\n    const testResult = getBimValue(llmParam, quantities, elementCount, true);\n    if (testResult.value > 0) {\n      paramToUse = llmParam;\n    }\n  }\n  \n  const result = getBimValue(paramToUse, quantities, elementCount, true);\n  rawValue = result.value;\n  usedParameters[paramToUse] = result.value;\n  bimParameterToUse = paramToUse;\n  \n  if (result.wasConverted) {\n    unitConversions.push({\n      param: paramToUse,\n      original: result.originalValue,\n      converted: result.value,\n      label: result.conversionLabel\n    });\n  }\n  \n  // Build display strings\n  const paramLabel = paramToUse.charAt(0).toUpperCase() + paramToUse.slice(1);\n  formulaDisplay = paramLabel;\n  \n  if (coefficient !== 1.0) {\n    calculationDisplay = `${rawValue.toFixed(2)} × ${coefficient}`;\n  } else {\n    calculationDisplay = `${rawValue.toFixed(2)}`;\n  }\n  \n  console.log(`    ${paramToUse} = ${result.originalValue}${result.wasConverted ? ` → ${rawValue.toFixed(4)} (${result.conversionLabel})` : ''}`);\n}\n\n// Fallback if still no value\nif (!rawValue || rawValue === 0) {\n  rawValue = elementCount;\n  usedParameters['element_count'] = elementCount;\n  formulaDisplay = 'Count';\n  calculationDisplay = `${elementCount}`;\n  console.log(`  ⚠️ Fallback to element_count: ${rawValue}`);\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// STEP 4: Final calculation\n// ═══════════════════════════════════════════════════════════════════════════════\n\n// Apply coefficient (for direct method)\nconst projectQuantity = rawValue * coefficient;\n\n// Apply unit divisor\nconst quantityInRateUnits = projectQuantity / unitDivisor;\n\nconsole.log(`\\n  FINAL CALCULATION:`);\nconsole.log(`    Raw value: ${rawValue}`);\nconsole.log(`    × Coefficient: ${coefficient}`);\nconsole.log(`    = Project quantity: ${projectQuantity}`);\nconsole.log(`    ÷ Unit divisor: ${unitDivisor}`);\nconsole.log(`    = Rate quantity: ${quantityInRateUnits.toFixed(4)}`);\n\nif (unitConversions.length > 0) {\n  console.log(`\\n  UNIT CONVERSIONS APPLIED:`);\n  unitConversions.forEach(conv => {\n    console.log(`    ${conv.param}: ${conv.original} → ${conv.converted.toFixed(4)} (${conv.label})`);\n  });\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// STEP 5: Build calculation details for display\n// ═══════════════════════════════════════════════════════════════════════════════\n\n// Format for display\nconst formatNumber = (n) => n.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 });\n\nconst calculationDetails = {\n  // Method used\n  method: method,\n  \n  // Parameter detection\n  detected_unit_type: detectedUnitType,\n  bim_parameter: bimParameterToUse,\n  bim_parameter_label: bimParameterToUse.charAt(0).toUpperCase() + bimParameterToUse.slice(1),\n  \n  // Values used in calculation (CONVERTED values for display)\n  raw_value: rawValue,\n  coefficient: coefficient,\n  coefficient_note: coefficientNote,\n  unit_divisor: unitDivisor,\n  divisor_note: divisorNote,\n  \n  // Parameters with their CONVERTED values (for display)\n  used_parameters: usedParameters,\n  \n  // Unit conversions applied\n  unit_conversions: unitConversions,\n  had_unit_conversions: unitConversions.length > 0,\n  \n  // Display strings\n  formula_display: formulaDisplay,              // \"(Width + Height) × 2\"\n  calculation_display: calculationDisplay,       // \"(1.18 + 1.17) × 2 = 4.70\"\n  \n  // Results\n  project_quantity: projectQuantity,\n  quantity_in_rate_units: quantityInRateUnits,\n  final_quantity: quantityInRateUnits,\n  rate_unit: rateUnit,\n  \n  // Formatted for display\n  raw_value_formatted: formatNumber(rawValue),\n  project_quantity_formatted: formatNumber(projectQuantity),\n  final_quantity_formatted: formatNumber(quantityInRateUnits),\n  \n  // Debug info\n  available_quantities: Object.entries(quantities)\n    .filter(([k, v]) => v > 0)\n    .map(([k, v]) => `${k}: ${v}`)\n};\n\nconsole.log(`\\n${'═'.repeat(60)}`);\nconsole.log(`✓ Final quantity: ${quantityInRateUnits.toFixed(4)} ${rateUnit}`);\nconsole.log(`  Formula: ${formulaDisplay}`);\nconsole.log(`  Calculation: ${calculationDisplay}`);\nif (unitConversions.length > 0) {\n  console.log(`  Unit conversions: ${unitConversions.map(c => c.label).join(', ')}`);\n}\nconsole.log(`${'═'.repeat(60)}\\n`);\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// OUTPUT\n// ═══════════════════════════════════════════════════════════════════════════════\n\nreturn { \n  json: { \n    ...item,\n    _typeKey: item._typeKey, \n    calculated_quantity: quantityInRateUnits, \n    project_quantity: projectQuantity,\n    project_unit: bimParameterToUse,\n    calculation_details: calculationDetails, \n    unit_divisor: unitDivisor,\n    stage: 'STAGE_6_QUANTITY_CALCULATED' \n  } \n};"},"typeVersion":2},{"id":"1ef6b653-2b6e-4d10-84ba-292b054b81e0","name":"Accumulate Work Results","type":"n8n-nodes-base.code","position":[2112,2528],"parameters":{"jsCode":"// Accumulate Work Results - FIXED v12.8: use _typeKey from item\n\nconst item = $input.first().json;\n\n// Use typeKey from item (passed from Parse Decomposition)\nconst typeKey = item._typeKey || `${item.type_name || 'Unknown'}|||${item.category || ''}`;\n\nconst staticData = $getWorkflowStaticData('global');\n\nif (!staticData.work_results) {\n  staticData.work_results = {};\n}\nif (!staticData.work_results[typeKey]) {\n  staticData.work_results[typeKey] = [];\n}\n\nstaticData.work_results[typeKey].push(item);\n\nconsole.log(`Accumulated [${typeKey}]: ${item.work_name}`);\nconsole.log(`  Total works for this type: ${staticData.work_results[typeKey].length}`);\n\nreturn [{ json: item }];"},"typeVersion":2},{"id":"750eb7c4-ce7f-4f6a-92b2-75d9085a1214","name":"Aggregate Type Works","type":"n8n-nodes-base.code","position":[1392,2784],"parameters":{"jsCode":"// Aggregate All Works for Current Type - FIXED v12.8\n\nconst staticData = $getWorkflowStaticData('global');\n\n// Get typeKey from current_type_key\nconst typeKey = staticData.current_type_key || '';\n\n// Get typeData from cache\nlet typeData = {};\nif (typeKey && staticData.type_cache && staticData.type_cache[typeKey]) {\n  typeData = { ...staticData.type_cache[typeKey] };\n} else {\n  try {\n    typeData = $('Loop Types for Decomposition').first()?.json || {};\n  } catch (e) {}\n}\n\nconst typeName = typeData.type_name || 'Unknown';\nconst category = typeData.category || '';\nconst finalTypeKey = typeKey || `${typeName}|||${category}`;\nconst currency = typeData.currency || 'EUR';\nconst locale = typeData.locale || 'de-DE';\n\n// Get works using typeKey\nconst typeWorks = staticData.work_results?.[finalTypeKey] || [];\n\nconsole.log(`\\nAggregating: ${typeName} [${category}]`);\nconsole.log(`TypeKey: ${finalTypeKey}`);\nconsole.log(`Works found: ${typeWorks.length}`);\n\n// Calculate totals\nconst typeTotalCost = typeWorks.reduce((sum, w) => sum + (w.total_cost || 0), 0);\nconst typeTotalLaborHours = typeWorks.reduce((sum, w) => sum + (w.estimated_labor_hours || 0), 0);\nconst typeResourceCost = typeWorks.reduce((sum, w) => sum + (w.total_resource_cost || 0), 0);\nconst typeMaterialCost = typeWorks.reduce((sum, w) => sum + (w.total_material_cost || 0), 0);\n\nconst qualityDist = {\n  high: typeWorks.filter(w => w.quality_level === 'high').length,\n  medium: typeWorks.filter(w => w.quality_level === 'medium').length,\n  low: typeWorks.filter(w => w.quality_level === 'low').length,\n  very_low: typeWorks.filter(w => w.quality_level === 'very_low').length,\n  not_found: typeWorks.filter(w => w.quality_level === 'not_found').length\n};\n\nconst worksWithConf = typeWorks.filter(w => w.search_confidence > 0);\nconst avgConf = worksWithConf.length > 0\n  ? worksWithConf.reduce((sum, w) => sum + w.search_confidence, 0) / worksWithConf.length\n  : 0;\n\nfunction formatCurrency(value) {\n  try {\n    return new Intl.NumberFormat(locale, { style: 'currency', currency: currency }).format(value);\n  } catch (e) {\n    return `${value.toFixed(2)} ${currency}`;\n  }\n}\n\n// Clean up\nif (staticData.work_results?.[finalTypeKey]) {\n  delete staticData.work_results[finalTypeKey];\n}\nif (staticData.type_cache?.[finalTypeKey]) {\n  delete staticData.type_cache[finalTypeKey];\n}\n\nconsole.log(`  Cost: ${formatCurrency(typeTotalCost)}, Works: ${typeWorks.length}`);\n\nreturn {\n  json: {\n    type_name: typeName,\n    type_index: typeData.type_index || 0,\n    total_types: typeData.total_types || 1,\n    category: category,\n    assigned_phase: typeData.assigned_phase,\n    element_count: typeData.element_count || 1,\n    \n    works: typeWorks,\n    works_count: typeWorks.length,\n    \n    type_total_cost: typeTotalCost,\n    type_total_cost_formatted: formatCurrency(typeTotalCost),\n    type_resource_cost: typeResourceCost,\n    type_resource_cost_formatted: formatCurrency(typeResourceCost),\n    type_material_cost: typeMaterialCost,\n    type_material_cost_formatted: formatCurrency(typeMaterialCost),\n    type_total_labor_hours: typeTotalLaborHours,\n    type_total_labor_hours_formatted: typeTotalLaborHours.toFixed(1) + ' h',\n    currency: currency,\n    locale: locale,\n    \n    type_quality_distribution: qualityDist,\n    type_avg_confidence: avgConf,\n    type_avg_confidence_percent: (avgConf * 100).toFixed(0) + '%',\n    \n    has_not_found: qualityDist.not_found > 0,\n    all_high_quality: qualityDist.high === typeWorks.length && typeWorks.length > 0,\n    \n    stage: 'TYPE_AGGREGATED'\n  }\n};"},"typeVersion":2},{"id":"5db5ec89-6d81-4171-a6e7-a3d65e1d82be","name":"Store Type Result","type":"n8n-nodes-base.code","position":[2144,2928],"parameters":{"jsCode":"// Store Type Result in Static Data for Final Aggregation\n\nconst item = $input.first().json;\n\nconst staticData = $getWorkflowStaticData('global');\n\nif (!staticData.accumulated_types) {\n  staticData.accumulated_types = [];\n}\n\nstaticData.accumulated_types.push(item);\n\nconsole.log(`Stored type ${item.type_index + 1}/${item.total_types}: ${item.type_name}`);\nconsole.log(`  Works: ${item.works_count}, Cost: ${item.type_total_cost_formatted}`);\nconsole.log(`  Total types stored: ${staticData.accumulated_types.length}`);\n\nreturn { \n  json: { \n    ...item, \n    accumulated_count: staticData.accumulated_types.length,\n    stage: 'TYPE_STORED'\n  } \n};"},"typeVersion":2},{"id":"0b45cb0c-a97b-4be7-a7c7-7fd1506a7b20","name":"Handle No Works","type":"n8n-nodes-base.code","position":[1392,2928],"parameters":{"jsCode":"// Handle Types Without Works - FIXED\n\nconst item = $input.first().json;\n\nconsole.log(`No works for: ${item.type_name} [${item.category}]`);\n\nreturn {\n  json: {\n    type_name: item.type_name,\n    type_index: item.type_index,\n    total_types: item.total_types,\n    category: item.category,\n    assigned_phase: item.assigned_phase || null,\n    element_count: item.element_count || 1,\n    \n    works: [],\n    works_count: 0,\n    \n    type_total_cost: 0,\n    type_total_cost_formatted: '0,00 €',\n    type_resource_cost: 0,\n    type_resource_cost_formatted: '0,00 €',\n    type_material_cost: 0,\n    type_material_cost_formatted: '0,00 €',\n    type_total_labor_hours: 0,\n    type_total_labor_hours_formatted: '0.0 h',\n    currency: item.currency || 'EUR',\n    locale: item.locale || 'de-DE',\n    \n    type_quality_distribution: { high: 0, medium: 0, low: 0, very_low: 0, not_found: 0 },\n    type_avg_confidence: 0,\n    type_avg_confidence_percent: '0%',\n    type_quality_percentage: '0.0%',\n    \n    no_works_generated: true,\n    has_not_found: true,\n    all_high_quality: false,\n    \n    stage: 'TYPE_NO_WORKS'\n  }\n};"},"typeVersion":2},{"id":"ff39ae0f-2a36-48b4-933c-63323dcfe2af","name":"STAGE 8 - Aggregate by Phases","type":"n8n-nodes-base.code","position":[1072,3184],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════════\n// STAGE 8: FINAL AGGREGATION BY PHASES - v13.0\n// FIXED: Category-based phase validation - types go to correct phases\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst staticData = $getWorkflowStaticData('global');\nlet accumulatedTypes = staticData.accumulated_types || [];\n\n\nconst PHASE_NAMES = {\n  SITE: { DE: 'Außenanlagen', EN: 'Site Work', FR: 'Aménagement extérieur', ES: 'Obra exterior', PT: 'Obra externa', RU: 'Благоустройство', ZH: '场地工程', AR: 'أعمال الموقع', HI: 'साइट कार्य' },\n  FOUND: { DE: 'Fundamente', EN: 'Foundations', FR: 'Fondations', ES: 'Cimentaciones', PT: 'Fundações', RU: 'Фундаменты', ZH: '基础工程', AR: 'الأساسات', HI: 'नींव' },\n  FRAME: { DE: 'Tragwerk', EN: 'Structure', FR: 'Structure', ES: 'Estructura', PT: 'Estrutura', RU: 'Каркас', ZH: '结构工程', AR: 'الهيكل', HI: 'संरचना' },\n  FLOORS: { DE: 'Geschossdecken', EN: 'Floors', FR: 'Planchers', ES: 'Forjados', PT: 'Lajes', RU: 'Перекрытия', ZH: '楼板', AR: 'الأرضيات', HI: 'फर्श' },\n  WALLS: { DE: 'Wände', EN: 'Walls', FR: 'Murs', ES: 'Muros', PT: 'Paredes', RU: 'Стены', ZH: '墙体', AR: 'الجدران', HI: 'दीवारें' },\n  STAIRS: { DE: 'Treppen', EN: 'Stairs', FR: 'Escaliers', ES: 'Escaleras', PT: 'Escadas', RU: 'Лестницы', ZH: '楼梯', AR: 'السلالم', HI: 'सीढ़ियाँ' },\n  ROOF: { DE: 'Dach', EN: 'Roofing', FR: 'Toiture', ES: 'Cubierta', PT: 'Cobertura', RU: 'Кровля', ZH: '屋面', AR: 'السقف', HI: 'छत' },\n  WINDOWS: { DE: 'Fenster und Türen', EN: 'Windows and Doors', FR: 'Fenêtres et portes', ES: 'Ventanas y puertas', PT: 'Janelas e portas', RU: 'Окна и двери', ZH: '门窗', AR: 'النوافذ والأبواب', HI: 'खिड़कियाँ और दरवाजे' },\n  MEP: { DE: 'Haustechnik', EN: 'MEP Systems', FR: 'Installations techniques', ES: 'Instalaciones', PT: 'Instalações', RU: 'Инженерные системы', ZH: '机电系统', AR: 'الأنظمة الميكانيكية', HI: 'एमईपी प्रणाली' },\n  INTERIOR: { DE: 'Innenausbau', EN: 'Interior Finishes', FR: 'Aménagement intérieur', ES: 'Acabados interiores', PT: 'Acabamentos interiores', RU: 'Внутренняя отделка', ZH: '室内装修', AR: 'التشطيبات الداخلية', HI: 'आंतरिक सज्जा' }\n};\n\nfunction getPhaseName(phaseCode, langCode) {\n  const lang = (langCode || 'EN').toUpperCase();\n  const phase = PHASE_NAMES[phaseCode];\n  if (!phase) return phaseCode;\n  return phase[lang] || phase.EN || phaseCode;\n}\n\n\nconsole.log('\\n' + '═'.repeat(70));\nconsole.log('STAGE 8: FINAL AGGREGATION - v13.0 WITH CATEGORY VALIDATION');\nconsole.log('═'.repeat(70));\nconsole.log(`Accumulated types from Loop: ${accumulatedTypes.length}`);\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// CATEGORY → PHASE MAPPING (hard-coded rules to override LLM mistakes)\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst CATEGORY_TO_PHASE = {\n  // ═══════════════════════════════════════════════════════════════════════════\n  // v6.1 EXTENDED MAPPING - Covers all common Revit categories\n  // ═══════════════════════════════════════════════════════════════════════════\n  \n  // Foundations\n  'ost_structuralfoundation': { code: 'FOUND', name: 'Foundations', name_de: 'Fundamente' },\n  'ost_foundation': { code: 'FOUND', name: 'Foundations', name_de: 'Fundamente' },\n  'structural foundation': { code: 'FOUND', name: 'Foundations', name_de: 'Fundamente' },\n  \n  // Floors\n  'ost_floors': { code: 'FLOORS', name: 'Floor structures', name_de: 'Geschossdecken' },\n  'ost_structuralfloor': { code: 'FLOORS', name: 'Floor structures', name_de: 'Geschossdecken' },\n  'floors': { code: 'FLOORS', name: 'Floor structures', name_de: 'Geschossdecken' },\n  \n  // Walls\n  'ost_walls': { code: 'WALLS', name: 'Walls and partitions', name_de: 'Wände und Trennwände' },\n  'ost_structuralwalls': { code: 'WALLS', name: 'Walls and partitions', name_de: 'Wände und Trennwände' },\n  'ost_curtainwallpanels': { code: 'WALLS', name: 'Walls and partitions', name_de: 'Wände und Trennwände' },\n  'walls': { code: 'WALLS', name: 'Walls and partitions', name_de: 'Wände und Trennwände' },\n  'curtain wall panels': { code: 'WALLS', name: 'Walls and partitions', name_de: 'Wände und Trennwände' },\n  \n  // Frame/Structure\n  'ost_columns': { code: 'FRAME', name: 'Frame/load-bearing structures', name_de: 'Tragwerk' },\n  'ost_structuralcolumns': { code: 'FRAME', name: 'Frame/load-bearing structures', name_de: 'Tragwerk' },\n  'ost_beams': { code: 'FRAME', name: 'Frame/load-bearing structures', name_de: 'Tragwerk' },\n  'ost_structuralframing': { code: 'FRAME', name: 'Frame/load-bearing structures', name_de: 'Tragwerk' },\n  'ost_stairs': { code: 'FRAME', name: 'Frame/load-bearing structures', name_de: 'Tragwerk' },\n  'ost_stairsrailing': { code: 'FRAME', name: 'Frame/load-bearing structures', name_de: 'Tragwerk' },\n  'ost_mass': { code: 'FRAME', name: 'Frame/load-bearing structures', name_de: 'Tragwerk' },\n  'structural columns': { code: 'FRAME', name: 'Frame/load-bearing structures', name_de: 'Tragwerk' },\n  'stairs': { code: 'FRAME', name: 'Frame/load-bearing structures', name_de: 'Tragwerk' },\n  'stairs railing': { code: 'FRAME', name: 'Frame/load-bearing structures', name_de: 'Tragwerk' },\n  'mass': { code: 'FRAME', name: 'Frame/load-bearing structures', name_de: 'Tragwerk' },\n  \n  // Roof\n  'ost_roofs': { code: 'ROOF', name: 'Roofing', name_de: 'Dach' },\n  'roofs': { code: 'ROOF', name: 'Roofing', name_de: 'Dach' },\n  \n  // Windows & Doors (Openings)\n  'ost_windows': { code: 'WINDOWS', name: 'Windows and doors', name_de: 'Fenster und Türen' },\n  'ost_doors': { code: 'WINDOWS', name: 'Windows and doors', name_de: 'Fenster und Türen' },\n  'ost_curtainsystems': { code: 'WINDOWS', name: 'Windows and doors', name_de: 'Fenster und Türen' },\n  'ost_curtainwallmullions': { code: 'WINDOWS', name: 'Windows and doors', name_de: 'Fenster und Türen' },\n  'windows': { code: 'WINDOWS', name: 'Windows and doors', name_de: 'Fenster und Türen' },\n  'doors': { code: 'WINDOWS', name: 'Windows and doors', name_de: 'Fenster und Türen' },\n  'curtain wall mullions': { code: 'WINDOWS', name: 'Windows and doors', name_de: 'Fenster und Türen' },\n  \n  // Interior Finishes\n  'ost_ceilings': { code: 'INTERIOR', name: 'Interior', name_de: 'Innenausbau' },\n  'ost_furniture': { code: 'INTERIOR', name: 'Interior', name_de: 'Innenausbau' },\n  'ost_furnituresystems': { code: 'INTERIOR', name: 'Interior', name_de: 'Innenausbau' },\n  'ost_casework': { code: 'INTERIOR', name: 'Interior', name_de: 'Innenausbau' },\n  'ost_genericmodels': { code: 'INTERIOR', name: 'Interior', name_de: 'Innenausbau' },\n  'ceilings': { code: 'INTERIOR', name: 'Interior', name_de: 'Innenausbau' },\n  'furniture': { code: 'INTERIOR', name: 'Interior', name_de: 'Innenausbau' },\n  'furniture systems': { code: 'INTERIOR', name: 'Interior', name_de: 'Innenausbau' },\n  'casework': { code: 'INTERIOR', name: 'Interior', name_de: 'Innenausbau' },\n  'generic models': { code: 'INTERIOR', name: 'Interior', name_de: 'Innenausbau' },\n  \n  // MEP / Engineering Systems\n  'ost_pipecurves': { code: 'MEP', name: 'Engineering systems', name_de: 'Haustechnik' },\n  'ost_plumbingfixtures': { code: 'MEP', name: 'Engineering systems', name_de: 'Haustechnik' },\n  'ost_ductcurves': { code: 'MEP', name: 'Engineering systems', name_de: 'Haustechnik' },\n  'ost_electricalfixtures': { code: 'MEP', name: 'Engineering systems', name_de: 'Haustechnik' },\n  'ost_electricalequipment': { code: 'MEP', name: 'Engineering systems', name_de: 'Haustechnik' },\n  'ost_mechanicalequipment': { code: 'MEP', name: 'Engineering systems', name_de: 'Haustechnik' },\n  'ost_lightingfixtures': { code: 'MEP', name: 'Engineering systems', name_de: 'Haustechnik' },\n  'ost_specialityequipment': { code: 'MEP', name: 'Engineering systems', name_de: 'Haustechnik' },\n  'plumbing fixtures': { code: 'MEP', name: 'Engineering systems', name_de: 'Haustechnik' },\n  'lighting fixtures': { code: 'MEP', name: 'Engineering systems', name_de: 'Haustechnik' },\n  'electrical equipment': { code: 'MEP', name: 'Engineering systems', name_de: 'Haustechnik' },\n  'mechanical equipment': { code: 'MEP', name: 'Engineering systems', name_de: 'Haustechnik' },\n  'speciality equipment': { code: 'MEP', name: 'Engineering systems', name_de: 'Haustechnik' },\n  \n  // Site Work\n  'ost_site': { code: 'SITE', name: 'Site Work', name_de: 'Außenanlagen' },\n  'ost_topography': { code: 'SITE', name: 'Site Work', name_de: 'Außenanlagen' },\n  'ost_parking': { code: 'SITE', name: 'Site Work', name_de: 'Außenanlagen' },\n  'ost_planting': { code: 'SITE', name: 'Site Work', name_de: 'Außenanlagen' },\n  'ost_buildingpad': { code: 'SITE', name: 'Site Work', name_de: 'Außenanlagen' },\n  'site': { code: 'SITE', name: 'Site Work', name_de: 'Außenanlagen' },\n  'planting': { code: 'SITE', name: 'Site Work', name_de: 'Außenanlagen' },\n  'topography': { code: 'SITE', name: 'Site Work', name_de: 'Außenanlagen' },\n  'building pad': { code: 'SITE', name: 'Site Work', name_de: 'Außenanlagen' }\n};\n\n// Function to get correct phase for a type based on its category\nfunction getCorrectPhaseForCategory(category) {\n  if (!category) return null;\n  \n  const categoryLower = category.toLowerCase().trim();\n  \n  // Direct match\n  if (CATEGORY_TO_PHASE[categoryLower]) {\n    return CATEGORY_TO_PHASE[categoryLower];\n  }\n  \n  // Partial match\n  for (const [key, phase] of Object.entries(CATEGORY_TO_PHASE)) {\n    if (categoryLower.includes(key.replace('ost_', '')) || key.includes(categoryLower.replace('ost_', ''))) {\n      return phase;\n    }\n  }\n  \n  // Keyword-based detection (v6.1 extended)\n  if (/foundation|footing/i.test(categoryLower)) return CATEGORY_TO_PHASE['ost_structuralfoundation'];\n  if (/floor|slab/i.test(categoryLower)) return CATEGORY_TO_PHASE['ost_floors'];\n  if (/wall|partition/i.test(categoryLower)) return CATEGORY_TO_PHASE['ost_walls'];\n  if (/column|beam|frame|stair|railing|mass/i.test(categoryLower)) return CATEGORY_TO_PHASE['ost_columns'];\n  if (/roof/i.test(categoryLower)) return CATEGORY_TO_PHASE['ost_roofs'];\n  if (/window|glazing|glass|mullion|curtain/i.test(categoryLower)) return CATEGORY_TO_PHASE['ost_windows'];\n  if (/door/i.test(categoryLower)) return CATEGORY_TO_PHASE['ost_doors'];\n  if (/ceiling|casework/i.test(categoryLower)) return CATEGORY_TO_PHASE['ost_ceilings'];\n  if (/furniture|generic/i.test(categoryLower)) return CATEGORY_TO_PHASE['ost_furniture'];\n  if (/pipe|plumb|duct|electric|hvac|mech|lighting|fixture|equipment|speciality/i.test(categoryLower)) return CATEGORY_TO_PHASE['ost_pipecurves'];\n  if (/site|land|park|topo|plant|pad/i.test(categoryLower)) return CATEGORY_TO_PHASE['ost_site'];\n  \n  return null;\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// FALLBACK: If accumulated_types is empty or incomplete, use Stage 3 data\n// ═══════════════════════════════════════════════════════════════════════════════\n\nlet phasesDetailed = [];\nlet allTypesFromStage3 = [];\nlet currency = 'EUR';\nlet locale = 'de-DE';\nlet projectAnalysis = {};\n\ntry {\n  const stage3Data = $('Parse Stage 3 - Final Structure').first().json;\n  projectAnalysis = stage3Data.project_context || {};\n  phasesDetailed = stage3Data.phases_with_types || [];\n  allTypesFromStage3 = stage3Data.types_with_phases || [];\n  currency = stage3Data.currency || 'EUR';\n  locale = stage3Data.locale || 'de-DE';\n  \n  console.log(`Stage 3 types available: ${allTypesFromStage3.length}`);\n} catch (e) {\n  console.log('Stage 3 data not available:', e.message);\n}\n\n// If we have fewer accumulated types than Stage 3 types, there's a problem\nif (accumulatedTypes.length < allTypesFromStage3.length) {\n  console.log(`\\n⚠️ WARNING: Missing types!`);\n  console.log(`  Expected: ${allTypesFromStage3.length}`);\n  console.log(`  Got: ${accumulatedTypes.length}`);\n  \n  // Find missing types and create placeholder entries\n  const accumulatedKeys = new Set(\n    accumulatedTypes.map(t => `${t.type_name}|||${t.category}`)\n  );\n  \n  allTypesFromStage3.forEach((stageType, idx) => {\n    const key = `${stageType.type_name}|||${stageType.category}`;\n    \n    if (!accumulatedKeys.has(key)) {\n      console.log(`  Adding missing type: ${stageType.type_name} [${stageType.category}]`);\n      \n      accumulatedTypes.push({\n        type_name: stageType.type_name,\n        type_index: idx,\n        total_types: allTypesFromStage3.length,\n        category: stageType.category,\n        assigned_phase: {\n          phase_id: stageType.assigned_phase_id,\n          phase_code: phasesDetailed.find(p => p.phase_id === stageType.assigned_phase_id)?.phase_code || 'UNASSIGNED',\n          phase_name_en: phasesDetailed.find(p => p.phase_id === stageType.assigned_phase_id)?.phase_name_en || 'Other',\n          phase_name_ru: phasesDetailed.find(p => p.phase_id === stageType.assigned_phase_id)?.phase_name_ru || ''\n        },\n        element_count: stageType.element_count || 1,\n        quantities: stageType.quantities || {},\n        \n        works: [],\n        works_count: 0,\n        \n        type_total_cost: 0,\n        type_total_cost_formatted: '0,00 €',\n        type_resource_cost: 0,\n        type_material_cost: 0,\n        type_total_labor_hours: 0,\n        \n        type_quality_distribution: { high: 0, medium: 0, low: 0, very_low: 0, not_found: 0 },\n        type_avg_confidence: 0,\n        \n        no_works_generated: true,\n        _added_by_fallback: true\n      });\n    }\n  });\n  \n  console.log(`  After fallback: ${accumulatedTypes.length} types`);\n}\n\n// Format currency helper\nfunction formatCurrency(value) {\n  if (value === null || value === undefined || isNaN(value)) return '0,00 €';\n  try {\n    return new Intl.NumberFormat(locale, { \n      style: 'currency', \n      currency: currency,\n      minimumFractionDigits: 2,\n      maximumFractionDigits: 2\n    }).format(value);\n  } catch (e) {\n    return `${value.toFixed(2)} €`;\n  }\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// CALCULATE GRAND TOTALS\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst grandTotalCost = accumulatedTypes.reduce((sum, t) => sum + (t.type_total_cost || 0), 0);\nconst grandResourceCost = accumulatedTypes.reduce((sum, t) => sum + (t.type_resource_cost || 0), 0);\nconst grandMaterialCost = accumulatedTypes.reduce((sum, t) => sum + (t.type_material_cost || 0), 0);\nconst grandLaborHours = accumulatedTypes.reduce((sum, t) => sum + (t.type_total_labor_hours || 0), 0);\nconst totalWorks = accumulatedTypes.reduce((sum, t) => sum + (t.works_count || 0), 0);\n\nconsole.log(`\\nGrand totals:`);\nconsole.log(`  Types: ${accumulatedTypes.length}`);\nconsole.log(`  Works: ${totalWorks}`);\nconsole.log(`  Cost: ${formatCurrency(grandTotalCost)}`);\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// GROUP BY PHASES - WITH CATEGORY VALIDATION\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst byPhase = {};\n\n// Build phase lookup from original phases\nconst phaseLookupByCode = {};\nphasesDetailed.forEach(phase => {\n  phaseLookupByCode[phase.phase_code] = {\n    phase_id: phase.phase_id,\n    phase_code: phase.phase_code,\n    phase_name: phase.phase_name_en || phase.phase_name_ru || `Phase ${phase.phase_id}`,\n    phase_name_local: phase.phase_name_ru || phase.phase_name_en || ''\n  };\n});\n\n// Initialize phases based on what we'll actually need (from category mapping)\nconst uniquePhaseCodes = new Set();\naccumulatedTypes.forEach(type => {\n  const correctPhase = getCorrectPhaseForCategory(type.category);\n  if (correctPhase) {\n    uniquePhaseCodes.add(correctPhase.code);\n  } else {\n    uniquePhaseCodes.add('UNASSIGNED');\n  }\n});\n\n// Create phase entries\nuniquePhaseCodes.forEach(code => {\n  // Try to find from original phases\n  const originalPhase = phaseLookupByCode[code];\n  \n  if (originalPhase) {\n    byPhase[code] = {\n      ...originalPhase,\n      types: [],\n      phase_total_cost: 0,\n      phase_resource_cost: 0,\n      phase_material_cost: 0,\n      phase_labor_hours: 0,\n      phase_works_count: 0\n    };\n  } else {\n    // Create from category mapping\n    const categoryPhase = Object.values(CATEGORY_TO_PHASE).find(p => p.code === code);\n    byPhase[code] = {\n      phase_id: code,\n      phase_code: code,\n      phase_name: categoryPhase?.name || code,\n      phase_name_local: categoryPhase?.name_de || '',\n      types: [],\n      phase_total_cost: 0,\n      phase_resource_cost: 0,\n      phase_material_cost: 0,\n      phase_labor_hours: 0,\n      phase_works_count: 0\n    };\n  }\n});\n\n// Always add unassigned phase\nif (!byPhase['UNASSIGNED']) {\n  byPhase['UNASSIGNED'] = {\n    phase_id: 'unassigned',\n    phase_code: 'UNASSIGNED',\n    phase_name: 'Other Elements',\n    phase_name_local: 'Sonstige Elemente',\n    types: [],\n    phase_total_cost: 0,\n    phase_resource_cost: 0,\n    phase_material_cost: 0,\n    phase_labor_hours: 0,\n    phase_works_count: 0\n  };\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// DISTRIBUTE TYPES TO PHASES - USING CATEGORY VALIDATION\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconsole.log(`\\nDistributing ${accumulatedTypes.length} types to phases (with category validation):`);\n\nlet correctionCount = 0;\n\naccumulatedTypes.forEach((type, idx) => {\n  // Get original assigned phase\n  let originalPhaseCode = 'UNASSIGNED';\n  if (type.assigned_phase) {\n    if (typeof type.assigned_phase === 'object') {\n      originalPhaseCode = type.assigned_phase.phase_code || String(type.assigned_phase.phase_id) || 'UNASSIGNED';\n    } else {\n      originalPhaseCode = String(type.assigned_phase);\n    }\n  }\n  \n  // Get correct phase based on category\n  const correctPhase = getCorrectPhaseForCategory(type.category);\n  const correctPhaseCode = correctPhase?.code || 'UNASSIGNED';\n  \n  // Use category-based phase (overrides LLM assignment)\n  let finalPhaseCode = correctPhaseCode;\n  \n  // Log if correction was made\n  if (originalPhaseCode !== correctPhaseCode && correctPhase) {\n    console.log(`  🔄 [${idx}] \"${type.type_name}\" [${type.category}]: ${originalPhaseCode} → ${correctPhaseCode}`);\n    correctionCount++;\n  } else {\n    console.log(`  ✓ [${idx}] \"${type.type_name}\" [${type.category}] → ${finalPhaseCode}`);\n  }\n  \n  // Ensure phase exists\n  if (!byPhase[finalPhaseCode]) {\n    byPhase[finalPhaseCode] = {\n      phase_id: finalPhaseCode,\n      phase_code: finalPhaseCode,\n      phase_name: correctPhase?.name || 'Other',\n      phase_name_local: correctPhase?.name_de || '',\n      types: [],\n      phase_total_cost: 0,\n      phase_resource_cost: 0,\n      phase_material_cost: 0,\n      phase_labor_hours: 0,\n      phase_works_count: 0\n    };\n  }\n  \n  // Update type's assigned_phase to match the corrected phase\n  type.assigned_phase = {\n    phase_id: finalPhaseCode,\n    phase_code: finalPhaseCode,\n    phase_name_en: correctPhase?.name || byPhase[finalPhaseCode].phase_name,\n    phase_name_de: correctPhase?.name_de || byPhase[finalPhaseCode].phase_name_local\n  };\n  \n  // Add type to phase\n  byPhase[finalPhaseCode].types.push(type);\n  byPhase[finalPhaseCode].phase_total_cost += type.type_total_cost || 0;\n  byPhase[finalPhaseCode].phase_resource_cost += type.type_resource_cost || 0;\n  byPhase[finalPhaseCode].phase_material_cost += type.type_material_cost || 0;\n  byPhase[finalPhaseCode].phase_labor_hours += type.type_total_labor_hours || 0;\n  byPhase[finalPhaseCode].phase_works_count += type.works_count || 0;\n});\n\nconsole.log(`\\n📊 Phase corrections made: ${correctionCount}/${accumulatedTypes.length}`);\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// CONVERT TO ARRAY AND SORT\n// ═══════════════════════════════════════════════════════════════════════════════\n\n// Define phase order for sorting\nconst PHASE_ORDER = ['SITE', 'FOUND', 'FRAME', 'FLOORS', 'WALLS', 'ROOF', 'WINDOWS', 'MEP', 'INTERIOR', 'UNASSIGNED'];\n\nconst phaseResults = Object.values(byPhase)\n  .filter(p => p.types.length > 0) // Only include phases with types\n  .map(p => ({\n    ...p,\n    phase_total_cost_formatted: formatCurrency(p.phase_total_cost),\n    phase_resource_cost_formatted: formatCurrency(p.phase_resource_cost),\n    phase_material_cost_formatted: formatCurrency(p.phase_material_cost),\n    phase_labor_hours_formatted: p.phase_labor_hours.toFixed(1) + ' h',\n    phase_percentage: grandTotalCost > 0 ? ((p.phase_total_cost / grandTotalCost) * 100).toFixed(1) + '%' : '0%'\n  }))\n  .sort((a, b) => {\n    const orderA = PHASE_ORDER.indexOf(a.phase_code);\n    const orderB = PHASE_ORDER.indexOf(b.phase_code);\n    \n    if (orderA === -1 && orderB === -1) return a.phase_code.localeCompare(b.phase_code);\n    if (orderA === -1) return 1;\n    if (orderB === -1) return -1;\n    return orderA - orderB;\n  });\n\nconsole.log(`\\nPhase summary:`);\nphaseResults.forEach(p => {\n  console.log(`  ${p.phase_code}: ${p.types.length} types, ${p.phase_works_count} works, ${p.phase_total_cost_formatted}`);\n});\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// QUALITY REPORT\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst allWorks = accumulatedTypes.flatMap(t => t.works || []);\n\nconst qualityReport = {\n  total_works: allWorks.length,\n  by_quality: {\n    high: allWorks.filter(w => w.quality_level === 'high').length,\n    medium: allWorks.filter(w => w.quality_level === 'medium').length,\n    low: allWorks.filter(w => w.quality_level === 'low').length,\n    very_low: allWorks.filter(w => w.quality_level === 'very_low').length,\n    not_found: allWorks.filter(w => w.quality_level === 'not_found').length\n  },\n  average_confidence: allWorks.length > 0\n    ? allWorks.reduce((sum, w) => sum + (w.search_confidence || 0), 0) / allWorks.length\n    : 0\n};\n\nqualityReport.average_confidence_percent = (qualityReport.average_confidence * 100).toFixed(1) + '%';\n\nif (qualityReport.total_works > 0) {\n  qualityReport.percentages = {\n    high: ((qualityReport.by_quality.high / qualityReport.total_works) * 100).toFixed(1) + '%',\n    medium: ((qualityReport.by_quality.medium / qualityReport.total_works) * 100).toFixed(1) + '%',\n    low: ((qualityReport.by_quality.low / qualityReport.total_works) * 100).toFixed(1) + '%',\n    not_found: ((qualityReport.by_quality.not_found / qualityReport.total_works) * 100).toFixed(1) + '%'\n  };\n}\n\n// Types without works\nconst typesWithoutWorks = accumulatedTypes.filter(t => t.no_works_generated || t.works_count === 0);\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// CLEAN UP STATIC DATA\n// ═══════════════════════════════════════════════════════════════════════════════\n\nstaticData.accumulated_types = [];\nstaticData.work_results = {};\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// OUTPUT\n// ═══════════════════════════════════════════════════════════════════════════════\n\n\n\nconsole.log('\\n' + '═'.repeat(70));\nconsole.log('FINAL AGGREGATION COMPLETE');\nconsole.log('═'.repeat(70));\nconsole.log(`Total types: ${accumulatedTypes.length}`);\nconsole.log(`Total works: ${totalWorks}`);\nconsole.log(`Phase corrections: ${correctionCount}`);\nconsole.log(`Types without works: ${typesWithoutWorks.length}`);\nconsole.log(`Grand total: ${formatCurrency(grandTotalCost)}`);\nconsole.log('═'.repeat(70) + '\\n');\n\nreturn {\n  json: {\n    // Project info\n    project_info: {\n      project_type: projectAnalysis.project_type || 'unknown',\n      project_scale: projectAnalysis.project_scale || 'unknown',\n      total_types: accumulatedTypes.length,\n      total_phases: phaseResults.length,\n      total_works: totalWorks,\n      types_without_works: typesWithoutWorks.length,\n      phase_corrections_made: correctionCount\n    },\n    \n    // Cost summary\n    cost_summary: {\n      grand_total_cost: grandTotalCost,\n      grand_total_cost_formatted: formatCurrency(grandTotalCost),\n      grand_resource_cost: grandResourceCost,\n      grand_resource_cost_formatted: formatCurrency(grandResourceCost),\n      grand_material_cost: grandMaterialCost,\n      grand_material_cost_formatted: formatCurrency(grandMaterialCost),\n      grand_labor_hours: grandLaborHours,\n      grand_labor_hours_formatted: grandLaborHours.toFixed(1) + ' h',\n      grand_labor_days: (grandLaborHours / 8).toFixed(1),\n      currency: currency\n    },\n    \n    // Data by phase\n    by_phase: phaseResults,\n    \n    // All types (flat)\n    by_type: accumulatedTypes,\n    \n    // Types without pricing\n    types_without_works: typesWithoutWorks,\n    \n    // Quality metrics\n    quality_report: qualityReport,\n    \n    // Metadata\n    currency: currency,\n    locale: locale,\n    generated_at: new Date().toISOString(),\n    stage: 'STAGE_8_FINAL_AGGREGATION_COMPLETE'\n  }\n};"},"typeVersion":2},{"id":"24e5f83f-7918-4726-b471-5c59f311dd47","name":"STAGE 7 - Calculate Costs","type":"n8n-nodes-base.code","notes":"Calculates total costs, scales resources by quantity","position":[1952,2528],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════════\n// STAGE 7: CALCULATE COSTS - РАСЧЕТ СТОИМОСТИ С РЕСУРСАМИ\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst input = $input.first().json;\n\nlet langConfig = {};\ntry {\n  langConfig = $('Configure Language & Vector DB').first().json;\n} catch (e) {\n  langConfig = { currency: 'EUR', currency_symbol: '€', locale: 'de-DE' };\n}\n\nconst currency = langConfig.currency || 'EUR';\nconst currencySymbol = langConfig.currency_symbol || '€';\nconst locale = langConfig.locale || 'de-DE';\n\nfunction formatCurrency(value) {\n  if (value === null || value === undefined || isNaN(value)) return '-';\n  try {\n    return new Intl.NumberFormat(locale, { style: 'currency', currency: currency, minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value);\n  } catch (e) {\n    return `${currencySymbol}${value.toFixed(2)}`;\n  }\n}\n\nfunction formatNumber(value, decimals = 3) {\n  if (value === null || value === undefined || isNaN(value)) return '-';\n  try {\n    return new Intl.NumberFormat(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }).format(value);\n  } catch (e) {\n    return value.toFixed(decimals);\n  }\n}\n\nconst rateInfo = input.rateInfo || {};\nconst resources = input.resources || {};\nconst calculationDetails = input.calculation_details || {};\n\nconst quantityInRateUnits = input.calculated_quantity || input.quantity_in_rate_units || 0;\nconst projectQuantity = input.project_quantity || 0;\nconst unitCost = rateInfo.total_cost_position || 0;\nconst workerHoursPerUnit = rateInfo.worker_labor_hours || 0;\nconst machinistHoursPerUnit = rateInfo.machinist_labor_hours || 0;\nconst totalHoursPerUnit = rateInfo.total_labor_hours || 0;\n\nconst totalCost = quantityInRateUnits * unitCost;\n\nconst costBreakdown = rateInfo.cost_breakdown || {};\nconst totalUnitCostBreakdown = (costBreakdown.workers_cost || 0) + (costBreakdown.machines_cost || 0) + (costBreakdown.materials_cost || 0) + (costBreakdown.machinists_cost || 0) + (costBreakdown.electricity_cost || 0);\n\nlet totalWorkersCost = 0, totalMachinesCost = 0, totalMaterialsCost = 0, totalMachinistsCost = 0, totalElectricityCost = 0;\n\nif (totalUnitCostBreakdown > 0) {\n  const ratio = totalCost / totalUnitCostBreakdown;\n  totalWorkersCost = (costBreakdown.workers_cost || 0) * ratio;\n  totalMachinesCost = (costBreakdown.machines_cost || 0) * ratio;\n  totalMaterialsCost = (costBreakdown.materials_cost || 0) * ratio;\n  totalMachinistsCost = (costBreakdown.machinists_cost || 0) * ratio;\n  totalElectricityCost = (costBreakdown.electricity_cost || 0) * ratio;\n} else if (unitCost > 0) {\n  totalWorkersCost = totalCost * 0.6;\n  totalMaterialsCost = totalCost * 0.4;\n}\n\nconst totalResourceCost = totalWorkersCost + totalMachinistsCost + totalMachinesCost;\nconst totalMaterialCostFinal = totalMaterialsCost + totalElectricityCost;\n\nconst estimatedWorkerHours = workerHoursPerUnit * quantityInRateUnits;\nconst estimatedMachinistHours = machinistHoursPerUnit * quantityInRateUnits;\nconst estimatedTotalHours = totalHoursPerUnit * quantityInRateUnits;\n\n// Scale resources\nconst scaledResources = {\n  workers: (resources.workers || []).map(r => ({ ...r, scaled_quantity: (r.resource_quantity || 0) * quantityInRateUnits, scaled_cost: (r.resource_cost || 0) * quantityInRateUnits })),\n  machines: (resources.machines || []).map(r => ({ ...r, scaled_quantity: (r.resource_quantity || 0) * quantityInRateUnits, scaled_cost: (r.resource_cost || 0) * quantityInRateUnits })),\n  materials: (resources.materials || []).map(r => ({ ...r, scaled_quantity: (r.resource_quantity || 0) * quantityInRateUnits, scaled_cost: (r.resource_cost || 0) * quantityInRateUnits })),\n  machinists: (resources.machinists || []).map(r => ({ ...r, scaled_quantity: (r.resource_quantity || 0) * quantityInRateUnits, scaled_cost: (r.resource_cost || 0) * quantityInRateUnits })),\n  electricity: (resources.electricity || []).map(r => ({ ...r, scaled_quantity: (r.resource_quantity || 0) * quantityInRateUnits, scaled_cost: (r.resource_cost || 0) * quantityInRateUnits }))\n};\n\nconst allScaledResources = [...scaledResources.workers, ...scaledResources.machines, ...scaledResources.machinists, ...scaledResources.materials, ...scaledResources.electricity].filter(r => r.resource_code || r.resource_name);\n\nconst searchConfidence = rateInfo.search_confidence || 0;\nconst qualityLevel = rateInfo.quality_level || 'not_found';\nlet qualityIndicator = '✗', qualityScore = 0;\n\nif (qualityLevel === 'high') { qualityIndicator = '●'; qualityScore = 100; }\nelse if (qualityLevel === 'medium') { qualityIndicator = '◐'; qualityScore = 75; }\nelse if (qualityLevel === 'low') { qualityIndicator = '○'; qualityScore = 50; }\nelse if (qualityLevel === 'very_low') { qualityIndicator = '◌'; qualityScore = 25; }\n\nconst passthrough = {\n    _typeKey: input._typeKey, ...input };\ndelete passthrough.rateInfo;\ndelete passthrough.resources;\n\nreturn {\n  json: {\n    ...passthrough,\n    work_id: input.work_id || `W${String(Date.now()).slice(-6)}`,\n    work_name: input.work_name || rateInfo.rate_name || 'Work',\n    work_sequence: input.work_sequence || 1,\n    rate_code: rateInfo.rate_code || 'NOT_FOUND',\n    rate_name: rateInfo.rate_name || 'Rate not found',\n    rate_unit: rateInfo.rate_unit || 'unit',\n    rate_hierarchy: rateInfo.hierarchy?.full_path || '',\n    project_quantity: projectQuantity,\n    project_quantity_formatted: formatNumber(projectQuantity, 3),\n    project_unit: input.project_unit || calculationDetails.bim_parameter_label || '',\n    quantity_in_rate_units: quantityInRateUnits,\n    quantity_formatted: formatNumber(quantityInRateUnits, 3),\n    calculation_formula: calculationDetails.calculation_formula || '',\n    unit_cost: unitCost,\n    unit_cost_formatted: formatCurrency(unitCost),\n    total_cost: totalCost,\n    total_cost_formatted: formatCurrency(totalCost),\n    total_resource_cost: totalResourceCost,\n    total_resource_cost_formatted: formatCurrency(totalResourceCost),\n    total_material_cost: totalMaterialCostFinal,\n    total_material_cost_formatted: formatCurrency(totalMaterialCostFinal),\n    cost_breakdown: {\n      workers: totalWorkersCost, workers_formatted: formatCurrency(totalWorkersCost),\n      machines: totalMachinesCost, machines_formatted: formatCurrency(totalMachinesCost),\n      machinists: totalMachinistsCost, machinists_formatted: formatCurrency(totalMachinistsCost),\n      materials: totalMaterialsCost, materials_formatted: formatCurrency(totalMaterialsCost),\n      electricity: totalElectricityCost, electricity_formatted: formatCurrency(totalElectricityCost)\n    },\n    currency: currency,\n    estimated_labor_hours: estimatedTotalHours,\n    estimated_labor_hours_formatted: formatNumber(estimatedTotalHours, 1) + ' h',\n    estimated_worker_hours: estimatedWorkerHours,\n    estimated_machinist_hours: estimatedMachinistHours,\n    worker_hours_per_unit: workerHoursPerUnit,\n    machinist_hours_per_unit: machinistHoursPerUnit,\n    labor_calculation_method: totalHoursPerUnit > 0 ? 'from_rate' : 'estimated',\n    search_confidence: searchConfidence,\n    search_confidence_percent: (searchConfidence * 100).toFixed(1) + '%',\n    quality_level: qualityLevel,\n    quality_reason: rateInfo.quality_reason || '',\n    quality_score: qualityScore,\n    quality_indicator: qualityIndicator,\n    resources_scaled: scaledResources,\n    resources_all: allScaledResources,\n    resources_count: allScaledResources.length,\n    resources_per_unit: { workers: resources.workers || [], machines: resources.machines || [], materials: resources.materials || [], machinists: resources.machinists || [], electricity: resources.electricity || [] },\n    calculation_details: { ...calculationDetails, unit_multiplier: calculationDetails.unit_multiplier || 1, unit_conversion_note: calculationDetails.unit_conversion_note || 'No conversion needed', rate_unit: rateInfo.rate_unit || 'unit', quantity_in_rate_units: quantityInRateUnits, final_quantity: quantityInRateUnits },\n    search_query_used: input.search_query || '',\n    results_count: input.results_count || 0,\n    type_name: input.type_name,\n    type_index: input.type_index,\n    total_types: input.total_types,\n    category: input.category,\n    assigned_phase: input.assigned_phase,\n    element_count: input.element_count,\n    element_analysis: input.element_analysis,\n    _work_index: input._work_index,\n    _total_works_in_type: input._total_works_in_type,\n    locale: locale,\n    stage: 'STAGE_7_COST_CALCULATED'\n  }\n};"},"notesInFlow":true,"typeVersion":2},{"id":"e89f224d-5f74-42cb-beb6-ebf7cc6c72e8","name":"STAGE 9 - Generate Cost Estimate","type":"n8n-nodes-base.code","position":[1280,3184],"parameters":{"jsCode":"\n// ═══════════════════════════════════════════════════════════════════════════════\n// MACHINE/LABOR HOURS DETECTION - 9 LANGUAGES\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst MACHINE_PATTERNS_9LANG = [\n  // DE\n  'masch', 'maschinenstunde', 'masch.-std', 'gerät',\n  // EN  \n  'machine', 'mach-h', 'equipment',\n  // FR (avoiding apostrophes)\n  'heure machine', 'engin', 'matériel',\n  // ES\n  'máquina', 'hora máquina', 'equipo',\n  // PT\n  'equipamento',\n  // RU\n  'машин', 'маш-ч', 'маш.ч', 'механизм',\n  // ZH\n  '机器', '机械', '设备', '台班',\n  // AR\n  'آلة', 'معدة', 'معدات',\n  // HI\n  'मशीन', 'यंत्र', 'उपकरण'\n];\n\nconst LABOR_PATTERNS_9LANG = [\n  // DE\n  'std', 'stunde', 'arbeitsstunde', 'arbeiter', 'facharbeiter',\n  // EN\n  'hour', 'man-hour', 'manhour', 'labor', 'worker',\n  // FR\n  'heure', 'ouvrier', 'travailleur',\n  // ES  \n  'hora', 'obrero', 'trabajador',\n  // PT\n  'operário',\n  // RU\n  'ч-ч', 'чел-ч', 'человеко-час', 'труд', 'рабочий', 'разряд',\n  // ZH\n  '工时', '人工', '工人', '劳动',\n  // AR\n  'ساعة', 'عامل', 'عمالة',\n  // HI\n  'घंटा', 'श्रम', 'मजदूर'\n];\n\nfunction isMachineResource(unit, name, code) {\n  const combined = ((unit || '') + ' ' + (name || '') + ' ' + (code || '')).toLowerCase();\n  return MACHINE_PATTERNS_9LANG.some(p => combined.includes(p.toLowerCase()));\n}\n\nfunction isLaborResource(unit, name, code) {\n  if (isMachineResource(unit, name, code)) return false;\n  const combined = ((unit || '') + ' ' + (name || '') + ' ' + (code || '')).toLowerCase();\n  return LABOR_PATTERNS_9LANG.some(p => combined.includes(p.toLowerCase()));\n}\n\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// STAGE 9: HTML + Excel Generator - Professional Design v13\n// IMPROVED: Formula display with multi-parameter calculations\n// - Shows \"(Width + Height) × 2\" formulas\n// - Displays actual values: \"(1.18 + 1.17) × 2 = 4.70\"\n// - Filter empty phases\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst input = $input.first().json;\n\n// Get language config\nlet langConfig = {};\ntry {\n  langConfig = $('Configure Language & Vector DB').first().json;\n} catch (e) {\n  langConfig = { \n    language: 'German', \n    language_code: 'DE', \n    currency: 'EUR', \n    currency_symbol: '€', \n    locale: 'de-DE', \n    city: 'Berlin',\n    pricing_level: 'Berlin'\n  };\n}\n\n// Get project path and extract project name\nlet projectPath = '';\nlet projectName = 'BIM Cost Estimation';\ntry {\n  const setup = $('Setup - Define file paths1').first().json;\n  projectPath = setup.project_file || '';\n  if (projectPath) {\n    const pathParts = projectPath.replace(/\\\\/g, '/').split('/');\n    const fullName = pathParts[pathParts.length - 1];\n    projectName = fullName.replace(/\\.[^/.]+$/, '');\n  }\n} catch (e) {\n  projectPath = '';\n}\n\nconst langCode = (langConfig.language_code || 'DE').toUpperCase();\nconst currency = langConfig.currency || 'EUR';\nconst currencySymbol = langConfig.currency_symbol || '€';\nconst locale = langConfig.locale || 'de-DE';\nconst pricingLevel = langConfig.pricing_level || langConfig.city || 'Unknown';\n\n// Link for all rates\nconst RATES_LINK = 'https://openconstructionestimate.com/all-estimates/?utm=OCE';\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// TRANSLATIONS\n// ═══════════════════════════════════════════════════════════════════════════════\n\n\nconst translations = {\n  'DE': {\n    doc_title: 'KOSTENVORANSCHLAG', project: 'Projekt',\n    pricing_level: 'Preisniveau', date: 'Datum', currency_label: 'Währung',\n    positions: 'Positionen', works: 'Arbeiten',\n    col_pos: 'Pos.', col_code: 'Kennziffer', col_description: 'Bezeichnung',\n    col_calc: 'Berechnung', col_unit: 'Einheit', col_qty: 'Menge', \n    col_price: 'EP', col_total: 'GP', col_labor: 'Std.', col_quality: 'Q',\n    subtotal: 'Zwischensumme', grand_total: 'GESAMTSUMME',\n    labor_cost: 'Lohnkosten', material_cost: 'Materialkosten', labor_days: 'Arbeitstage',\n    found_rates: 'Gefunden', manual_check: 'prüfen', of: 'von',\n    kpi_total: 'Gesamtkosten', kpi_hours: 'Arbeitsstunden', kpi_days: 'Arbeitstage',\n    chart_cost_structure: 'Kostenstruktur', chart_by_phase: 'Nach Phase',\n    chart_labor: 'Lohn', chart_material: 'Material', chart_machines: 'Maschinen',\n    chart_timeline: 'Zeitplan', chart_hierarchy: 'Kostenhierarchie',\n    collapse_all: 'Alles einklappen', expand_all: 'Alles ausklappen',\n    res_labor: 'Lohn', res_material: 'Mat.', res_machine: 'Masch.'\n  },\n  'EN': {\n    doc_title: 'COST ESTIMATE', project: 'Project',\n    pricing_level: 'Pricing Level', date: 'Date', currency_label: 'Currency',\n    positions: 'Items', works: 'Works',\n    col_pos: 'Pos.', col_code: 'Code', col_description: 'Description',\n    col_calc: 'Calculation', col_unit: 'Unit', col_qty: 'Qty', \n    col_price: 'UP', col_total: 'Total', col_labor: 'Hrs', col_quality: 'Q',\n    subtotal: 'Subtotal', grand_total: 'GRAND TOTAL',\n    labor_cost: 'Labor Cost', material_cost: 'Material Cost', labor_days: 'Work Days',\n    found_rates: 'Found', manual_check: 'check', of: 'of',\n    kpi_total: 'Total Cost', kpi_hours: 'Work Hours', kpi_days: 'Work Days',\n    chart_cost_structure: 'Cost Structure', chart_by_phase: 'By Phase',\n    chart_labor: 'Labor', chart_material: 'Material', chart_machines: 'Machines',\n    chart_timeline: 'Timeline', chart_hierarchy: 'Cost Hierarchy',\n    collapse_all: 'Collapse All', expand_all: 'Expand All',\n    res_labor: 'Labor', res_material: 'Mat.', res_machine: 'Mach.'\n  },\n  'FR': {\n    doc_title: 'DEVIS ESTIMATIF', project: 'Projet',\n    pricing_level: 'Niveau de prix', date: 'Date', currency_label: 'Devise',\n    positions: 'Postes', works: 'Travaux',\n    col_pos: 'Pos.', col_code: 'Code', col_description: 'Désignation',\n    col_calc: 'Calcul', col_unit: 'Unité', col_qty: 'Qté', \n    col_price: 'PU', col_total: 'Total', col_labor: 'Hrs', col_quality: 'Q',\n    subtotal: 'Sous-total', grand_total: 'TOTAL GÉNÉRAL',\n    labor_cost: 'Main-d-oeuvre', material_cost: 'Matériaux', labor_days: 'Jours',\n    found_rates: 'Trouvés', manual_check: 'vérifier', of: 'de',\n    kpi_total: 'Coût Total', kpi_hours: 'Heures', kpi_days: 'Jours',\n    chart_cost_structure: 'Structure des coûts', chart_by_phase: 'Par phase',\n    chart_labor: 'Main-d-oeuvre', chart_material: 'Matériaux', chart_machines: 'Machines',\n    chart_timeline: 'Planning', chart_hierarchy: 'Hiérarchie',\n    collapse_all: 'Tout réduire', expand_all: 'Tout développer',\n    res_labor: 'M.O.', res_material: 'Mat.', res_machine: 'Mach.'\n  },\n  'ES': {\n    doc_title: 'PRESUPUESTO', project: 'Proyecto',\n    pricing_level: 'Nivel de precios', date: 'Fecha', currency_label: 'Moneda',\n    positions: 'Partidas', works: 'Trabajos',\n    col_pos: 'Pos.', col_code: 'Código', col_description: 'Descripción',\n    col_calc: 'Cálculo', col_unit: 'Unidad', col_qty: 'Cant.', \n    col_price: 'P.U.', col_total: 'Total', col_labor: 'Hrs', col_quality: 'Q',\n    subtotal: 'Subtotal', grand_total: 'TOTAL GENERAL',\n    labor_cost: 'Mano obra', material_cost: 'Materiales', labor_days: 'Días',\n    found_rates: 'Encontrados', manual_check: 'verificar', of: 'de',\n    kpi_total: 'Coste Total', kpi_hours: 'Horas', kpi_days: 'Días',\n    chart_cost_structure: 'Estructura de costes', chart_by_phase: 'Por fase',\n    chart_labor: 'Mano obra', chart_material: 'Material', chart_machines: 'Máquinas',\n    chart_timeline: 'Cronograma', chart_hierarchy: 'Jerarquía',\n    collapse_all: 'Contraer todo', expand_all: 'Expandir todo',\n    res_labor: 'M.O.', res_material: 'Mat.', res_machine: 'Máq.'\n  },\n  'PT': {\n    doc_title: 'ORÇAMENTO', project: 'Projeto',\n    pricing_level: 'Nível de preços', date: 'Data', currency_label: 'Moeda',\n    positions: 'Itens', works: 'Trabalhos',\n    col_pos: 'Pos.', col_code: 'Código', col_description: 'Descrição',\n    col_calc: 'Cálculo', col_unit: 'Unidade', col_qty: 'Qtd.', \n    col_price: 'P.U.', col_total: 'Total', col_labor: 'Hrs', col_quality: 'Q',\n    subtotal: 'Subtotal', grand_total: 'TOTAL GERAL',\n    labor_cost: 'Mão obra', material_cost: 'Materiais', labor_days: 'Dias',\n    found_rates: 'Encontrados', manual_check: 'verificar', of: 'de',\n    kpi_total: 'Custo Total', kpi_hours: 'Horas', kpi_days: 'Dias',\n    chart_cost_structure: 'Estrutura de custos', chart_by_phase: 'Por fase',\n    chart_labor: 'Mão obra', chart_material: 'Material', chart_machines: 'Máquinas',\n    chart_timeline: 'Cronograma', chart_hierarchy: 'Hierarquia',\n    collapse_all: 'Recolher tudo', expand_all: 'Expandir tudo',\n    res_labor: 'M.O.', res_material: 'Mat.', res_machine: 'Máq.'\n  },\n  'RU': {\n    doc_title: 'СМЕТА', project: 'Проект',\n    pricing_level: 'Уровень цен', date: 'Дата', currency_label: 'Валюта',\n    positions: 'Позиций', works: 'Работ',\n    col_pos: 'N', col_code: 'Шифр', col_description: 'Наименование',\n    col_calc: 'Расчёт', col_unit: 'Ед.', col_qty: 'Кол-во', \n    col_price: 'Цена', col_total: 'Сумма', col_labor: 'Ч/ч', col_quality: 'К',\n    subtotal: 'Итого', grand_total: 'ВСЕГО',\n    labor_cost: 'Труд', material_cost: 'Материал', labor_days: 'Дней',\n    found_rates: 'Найдено', manual_check: 'проверить', of: 'из',\n    kpi_total: 'Общая Стоимость', kpi_hours: 'Часы', kpi_days: 'Дни',\n    chart_cost_structure: 'Структура Затрат', chart_by_phase: 'По Фазе',\n    chart_labor: 'Труд', chart_material: 'Материалы', chart_machines: 'Машины',\n    chart_timeline: 'График', chart_hierarchy: 'Иерархия',\n    collapse_all: 'Свернуть', expand_all: 'Развернуть',\n    res_labor: 'Труд', res_material: 'Мат.', res_machine: 'Маш.'\n  },\n  'ZH': {\n    doc_title: '工程预算', project: '项目',\n    pricing_level: '价格水平', date: '日期', currency_label: '货币',\n    positions: '项目', works: '工作',\n    col_pos: '序号', col_code: '编码', col_description: '名称',\n    col_calc: '计算', col_unit: '单位', col_qty: '数量', \n    col_price: '单价', col_total: '合计', col_labor: '工时', col_quality: '质',\n    subtotal: '小计', grand_total: '总计',\n    labor_cost: '人工费', material_cost: '材料费', labor_days: '工作日',\n    found_rates: '已找到', manual_check: '核查', of: '/',\n    kpi_total: '总成本', kpi_hours: '工时', kpi_days: '工作日',\n    chart_cost_structure: '成本结构', chart_by_phase: '按阶段',\n    chart_labor: '人工', chart_material: '材料', chart_machines: '机械',\n    chart_timeline: '进度', chart_hierarchy: '层次',\n    collapse_all: '全部折叠', expand_all: '全部展开',\n    res_labor: '人工', res_material: '材料', res_machine: '机械'\n  },\n  'AR': {\n    doc_title: 'تقدير التكلفة', project: 'المشروع',\n    pricing_level: 'مستوى الأسعار', date: 'التاريخ', currency_label: 'العملة',\n    positions: 'البنود', works: 'الأعمال',\n    col_pos: 'رقم', col_code: 'الرمز', col_description: 'الوصف',\n    col_calc: 'الحساب', col_unit: 'الوحدة', col_qty: 'الكمية', \n    col_price: 'سعر الوحدة', col_total: 'المجموع', col_labor: 'ساعات', col_quality: 'ج',\n    subtotal: 'المجموع الفرعي', grand_total: 'المجموع الكلي',\n    labor_cost: 'تكلفة العمالة', material_cost: 'تكلفة المواد', labor_days: 'أيام العمل',\n    found_rates: 'تم العثور', manual_check: 'تحقق', of: 'من',\n    kpi_total: 'التكلفة الإجمالية', kpi_hours: 'ساعات العمل', kpi_days: 'أيام العمل',\n    chart_cost_structure: 'هيكل التكلفة', chart_by_phase: 'حسب المرحلة',\n    chart_labor: 'العمالة', chart_material: 'المواد', chart_machines: 'المعدات',\n    chart_timeline: 'الجدول الزمني', chart_hierarchy: 'تسلسل التكاليف',\n    collapse_all: 'طي الكل', expand_all: 'توسيع الكل',\n    res_labor: 'عمالة', res_material: 'مواد', res_machine: 'معدات'\n  },\n  'HI': {\n    doc_title: 'लागत अनुमान', project: 'परियोजना',\n    pricing_level: 'मूल्य स्तर', date: 'तारीख', currency_label: 'मुद्रा',\n    positions: 'मद', works: 'कार्य',\n    col_pos: 'क्रम', col_code: 'कोड', col_description: 'विवरण',\n    col_calc: 'गणना', col_unit: 'इकाई', col_qty: 'मात्रा', \n    col_price: 'दर', col_total: 'कुल', col_labor: 'घंटे', col_quality: 'गु',\n    subtotal: 'उप-योग', grand_total: 'कुल योग',\n    labor_cost: 'श्रम लागत', material_cost: 'सामग्री लागत', labor_days: 'कार्य दिवस',\n    found_rates: 'मिला', manual_check: 'जाँचें', of: 'में से',\n    kpi_total: 'कुल लागत', kpi_hours: 'कार्य घंटे', kpi_days: 'कार्य दिवस',\n    chart_cost_structure: 'लागत संरचना', chart_by_phase: 'चरण अनुसार',\n    chart_labor: 'श्रम', chart_material: 'सामग्री', chart_machines: 'मशीनें',\n    chart_timeline: 'समयरेखा', chart_hierarchy: 'पदानुक्रम',\n    collapse_all: 'सभी संक्षिप्त करें', expand_all: 'सभी विस्तृत करें',\n    res_labor: 'श्रम', res_material: 'सामग्री', res_machine: 'मशीन'\n  }\n};\n\n\nconst t = translations[langCode] || translations['DE'];\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// HELPERS\n// ═══════════════════════════════════════════════════════════════════════════════\n\nfunction formatCurrency(value) {\n  if (value === null || value === undefined || isNaN(value)) return '—';\n  try {\n    return new Intl.NumberFormat(locale, { \n      style: 'currency', currency: currency, \n      minimumFractionDigits: 2, maximumFractionDigits: 2 \n    }).format(value);\n  } catch (e) {\n    return `${currencySymbol}${value.toFixed(2)}`;\n  }\n}\n\nfunction formatNumber(value, decimals = 3) {\n  if (value === null || value === undefined || isNaN(value)) return '—';\n  try {\n    return new Intl.NumberFormat(locale, {\n      minimumFractionDigits: decimals, maximumFractionDigits: decimals\n    }).format(value);\n  } catch (e) {\n    return value.toFixed(decimals);\n  }\n}\n\nfunction formatDateTime() {\n  try {\n    return new Intl.DateTimeFormat(locale, { \n      year: 'numeric', month: '2-digit', day: '2-digit',\n      hour: '2-digit', minute: '2-digit'\n    }).format(new Date());\n  } catch (e) {\n    const now = new Date();\n    return now.toISOString().split('T')[0] + ' ' + now.toTimeString().substring(0,5);\n  }\n}\n\nfunction escapeHtml(text) {\n  if (!text) return '';\n  return String(text)\n    .replace(/&/g, '&amp;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;')\n    .replace(/\"/g, '&quot;');\n}\n\nfunction cleanCategoryName(category) {\n  if (!category) return 'Unknown';\n  let clean = category;\n  if (clean.toLowerCase().startsWith('ost_')) {\n    clean = clean.substring(4);\n  }\n  clean = clean.replace(/_/g, ' ');\n  clean = clean.split(' ').map(word => \n    word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()\n  ).join(' ');\n  return clean;\n}\n\nfunction cleanPhaseName(phase) {\n  const phaseName = phase.phase_name || '';\n  const phaseCode = phase.phase_code || '';\n  \n  if (phaseCode === 'UNASSIGNED' || phaseName.toLowerCase().includes('unassigned')) {\n    const types = phase.types || [];\n    if (types.length > 0 && types[0].category) {\n      return cleanCategoryName(types[0].category);\n    }\n    return 'Other Elements';\n  }\n  return phaseName;\n}\n\n// Format BIM parameter name nicely\nfunction formatParamName(param) {\n  if (!param) return '';\n  const mapping = {\n    'area': 'Area',\n    'volume': 'Volume',\n    'length': 'Length',\n    'element_count': 'Count',\n    'count': 'Count',\n    'width': 'Width',\n    'height': 'Height',\n    'perimeter': 'Perimeter',\n    'weight': 'Weight',\n    'thickness': 'Thickness'\n  };\n  return mapping[param.toLowerCase()] || param.charAt(0).toUpperCase() + param.slice(1);\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// DATA\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst phases = input.by_phase || [];\nconst costSummary = input.cost_summary || {};\nconst grandTotal = costSummary.grand_total_cost || 0;\nconst grandLaborHours = costSummary.grand_labor_hours || 0;\nconst grandResourceCost = costSummary.grand_resource_cost || 0;\nconst grandMaterialCost = costSummary.grand_material_cost || 0;\n\n// Quality metrics\nlet totalTypes = 0, totalWorks = 0;\nlet qualityHigh = 0, qualityMedium = 0, qualityLow = 0, qualityNotFound = 0;\n\nphases.forEach(phase => {\n  const types = phase.types || [];\n  totalTypes += types.length;\n  types.forEach(type => {\n    const works = type.works || [];\n    totalWorks += works.length;\n    works.forEach(work => {\n      const level = work.quality_level || 'not_found';\n      if (level === 'high') qualityHigh++;\n      else if (level === 'medium') qualityMedium++;\n      else if (level === 'low') qualityLow++;\n      else qualityNotFound++;\n    });\n  });\n});\n\nconst foundRates = qualityHigh + qualityMedium + qualityLow;\nconst foundPercent = totalWorks > 0 ? Math.round(foundRates / totalWorks * 100) : 0;\n\n// Collect phase data for charts\nconst phaseChartData = [];\nphases.forEach(phase => {\n  const types = phase.types || [];\n  let phaseCost = 0;\n  let phaseHours = 0;\n  types.forEach(type => {\n    phaseCost += type.type_total_cost || 0;\n    phaseHours += type.type_total_labor_hours || 0;\n  });\n  if (types.length > 0) {\n    phaseChartData.push({\n      name: phase.phase_name || phase.phase_code || 'Phase',\n      cost: phaseCost,\n      hours: phaseHours\n    });\n  }\n});\n\nconst laborDays = Math.ceil(grandLaborHours / 8);\nconst laborPercent = grandTotal > 0 ? Math.round(grandResourceCost / grandTotal * 100) : 0;\nconst materialPercent = grandTotal > 0 ? Math.round(grandMaterialCost / grandTotal * 100) : 0;\nconst machinesCost = grandTotal - grandResourceCost - grandMaterialCost;\nconst machinesPercent = grandTotal > 0 ? Math.round(machinesCost / grandTotal * 100) : 0;\n\nconst dateTimeStr = formatDateTime();\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// HTML GENERATION\n// ═══════════════════════════════════════════════════════════════════════════════\n\nlet html = `<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n\n<title>${t.doc_title} - ${projectName}</title>\n<style>\n  :root {\n    --primary: #007AFF;\n    --primary-light: #5AC8FA;\n    --primary-dark: #0051D5;\n    --text-primary: #1D1D1F;\n    --text-secondary: #86868B;\n    --text-muted: #AEAEB2;\n    --bg-white: #FFFFFF;\n    --bg-light: #F5F5F7;\n    --bg-medium: #E8E8ED;\n    --border: #D2D2D7;\n    --success: #34C759;\n    --warning: #FF9500;\n    --error: #FF3B30;\n    --accent: #007AFF;\n  }\n  \n  body { \n    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; \n    margin: 0; \n    padding: 20px; \n    background: #F5F5F7;\n    color: var(--text-primary);\n    line-height: 1.5;\n    font-size: 11px;\n  }\n  \n  .container {\n    background: var(--bg-white);\n    max-width: 1350px;\n    margin: 0 auto;\n    box-shadow: 0 4px 20px rgba(0,0,0,0.08);\n    border-radius: 20px;\n    overflow: hidden;\n  }\n  \n  table { border-collapse: collapse; width: 100%; }\n  td, th { padding: 8px 10px; text-align: left; vertical-align: middle; }\n  \n  /* HEADER */\n  .header { background: linear-gradient(135deg, #1E40AF 0%, #1E3A8A 100%); color: #FFFFFF; }\n  .header-title { font-size: 18px; font-weight: 700; padding: 12px 16px 4px; letter-spacing: 0.3px; }\n  .header-project { font-size: 13px; font-weight: 500; padding: 2px 16px; opacity: 0.95; }\n  .header-info { font-size: 11px; font-weight: 400; padding: 4px 16px; opacity: 0.9; }\n  .header-right { text-align: right; }\n  \n  /* QUALITY BAR */\n  .quality-bar {\n    background: var(--bg-medium);\n    padding: 8px 16px;\n    font-size: 10px;\n    color: var(--text-secondary);\n    display: flex;\n    align-items: center;\n    gap: 16px;\n    flex-wrap: wrap;\n  }\n  .quality-item { display: flex; align-items: center; gap: 4px; }\n  .quality-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }\n  .dot-high { background: #1D1D1F; }\n  .dot-medium { background: #86868B; }\n  .dot-low { background: #AEAEB2; }\n  .dot-notfound { background: #D2D2D7; }\n  .quality-percent { font-weight: 600; color: var(--text-primary); }\n  \n  /* INFO ICON - More visible */\n  .info-icon {\n    display: inline-block;\n    width: 14px;\n    height: 14px;\n    line-height: 14px;\n    text-align: center;\n    background: #E8E8ED;\n    color: #636366;\n    border: 1px solid #AEAEB2;\n    border-radius: 50%;\n    font-size: 9px;\n    font-weight: 600;\n    font-style: italic;\n    cursor: help;\n    margin-left: 4px;\n    vertical-align: middle;\n  }\n  .info-icon:hover {\n    background: #007AFF;\n    color: white;\n    border-color: #007AFF;\n  }\n  \n  /* COLUMN HEADERS */\n  .col-header { \n    background: var(--bg-medium); color: var(--text-secondary); \n    font-weight: 600; font-size: 9px; text-transform: uppercase;\n    letter-spacing: 0.5px; text-align: center; padding: 8px 6px;\n    border-bottom: 2px solid var(--border);\n  }\n  \n  /* DATA ROWS */\n  .phase { background: var(--primary); color: #FFFFFF; font-weight: 600; font-size: 11px; }\n  .type { background: #EFF6FF; color: var(--text-primary); font-weight: 600; font-size: 10px; border-left: 3px solid var(--primary); }\n  .work { background: var(--bg-white); color: var(--text-primary); font-size: 10px; border-bottom: 1px solid var(--bg-medium); }\n  .resource { background: var(--bg-light); color: var(--text-muted); font-size: 9px; border-bottom: 1px solid var(--bg-medium); }\n  .subtotal { background: var(--bg-light); color: var(--text-primary); font-weight: 600; font-size: 12px; border-top: 1px solid var(--border); }\n  \n  /* GRAND TOTAL */\n  .grand-total { background: var(--text-primary); color: #FFFFFF; font-weight: 600; font-size: 14px; }\n  .grand-total-info { background: var(--primary-dark); color: rgba(255,255,255,0.9); font-size: 10px; }\n  .highlight { color: #FCD34D; font-weight: 600; }\n  \n  /* FOOTER */\n  .footer { background: var(--bg-medium); padding: 10px 16px; border-top: 1px solid var(--border); }\n  .footer-main { display: flex; align-items: flex-start; gap: 16px; flex-wrap: wrap; }\n  .footer-brand { font-weight: 700; font-size: 10px; color: var(--primary); white-space: nowrap; }\n  .footer-info { font-size: 9px; color: var(--text-muted); flex: 1; min-width: 200px; }\n  .footer-links { display: flex; gap: 10px; font-size: 9px; }\n  .footer-links a { color: var(--primary); text-decoration: none; white-space: nowrap; }\n  .footer-db { font-size: 8px; color: var(--text-muted); margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--border); }\n  \n  /* CALCULATION COLUMN - Improved for formulas */\n  .calc-cell { \n    font-size: 9px; \n    color: var(--text-muted);\n    line-height: 1.4;\n    max-width: 200px;\n  }\n  .calc-type { \n    color: var(--primary); \n    font-weight: 600;\n    font-size: 9px;\n    display: block;\n    margin-bottom: 2px;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n  .calc-formula { \n    font-family: 'SF Mono', Consolas, 'Courier New', monospace;\n    font-size: 8px;\n    color: var(--text-secondary);\n    background: #F0F9FF;\n    border: 1px solid #BAE6FD;\n    padding: 3px 6px;\n    border-radius: 4px;\n    display: block;\n    line-height: 1.5;\n  }\n  .calc-formula-line {\n    display: block;\n    white-space: nowrap;\n  }\n  .calc-param {\n    color: var(--primary-dark);\n    font-weight: 600;\n  }\n  .calc-value {\n    color: var(--success);\n    font-weight: 500;\n  }\n  .calc-op {\n    color: var(--text-muted);\n    margin: 0 2px;\n  }\n  .calc-result {\n    color: var(--primary);\n    font-weight: 600;\n  }\n  .calc-conversion {\n    color: #7C3AED;\n    font-size: 7px;\n    opacity: 0.8;\n  }\n  \n  /* UTILITIES */\n  .right { text-align: right; }\n  .center { text-align: center; }\n  .num { font-variant-numeric: tabular-nums; }\n  .qty-original { font-size: 8px; color: var(--text-muted); margin-top: 2px; font-weight: 400; }\n  \n  /* RESOURCE TYPE TAGS - iOS style, matching Kostenstruktur colors */\n  .res-tag {\n    display: inline-block;\n    padding: 2px 6px;\n    border-radius: 4px;\n    font-size: 8px;\n    font-weight: 600;\n    text-transform: uppercase;\n    letter-spacing: 0.3px;\n    margin-left: 4px;\n    vertical-align: middle;\n  }\n  .res-tag-labor { background: #1D1D1F; color: white; }\n  .res-tag-material { background: #86868B; color: white; }\n  .res-tag-machine { background: #AEAEB2; color: white; }\n  .link { color: var(--primary); text-decoration: none; }\n  .link:hover { text-decoration: underline; }\n  \n  \n  /* HEADER - Logo on Right */\n  .header-new { \n    background: var(--bg-white);\n    color: var(--text-primary); \n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    padding: 14px 20px;\n    border-bottom: 1px solid var(--border);\n  }\n  .header-left { display: flex; flex-direction: column; }\n  .header-title-new { font-size: 18px; font-weight: 600; letter-spacing: -0.3px; color: var(--text-primary); }\n  .header-project-new { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-top: 2px; }\n  .header-info-new { font-size: 11px; font-weight: 400; color: var(--text-muted); margin-top: 2px; }\n  .header-right { display: flex; align-items: center; gap: 12px; }\n  .header-logo { height: 32px; opacity: 0.9; }\n  .header-logo:hover { opacity: 1; }\n  .header-btn {\n    display: inline-flex;\n    align-items: center;\n    gap: 5px;\n    padding: 7px 12px;\n    background: var(--bg-light);\n    border: 1px solid var(--border);\n    border-radius: 16px;\n    color: var(--text-secondary);\n    text-decoration: none;\n    font-size: 11px;\n    font-weight: 500;\n    transition: all 0.2s;\n  }\n  .header-btn:hover { background: var(--bg-medium); color: var(--text-primary); }\n  .header-btn svg { width: 14px; height: 14px; }\n  \n  /* CHARTS - Professional Style */\n  .charts-section { background: var(--bg-light); padding: 16px 20px; }\n  .chart-card { background: var(--bg-white); border-radius: 10px; padding: 14px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); border: 1px solid var(--border); }\n  .chart-title { font-size: 10px; font-weight: 600; color: var(--text-muted); margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.4px; }\n  \n  /* COLLAPSE BUTTONS */\n  .table-controls { display: flex; gap: 8px; padding: 10px 16px; background: var(--bg-light); border-bottom: 1px solid var(--border); }\n  .control-btn { padding: 6px 12px; background: var(--bg-white); border: 1px solid var(--border); border-radius: 6px; font-size: 11px; font-weight: 500; color: var(--text-secondary); cursor: pointer; transition: all 0.2s; }\n  .control-btn:hover { background: var(--bg-medium); color: var(--text-primary); }\n  \n  /* ROW 1: KPI (3 cards) + COST STRUCTURE */\n  .row-1 { display: grid; grid-template-columns: auto 1fr; gap: 12px; margin-bottom: 12px; }\n  .kpi-row { display: flex; gap: 6px; }\n  .kpi-card { \n    background: var(--bg-white); \n    border-radius: 8px; \n    padding: 10px 14px; \n    display: flex; \n    align-items: center; \n    gap: 8px; \n    box-shadow: 0 1px 3px rgba(0,0,0,0.04);\n    border: 1px solid var(--border);\n    white-space: nowrap;\n  }\n  .kpi-icon { font-size: 16px; opacity: 0.5; }\n  .kpi-content { }\n  .kpi-value { font-size: 16px; font-weight: 600; letter-spacing: -0.3px; color: var(--text-primary); line-height: 1.1; }\n  .kpi-label { font-size: 8px; color: var(--text-muted); font-weight: 500; text-transform: uppercase; letter-spacing: 0.2px; }\n  \n  /* ROW 2: PHASE + TIMELINE */\n  .row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; }\n  \n  /* ROW 3: TREEMAP + QUALITY */\n  .row-3 { display: grid; grid-template-columns: 1fr 120px; gap: 12px; }\n  \n  /* BARS - Clean Style */\n  .h-bar { margin-bottom: 8px; }\n  .h-bar-label { display: flex; justify-content: space-between; font-size: 11px; margin-bottom: 3px; color: var(--text-primary); }\n  .h-bar-label span:last-child { font-weight: 600; }\n  .h-bar-track { background: var(--bg-medium); border-radius: 2px; height: 5px; overflow: hidden; display: flex; }\n  .h-bar-fill { height: 100%; }\n  \n  /* TREEMAP - Clickable Style */\n  .treemap { display: flex; gap: 6px; height: 60px; }\n  .treemap-item { \n    border-radius: 8px; \n    padding: 8px 10px; \n    background: var(--bg-light); \n    color: var(--text-primary); \n    display: flex; \n    flex-direction: column; \n    justify-content: space-between; \n    border: 1px solid var(--border); \n    min-width: 0;\n    cursor: pointer;\n    transition: all 0.15s;\n  }\n  .treemap-item:hover { \n    background: var(--bg-medium); \n    border-color: var(--text-secondary);\n    transform: translateY(-1px);\n  }\n  .treemap-name { font-size: 9px; color: var(--text-muted); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n  .treemap-value { font-size: 12px; font-weight: 600; color: var(--text-primary); }\n  .treemap-percent { font-size: 9px; color: var(--text-secondary); }\n  \n  /* GAUGE - Compact Style */\n  .quality-card { display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%; }\n  .gauge-value { font-size: 28px; font-weight: 600; color: var(--text-primary); letter-spacing: -1px; line-height: 1; }\n  .gauge-label { font-size: 10px; color: var(--text-muted); margin-top: 6px; }\n  \n  /* TIMELINE - Compact Style */\n  .timeline-row { display: flex; align-items: center; margin-bottom: 8px; }\n  .timeline-label { width: 100px; font-size: 11px; font-weight: 500; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n  .timeline-track { flex: 1; height: 18px; background: var(--bg-medium); border-radius: 4px; position: relative; overflow: hidden; }\n  .timeline-bar { position: absolute; height: 100%; border-radius: 4px; font-size: 10px; color: white; display: flex; align-items: center; justify-content: center; font-weight: 500; background: #1D1D1F; min-width: 4px; }\n  .timeline-days { position: absolute; font-size: 9px; font-weight: 600; color: var(--text-primary); white-space: nowrap; top: 50%; transform: translateY(-50%); }\n  \n  /* COLLAPSIBLE */\n  .toggle-icon { display: inline-block; width: 16px; transition: transform 0.2s; cursor: pointer; }\n  tr.hidden { display: none; }\n  .phase, .type { cursor: pointer; }\n  .phase:hover, .type:hover { opacity: 0.95; }\n\n  @media print {\n    body { padding: 0; background: white; }\n    .container { box-shadow: none; }\n    .info-icon { display: none; }\n  }\n</style>\n</head>\n<body>\n<div class=\"container\">\n\n<!-- HEADER - Logo on Right -->\n<div class=\"header-new\">\n  <div class=\"header-left\">\n    <div class=\"header-title-new\">${t.doc_title}</div>\n    <div class=\"header-project-new\">${escapeHtml(projectName)}</div>\n    <div class=\"header-info-new\">${t.pricing_level}: ${pricingLevel} | ${dateTimeStr}</div>\n  </div>\n  <div class=\"header-right\">\n    <a href=\"https://github.com/datadrivenconstruction\" target=\"_blank\" class=\"header-btn\">\n      <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.+123456789066 1.983-.399 3.+123456789038 3.+12345678907-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z\"/></svg>\n      GitHub\n    </a>\n    <a href=\"https://datadrivenconstruction.io/\" target=\"_blank\">\n      <img src=\"https://datadrivenconstruction.io/wp-content/uploads/2023/07/DataDrivenConstruction-1-1.png\" alt=\"DDC\" class=\"header-logo\">\n    </a>\n  </div>\n</div>\n\n<!-- CHARTS SECTION -->\n<div class=\"charts-section\">\n  <!-- ROW 1: KPI (3 cards) + COST STRUCTURE -->\n  <div class=\"row-1\">\n    <div class=\"kpi-row\">\n      <div class=\"kpi-card\">\n        <div class=\"kpi-icon\">💰</div>\n        <div class=\"kpi-content\">\n          <div class=\"kpi-value\">${formatCurrency(grandTotal)}</div>\n          <div class=\"kpi-label\">${t.kpi_total || 'Total'}</div>\n        </div>\n      </div>\n      <div class=\"kpi-card\">\n        <div class=\"kpi-icon\">⏱️</div>\n        <div class=\"kpi-content\">\n          <div class=\"kpi-value\">${formatNumber(grandLaborHours, 0)} h</div>\n          <div class=\"kpi-label\">${t.kpi_hours || 'Hours'}</div>\n        </div>\n      </div>\n      <div class=\"kpi-card\">\n        <div class=\"kpi-icon\">📅</div>\n        <div class=\"kpi-content\">\n          <div class=\"kpi-value\">${laborDays}</div>\n          <div class=\"kpi-label\">${t.kpi_days || 'Days'}</div>\n        </div>\n      </div>\n    </div>\n    <div class=\"chart-card\">\n      <div class=\"chart-title\">${t.chart_cost_structure || 'Cost Structure'}</div>\n      <div class=\"h-bar\" style=\"margin-bottom:10px;\">\n        <div class=\"h-bar-track\" style=\"height:8px;\">\n          <div class=\"h-bar-fill\" style=\"width:${laborPercent}%;background:#1D1D1F;\"></div>\n          <div class=\"h-bar-fill\" style=\"width:${materialPercent}%;background:#86868B;\"></div>\n          <div class=\"h-bar-fill\" style=\"width:${machinesPercent}%;background:#AEAEB2;\"></div>\n        </div>\n      </div>\n      <div style=\"display:flex;flex-direction:column;gap:5px;font-size:10px;\">\n        <div style=\"display:flex;justify-content:space-between;\"><span><span style=\"display:inline-block;width:8px;height:8px;background:#1D1D1F;border-radius:2px;margin-right:6px;\"></span>${t.chart_labor || 'Labor'}</span><span style=\"font-weight:600;\">${formatCurrency(grandResourceCost)} (${laborPercent}%)</span></div>\n        <div style=\"display:flex;justify-content:space-between;\"><span><span style=\"display:inline-block;width:8px;height:8px;background:#86868B;border-radius:2px;margin-right:6px;\"></span>${t.chart_material || 'Material'}</span><span style=\"font-weight:600;\">${formatCurrency(grandMaterialCost)} (${materialPercent}%)</span></div>\n        <div style=\"display:flex;justify-content:space-between;\"><span><span style=\"display:inline-block;width:8px;height:8px;background:#AEAEB2;border-radius:2px;margin-right:6px;\"></span>${t.chart_machines || 'Machines'}</span><span style=\"font-weight:600;\">${formatCurrency(machinesCost)} (${machinesPercent}%)</span></div>\n      </div>\n    </div>\n  </div>\n  \n  <!-- ROW 2: PHASE + TIMELINE -->\n  <div class=\"row-2\">\n    <div class=\"chart-card\">\n      <div class=\"chart-title\">${t.chart_by_phase || 'By Phase'}</div>\n      ${phaseChartData.map((p, i) => {\n        const pct = grandTotal > 0 ? Math.round(p.cost / grandTotal * 100) : 0;\n        return '<div class=\"h-bar\"><div class=\"h-bar-label\"><span>' + escapeHtml(p.name) + '</span><span>' + formatCurrency(p.cost) + '</span></div><div class=\"h-bar-track\"><div class=\"h-bar-fill\" style=\"width:' + pct + '%;background:#1D1D1F;\"></div></div></div>';\n      }).join('')}\n    </div>\n    <div class=\"chart-card\">\n      <div class=\"chart-title\">${t.chart_timeline || 'Timeline'}</div>\n      ${phaseChartData.map((p, i) => {\n        const totalDays = laborDays || 1;\n        const phaseDays = Math.ceil(p.hours / 8);\n        const startPct = i === 0 ? 0 : phaseChartData.slice(0, i).reduce((sum, pp) => sum + Math.ceil(pp.hours / 8), 0) / totalDays * 100;\n        const widthPct = Math.min(phaseDays / totalDays * 100, 100 - startPct);\n        const barWidth = Math.max(widthPct, 4);\n        const endPct = startPct + barWidth;\n        const showInside = widthPct > 15;\n        const spaceOnRight = 100 - endPct;\n        \n        if (showInside) {\n          return '<div class=\"timeline-row\"><div class=\"timeline-label\">' + escapeHtml(p.name) + '</div><div class=\"timeline-track\"><div class=\"timeline-bar\" style=\"left:' + startPct + '%;width:' + barWidth + '%;\">' + phaseDays + 'd</div></div></div>';\n        } else if (spaceOnRight > 10) {\n          // Show label on right\n          return '<div class=\"timeline-row\"><div class=\"timeline-label\">' + escapeHtml(p.name) + '</div><div class=\"timeline-track\"><div class=\"timeline-bar\" style=\"left:' + startPct + '%;width:' + barWidth + '%;\"></div><span class=\"timeline-days\" style=\"left:' + (endPct + 2) + '%;\">' + phaseDays + 'd</span></div></div>';\n        } else {\n          // Show label on left\n          return '<div class=\"timeline-row\"><div class=\"timeline-label\">' + escapeHtml(p.name) + '</div><div class=\"timeline-track\"><span class=\"timeline-days\" style=\"right:' + (100 - startPct + 2) + '%;\">' + phaseDays + 'd</span><div class=\"timeline-bar\" style=\"left:' + startPct + '%;width:' + barWidth + '%;\"></div></div></div>';\n        }\n      }).join('')}\n    </div>\n  </div>\n  \n  <!-- ROW 3: TREEMAP with navigation -->\n  <div class=\"row-3\" style=\"grid-template-columns: 1fr;\">\n    <div class=\"chart-card\">\n      <div class=\"chart-title\">${t.chart_hierarchy || 'Cost Hierarchy'} <span style=\"font-weight:400;opacity:0.6;font-size:8px;\">— ${t.click_to_navigate || 'click to navigate'}</span></div>\n      <div class=\"treemap\">\n        ${phaseChartData.map((p, i) => {\n          const pct = grandTotal > 0 ? Math.round(p.cost / grandTotal * 100) : 0;\n          return '<div class=\"treemap-item\" data-phase=\"' + (i+1) + '\" style=\"flex:' + (pct || 1) + ';\"><div class=\"treemap-name\">' + escapeHtml(p.name) + '</div><div class=\"treemap-value\">' + formatCurrency(p.cost) + '</div><div class=\"treemap-percent\">' + pct + '%</div></div>';\n        }).join('')}\n      </div>\n    </div>\n  </div>\n</div>\n\n<!-- TABLE CONTROLS -->\n<div class=\"table-controls\">\n  <button class=\"control-btn\" onclick=\"collapseAll()\">${t.collapse_all || '▲ Collapse All'}</button>\n  <button class=\"control-btn\" onclick=\"expandAll()\">${t.expand_all || '▼ Expand All'}</button>\n</div>\n\n<!-- QUALITY BAR -->\n<div class=\"quality-bar\">\n  <div class=\"quality-item\">\n    <span>${t.found_rates}<span class=\"info-icon\" title=\"${t.tip_found}\">i</span>:</span>\n    <span class=\"quality-percent\">${foundPercent}%</span>\n    <span>(${foundRates}/${totalWorks})</span>\n  </div>\n  <div class=\"quality-item\"><span class=\"quality-dot dot-high\"></span><span>${qualityHigh}</span></div>\n  <div class=\"quality-item\"><span class=\"quality-dot dot-medium\"></span><span>${qualityMedium}</span></div>\n  <div class=\"quality-item\"><span class=\"quality-dot dot-low\"></span><span>${qualityLow}</span></div>\n  <div class=\"quality-item\"><span class=\"quality-dot dot-notfound\"></span><span>${qualityNotFound}</span></div>\n  ${qualityNotFound > 0 ? `<div class=\"quality-item\" style=\"color:var(--warning)\">⚠ ${qualityNotFound} ${t.manual_check}</div>` : ''}\n</div>\n\n<table>\n<!-- COLUMNS -->\n<tr class=\"col-header\">\n  <td style=\"width:4%\">${t.col_pos}<span class=\"info-icon\" title=\"${t.tip_pos}\">i</span></td>\n  <td style=\"width:9%\">${t.col_code}<span class=\"info-icon\" title=\"${t.tip_code}\">i</span></td>\n  <td style=\"width:24%\">${t.col_description}<span class=\"info-icon\" title=\"${t.tip_desc}\">i</span></td>\n  <td style=\"width:16%\">${t.col_calc}<span class=\"info-icon\" title=\"${t.tip_calc}\">i</span></td>\n  <td style=\"width:6%\">${t.col_unit}<span class=\"info-icon\" title=\"${t.tip_unit}\">i</span></td>\n  <td style=\"width:7%\" class=\"right\">${t.col_qty}<span class=\"info-icon\" title=\"${t.tip_qty}\">i</span></td>\n  <td style=\"width:9%\" class=\"right\">${t.col_price}<span class=\"info-icon\" title=\"${t.tip_price}\">i</span></td>\n  <td style=\"width:9%\" class=\"right\">${t.col_total}<span class=\"info-icon\" title=\"${t.tip_total}\">i</span></td>\n  <td style=\"width:5%\" class=\"right\">${t.col_labor}<span class=\"info-icon\" title=\"${t.tip_labor}\">i</span></td>\n  <td style=\"width:4%\" class=\"center\">${t.col_quality}<span class=\"info-icon\" title=\"${t.tip_quality}\">i</span></td>\n</tr>`;\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// DATA ROWS - Filter empty phases\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst phasesWithTypes = phases.filter(phase => (phase.types || []).length > 0);\n\nphasesWithTypes.forEach((phase, phaseIdx) => {\n  const phaseNum = phaseIdx + 1;\n  const phasePercent = grandTotal > 0 ? ((phase.phase_total_cost || 0) / grandTotal * 100).toFixed(0) : '0';\n  \n  const phaseName = cleanPhaseName(phase);\n  const phaseCode = phase.phase_code || '';\n  const cleanPhaseCode = phaseCode === 'UNASSIGNED' ? '' : phaseCode;\n  \n  html += `\n<tr class=\"phase\" id=\"phase-${phaseNum}\" data-phase=\"${phaseNum}\">\n  <td>${phaseNum}</td>\n  <td>${escapeHtml(cleanPhaseCode)}</td>\n  <td colspan=\"2\"><span class=\"toggle-icon\">▼</span> ${escapeHtml(phaseName)}</td>\n  <td></td>\n  <td></td>\n  <td></td>\n  <td class=\"right num\">${formatCurrency(phase.phase_total_cost || 0)}</td>\n  <td class=\"right num\">${formatNumber(phase.phase_labor_hours || 0, 1)}</td>\n  <td class=\"center\">${phasePercent}%</td>\n</tr>`;\n\n  // Types - merge duplicates\n  const types = phase.types || [];\n  const typeMap = new Map();\n  types.forEach(type => {\n    const key = `${type.type_name || 'Unknown'}|||${type.category || ''}`;\n    if (!typeMap.has(key)) {\n      typeMap.set(key, { ...type, _merged: 1 });\n    } else {\n      const existing = typeMap.get(key);\n      existing.element_count = (existing.element_count || 0) + (type.element_count || 0);\n      existing.type_total_cost = (existing.type_total_cost || 0) + (type.type_total_cost || 0);\n      existing.type_total_labor_hours = (existing.type_total_labor_hours || 0) + (type.type_total_labor_hours || 0);\n      existing.works = [...(existing.works || []), ...(type.works || [])];\n    }\n  });\n  \n  const activeTypes = Array.from(typeMap.values());\n  \n  activeTypes.forEach((type, typeIdx) => {\n    const typeNum = typeIdx + 1;\n    const typeName = type.type_name || 'Unknown Type';\n    const elementCount = type.element_count || 1;\n    const category = cleanCategoryName(type.category);\n    \n    html += `\n<tr class=\"type\" data-phase=\"${phaseNum}\" data-type=\"${typeNum}\">\n  <td>${phaseNum}.${typeNum}</td>\n  <td></td>\n  <td><span class=\"toggle-icon\">▽</span> ${escapeHtml(typeName)} <span style=\"color:var(--text-muted)\">(×${elementCount})</span></td>\n  <td class=\"calc-cell\"><span class=\"calc-type\">${escapeHtml(category)}</span></td>\n  <td></td>\n  <td></td>\n  <td></td>\n  <td class=\"right num\">${formatCurrency(type.type_total_cost || 0)}</td>\n  <td class=\"right num\">${formatNumber(type.type_total_labor_hours || 0, 1)}</td>\n  <td></td>\n</tr>`;\n\n    // Works\n    const works = type.works || [];\n    works.forEach((work, workIdx) => {\n      const workNum = workIdx + 1;\n      const level = work.quality_level || 'not_found';\n      let dotClass = 'dot-notfound';\n      if (level === 'high') dotClass = 'dot-high';\n      else if (level === 'medium') dotClass = 'dot-medium';\n      else if (level === 'low') dotClass = 'dot-low';\n      \n      const rateCode = work.rate_code || '';\n      const codeLink = rateCode ? \n        `<a href=\"${RATES_LINK}\" class=\"link\" title=\"Alle Preise anzeigen\" target=\"_blank\">${escapeHtml(rateCode)}</a>` : \n        '—';\n      \n      // ═══════════════════════════════════════════════════════════════════════\n      // BUILD CALCULATION DISPLAY - Improved Formula Display v2.0\n      // ═══════════════════════════════════════════════════════════════════════\n      \n      const calcDetails = work.calculation_details || {};\n      const method = calcDetails.method || 'direct';\n      const usedParameters = calcDetails.used_parameters || {};\n      const formulaDisplay = calcDetails.formula_display || '';\n      const calculationDisplay = calcDetails.calculation_display || '';\n      const unitDivisor = calcDetails.unit_divisor || work.unit_divisor || 1;\n      const coefficient = calcDetails.coefficient || 1;\n      const rawValue = calcDetails.raw_value || 0;\n      const bimParam = calcDetails.bim_parameter || work.project_unit || '';\n      \n      let calcDisplay = '';\n      \n      // Type name (short)\n      const shortTypeName = typeName.length > 18 ? typeName.substring(0, 18) + '…' : typeName;\n      calcDisplay += `<span class=\"calc-type\">${escapeHtml(shortTypeName)}</span>`;\n      \n      // Build formula display based on method and parameters\n      let formulaHtml = '';\n      const paramCount = Object.keys(usedParameters).length;\n      const unitConversions = calcDetails.unit_conversions || [];\n      const hadConversions = unitConversions.length > 0;\n      \n      if (method === 'formula' && formulaDisplay && paramCount > 1) {\n        // ═══════════════════════════════════════════════════════════════════\n        // FORMULA with multiple parameters\n        // Line 1: Formula template \"(Width + Height) × 2\"\n        // Line 2: Values substituted \"(1.18 + 1.17) × 2 = 4.70\"\n        // Line 3: Unit conversion note if applied\n        // ═══════════════════════════════════════════════════════════════════\n        \n        formulaHtml += `<span class=\"calc-formula-line\"><span class=\"calc-param\">${escapeHtml(formulaDisplay)}</span></span>`;\n        \n        if (calculationDisplay) {\n          formulaHtml += `<span class=\"calc-formula-line\"><span class=\"calc-value\">${escapeHtml(calculationDisplay)}</span></span>`;\n        }\n        \n        // Show unit conversion note\n        if (hadConversions) {\n          const convLabel = unitConversions.map(c => c.label).join(', ');\n          formulaHtml += `<span class=\"calc-formula-line calc-conversion\">🔄 ${convLabel}</span>`;\n        }\n        \n        // Add divisor line if needed\n        if (unitDivisor > 1) {\n          const finalQty = work.calculated_quantity || work.quantity_in_rate_units || 0;\n          formulaHtml += `<span class=\"calc-formula-line\"><span class=\"calc-op\">÷</span> <span class=\"calc-value\">${unitDivisor}</span> <span class=\"calc-op\">=</span> <span class=\"calc-result\">${formatNumber(finalQty, 2)}</span></span>`;\n        }\n        \n      } else if (paramCount === 1 || (paramCount === 0 && rawValue > 0)) {\n        // ═══════════════════════════════════════════════════════════════════\n        // DIRECT: single parameter \"Area = 123.39\"\n        // ═══════════════════════════════════════════════════════════════════\n        \n        const paramName = paramCount > 0 ? Object.keys(usedParameters)[0] : bimParam;\n        const paramValue = paramCount > 0 ? Object.values(usedParameters)[0] : rawValue;\n        const paramLabel = formatParamName(paramName);\n        \n        let parts = [];\n        parts.push(`<span class=\"calc-param\">${paramLabel}</span>`);\n        parts.push(`<span class=\"calc-op\">=</span>`);\n        parts.push(`<span class=\"calc-value\">${formatNumber(paramValue, 2)}</span>`);\n        \n        // Show coefficient if not 1\n        if (coefficient && coefficient !== 1) {\n          parts.push(`<span class=\"calc-op\">×</span>`);\n          parts.push(`<span class=\"calc-value\">${coefficient}</span>`);\n        }\n        \n        // Show divisor if not 1\n        if (unitDivisor && unitDivisor !== 1) {\n          parts.push(`<span class=\"calc-op\">÷</span>`);\n          parts.push(`<span class=\"calc-value\">${unitDivisor}</span>`);\n        }\n        \n        formulaHtml = `<span class=\"calc-formula-line\">${parts.join(' ')}</span>`;\n        \n        // Show unit conversion note if applied\n        if (hadConversions) {\n          const convLabel = unitConversions.map(c => c.label).join(', ');\n          formulaHtml += `<span class=\"calc-formula-line calc-conversion\">🔄 ${convLabel}</span>`;\n        }\n        \n      } else if (paramCount > 1) {\n        // ═══════════════════════════════════════════════════════════════════\n        // Multiple parameters without formula - show all\n        // ═══════════════════════════════════════════════════════════════════\n        \n        const paramEntries = Object.entries(usedParameters);\n        let parts = [];\n        \n        paramEntries.forEach(([name, value], idx) => {\n          if (idx > 0) parts.push(`<span class=\"calc-op\">·</span>`);\n          parts.push(`<span class=\"calc-param\">${formatParamName(name)}</span>`);\n          parts.push(`<span class=\"calc-op\">=</span>`);\n          parts.push(`<span class=\"calc-value\">${formatNumber(value, 2)}</span>`);\n        });\n        \n        formulaHtml = `<span class=\"calc-formula-line\">${parts.join(' ')}</span>`;\n        \n      } else {\n        // ═══════════════════════════════════════════════════════════════════\n        // Fallback: just show final quantity\n        // ═══════════════════════════════════════════════════════════════════\n        \n        const qty = work.calculated_quantity || work.quantity_in_rate_units || 0;\n        if (qty > 0) {\n          formulaHtml = `<span class=\"calc-formula-line\"><span class=\"calc-param\">Qty</span> <span class=\"calc-op\">=</span> <span class=\"calc-value\">${formatNumber(qty, 4)}</span></span>`;\n        }\n      }\n      \n      if (formulaHtml) {\n        calcDisplay += `<span class=\"calc-formula\">${formulaHtml}</span>`;\n      }\n      \n      // Build original quantity display (from BIM)\n      const finalQty = work.calculated_quantity || work.quantity_in_rate_units || 0;\n      const originalQty = rawValue || calcDetails.original_quantity || 0;\n      const originalUnit = work.project_unit || bimParam || '';\n      const showOriginal = originalQty > 0 && unitDivisor > 1 && Math.abs(originalQty - finalQty) > 0.001;\n      \n      let qtyDisplay = formatNumber(finalQty, 4);\n      if (showOriginal) {\n        qtyDisplay += `<div class=\"qty-original\">(${formatNumber(originalQty, 2)} ${escapeHtml(originalUnit)})</div>`;\n      }\n      \n      html += `\n<tr class=\"work\" data-phase=\"${phaseNum}\" data-type=\"${typeNum}\">\n  <td style=\"padding-left:16px\">${phaseNum}.${typeNum}.${workNum}</td>\n  <td>${codeLink}</td>\n  <td style=\"padding-left:24px\">${escapeHtml(work.rate_name || work.work_name || 'Work')}${(work.work_name && work.rate_name && work.work_name !== work.rate_name) ? '<div style=\"font-size:8px;color:#64748B;margin-top:2px;font-style:italic;\">← ' + escapeHtml(work.work_name) + '</div>' : ''}</td>\n  <td class=\"calc-cell\">${calcDisplay}</td>\n  <td>${escapeHtml(work.rate_unit) || '—'}</td>\n  <td class=\"right num\">${qtyDisplay}</td>\n  <td class=\"right num\">${formatCurrency(work.unit_cost || 0)}</td>\n  <td class=\"right num\">${formatCurrency(work.total_cost || 0)}</td>\n  <td class=\"right num\">${formatNumber(work.estimated_labor_hours || 0, 1)}</td>\n  <td class=\"center\"><span class=\"quality-dot ${dotClass}\"></span></td>\n</tr>`;\n\n      // Resources\n      const resources = work.resources_all || [];\n      if (resources.length > 0) {\n        resources.forEach((res, resIdx) => {\n          const isLast = resIdx === resources.length - 1;\n          const prefix = isLast ? '└' : '├';\n          \n          // Determine resource type for tag\n          let resType = 'material'; // default\n          const resUnit = (res.resource_unit || '').toLowerCase();\n          const resName = (res.resource_name || '').toLowerCase();\n          const resCode = (res.resource_code || '').toLowerCase();\n          \n          // Check explicit type fields first\n          if (res.resource_type) {\n            const rt = res.resource_type.toLowerCase();\n            if (rt.includes('labor') || rt.includes('труд') || rt.includes('arbeit')) resType = 'labor';\n            else if (rt.includes('machine') || rt.includes('маш') || rt.includes('gerät')) resType = 'machine';\n            else resType = 'material';\n          } else if (res.resource_class) {\n            const rc = res.resource_class.toLowerCase();\n            if (rc === '1' || rc.includes('labor') || rc.includes('труд')) resType = 'labor';\n            else if (rc === '3' || rc.includes('machine') || rc.includes('маш')) resType = 'machine';\n            else resType = 'material';\n          } else {\n            // Detect by unit - CHECK MACHINES FIRST (before labor)\n            // Machine hours in different languages: Masch.-Std., маш-ч, mach-h, Maschinenstunde\n            if (resUnit.includes('маш') || resUnit.includes('masch') || resUnit.includes('mach') || \n                resUnit.includes('gerät') || resUnit.includes('machine')) {\n              resType = 'machine';\n            }\n            // Labor hours: ч-ч, чел-ч, man-h, Std. (but NOT Masch.-Std.)\n            else if (resUnit.includes('ч-ч') || resUnit.includes('чел') || resUnit.includes('man-') || \n                resUnit.includes('man.') || resUnit.includes('manhour') ||\n                (resUnit.includes('std') && !resUnit.includes('masch')) ||\n                (resUnit.includes('h') && !resUnit.includes('masch') && !resUnit.includes('mach')) ||\n                resUnit.includes('час') || resUnit.includes('arb') || resUnit.includes('person')) {\n              resType = 'labor';\n            }\n            \n            // Override by name keywords if still material\n            if (resType === 'material') {\n              // Check for machine names\n              if (resName.includes('машин') || resName.includes('machine') || resName.includes('maschine') ||\n                  resName.includes('кран') || resName.includes('экскаватор') || resName.includes('бульдозер') ||\n                  resName.includes('crane') || resName.includes('excavator') || resName.includes('loader') ||\n                  resName.includes('автомобиль') || resName.includes('насос') || resName.includes('бетонона') ||\n                  resName.includes('компрессор') || resName.includes('сварочн') || resName.includes('вибратор') ||\n                  resName.includes('bagger') || resName.includes('pumpe') || resName.includes('kran')) {\n                resType = 'machine';\n              }\n            }\n            \n            if (resType === 'material') {\n              // Check for labor names\n              if (resName.includes('рабочий') || resName.includes('worker') || resName.includes('arbeiter') ||\n                  resName.includes('труд') || resName.includes('labor') || resName.includes('arbeit') ||\n                  resName.includes('монтажник') || resName.includes('каменщик') || resName.includes('маляр') ||\n                  resName.includes('плотник') || resName.includes('слесарь') || resName.includes('сварщик') ||\n                  resName.includes('разряд') || resName.includes('средн') || resName.includes('затраты труда') ||\n                  resName.includes('бетонщик') || resName.includes('штукатур') || resName.includes('кровельщик') ||\n                  resName.includes('электрик') || resName.includes('сантехник') || resName.includes('facharbeiter') ||\n                  resName.includes('helfer') || resName.includes('monteur') || resName.includes('techniker')) {\n                resType = 'labor';\n              }\n            }\n          }\n          \n          // Tag labels by language\n          const tagLabels = {\n            labor: t.res_labor || 'Labor',\n            material: t.res_material || 'Mat.',\n            machine: t.res_machine || 'Mach.'\n          };\n          const tagLabel = tagLabels[resType];\n          const tagClass = 'res-tag res-tag-' + resType;\n          \n          html += `\n<tr class=\"resource\" data-phase=\"${phaseNum}\" data-type=\"${typeNum}\">\n  <td></td>\n  <td style=\"font-size:9px;\"><span style=\"opacity:0.5;\">${escapeHtml(res.resource_code) || ''}</span><span class=\"${tagClass}\">${tagLabel}</span></td>\n  <td style=\"padding-left:36px\">${prefix} ${escapeHtml(res.resource_name) || 'Resource'}</td>\n  <td></td>\n  <td>${escapeHtml(res.resource_unit) || ''}</td>\n  <td class=\"right num\">${formatNumber(res.scaled_quantity || res.resource_quantity || 0, 4)}</td>\n  <td class=\"right num\">${formatCurrency(res.price_per_unit || 0)}</td>\n  <td class=\"right num\">${formatCurrency(res.scaled_cost || res.resource_cost || 0)}</td>\n  <td></td>\n  <td></td>\n</tr>`;\n        });\n      }\n    });\n  });\n  \n  html += `\n<tr class=\"subtotal\" data-phase=\"${phaseNum}\">\n  <td></td>\n  <td></td>\n  <td colspan=\"2\">${t.subtotal} ${phaseNum}</td>\n  <td></td>\n  <td></td>\n  <td></td>\n  <td class=\"right num\">${formatCurrency(phase.phase_total_cost || 0)}</td>\n  <td class=\"right num\">${formatNumber(phase.phase_labor_hours || 0, 1)}</td>\n  <td class=\"center\">${phasePercent}%</td>\n</tr>`;\n});\n\n// GRAND TOTAL\n// laborPercent and laborDays already declared above\n\nhtml += `\n<tr class=\"grand-total\">\n  <td colspan=\"4\" style=\"padding-left:16px\">${t.grand_total}</td>\n  <td></td>\n  <td></td>\n  <td></td>\n  <td class=\"right num\">${formatCurrency(grandTotal)}</td>\n  <td class=\"right num\">${formatNumber(grandLaborHours, 1)}</td>\n  <td class=\"center\">100%</td>\n</tr>\n<tr class=\"grand-total-info\">\n  <td colspan=\"4\" style=\"padding-left:16px\">${t.labor_cost}: <span class=\"highlight\">${formatCurrency(grandResourceCost)}</span> (${laborPercent}%)</td>\n  <td colspan=\"3\">${t.material_cost}: <span class=\"highlight\">${formatCurrency(grandMaterialCost)}</span></td>\n  <td colspan=\"3\" class=\"right\" style=\"padding-right:16px\"><span class=\"highlight\">${laborDays}</span> ${t.labor_days}</td>\n</tr>\n</table>\n\n<!-- FOOTER -->\n<div class=\"footer\">\n  <div class=\"footer-main\">\n    <div class=\"footer-brand\">OPEN CONSTRUCTION ESTIMATE</div>\n    <div class=\"footer-info\">${t.footer_info}</div>\n    <div class=\"footer-links\">\n      <a href=\"${RATES_LINK}\" target=\"_blank\">openconstructionestimate.com</a>\n      <a href=\"https://github.com/datadrivenconstruction/OpenConstructionEstimate-DDC-CWICR\" target=\"_blank\">GitHub</a>\n      <a href=\"https://datadrivenconstruction.com\" target=\"_blank\">datadrivenconstruction.com</a>\n    </div>\n  </div>\n  <div class=\"footer-db\">DDC CWICR — Construction Work Items, Costs & Resources Database</div>\n</div>\n\n</div>\n\n<script>\n// Collapse All\nfunction collapseAll() {\n  document.querySelectorAll('tr.phase').forEach(function(row) {\n    row.classList.add('collapsed');\n    const icon = row.querySelector('.toggle-icon');\n    if (icon) icon.textContent = '▶';\n    const phaseId = row.dataset.phase;\n    document.querySelectorAll('tr[data-phase=\"' + phaseId + '\"]:not(.phase)').forEach(function(child) {\n      child.classList.add('hidden');\n    });\n  });\n}\n\n// Expand All\nfunction expandAll() {\n  document.querySelectorAll('tr.phase').forEach(function(row) {\n    row.classList.remove('collapsed');\n    const icon = row.querySelector('.toggle-icon');\n    if (icon) icon.textContent = '▼';\n    const phaseId = row.dataset.phase;\n    document.querySelectorAll('tr[data-phase=\"' + phaseId + '\"]:not(.phase)').forEach(function(child) {\n      child.classList.remove('hidden');\n    });\n  });\n  document.querySelectorAll('tr.type').forEach(function(row) {\n    row.classList.remove('collapsed');\n    const icon = row.querySelector('.toggle-icon');\n    if (icon) icon.textContent = '▽';\n  });\n}\n\n// Collapsible rows + phase tags\ndocument.addEventListener('DOMContentLoaded', function() {\n  // Phase row click\n  document.querySelectorAll('tr.phase').forEach(function(row) {\n    row.style.cursor = 'pointer';\n    row.addEventListener('click', function(e) {\n      if (e.target.tagName === 'A') return;\n      const phaseId = this.dataset.phase;\n      this.classList.toggle('collapsed');\n      const icon = this.querySelector('.toggle-icon');\n      if (icon) icon.textContent = this.classList.contains('collapsed') ? '▶' : '▼';\n      document.querySelectorAll('tr[data-phase=\"' + phaseId + '\"]:not(.phase)').forEach(function(child) {\n        if (row.classList.contains('collapsed')) {\n          child.classList.add('hidden');\n        } else {\n          child.classList.remove('hidden');\n        }\n      });\n    });\n  });\n  \n  // Type row click\n  document.querySelectorAll('tr.type').forEach(function(row) {\n    row.style.cursor = 'pointer';\n    row.addEventListener('click', function(e) {\n      e.stopPropagation();\n      if (e.target.tagName === 'A') return;\n      const phaseId = this.dataset.phase;\n      const typeId = this.dataset.type;\n      this.classList.toggle('collapsed');\n      const icon = this.querySelector('.toggle-icon');\n      if (icon) icon.textContent = this.classList.contains('collapsed') ? '▷' : '▽';\n      document.querySelectorAll('tr[data-phase=\"' + phaseId + '\"][data-type=\"' + typeId + '\"]:not(.type)').forEach(function(child) {\n        if (row.classList.contains('collapsed')) {\n          child.classList.add('hidden');\n        } else {\n          child.classList.remove('hidden');\n        }\n      });\n    });\n  });\n  \n  // Treemap item click - scroll to phase\n  document.querySelectorAll('.treemap-item').forEach(function(item) {\n    item.addEventListener('click', function(e) {\n      const phaseId = this.dataset.phase;\n      const targetRow = document.getElementById('phase-' + phaseId);\n      if (targetRow) {\n        // Expand if collapsed\n        if (targetRow.classList.contains('collapsed')) {\n          targetRow.click();\n        }\n        // Scroll into view\n        targetRow.scrollIntoView({ behavior: 'smooth', block: 'start' });\n        // Highlight\n        targetRow.style.background = '#FFFBEB';\n        setTimeout(function() { targetRow.style.background = ''; }, 1500);\n      }\n    });\n  });\n});\n</script>\n</body>\n</html>`;\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// GENERATE FILENAMES\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst safeProjectName = projectName.replace(/[^a-zA-Z0-9_-]/g, '_').substring(0, 50);\nconst timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 16);\nconst baseFilename = `${safeProjectName}_${timestamp}`;\nconst htmlFilename = `${baseFilename}.html`;\nconst xlsFilename = `${baseFilename}.xls`;\n\nconsole.log(`\\n${'═'.repeat(60)}`);\nconsole.log(`STAGE 9: HTML + XLS Generated v14 (with unit conversion display)`);\nconsole.log(`${'═'.repeat(60)}`);\nconsole.log(`  Project: ${projectName}`);\nconsole.log(`  Language: ${langCode}`);\nconsole.log(`  Phases: ${phasesWithTypes.length} (filtered from ${phases.length})`);\nconsole.log(`  HTML: ${htmlFilename}`);\nconsole.log(`  Quality: ${foundRates}/${totalWorks} (${foundPercent}%)`);\nconsole.log(`  Grand Total: ${formatCurrency(grandTotal)}`);\nconsole.log(`${'═'.repeat(60)}\\n`);\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// RETURN BOTH HTML AND XLS\n// ═══════════════════════════════════════════════════════════════════════════════\n\nreturn [{\n  json: {\n    html_filename: htmlFilename,\n    xls_filename: xlsFilename,\n    project_name: projectName,\n    pricing_level: pricingLevel,\n    language: langCode,\n    total_cost: grandTotal,\n    total_works: totalWorks,\n    quality_percent: foundPercent,\n    project_path: projectPath,\n    generated_at: dateTimeStr\n  },\n  binary: {\n    html: {\n      data: Buffer.from(html, 'utf-8').toString('base64'),\n      mimeType: 'text/html',\n      fileName: htmlFilename\n    },\n    xls: {\n      data: Buffer.from(html, 'utf-8').toString('base64'),\n      mimeType: 'application/vnd.ms-excel',\n      fileName: xlsFilename\n    }\n  }\n}];"},"typeVersion":2},{"id":"d65685c1-3e6d-47d0-bb56-6a7eee3db738","name":"STAGE 7.5 - Parse Validation","type":"n8n-nodes-base.code","position":[2000,2784],"parameters":{"jsCode":"// STAGE 7.5 - Parse Validation\nconst aiResponse = $input.first().json;\nconst aggregatedData = $('Aggregate Type Works').first().json;\n\nlet validation = { validation_status: 'unknown', completeness_score: 0, issues: [], recommended_additions: [], overall_assessment: '' };\n\ntry {\n  let content = '';\n  if (typeof aiResponse.message?.content === 'string') content = aiResponse.message.content;\n  else if (typeof aiResponse.content === 'string') content = aiResponse.content;\n  else if (aiResponse.validation_status) validation = aiResponse;\n  \n  if (content) {\n    content = content.replace(/```json\\s*/gi, '').replace(/```\\s*/gi, '').trim();\n    const jsonMatch = content.match(/\\{[\\s\\S]*\\}/);\n    if (jsonMatch) validation = JSON.parse(jsonMatch[0]);\n  }\n} catch (e) {\n  console.error('Parse validation error:', e.message);\n}\n\nconst completenessScore = (validation.completeness_score || 0) * 100;\nlet finalStatus = completenessScore >= 85 ? 'excellent' : completenessScore >= 70 ? 'good' : completenessScore >= 50 ? 'acceptable' : 'needs_improvement';\n\nconsole.log(`STAGE 7.5: ${aggregatedData.type_name} - ${finalStatus} (${completenessScore.toFixed(0)}%)`);\n\nreturn {\n  json: {\n    ...aggregatedData,\n    _typeKey: aggregatedData._typeKey,\n    validation: {\n      status: validation.validation_status,\n      completeness_score: validation.completeness_score,\n      issues: validation.issues || [],\n      recommended_additions: validation.recommended_additions || [],\n      assessment: validation.overall_assessment\n    },\n    quality_summary: {\n      completeness_percent: completenessScore.toFixed(0) + '%',\n      final_status: finalStatus,\n      issues_count: (validation.issues || []).length,\n      additions_needed: (validation.recommended_additions || []).length\n    },\n    is_validated: true,\n    stage: 'STAGE_7_5_VALIDATED'\n  }\n};"},"typeVersion":2},{"id":"655c7134-7658-47ac-b251-6a47f342715f","name":"STAGE 5.1 - Prepare Search Strategies1","type":"n8n-nodes-base.code","position":[1376,2336],"parameters":{"jsCode":"// STAGE 5.1 - OPTIMIZED v12.9 - Minimize data transfer\nconst item = $input.first().json;\nconst staticData = $getWorkflowStaticData('global');\n\n// Store full data in cache (retrieved in STAGE 5.2)\nif (!staticData.work_items_cache) staticData.work_items_cache = {};\nconst workKey = `${item._typeKey}|||${item.work_id || item._work_index}`;\n\nstaticData.work_items_cache[workKey] = {\n  work_id: item.work_id, work_name: item.work_name, work_sequence: item.work_sequence,\n  search_query: item.search_query, search_query_original: item.search_query_original,\n  _work_index: item._work_index, _total_works_in_type: item._total_works_in_type,\n  type_name: item.type_name, type_index: item.type_index, total_types: item.total_types,\n  category: item.category, assigned_phase: item.assigned_phase, _typeKey: item._typeKey,\n  quantities: item.quantities || {}, element_count: item.element_count || 1,\n  element_analysis: item.element_analysis || {}, quantity_source: item.quantity_source || {},\n  expected_unit: item.expected_unit || 'm²', qdrant_collection: item.qdrant_collection,\n  language: item.language, currency: item.currency || 'EUR', locale: item.locale || 'de-DE',\n  system_prompt_lang: item.system_prompt_lang\n};\n\nstaticData.current_type_data = {\n  type_name: item.type_name, type_index: item.type_index, total_types: item.total_types,\n  category: item.category, assigned_phase: item.assigned_phase, element_count: item.element_count,\n  currency: item.currency || 'EUR', locale: item.locale || 'de-DE'\n};\n\nfunction extractKeyTerms(text) {\n  if (!text) return '';\n  return text.replace(/\\b(und|oder|von|für|mit|bei|der|die|das|ein|eine|zu|zur|zum|auf|in|im|an|am|aus|nach|durch|über|unter|работы|устройство|монтаж|installation|works|device)\\b/gi, ' ').replace(/\\s+/g, ' ').trim();\n}\n\nconst workName = item.work_name || '';\nconst searchQuery = item.search_query || workName;\nconst expectedUnit = item.expected_unit || 'm²';\nconst category = item.category || '';\nconst keyTerms = extractKeyTerms(workName);\n\nconst catTrans = {'OST_Windows':'Fenster','OST_Doors':'Türen','OST_Walls':'Wände','OST_Floors':'Böden','OST_Roofs':'Dächer','OST_Stairs':'Treppen','OST_StructuralFoundation':'Fundamente','OST_Ceilings':'Decken'};\nconst categoryLocal = catTrans[category] || category.replace('OST_', '');\n\nconsole.log(`STAGE 5.1: ${workName} [${workKey}]`);\n\nconst strategies = [\n  { strategy_name: 'exact', query: searchQuery, k: 5, priority: 1 },\n  { strategy_name: 'work_name_unit', query: `${workName} ${expectedUnit}`, k: 5, priority: 2 },\n  { strategy_name: 'key_terms', query: keyTerms, k: 7, priority: 3 },\n  { strategy_name: 'category_focused', query: `${keyTerms} ${categoryLocal}`.trim(), k: 7, priority: 4 }\n];\n\nconst unique = [], seen = new Set();\nfor (const s of strategies) { const q = s.query.toLowerCase().trim(); if (q && !seen.has(q)) { seen.add(q); unique.push(s); } }\n\nreturn { json: {\n  search_query: searchQuery, work_name: workName, expected_unit: expectedUnit,\n  search_strategies: unique,\n  search_config: { work_name: workName, search_query: searchQuery, expected_unit: expectedUnit, category_local: categoryLocal, key_terms: keyTerms },\n  _workKey: workKey, _typeKey: item._typeKey, type_name: item.type_name, category: item.category,\n  qdrant_collection: item.qdrant_collection, stage: 'STAGE_5_1_OPTIMIZED'\n}};"},"typeVersion":2},{"id":"264f273d-cb23-4734-a5d5-944d3f234d15","name":"STAGE 5.2 - Parse Results","type":"n8n-nodes-base.code","position":[1952,2336],"parameters":{"jsCode":"\n// ═══════════════════════════════════════════════════════════════════════════════\n// MACHINE/LABOR HOURS DETECTION - 9 LANGUAGES\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst MACHINE_PATTERNS_9LANG = [\n  // DE\n  'masch', 'maschinenstunde', 'masch.-std', 'gerät',\n  // EN  \n  'machine', 'mach-h', 'equipment',\n  // FR (avoiding apostrophes)\n  'heure machine', 'engin', 'matériel',\n  // ES\n  'máquina', 'hora máquina', 'equipo',\n  // PT\n  'equipamento',\n  // RU\n  'машин', 'маш-ч', 'маш.ч', 'механизм',\n  // ZH\n  '机器', '机械', '设备', '台班',\n  // AR\n  'آلة', 'معدة', 'معدات',\n  // HI\n  'मशीन', 'यंत्र', 'उपकरण'\n];\n\nconst LABOR_PATTERNS_9LANG = [\n  // DE\n  'std', 'stunde', 'arbeitsstunde', 'arbeiter', 'facharbeiter',\n  // EN\n  'hour', 'man-hour', 'manhour', 'labor', 'worker',\n  // FR\n  'heure', 'ouvrier', 'travailleur',\n  // ES  \n  'hora', 'obrero', 'trabajador',\n  // PT\n  'operário',\n  // RU\n  'ч-ч', 'чел-ч', 'человеко-час', 'труд', 'рабочий', 'разряд',\n  // ZH\n  '工时', '人工', '工人', '劳动',\n  // AR\n  'ساعة', 'عامل', 'عمالة',\n  // HI\n  'घंटा', 'श्रम', 'मजदूर'\n];\n\nfunction isMachineResource(unit, name, code) {\n  const combined = ((unit || '') + ' ' + (name || '') + ' ' + (code || '')).toLowerCase();\n  return MACHINE_PATTERNS_9LANG.some(p => combined.includes(p.toLowerCase()));\n}\n\nfunction isLaborResource(unit, name, code) {\n  if (isMachineResource(unit, name, code)) return false;\n  const combined = ((unit || '') + ' ' + (name || '') + ' ' + (code || '')).toLowerCase();\n  return LABOR_PATTERNS_9LANG.some(p => combined.includes(p.toLowerCase()));\n}\n\n\n// ═══════════════════════════════════════════════════════════════════════════════\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// v5.6 - SIMPLIFIED SCORING\n// Category-based hardcoded rules REMOVED\n// Quality is now based on: vector_score + unit_match + name_similarity\n// AI validation happens in STAGE 5.3 (if enabled)\n// ═══════════════════════════════════════════════════════════════════════════════\n\n// STAGE 5.2 - Parse Vector Search Results v9.0\n// IMPROVED: Smart quality scoring based on unit, material, category matching\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst searchResults = $input.all();\n\n// Get work data from loop\nlet workData = {};\nconst staticData = $getWorkflowStaticData('global');\n\ntry {\n  const inputData = $('STAGE 5.1 - Prepare Search Strategies1').first().json;\n  const workKey = inputData._workKey;\n  \n  if (workKey && staticData.work_items_cache?.[workKey]) {\n    workData = { ...staticData.work_items_cache[workKey], ...inputData };\n    console.log(`✓ Restored: ${workKey}`);\n  } else {\n    workData = $('Loop Work Items').first().json;\n  }\n} catch (e) {\n  try { workData = $('Loop Work Items').first().json; } catch (e2) { workData = {}; }\n}\n\nconsole.log(`\\n${'═'.repeat(60)}`);\nconsole.log(`STAGE 5.2: Parsing ${searchResults.length} search results`);\nconsole.log(`Work: ${workData.work_name || 'N/A'}`);\nconsole.log(`${'═'.repeat(60)}`);\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// NOT FOUND FALLBACK\n// ═══════════════════════════════════════════════════════════════════════════════\n\nif (searchResults.length === 0) {\n  console.log('❌ No search results!');\n  return [{\n    json: {\n      ...workData,\n      _typeKey: workData._typeKey,\n      rateInfo: {\n        rate_code: 'NOT_FOUND',\n        rate_name: `[Not Found] ${workData.work_name || 'Unknown'}`,\n        rate_unit: workData.expected_unit || 'm²',\n        total_cost_position: 0,\n        worker_labor_hours: 0,\n        total_labor_hours: 0,\n        hierarchy: {},\n        cost_breakdown: { workers_cost: 0, machines_cost: 0, materials_cost: 0 },\n        search_confidence: 0,\n        quality_level: 'not_found',\n        quality_score: 0,\n        quality_factors: [],\n        quality_reason: 'No search results returned'\n      },\n      resources: { workers: [], machines: [], machinists: [], materials: [], electricity: [] },\n      stage: 'STAGE_5_NOT_FOUND'\n    }\n  }];\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// HELPER: Get content as string safely\n// ═══════════════════════════════════════════════════════════════════════════════\n\nfunction getContentAsString(data) {\n  let content = data.document || data.pageContent || data.content || '';\n  \n  if (typeof content === 'object' && content !== null) {\n    if (content.text) return String(content.text);\n    if (content.content) return String(content.content);\n    if (content.pageContent) return String(content.pageContent);\n    try {\n      return JSON.stringify(content);\n    } catch (e) {\n      return '';\n    }\n  }\n  \n  if (typeof content === 'string') {\n    return content;\n  }\n  \n  return String(content || '');\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// HELPER: Parse content text (DE/EN/RU formats)\n// ═══════════════════════════════════════════════════════════════════════════════\n\nfunction parseContentText(content) {\n  if (!content || typeof content !== 'string') {\n    return { resources: [] };\n  }\n  \n  const result = {\n    rate_code: '',\n    rate_name: '',\n    rate_unit: '',\n    total_cost: 0,\n    resources_cost: 0,\n    materials_cost: 0,\n    worker_labor_hours: 0,\n    resources: []\n  };\n  \n  // Extract basic data (multi-language support)\n  const codeMatch = content.match(/(?:CODE|КОД):\\s*([A-Z0-9_-]+)/i);\n  if (codeMatch) result.rate_code = codeMatch[1];\n  \n  const nameMatch = content.match(/(?:NAME|BEZEICHNUNG|НАИМЕНОВАНИЕ):\\s*([^\\n]+?)(?=\\s*(?:UNIT|EINHEIT|ЕД\\.ИЗМ|KATEGORIE|CATEGORY|$))/i);\n  if (nameMatch) result.rate_name = nameMatch[1].trim();\n  \n  const unitMatch = content.match(/(?:UNIT|EINHEIT|ЕД\\.?\\s*ИЗМ):\\s*([^\\n]+?)(?=\\s*(?:CATEGORY|KATEGORIE|КАТЕГОРИЯ|$))/i);\n  if (unitMatch) result.rate_unit = unitMatch[1].trim();\n  \n  // Extract resources\n  const resourcesSectionMatch = content.match(/(?:RESOURCES|RESSOURCEN|РЕСУРСЫ):\\s*([\\s\\S]*?)(?:MACHINES|MASCHINEN|МАШИНЫ|$)/i);\n  \n  if (resourcesSectionMatch) {\n    const resourcesText = resourcesSectionMatch[1];\n    const lines = resourcesText.split(/\\n/);\n    \n    for (const line of lines) {\n      const resourceMatch = line.match(/^\\s*-\\s*([A-Z0-9_-]+)\\s*—\\s*(.+?)\\s*—\\s*([\\d.,]+)\\s*([^(\\n]+?)(?:\\s*\\(Preis=([\\d.,]+),?\\s*Kosten=([\\d.,]+)\\))?\\s*$/i);\n      \n      if (resourceMatch) {\n        const code = resourceMatch[1].trim();\n        const name = resourceMatch[2].trim();\n        const quantity = parseFloat(resourceMatch[3].replace(',', '.')) || 0;\n        const unit = resourceMatch[4].trim();\n        const pricePerUnit = parseFloat((resourceMatch[5] || '0').replace(',', '.')) || 0;\n        const cost = parseFloat((resourceMatch[6] || '0').replace(',', '.')) || 0;\n        \n        // Categorize resource\n        let category = 'material';\n        const unitLower = unit.toLowerCase();\n        \n        if (code.startsWith('ME_') || code.startsWith('PU_')) {\n          category = 'worker';\n        } else if (code.startsWith('DXME')) {\n          category = 'machine';\n        } else if (unitLower.includes('std') || unitLower.includes('hrs') || \n                   unitLower.includes('ч.ч') || unitLower.includes('чел') ||\n                   unitLower.includes('masch.-std')) {\n          if (unitLower.includes('masch') || code.startsWith('DXME')) {\n            category = 'machine';\n          } else {\n            category = 'worker';\n          }\n        }\n        \n        result.resources.push({\n          resource_code: code,\n          resource_name: name,\n          resource_quantity: quantity,\n          resource_unit: unit,\n          price_per_unit: pricePerUnit,\n          resource_cost: cost,\n          category: category\n        });\n        \n        if (category === 'worker') {\n          result.worker_labor_hours += quantity;\n          result.resources_cost += cost;\n        } else if (category === 'machine') {\n          result.resources_cost += cost;\n        } else {\n          result.materials_cost += cost;\n        }\n      }\n    }\n  }\n  \n  result.total_cost = result.resources_cost + result.materials_cost;\n  \n  if (result.total_cost === 0) {\n    const totalMatch = content.match(/(?:Total\\s*cost|Gesamtkosten|ИТОГО):\\s*([\\d.,]+)/i);\n    if (totalMatch) {\n      result.total_cost = parseFloat(totalMatch[1].replace(',', '.')) || 0;\n    }\n  }\n  \n  return result;\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// HELPER: Categorize resources\n// ═══════════════════════════════════════════════════════════════════════════════\n\nfunction categorizeResources(resources) {\n  const workers = [];\n  const machines = [];\n  const machinists = [];\n  const materials = [];\n  const electricity = [];\n  \n  resources.forEach(r => {\n    const cat = r.category || 'material';\n    switch (cat) {\n      case 'worker': workers.push(r); break;\n      case 'machine': machines.push(r); break;\n      case 'machinist': machinists.push(r); break;\n      case 'electricity': electricity.push(r); break;\n      default: materials.push(r); break;\n    }\n  });\n  \n  return { workers, machines, machinists, materials, electricity };\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// QUALITY SCORING FUNCTIONS\n// ═══════════════════════════════════════════════════════════════════════════════\n\n/**\n * Normalize unit for comparison\n * Groups equivalent units together\n */\nfunction normalizeUnit(unit) {\n  if (!unit) return '';\n  const u = unit.toLowerCase().trim();\n  \n  const unitGroups = {\n    'm2': ['m²', 'm2', 'qm', 'квм', 'кв.м', 'кв. м', 'sq.m', 'sqm'],\n    'm3': ['m³', 'm3', 'cbm', 'куб.м', 'куб. м', 'cu.m', 'cum'],\n    'm': ['m', 'м', 'lm', 'lfm', 'п.м', 'пог.м', 'lin.m'],\n    'stk': ['stk', 'stück', 'st', 'st.', 'шт', 'шт.', 'pcs', 'ea', 'each', 'unit'],\n    't': ['t', 'т', 'тонна', 'ton', 'tonne', 'tons'],\n    'kg': ['kg', 'кг', 'kilogram', 'kilograms'],\n    'std': ['std', 'std.', 'h', 'hr', 'hrs', 'час', 'ч.ч', 'hours', 'hour'],\n    '100m2': ['100 m²', '100m²', '100 m2', '100m2', '100 qm'],\n    '100m': ['100 m', '100m', '100 п.м'],\n    '100stk': ['100 stk', '100stk', '100 st', '100 шт']\n  };\n  \n  for (const [normalized, variants] of Object.entries(unitGroups)) {\n    if (variants.some(v => u === v || u.includes(v))) {\n      return normalized;\n    }\n  }\n  return u;\n}\n\n/**\n * Check if rate material matches element materials\n */\nfunction checkMaterialMatch(rateName, rateHierarchy, elementMaterials) {\n  if (!elementMaterials || elementMaterials.length === 0) return { match: false, score: 0 };\n  \n  const searchText = ((rateName || '') + ' ' + (rateHierarchy || '')).toLowerCase();\n  \n  const materialKeywords = {\n    'aluminum': ['aluminium', 'aluminum', 'alu-', 'alu ', 'алюмин'],\n    'wood': ['holz', 'timber', 'wood', 'wooden', 'дерев', 'древес', 'lumber'],\n    'plastic': ['kunststoff', 'pvc', 'plastic', 'пластик', 'пвх', 'vinyl'],\n    'steel': ['stahl', 'steel', 'metal', 'металл', 'сталь', 'iron'],\n    'concrete': ['beton', 'concrete', 'бетон', 'cement', 'цемент'],\n    'glass': ['glas', 'glass', 'стекл', 'glazing', 'остекл'],\n    'brick': ['ziegel', 'brick', 'кирпич', 'masonry', 'клинкер'],\n    'stone': ['stein', 'stone', 'камен', 'granite', 'marble'],\n    'insulation': ['dämmung', 'insulation', 'изоляц', 'утепл', 'mineral', 'wool'],\n    'gypsum': ['gips', 'gypsum', 'гипс', 'drywall', 'plasterboard'],\n    'ceramic': ['keramik', 'ceramic', 'керамик', 'tile', 'плитк'],\n    'copper': ['kupfer', 'copper', 'медь', 'медн'],\n    'zinc': ['zink', 'zinc', 'цинк'],\n    'bitumen': ['bitumen', 'битум', 'asphalt', 'tar']\n  };\n  \n  let matchCount = 0;\n  let matchedMaterials = [];\n  \n  for (const mat of elementMaterials) {\n    const matLower = (mat || '').toLowerCase();\n    \n    for (const [materialType, keywords] of Object.entries(materialKeywords)) {\n      const matHasKeyword = keywords.some(kw => matLower.includes(kw));\n      const rateHasKeyword = keywords.some(kw => searchText.includes(kw));\n      \n      if (matHasKeyword && rateHasKeyword) {\n        matchCount++;\n        matchedMaterials.push(materialType);\n        break;\n      }\n    }\n  }\n  \n  if (matchCount > 0) {\n    return { \n      match: true, \n      score: Math.min(15, matchCount * 8),\n      materials: matchedMaterials\n    };\n  }\n  \n  return { match: false, score: 0, materials: [] };\n}\n\n/**\n * Check if rate is relevant to element category\n */\n// REMOVED: function checkCategoryRelevance(rateName, rateHierarchy, elementCategory) {\n//   if (!elementCategory) return { relevant: true, score: 10 };\n//   \n//   const catLower = elementCategory.replace(/^ost_/i, '').toLowerCase();\n//   const searchText = ((rateName || '') + ' ' + (rateHierarchy || '')).toLowerCase();\n//   \n//   const categoryKeywords = {\n//     'windows': ['fenster', 'window', 'verglasung', 'glazing', 'окн', 'остекл', 'frame'],\n//     'doors': ['tür', 'door', 'двер', 'portal', 'entrance'],\n//     'walls': ['wand', 'wall', 'mauer', 'стен', 'перегород', 'partition', 'facade'],\n//     'structuralfoundation': ['fundament', 'foundation', 'gründung', 'фундамент', 'основан', 'footing', 'slab'],\n//     'floors': ['boden', 'floor', 'decke', 'пол', 'перекрыт', 'slab', 'screed'],\n//     'roofs': ['dach', 'roof', 'кровл', 'крыш', 'roofing', 'shingle'],\n//     'ceilings': ['decke', 'ceiling', 'потолок', 'suspended'],\n//     'columns': ['stütze', 'column', 'pillar', 'колонн', 'столб'],\n//     'beams': ['träger', 'beam', 'балк', 'girder', 'lintel'],\n//     'stairs': ['treppe', 'stair', 'лестниц', 'step', 'railing'],\n//     'curtainwalls': ['fassade', 'curtain', 'витраж', 'facade', 'glazed']\n//   };\n//   \n//   const keywords = categoryKeywords[catLower] || [];\n//   \n//   if (keywords.length === 0) {\n//     return { relevant: true, score: 5 };\n//   }\n//   \n//   const isRelevant = keywords.some(kw => searchText.includes(kw));\n//   \n//   return { \n//     relevant: isRelevant, \n//     score: isRelevant ? 10 : 0 \n//   };\n// }\n\n/**\n * Check work type relevance (installation, demolition, finishing, etc.)\n */\nfunction checkWorkTypeMatch(searchQuery, rateName) {\n  if (!searchQuery || !rateName) return { match: false, score: 0 };\n  \n  const queryLower = searchQuery.toLowerCase();\n  const rateLower = rateName.toLowerCase();\n  \n  const workTypeKeywords = {\n    'installation': ['montage', 'installation', 'einbau', 'install', 'монтаж', 'установк'],\n    'demolition': ['abbruch', 'demolition', 'rückbau', 'demontage', 'демонтаж', 'снос'],\n    'preparation': ['vorbereitung', 'preparation', 'подготов', 'prep'],\n    'sealing': ['abdichtung', 'sealing', 'dichtung', 'герметиз', 'уплотн'],\n    'insulation': ['dämmung', 'insulation', 'isolierung', 'изоляц', 'утепл'],\n    'finishing': ['verputz', 'finish', 'anstrich', 'отделк', 'покраск'],\n    'excavation': ['aushub', 'excavation', 'erdarbeit', 'выемк', 'копк'],\n    'concrete': ['betonier', 'concrete', 'бетонир'],\n    'reinforcement': ['bewehrung', 'reinforcement', 'armierung', 'армир']\n  };\n  \n  for (const [workType, keywords] of Object.entries(workTypeKeywords)) {\n    const queryHas = keywords.some(kw => queryLower.includes(kw));\n    const rateHas = keywords.some(kw => rateLower.includes(kw));\n    \n    if (queryHas && rateHas) {\n      return { match: true, score: 10, workType };\n    }\n  }\n  \n  return { match: false, score: 0 };\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// UNIVERSAL Domain Mismatch Detection v5.0\n// Multilingual patterns, no hardcoded work types\n// ═══════════════════════════════════════════════════════════════════════════════\n\n// REMOVED: function checkCategoryMismatch(rateName, rateHierarchy, workName, elementCategory) {\n//   const text = ((rateName || '') + ' ' + (rateHierarchy || '')).toLowerCase();\n//   const catLower = (elementCategory || '').toLowerCase().replace('ost_', '');\n//   \n//   // Determine category group (multilingual keywords)\n//   const groups = {\n//     structural: ['floor', 'wall', 'roof', 'foundation', 'stair', 'column', 'beam', 'slab', 'ceiling',\n//                  'decke', 'wand', 'dach', 'fundament', 'treppe', 'stütze', 'balken',\n//                  'перекрыт', 'стен', 'крыш', 'фундамент', 'лестниц', 'колонн', 'балк', 'плита'],\n//     openings: ['window', 'door', 'curtain', 'fenster', 'tür', 'окн', 'двер'],\n//     site: ['plant', 'tree', 'topograph', 'site', 'parking', 'pflanz', 'baum', 'gelände', 'растен', 'дерев'],\n//     mep: ['plumb', 'mechan', 'electr', 'pipe', 'duct', 'санитар', 'отоплен', 'вентиляц']\n//   };\n//   \n//   let catGroup = 'other';\n//   for (const [group, keywords] of Object.entries(groups)) {\n//     if (keywords.some(kw => catLower.includes(kw))) { catGroup = group; break; }\n//   }\n//   \n//   // Domain patterns (multilingual)\n//   const domains = {\n//     electrical: [\n//       /\\b(volt|kv|amp|watt|hz)\\b/i,\n//       /\\b(schalt|switch|panel|transform|trafo)\\b/i,\n//       /\\b(elektr|electric|электр|élect)\\b/i,\n//       /\\b(hochspannung|high.*voltage|высок.*напряж)\\b/i,\n//       /\\b(generator|генератор)\\b/i\n//     ],\n//     industrial: [\n//       /\\b(förder|conveyor|конвейер)\\b/i,\n//       /\\b(vakuum|vacuum|вакуум)\\b/i,\n//       /\\b(kompressor|compressor|компрессор)\\b/i,\n//       /\\b(industrial|industrie|промышл)\\b/i\n//     ],\n//     vertical_transport: [\n//       /\\b(aufzug|elevator|lift|fahrstuhl|лифт|ascenseur)\\b/i,\n//       /\\b(escalator|rolltreppe|эскалатор)\\b/i\n//     ],\n//     facade_maintenance: [\n//       /\\b(graffiti|vandalismus|вандал)\\b/i,\n//       /\\b(hänge.*bühne|suspended.*platform|подвес.*люльк)\\b/i,\n//       /\\b(fassaden.*reinig|clean.*facade|очистк.*фасад)\\b/i\n//     ],\n//     landscape_maintenance: [\n//       /\\b(hecke|hedge|живая.*изгородь)\\b/i,\n//       /\\b(schneid|trim|prun|стрижк|обрезк)\\b/i\n//     ]\n//   };\n//   \n//   // Detect domains\n//   const detected = [];\n//   for (const [domain, patterns] of Object.entries(domains)) {\n//     if (patterns.some(p => p.test(text))) detected.push(domain);\n//   }\n//   \n//   // Incompatibilities\n//   const blocked = {\n//     structural: ['electrical', 'industrial', 'vertical_transport', 'facade_maintenance'],\n//     openings: ['industrial', 'vertical_transport', 'electrical'],\n//     site: ['electrical', 'industrial', 'facade_maintenance']\n//   };\n//   \n//   let penalty = 0;\n//   const reasons = [];\n//   \n//   for (const domain of detected) {\n//     if ((blocked[catGroup] || []).includes(domain)) {\n//       const p = (domain === 'electrical' || domain === 'vertical_transport') ? -100 : -70;\n//       penalty += p;\n//       reasons.push(`${domain} for ${catGroup}`);\n//     }\n//   }\n//   \n//   // Special: landscape maintenance for planting\n//   if (catGroup === 'site' && catLower.includes('plant') && detected.includes('landscape_maintenance')) {\n//     const isInstall = /\\b(pflanz|plant|посадк|install|setz)\\b/i.test(workName || '');\n//     if (!isInstall) {\n//       penalty += -50;\n//       reasons.push('maintenance instead of planting');\n//     }\n//   }\n//   \n//   return { penalty, reasons };\n// }\n\n\n\n\n\n// In the scoring section, ADD THIS after workTypeCheck:\n  // REMOVED: // const mismatchCheck = checkCategoryMismatch(result.rate_name, result.hierarchy?.full_path, workData.work_name, elementCategory);\n  // REMOVED: // if (mismatchCheck.penalty < 0) {\n  // REMOVED: //   qualityScore += mismatchCheck.penalty;\n  // REMOVED: //   qualityFactors.push(...mismatchCheck.reasons.map(r => `MISMATCH:${r}`));\n// }\n\n\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// PARSE ALL RESULTS\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst parsedResults = searchResults.map((item, idx) => {\n  const data = item.json;\n  const content = getContentAsString(data);\n  const metadata = data.metadata || {};\n  \n  console.log(`\\n  [${idx + 1}] Processing...`);\n  console.log(`      Content length: ${content.length} chars`);\n  \n  const parsed = parseContentText(content);\n  \n  // Combine parsed data with metadata (metadata takes priority)\n  const rateCode = metadata.rsts || parsed.rate_code || '';\n  const rateName = metadata.names || parsed.rate_name || '';\n  const rateUnit = metadata.unit || parsed.rate_unit || '';\n  const hierarchy = metadata.hierarchy || '';\n  \n  const totalCostPosition = parsed.total_cost || 0;\n  const totalResourceCost = parsed.resources_cost || 0;\n  const totalMaterialCost = parsed.materials_cost || 0;\n  const workerLaborHours = parsed.worker_labor_hours || 0;\n  \n  const categorized = categorizeResources(parsed.resources);\n  \n  const costBreakdown = {\n    workers_cost: categorized.workers.reduce((sum, r) => sum + (r.resource_cost || 0), 0),\n    machines_cost: categorized.machines.reduce((sum, r) => sum + (r.resource_cost || 0), 0),\n    machinists_cost: categorized.machinists.reduce((sum, r) => sum + (r.resource_cost || 0), 0),\n    materials_cost: categorized.materials.reduce((sum, r) => sum + (r.resource_cost || 0), 0),\n    electricity_cost: categorized.electricity.reduce((sum, r) => sum + (r.resource_cost || 0), 0)\n  };\n  \n  console.log(`      Code: ${rateCode}`);\n  console.log(`      Cost: ${totalCostPosition.toFixed(2)} | Resources: ${parsed.resources.length}`);\n  \n  return {\n    rate_code: rateCode,\n    rate_name: rateName,\n    rate_unit: rateUnit,\n    total_cost_position: totalCostPosition,\n    total_resource_cost: totalResourceCost,\n    total_material_cost: totalMaterialCost,\n    worker_labor_hours: workerLaborHours,\n    machinist_labor_hours: 0,\n    total_labor_hours: workerLaborHours,\n    hierarchy: {\n      full_path: hierarchy\n    },\n    cost_breakdown: costBreakdown,\n    resources: categorized,\n    all_resources: parsed.resources,\n    resources_count: parsed.resources.length,\n    search_score: 0.8,\n    content_snippet: content.substring(0, 300),\n    metadata_raw: metadata\n  };\n});\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// SELECT BEST RESULT - SMART QUALITY SCORING v2.0\n// ═══════════════════════════════════════════════════════════════════════════════\n\n// Get context for quality evaluation\nconst expectedUnit = (workData.expected_unit || '').toLowerCase().trim();\nconst searchQuery = workData.search_query || workData.work_name || '';\nconst elementMaterials = workData.element_analysis?.layers_detected || [];\nconst elementCategory = workData.category || '';\n\nconsole.log(`\\n  Quality evaluation context:`);\nconsole.log(`    Expected unit: ${expectedUnit}`);\nconsole.log(`    Search query: ${searchQuery}`);\nconsole.log(`    Element materials: ${elementMaterials.join(', ') || 'none'}`);\nconsole.log(`    Category: ${elementCategory}`);\n\n// Score each result\nconst scoredResults = parsedResults.map(result => {\n  let qualityScore = 0;\n  let qualityFactors = [];\n  \n  const resultUnit = normalizeUnit(result.rate_unit);\n  const expectedUnitNorm = normalizeUnit(expectedUnit);\n  \n  // Factor 1: Has price (max 30 points)\n  if (result.total_cost_position > 0) {\n    qualityScore += 30;\n    qualityFactors.push('has_price');\n  }\n  \n  // Factor 2: Has resources (max 25 points)\n  if (result.resources_count > 0) {\n    const resourcePoints = Math.min(25, result.resources_count * 5);\n    qualityScore += resourcePoints;\n    qualityFactors.push(`resources:${result.resources_count}`);\n  }\n  \n  // Factor 3: Unit match (max 20 points)\n  if (resultUnit && expectedUnitNorm) {\n    if (resultUnit === expectedUnitNorm) {\n      qualityScore += 20;\n      qualityFactors.push('unit_exact');\n    } else if (resultUnit.includes(expectedUnitNorm) || expectedUnitNorm.includes(resultUnit)) {\n      qualityScore += 10;\n      qualityFactors.push('unit_partial');\n    } else {\n      // Different units - penalty\n      qualityFactors.push('unit_mismatch');\n    }\n  }\n  \n  // Factor 4: Material match (max 15 points)\n  const materialCheck = checkMaterialMatch(result.rate_name, result.hierarchy?.full_path, elementMaterials);\n  if (materialCheck.match) {\n    qualityScore += materialCheck.score;\n    qualityFactors.push(`material:${materialCheck.materials.join(',')}`);\n  }\n  \n  // Factor 5: Category relevance (max 10 points)\n  // REMOVED: const categoryCheck = checkCategoryRelevance(result.rate_name, result.hierarchy?.full_path, elementCategory);\n  // REMOVED: if (categoryCheck.relevant) {\n  // REMOVED:   qualityScore += categoryCheck.score;\n  // REMOVED:   qualityFactors.push('category_match');\n  // REMOVED: } else {\n  // REMOVED:   qualityFactors.push('category_mismatch');\n  // REMOVED: }\n  \n  // Factor 6: Work type match (max 10 points)\n  const workTypeCheck = checkWorkTypeMatch(searchQuery, result.rate_name);\n  if (workTypeCheck.match) {\n    qualityScore += workTypeCheck.score;\n    qualityFactors.push(`worktype:${workTypeCheck.workType}`);\n  }\n\n  // Factor 7: CRITICAL - Category mismatch check (can heavily penalize)\n  // REMOVED: const mismatchCheck = checkCategoryMismatch(result.rate_name, result.hierarchy?.full_path, workData.work_name, elementCategory);\n  // REMOVED: if (mismatchCheck.penalty < 0) {\n  // REMOVED:   qualityScore += mismatchCheck.penalty;\n  // REMOVED:   qualityFactors.push(...mismatchCheck.reasons.map(r => `MISMATCH:${r}`));\n  // REMOVED: }\n  \n  // Bonus: Has both workers and materials (complete rate)\n  if (result.resources.workers?.length > 0 && result.resources.materials?.length > 0) {\n    qualityScore += 5;\n    qualityFactors.push('complete_rate');\n  }\n  \n  return {\n    ...result,\n    quality_score: qualityScore,\n    quality_factors: qualityFactors\n  };\n});\n\n// Sort by quality score\nconst sortedResults = scoredResults.sort((a, b) => b.quality_score - a.quality_score);\nconst bestResult = sortedResults[0];\n\n// Determine quality level based on score\nlet qualityLevel = 'not_found';\nlet searchConfidence = 0;\nlet qualityReason = '';\n\nconst score = bestResult.quality_score;\nconst factors = bestResult.quality_factors || [];\n\nif (score >= 70) {\n  qualityLevel = 'high';\n  searchConfidence = 0.90;\n  qualityReason = `Excellent match (${score}/100): ${factors.join(', ')}`;\n} else if (score >= 45) {\n  qualityLevel = 'medium';\n  searchConfidence = 0.70;\n  qualityReason = `Good match (${score}/100): ${factors.join(', ')}`;\n} else if (score >= 25) {\n  qualityLevel = 'low';\n  searchConfidence = 0.50;\n  qualityReason = `Partial match (${score}/100): ${factors.join(', ')}`;\n} else {\n  qualityLevel = 'not_found';\n  searchConfidence = 0.25;\n  qualityReason = `Poor match (${score}/100): manual review recommended`;\n}\n\nconsole.log(`\\n${'═'.repeat(60)}`);\nconsole.log(`✓ SELECTED: ${bestResult.rate_code}`);\nconsole.log(`  Name: ${bestResult.rate_name}`);\nconsole.log(`  Cost: ${bestResult.total_cost_position.toFixed(2)}`);\nconsole.log(`  Unit: ${bestResult.rate_unit} (expected: ${expectedUnit})`);\nconsole.log(`  Quality: ${qualityLevel.toUpperCase()} (score: ${score}/100)`);\nconsole.log(`  Factors: ${factors.join(', ')}`);\nconsole.log(`${'═'.repeat(60)}`);\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// OUTPUT\n// ═══════════════════════════════════════════════════════════════════════════════\n\nreturn [{\n  json: {\n    // Work data passthrough\n    work_id: workData.work_id,\n    work_name: workData.work_name,\n    search_query: workData.search_query || workData.work_name,\n    expected_unit: workData.expected_unit,\n    work_sequence: workData.work_sequence,\n    quantity_source: workData.quantity_source,\n    \n    type_name: workData.type_name,\n    type_index: workData.type_index,\n    total_types: workData.total_types,\n    category: workData.category,\n    assigned_phase: workData.assigned_phase,\n    quantities: workData.quantities,\n    element_count: workData.element_count,\n    element_analysis: workData.element_analysis,\n    \n    // Rate info with quality scoring\n    rateInfo: {\n      rate_code: bestResult.rate_code,\n      rate_name: bestResult.rate_name,\n      rate_unit: bestResult.rate_unit,\n      total_cost_position: bestResult.total_cost_position,\n      total_resource_cost: bestResult.total_resource_cost,\n      total_material_cost: bestResult.total_material_cost,\n      worker_labor_hours: bestResult.worker_labor_hours,\n      machinist_labor_hours: bestResult.machinist_labor_hours,\n      total_labor_hours: bestResult.total_labor_hours,\n      hierarchy: bestResult.hierarchy,\n      cost_breakdown: bestResult.cost_breakdown,\n      \n      // Quality metrics\n      search_confidence: searchConfidence,\n      search_confidence_percent: (searchConfidence * 100).toFixed(1) + '%',\n      quality_level: qualityLevel,\n      quality_score: score,\n      quality_factors: factors,\n      quality_reason: qualityReason\n    },\n    \n    // Resources\n    resources: bestResult.resources,\n    \n    // All results for debugging\n    all_search_results: sortedResults,\n    search_results_count: sortedResults.length,\n    \n    // Config passthrough\n    qdrant_collection: workData.qdrant_collection,\n    language: workData.language,\n    currency: workData.currency,\n    locale: workData.locale,\n    \n    _work_index: workData._work_index,\n    _total_works_in_type: workData._total_works_in_type,\n    \n    stage: 'STAGE_5_RATE_FOUND'\n  }\n}];"},"typeVersion":2},{"id":"ac07d2b6-c533-4440-bcaf-10d05302212a","name":"Save to Project Folder","type":"n8n-nodes-base.code","position":[1472,3184],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════════\n// SAVE TO PROJECT FOLDER - HTML + XLS version\n// Saves both HTML and XLS files to the same folder as project_file\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst input = $input.first();\nconst json = input.json;\nconst binary = input.binary;\n\n// Get project path from Setup node\nlet projectPath = '';\ntry {\n  const setup = $('Setup - Define file paths1').first().json;\n  projectPath = setup.project_file || '';\n} catch (e) {\n  projectPath = json.project_path || '';\n}\n\nif (!projectPath) {\n  console.log('⚠️ No project_file path found, using default folder');\n  // Use current directory as fallback\n  projectPath = './output.rvt';\n}\n\n// Extract folder from project_file path\nlet folder = '';\nif (projectPath.includes('\\\\')) {\n  folder = projectPath.substring(0, projectPath.lastIndexOf('\\\\'));\n} else {\n  folder = projectPath.substring(0, projectPath.lastIndexOf('/'));\n}\n\n// Build full paths\nconst sep = folder.includes('\\\\') ? '\\\\' : '/';\nconst htmlPath = folder + sep + json.html_filename;\nconst xlsPath = folder + sep + json.xls_filename;\n\nconsole.log(`\\n${'═'.repeat(60)}`);\nconsole.log(`SAVE TO PROJECT FOLDER`);\nconsole.log(`${'═'.repeat(60)}`);\nconsole.log(`  Project file: ${projectPath}`);\nconsole.log(`  Target folder: ${folder}`);\nconsole.log(`  HTML file: ${htmlPath}`);\nconsole.log(`  XLS file: ${xlsPath}`);\nconsole.log(`${'═'.repeat(60)}\\n`);\n\nreturn [{\n  json: {\n    ...json,\n    output_folder: folder,\n    html_path: htmlPath,\n    xls_path: xlsPath\n  },\n  binary: binary\n}];\n"},"typeVersion":2},{"id":"1f57f43b-4382-4869-8396-5a265fa7cb73","name":"Write HTML File","type":"n8n-nodes-base.writeBinaryFile","position":[1648,3296],"parameters":{"options":{},"fileName":"={{ $json.html_path }}","dataPropertyName":"html"},"typeVersion":1},{"id":"a79081a6-9633-4ce0-9aa3-8f741587d9be","name":"CONFIG - AI Classify","type":"n8n-nodes-base.set","position":[768,1472],"parameters":{"options":{},"assignments":{"assignments":[{"id":"b43ed042-f92f-47c3-a48c-873f3050790e","name":"system_prompt","type":"string","value":"You are an expert in Revit, BIM and construction classification. Classify category values as building elements (true) or non-building elements (false).\n\nBuilding: walls, floors, roofs, columns, beams, MEP, doors, windows, stairs, fixtures\nNon-building: annotations, dimensions, grids, text, schedules, views, sheets, legends"},{"id":"ba1e0abe-6dd1-4016-9fee-8e7009b902b3","name":"chatInput","type":"string","value":"=You are an expert in Revit, BIM and construction classification. Classify category values as building elements (true) or non-building elements (false).\n\nBuilding: walls, floors, roofs, columns, beams, MEP, doors, windows, stairs, fixtures\nNon-building: annotations, dimensions, grids, text, schedules, views, sheets, legends\n\nCategories to classify:\n{{ $json.categoryValues }}\n\nReturn JSON array: [{\"category\": \"name\", \"is_building_element\": true/false}] as JSON-string without markdown wrap"}]}},"typeVersion":3.4},{"id":"d4cba1b9-7555-482f-87af-06815dc078b7","name":"AI Classify Categories","type":"@n8n/n8n-nodes-langchain.chainLlm","position":[896,1472],"parameters":{"messages":{"messageValues":[{"message":"={{ $('CONFIG - AI Classify').item.json.chatInput }}"}]}},"typeVersion":1.4},{"id":"7b9a07e7-6fe0-413a-9f78-3dc378e63c61","name":"CONFIG - AI Headers","type":"n8n-nodes-base.set","position":[1344,1104],"parameters":{"options":{},"assignments":{"assignments":[{"id":"23f64be6-5c8a-48d7-89be-974333384886","name":"system_prompt","type":"string","value":"=You are an expert in construction classification systems. Analyze building element groups and assign aggregation methods for grouping data.\n\nRules:\n1. 'sum' - for quantities that should be totaled: Volume, Area, Length, Width, Height, Depth, Size, Count, Quantity\n2. 'first' - for identifiers and descriptions: ID, Name, Type, Category, Level, Material\n3. 'unique' - for values that should be collected: Comments, Notes"},{"id":"ad2dc254-c619-4126-b24e-dd22fe5a9935","name":"chatInput","type":"string","value":"=You are an expert in construction classification systems. Analyze building element groups and assign aggregation methods for grouping data.\n\nRules:\n1. 'sum' - for quantities that should be totaled: Volume, Area, Length, Width, Height, Depth, Size, Count, Quantity\n2. 'first' - for identifiers and descriptions: ID, Name, Type, Category, Level, Material\n3. 'unique' - for values that should be collected: Comments, Notes\n\nHeaders to analyze:\n{{ $json.headers }}\n\nGroups:\n{ JSON.stringify($json.groups) }\n\nReturn JSON with aggregation rules for each header as JSON-string without markdown wrap."}]}},"typeVersion":3.4},{"id":"11bbad47-f80d-49de-9b65-1976f521d66a","name":"AI Analyze All Headers","type":"@n8n/n8n-nodes-langchain.chainLlm","position":[1456,1104],"parameters":{"messages":{"messageValues":[{"message":"={{ $('CONFIG - AI Headers').item.json.chatInput }}"}]}},"typeVersion":1.4},{"id":"8d401ecb-18b4-47e6-96b4-48e488a1619e","name":"CONFIG - STAGE 1","type":"n8n-nodes-base.set","position":[1872,1456],"parameters":{"options":{},"assignments":{"assignments":[{"id":"a6046f1a-6d3c-4430-85c5-ec03f3c9881f","name":"system_prompt","type":"string","value":"You are an experienced cost estimator and construction manager. Your task is to DETERMINE THE PROJECT TYPE based on element composition.\n\nDO NOT:\n- Determine specific works\n- Assign pricing rates\n- Classify elements in detail\n\nDO:\n1. Analyze element categories and quantities\n2. Determine project type (Residential, Commercial, Industrial, Infrastructure, Renovation)\n3. Identify key building systems present"},{"id":"0be81b2c-522a-4b01-b3b4-a3a59ed0d765","name":"chatInput","type":"string","value":"=You are an experienced cost estimator and construction manager. Your task is to DETERMINE THE PROJECT TYPE based on element composition.\n\n{{ $json.system_prompt_lang }}\n\nDO NOT:\n- Determine specific works\n- Assign pricing rates\n- Classify elements in detail\n\nDO:\n1. Look at the OVERALL COMPOSITION of elements (which categories exist)\n2. Determine PROJECT TYPE:\n   - new_construction - foundations, frame, roof present\n   - reconstruction - mix of new and existing\n   - renovation (major repair) - mainly finishing, MEP replacement\n   - current_repair (minor repair) - only finishing, minor works\n   - demolition - predominantly demolition works\n   - mixed - unclear, heterogeneous composition\n\n3. Determine PROJECT SCALE:\n   - small (up to 50 element types)\n   - medium (50-200)\n   - large (200+)\n\n4. Identify MAIN CATEGORIES of elements present in the project\n\nReturn JSON as JSON-string without markdown wrap:\n{\n  \"project_type\": {\n    \"detected\": \"new_construction\",\n    \"confidence\": 0.85,\n    \"reasoning\": \"Foundations (OST_StructuralFoundation), load-bearing walls, floors and roof present - typical set for new construction\"\n  },\n  \"project_scale\": \"medium\",\n  \"main_categories\": [\n    {\"category\": \"Foundations\", \"revit_categories\": [\"OST_StructuralFoundation\"], \"count\": 3},\n    {\"category\": \"Walls\", \"revit_categories\": [\"OST_Walls\"], \"count\": 15},\n    ...\n  ],\n  \"notes\": \"Additional observations about the project\"\n}"}]}},"typeVersion":3.4},{"id":"32191714-ffc9-4da0-93ac-286eb955587c","name":"STAGE 1 - Detect Project Type","type":"@n8n/n8n-nodes-langchain.chainLlm","position":[1984,1456],"parameters":{"messages":{"messageValues":[{"message":"={{ $('CONFIG - STAGE 1').item.json.chatInput }}"}]}},"typeVersion":1.4},{"id":"88f30a10-d71f-4c04-b80a-fdc560dbc7c6","name":"CONFIG - STAGE 2","type":"n8n-nodes-base.set","position":[656,1776],"parameters":{"options":{},"assignments":{"assignments":[{"id":"81d97eab-a905-47ef-8c5b-13038190966c","name":"system_prompt","type":"string","value":"You are an experienced construction manager. You need to CREATE A CONSTRUCTION PHASE PLAN for the project.\n\nPhases should follow standard construction sequence:\n1. Site Preparation\n2. Foundation\n3. Structure\n4. Envelope\n5. MEP Rough-in\n6. Interior Finishes\n7. MEP Finishes\n8. External Works"},{"id":"98dbffdb-42fc-4b3a-a647-8d93f9f684f8","name":"chatInput","type":"string","value":"=You are an experienced construction manager. CREATE A CONSTRUCTION PHASE PLAN for the project.\n{{ $json.system_prompt_lang }}\n\nCRITICAL LANGUAGE REQUIREMENT:\n- ALL phase names (phase_name) MUST be in {{ $json.language }} language!\n- Example for German: \"Fenster und Türen\", NOT \"Windows and doors\"\n- Example for Russian: \"Окна и двери\", NOT \"Windows and doors\"\n\n══════════════════════════════════════════════════════════════════════════════════\nAVAILABLE PHASES REFERENCE (select appropriate ones for this project)\n══════════════════════════════════════════════════════════════════════════════════\n\nSITE & PREPARATION:\n- DEMO: Demolition (OST_Demolition, demolished elements)\n- SITE_PREP: Site preparation, temporary structures\n- EARTH: Earthworks (OST_Topography, OST_BuildingPad)\n\nSUBSTRUCTURE:\n- FOUND: Foundations (OST_StructuralFoundation, OST_Footing)\n- PILING: Piling works\n- WATERPROOF: Foundation waterproofing\n\nSUPERSTRUCTURE:\n- FRAME: Frame/load-bearing structures (OST_StructuralColumns, OST_StructuralFraming, OST_Beams)\n- CONCRETE: In-situ concrete works\n- STEEL: Steel structures\n- TIMBER: Timber frame\n- MASONRY: Masonry walls\n\nHORIZONTAL ELEMENTS:\n- FLOORS: Floor structures (OST_Floors, OST_StructuralFloor)\n- STAIRS: Stairs and railings (OST_Stairs, OST_StairsRailing, OST_Ramps)\n- BALCONIES: Balconies and terraces\n\nVERTICAL ENCLOSURE:\n- WALLS: Walls and partitions (OST_Walls, OST_StructuralWalls)\n- CURTAIN: Curtain walls (OST_CurtainWallPanels, OST_CurtainWallMullions, OST_CurtainSystems)\n\nROOF:\n- ROOF: Roofing (OST_Roofs)\n- SKYLIGHTS: Skylights and roof windows\n\nOPENINGS:\n- WINDOWS: Windows (OST_Windows)\n- DOORS: Doors (OST_Doors)\n- GLAZING: Glazing systems\n\nFACADE:\n- FACADE: Facade finishing\n- INSULATION: Thermal insulation\n- CLADDING: External cladding\n\nMEP - ELECTRICAL:\n- ELEC: Electrical systems (OST_ElectricalEquipment, OST_ElectricalFixtures)\n- LIGHTING: Lighting (OST_LightingFixtures, OST_LightingDevices)\n- LOW_VOLTAGE: Low voltage, security, fire alarm\n\nMEP - PLUMBING:\n- PLUMB: Plumbing (OST_PipeCurves, OST_PipeAccessory, OST_PipeFitting)\n- FIXTURES: Sanitary fixtures (OST_PlumbingFixtures)\n- SEWAGE: Sewage and drainage\n\nMEP - HVAC:\n- HVAC: HVAC systems (OST_DuctCurves, OST_MechanicalEquipment)\n- HEATING: Heating systems\n- VENTILATION: Ventilation\n- AC: Air conditioning\n\nMEP - FIRE PROTECTION:\n- FIRE_PROT: Fire protection (OST_Sprinklers, fire dampers)\n\nVERTICAL TRANSPORT:\n- ELEVATORS: Elevators and lifts\n- ESCALATORS: Escalators and moving walks\n\nINTERIOR:\n- INT_FINISH: Interior finishes\n- CEILINGS: Suspended ceilings (OST_Ceilings)\n- FLOOR_FIN: Floor finishes\n- WALL_FIN: Wall finishes\n- PAINTING: Painting works\n\nFURNITURE & EQUIPMENT:\n- FURNITURE: Furniture (OST_Furniture, OST_FurnitureSystems)\n- CASEWORK: Built-in casework (OST_Casework)\n- EQUIPMENT: Specialty equipment (OST_SpecialityEquipment, OST_MechanicalEquipment)\n- GENERIC: Generic models (OST_GenericModel)\n\nEXTERIOR:\n- LANDSCAPE: Landscaping (OST_Planting, OST_Site)\n- PAVING: Paving and hardscape\n- FENCING: Fencing and gates\n- SITE_EQUIP: Site furniture and equipment\n\n══════════════════════════════════════════════════════════════════════════════════\nPHASE SELECTION RULES\n══════════════════════════════════════════════════════════════════════════════════\n\n1. SELECT 5-15 phases based on project complexity:\n   - Small residential: 5-7 phases (consolidate MEP, Interior)\n   - Medium commercial: 8-12 phases (separate major systems)\n   - Large/complex: 12-15 phases (detailed breakdown)\n\n2. ALWAYS include these if elements exist:\n   - FOUND (if foundations present)\n   - FRAME or WALLS (structural elements)\n   - ROOF (if roofing present)\n   - WINDOWS/DOORS or OPENINGS (if openings present)\n   - MEP or individual systems (if MEP present)\n   - INT_FINISH or INTERIOR (if interior elements present)\n\n3. CONSOLIDATE similar works:\n   - Combine WINDOWS + DOORS into \"OPENINGS\" for small projects\n   - Combine all MEP into single phase for residential\n   - Split MEP into ELEC/PLUMB/HVAC for commercial\n\n4. FOLLOW technological sequence:\n   - Site → Foundation → Structure → Envelope → MEP → Finishes\n   - Cannot install ceilings before MEP rough-in\n   - Cannot paint before plastering\n\n══════════════════════════════════════════════════════════════════════════════════\nCATEGORY MAPPING (ensure ALL categories have a phase!)\n══════════════════════════════════════════════════════════════════════════════════\n\nEvery Revit category MUST be assigned to exactly ONE phase:\n\nOST_StructuralFoundation → FOUND\nOST_Floors, OST_StructuralFloor → FLOORS\nOST_Walls, OST_StructuralWalls → WALLS\nOST_StructuralColumns, OST_StructuralFraming → FRAME\nOST_Stairs, OST_StairsRailing → STAIRS (or FRAME)\nOST_Roofs → ROOF\nOST_Windows → WINDOWS (or OPENINGS)\nOST_Doors → DOORS (or OPENINGS)\nOST_CurtainWallPanels, OST_CurtainWallMullions → CURTAIN (or FACADE)\nOST_Ceilings → CEILINGS (or INT_FINISH)\nOST_Furniture, OST_FurnitureSystems → FURNITURE (or INT_FINISH)\nOST_Casework → CASEWORK (or INT_FINISH)\nOST_GenericModel → GENERIC (or INT_FINISH)\nOST_SpecialityEquipment → EQUIPMENT (or MEP)\nOST_LightingFixtures → LIGHTING (or MEP or ELEC)\nOST_PlumbingFixtures → FIXTURES (or MEP or PLUMB)\nOST_ElectricalEquipment, OST_ElectricalFixtures → ELEC (or MEP)\nOST_MechanicalEquipment → HVAC (or MEP)\nOST_PipeCurves → PLUMB (or MEP)\nOST_DuctCurves → HVAC (or MEP)\nOST_Planting → LANDSCAPE (or SITE)\nOST_Topography, OST_BuildingPad → SITE (or EARTH)\nOST_Site → SITE\nOST_Mass → FRAME (or DESIGN)\nOST_Ramps → STAIRS (or FRAME)\n\n══════════════════════════════════════════════════════════════════════════════════\nOUTPUT FORMAT\n══════════════════════════════════════════════════════════════════════════════════\n\nReturn JSON (no markdown):\n{\n  \"construction_phases\": [\n    {\n      \"phase_id\": 1,\n      \"phase_code\": \"FOUND\",\n      \"phase_name\": \"Fundamente\",  // ← MUST be in {{ $json.language }}!\n      \"phase_name_en\": \"Foundations\",\n      \"description\": \"Foundation works including excavation and concrete\",\n      \"target_categories\": [\"OST_StructuralFoundation\", \"OST_Footing\"],\n      \"sequence_order\": 1,\n      \"typical_duration_percent\": 5\n    }\n  ],\n  \"category_phase_map\": {\n    \"OST_StructuralFoundation\": \"FOUND\",\n    \"OST_Floors\": \"FLOORS\",\n    \"OST_Furniture\": \"INT_FINISH\"\n  },\n  \"summary\": {\n    \"total_phases\": 9,\n    \"project_complexity\": \"medium\",\n    \"reasoning\": \"Standard residential project with separate MEP phase\"\n  }\n}"}]}},"typeVersion":3.4},{"id":"1c70d0ca-c473-4112-9910-d68547aca9f4","name":"STAGE 2 - Generate Construction Phases","type":"@n8n/n8n-nodes-langchain.chainLlm","position":[784,1776],"parameters":{"messages":{"messageValues":[{"message":"={{ $('CONFIG - STAGE 2').item.json.chatInput }}"}]}},"typeVersion":1.4},{"id":"d05fa841-d1eb-490c-95ee-a508901f0f9c","name":"CONFIG - STAGE 4","type":"n8n-nodes-base.set","position":[768,2272],"parameters":{"options":{},"assignments":{"assignments":[{"id":"9f9774d6-eaf1-4641-b303-04b40d9852ad","name":"system_prompt","type":"string","value":"You are an experienced cost estimator and construction manager with 30 years of experience. Your task is to determine WHAT WORKS are needed for any building element.\n\nConsider:\n1. Demolition/removal (if renovation)\n2. Preparation works\n3. Main installation\n4. Finishing works\n5. Quality control"},{"id":"961d5719-817c-47b2-a650-496350228b86","name":"chatInput","type":"string","value":"You are an experienced construction cost estimator with 30 years experience.\n\n{{ $json.system_prompt_lang }}\n\n═══════════════════════════════════════════════════════════════════════════════\n⚠️ CRITICAL: RESPECT THE REVIT CATEGORY\n═══════════════════════════════════════════════════════════════════════════════\n\nThe Revit CATEGORY tells you EXACTLY what type of element this is.\nDO NOT MIX DOMAINS! Generate ONLY works relevant to the element's category.\n\n═══════════════════════════════════════════════════════════════════════════════\nCOMPLETE CATEGORY → WORK GROUP MAPPING\n═══════════════════════════════════════════════════════════════════════════════\n\n┌─────────────────────────────────────────────────────────────────────────────┐\n│ GROUP: FOUNDATIONS                                                          │\n│ Phase: FOUND                                                                │\n├─────────────────────────────────────────────────────────────────────────────┤\n│ Categories:                                                                 │\n│   • OST_StructuralFoundation                                                │\n│                                                                             │\n│ Typical Works Sequence:                                                     │\n│   1. Excavation (m³) - pit/trench excavation                                │\n│   2. Preparation (m²) - compaction, gravel bed                              │\n│   3. Formwork (m²) - foundation formwork                                    │\n│   4. Reinforcement (t) - rebar installation                                 │\n│   5. Concrete (m³) - concrete placement                                     │\n│   6. Waterproofing (m²) - membrane/coating application                      │\n│   7. Backfill (m³) - backfilling and compaction                             │\n│                                                                             │\n│ ✅ APPROPRIATE: earthwork, formwork, reinforcement, concrete, waterproofing │\n│ ❌ NOT APPROPRIATE: MEP, windows, interior finishes                         │\n└─────────────────────────────────────────────────────────────────────────────┘\n\n┌─────────────────────────────────────────────────────────────────────────────┐\n│ GROUP: STRUCTURAL_FRAME                                                     │\n│ Phase: FRAME, CONCRETE, STEEL                                               │\n├─────────────────────────────────────────────────────────────────────────────┤\n│ Categories:                                                                 │\n│   • OST_Columns, OST_StructuralColumns                                      │\n│   • OST_StructuralFraming, OST_StructuralFramingSystem                      │\n│   • OST_Girder, OST_Joist, OST_Purlin                                       │\n│   • OST_HorizontalBracing, OST_VerticalBracing, OST_KickerBracing           │\n│   • OST_StructuralStiffener, OST_Truss                                      │\n│   • OST_PierCaps, OST_PierColumns, OST_PierPiles, OST_PierWalls             │\n│                                                                             │\n│ Typical Works Sequence (concrete):                                          │\n│   1. Formwork (m²) - column/beam formwork                                   │\n│   2. Reinforcement (t) - rebar cages                                        │\n│   3. Concrete (m³) - concrete placement                                     │\n│   4. Formwork removal (m²)                                                  │\n│                                                                             │\n│ Typical Works Sequence (steel):                                             │\n│   1. Steel fabrication (t)                                                  │\n│   2. Steel erection (t)                                                     │\n│   3. Bolted connections (pcs)                                               │\n│   4. Welding (m)                                                            │\n│   5. Fire protection (m²)                                                   │\n│                                                                             │\n│ ✅ APPROPRIATE: formwork, reinforcement, concrete, steel erection, welding  │\n│ ❌ NOT APPROPRIATE: glazing, plumbing fixtures, landscaping                 │\n└─────────────────────────────────────────────────────────────────────────────┘\n\n┌─────────────────────────────────────────────────────────────────────────────┐\n│ GROUP: FLOORS                                                               │\n│ Phase: FLOORS                                                               │\n├─────────────────────────────────────────────────────────────────────────────┤\n│ Categories:                                                                 │\n│   • OST_Floors, OST_FloorsStructure, OST_FloorsInsulation                   │\n│   • OST_EdgeSlab                                                            │\n│                                                                             │\n│ Typical Works Sequence:                                                     │\n│   1. Formwork/decking (m²) - slab formwork or metal deck                    │\n│   2. Reinforcement (t) - mesh or rebar                                      │\n│   3. Concrete (m³) - slab concrete                                          │\n│   4. Insulation (m²) - thermal/acoustic insulation                          │\n│   5. Screed (m²) - leveling screed                                          │\n│   6. Floor finish (m²) - tiles, parquet, carpet, etc.                       │\n│                                                                             │\n│ ✅ APPROPRIATE: formwork, reinforcement, concrete, screed, floor finishes   │\n│ ❌ NOT APPROPRIATE: roofing, windows, electrical equipment                  │\n└─────────────────────────────────────────────────────────────────────────────┘\n\n┌─────────────────────────────────────────────────────────────────────────────┐\n│ GROUP: WALLS                                                                │\n│ Phase: WALLS, MASONRY, CURTAIN                                              │\n├─────────────────────────────────────────────────────────────────────────────┤\n│ Categories:                                                                 │\n│   • OST_Walls, OST_WallsStructure, OST_WallsInsulation                      │\n│   • OST_WallsMembrane, OST_WallsSubstrate, OST_WallsDefault                 │\n│   • OST_StackedWalls                                                        │\n│                                                                             │\n│ Typical Works Sequence (concrete walls):                                    │\n│   1. Formwork (m²)                                                          │\n│   2. Reinforcement (t)                                                      │\n│   3. Concrete (m³)                                                          │\n│   4. Insulation (m²)                                                        │\n│   5. Finish (m²) - plaster, paint                                           │\n│                                                                             │\n│ Typical Works Sequence (masonry):                                           │\n│   1. Masonry (m² or m³) - brick/block laying                                │\n│   2. Lintels (pcs) - over openings                                          │\n│   3. Insulation (m²)                                                        │\n│   4. Plaster (m²) - internal/external                                       │\n│   5. Paint (m²)                                                             │\n│                                                                             │\n│ Typical Works Sequence (drywall/partitions):                                │\n│   1. Metal framing (m²)                                                     │\n│   2. Insulation (m²)                                                        │\n│   3. Boarding (m²) - gypsum boards                                          │\n│   4. Jointing/taping (m²)                                                   │\n│   5. Paint (m²)                                                             │\n│                                                                             │\n│ ✅ APPROPRIATE: masonry, framing, insulation, plaster, paint, boarding      │\n│ ❌ NOT APPROPRIATE: roofing membrane, floor screed, MEP equipment           │\n└─────────────────────────────────────────────────────────────────────────────┘\n\n┌─────────────────────────────────────────────────────────────────────────────┐\n│ GROUP: CURTAIN_WALLS                                                        │\n│ Phase: CURTAIN, FACADE                                                      │\n├─────────────────────────────────────────────────────────────────────────────┤\n│ Categories:                                                                 │\n│   • OST_CurtainWallPanels, OST_CurtainWallMullions                          │\n│   • OST_CurtaSystem, OST_Curtain_Systems                                    │\n│   • OST_CurtainGridsRoof, OST_CurtainGridsWall, OST_CurtainGridsSystem      │\n│                                                                             │\n│ Typical Works Sequence:                                                     │\n│   1. Anchors/brackets (pcs) - attachment to structure                       │\n│   2. Mullions (m) - frame installation                                      │\n│   3. Transoms (m) - horizontal members                                      │\n│   4. Glazing panels (m²) - glass installation                               │\n│   5. Spandrel panels (m²) - opaque panels                                   │\n│   6. Sealing (m) - weatherproofing                                          │\n│   7. Hardware (pcs) - vents, operators                                      │\n│                                                                             │\n│ ✅ APPROPRIATE: mullions, glazing, panels, sealing, brackets                │\n│ ❌ NOT APPROPRIATE: concrete, masonry, MEP systems                          │\n└─────────────────────────────────────────────────────────────────────────────┘\n\n┌─────────────────────────────────────────────────────────────────────────────┐\n│ GROUP: ROOFING                                                              │\n│ Phase: ROOF                                                                 │\n├─────────────────────────────────────────────────────────────────────────────┤\n│ Categories:                                                                 │\n│   • OST_Roofs, OST_RoofsStructure, OST_RoofsInsulation, OST_RoofsDefault    │\n│   • OST_Fascia, OST_Gutter, OST_RoofSoffit, OST_Cornices                    │\n│                                                                             │\n│ Typical Works Sequence (flat roof):                                         │\n│   1. Structure (m²) - deck/slab                                             │\n│   2. Vapor barrier (m²)                                                     │\n│   3. Insulation (m²) - thermal insulation                                   │\n│   4. Waterproof membrane (m²)                                               │\n│   5. Protection layer (m²) - gravel, pavers                                 │\n│   6. Flashings (m) - at edges and penetrations                              │\n│                                                                             │\n│ Typical Works Sequence (pitched roof):                                      │\n│   1. Rafters/trusses (m² or pcs)                                            │\n│   2. Boarding/battens (m²)                                                  │\n│   3. Underlay membrane (m²)                                                 │\n│   4. Insulation (m²)                                                        │\n│   5. Covering (m²) - tiles, metal sheets, shingles                          │\n│   6. Ridge/hip tiles (m)                                                    │\n│   7. Gutters and downpipes (m)                                              │\n│   8. Fascia and soffit (m²)                                                 │\n│                                                                             │\n│ ✅ APPROPRIATE: structure, insulation, membrane, covering, gutters, fascia  │\n│ ❌ NOT APPROPRIATE: floor finishes, windows, interior works                 │\n└─────────────────────────────────────────────────────────────────────────────┘\n\n┌─────────────────────────────────────────────────────────────────────────────┐\n│ GROUP: STAIRS_RAMPS                                                         │\n│ Phase: STAIRS                                                               │\n├─────────────────────────────────────────────────────────────────────────────┤\n│ Categories:                                                                 │\n│   • OST_Stairs, OST_StairsRuns, OST_StairsStringerCarriage                  │\n│   • OST_StairsSupportsAboveCut, OST_StairsPaths                             │\n│   • OST_StairsRailing, OST_StairsRailingBaluster, OST_StairsRailingRail     │\n│   • OST_StairsRailingAboveCut                                               │\n│   • OST_Ramps, OST_RampsStringer                                            │\n│                                                                             │\n│ Typical Works Sequence (concrete stairs):                                   │\n│   1. Formwork (m²) - stair formwork                                         │\n│   2. Reinforcement (t)                                                      │\n│   3. Concrete (m³)                                                          │\n│   4. Tread finish (m²) - tiles, stone, nosings                              │\n│   5. Railings (m) - metal/glass/wood railings                               │\n│   6. Balustrades (pcs)                                                      │\n│                                                                             │\n│ Typical Works Sequence (steel/prefab stairs):                               │\n│   1. Steel stringers (t or m)                                               │\n│   2. Treads (pcs) - metal/wood treads                                       │\n│   3. Landings (m²)                                                          │\n│   4. Railings (m)                                                           │\n│   5. Finish (m²)                                                            │\n│                                                                             │\n│ ✅ APPROPRIATE: formwork, concrete, treads, railings, finishes              │\n│ ❌ NOT APPROPRIATE: roofing, MEP, landscaping                               │\n└─────────────────────────────────────────────────────────────────────────────┘\n\n┌─────────────────────────────────────────────────────────────────────────────┐\n│ GROUP: WINDOWS                                                              │\n│ Phase: WINDOWS                                                              │\n├─────────────────────────────────────────────────────────────────────────────┤\n│ Categories:                                                                 │\n│   • OST_Windows                                                             │\n│                                                                             │\n│ Typical Works Sequence:                                                     │\n│   1. Frame installation (pcs) - window frame                                │\n│   2. Glazing (m²) - glass units                                             │\n│   3. Hardware (pcs) - handles, hinges, locks                                │\n│   4. Sealing (m) - foam, sealant                                            │\n│   5. Internal sill (m) - windowsill                                         │\n│   6. External sill (m) - drip sill                                          │\n│   7. Trim/architrave (m) - internal finishing                               │\n│                                                                             │\n│ ✅ APPROPRIATE: frame, glazing, hardware, sealing, sills, trim              │\n│ ❌ NOT APPROPRIATE: concrete, structural works, MEP                         │\n└─────────────────────────────────────────────────────────────────────────────┘\n\n┌─────────────────────────────────────────────────────────────────────────────┐\n│ GROUP: DOORS                                                                │\n│ Phase: DOORS                                                                │\n├─────────────────────────────────────────────────────────────────────────────┤\n│ Categories:                                                                 │\n│   • OST_Doors                                                               │\n│                                                                             │\n│ Typical Works Sequence:                                                     │\n│   1. Frame installation (pcs) - door frame                                  │\n│   2. Door leaf (pcs) - door panel                                           │\n│   3. Hardware (set) - hinges, handles, locks, closers                       │\n│   4. Sealing (m) - weatherstripping, threshold                              │\n│   5. Trim/architrave (m) - finishing trim                                   │\n│   6. Threshold (pcs) - door threshold                                       │\n│   7. Glazing (m²) - if glazed door                                          │\n│                                                                             │\n│ ✅ APPROPRIATE: frame, leaf, hardware, sealing, trim, threshold             │\n│ ❌ NOT APPROPRIATE: concrete, roofing, electrical                           │\n└─────────────────────────────────────────────────────────────────────────────┘\n\n┌─────────────────────────────────────────────────────────────────────────────┐\n│ GROUP: CEILINGS                                                             │\n│ Phase: CEILINGS, INT_FINISH                                                 │\n├─────────────────────────────────────────────────────────────────────────────┤\n│ Categories:                                                                 │\n│   • OST_Ceilings                                                            │\n│                                                                             │\n│ Typical Works Sequence (suspended ceiling):                                 │\n│   1. Suspension system (m²) - hangers, main runners                         │\n│   2. Grid (m²) - T-bar grid                                                 │\n│   3. Tiles/panels (m²) - ceiling tiles                                      │\n│   4. Access panels (pcs)                                                    │\n│   5. Edge trim (m) - perimeter trim                                         │\n│                                                                             │\n│ Typical Works Sequence (drywall ceiling):                                   │\n│   1. Metal framing (m²)                                                     │\n│   2. Boarding (m²) - gypsum boards                                          │\n│   3. Jointing (m²)                                                          │\n│   4. Paint (m²)                                                             │\n│                                                                             │\n│ ✅ APPROPRIATE: suspension, grid, tiles, framing, boarding, paint           │\n│ ❌ NOT APPROPRIATE: roofing, foundations, structural concrete               │\n└─────────────────────────────────────────────────────────────────────────────┘\n\n┌─────────────────────────────────────────────────────────────────────────────┐\n│ GROUP: INTERIOR_FURNITURE                                                   │\n│ Phase: FURNITURE, CASEWORK, INT_FINISH                                      │\n├─────────────────────────────────────────────────────────────────────────────┤\n│ Categories:                                                                 │\n│   • OST_Furniture, OST_FurnitureSystems                                     │\n│   • OST_Casework                                                            │\n│   • OST_Reveals                                                             │\n│                                                                             │\n│ Typical Works Sequence:                                                     │\n│   1. Delivery (pcs)                                                         │\n│   2. Assembly (pcs) - if required                                           │\n│   3. Installation (pcs) - placement and fixing                              │\n│   4. Connection (pcs) - to utilities if needed                              │\n│   5. Adjustment (pcs) - final adjustments                                   │\n│                                                                             │\n│ ✅ APPROPRIATE: delivery, assembly, installation, fixing                    │\n│ ❌ NOT APPROPRIATE: structural works, roofing, excavation                   │\n└─────────────────────────────────────────────────────────────────────────────┘\n\n┌─────────────────────────────────────────────────────────────────────────────┐\n│ GROUP: REINFORCEMENT                                                        │\n│ Phase: (follows host element phase)                                         │\n├─────────────────────────────────────────────────────────────────────────────┤\n│ Categories:                                                                 │\n│   • OST_Rebar, OST_RebarCover, OST_RebarShape                               │\n│   • OST_FabricReinforcement                                                 │\n│   • OST_Coupler                                                             │\n│   • OST_StructuralTendons                                                   │\n│   • OST_StructConnections, OST_StructConnectionPlates                       │\n│   • OST_StructConnectionBolts, OST_StructConnectionOthers                   │\n│   • OST_StructConnectionModifiers, OST_ConnectorElem                        │\n│                                                                             │\n│ Typical Works Sequence:                                                     │\n│   1. Rebar cutting (t)                                                      │\n│   2. Rebar bending (t)                                                      │\n│   3. Rebar installation (t)                                                 │\n│   4. Mesh installation (m²)                                                 │\n│   5. Couplers (pcs)                                                         │\n│   6. Post-tensioning (t) - for tendons                                      │\n│                                                                             │\n│ ✅ APPROPRIATE: cutting, bending, placing, tying, coupling, tensioning      │\n│ ❌ NOT APPROPRIATE: concrete, formwork, finishes (these are separate)       │\n└─────────────────────────────────────────────────────────────────────────────┘\n\n┌─────────────────────────────────────────────────────────────────────────────┐\n│ GROUP: MEP_HVAC                                                             │\n│ Phase: HVAC, VENTILATION, AC, HEATING                                       │\n├─────────────────────────────────────────────────────────────────────────────┤\n│ Categories:                                                                 │\n│   • OST_DuctCurves, OST_DuctFitting, OST_DuctTerminal                       │\n│   • OST_DuctSystem, OST_DuctAccessory                                       │\n│   • OST_DuctInsulations, OST_DuctLinings                                    │\n│   • OST_DuctCurvesRiseDrop, OST_DuctCurvesDrop                              │\n│   • OST_PlaceHolderDucts                                                    │\n│   • OST_FabricationDuctwork, OST_FabricationHangers                         │\n│   • OST_FabricationDuctworkRise                                             │\n│   • OST_HVAC_Zones, OST_HVAC_Zones_Boundary                                 │\n│                                                                             │\n│ Typical Works Sequence:                                                     │\n│   1. Supports/hangers (pcs) - duct supports                                 │\n│   2. Ductwork (m² or m) - duct installation                                 │\n│   3. Fittings (pcs) - bends, tees, reducers                                 │\n│   4. Insulation (m²) - duct insulation                                      │\n│   5. Terminals (pcs) - grilles, diffusers                                   │\n│   6. Accessories (pcs) - dampers, VAV boxes                                 │\n│   7. Testing (m²) - air balancing, leak testing                             │\n│                                                                             │\n│ ✅ APPROPRIATE: ductwork, fittings, insulation, terminals, testing          │\n│ ❌ NOT APPROPRIATE: structural concrete, masonry, roofing                   │\n└─────────────────────────────────────────────────────────────────────────────┘\n\n┌─────────────────────────────────────────────────────────────────────────────┐\n│ GROUP: MEP_PLUMBING                                                         │\n│ Phase: PLUMB, FIXTURES, SEWAGE                                              │\n├─────────────────────────────────────────────────────────────────────────────┤\n│ Categories:                                                                 │\n│   • OST_PlumbingFixtures, OST_PlumbingEquipment                             │\n│   • OST_PipeCurves, OST_PipeFitting, OST_PipeAccessory                      │\n│   • OST_PipeInsulations, OST_PipingSystem, OST_PipeSegments                 │\n│   • OST_FlexPipeCurves, OST_PlaceHolderPipes                                │\n│   • OST_PipeCurvesDrop, OST_PipeConnections, OST_PipeSchedules              │\n│   • OST_FabricationPipework, OST_FabricationPipeworkRise                    │\n│   • OST_FabricationPipeworkDrop, OST_FabricationPipeworkInsulation          │\n│   • OST_Fluids, OST_Sprinklers                                              │\n│                                                                             │\n│ Typical Works Sequence (piping):                                            │\n│   1. Supports/hangers (pcs)                                                 │\n│   2. Piping (m) - pipe installation                                         │\n│   3. Fittings (pcs) - elbows, tees, valves                                  │\n│   4. Insulation (m) - pipe insulation                                       │\n│   5. Testing (m) - pressure testing                                         │\n│                                                                             │\n│ Typical Works Sequence (fixtures):                                          │\n│   1. Rough-in (pcs) - connection points                                     │\n│   2. Fixture installation (pcs)                                             │\n│   3. Trim (pcs) - faucets, accessories                                      │\n│   4. Connection (pcs)                                                       │\n│   5. Testing (pcs)                                                          │\n│                                                                             │\n│ ✅ APPROPRIATE: piping, fittings, fixtures, insulation, testing             │\n│ ❌ NOT APPROPRIATE: structural works, roofing, landscaping                  │\n└─────────────────────────────────────────────────────────────────────────────┘\n\n┌─────────────────────────────────────────────────────────────────────────────┐\n│ GROUP: MEP_ELECTRICAL                                                       │\n│ Phase: ELEC, LIGHTING, LOW_VOLTAGE                                          │\n├─────────────────────────────────────────────────────────────────────────────┤\n│ Categories:                                                                 │\n│   • OST_ElectricalEquipment, OST_ElectricalCircuit                          │\n│   • OST_Wire, OST_WireHomeRunArrows, OST_WireMaterials                      │\n│   • OST_CableTray, OST_CableTrayFitting, OST_CableTrayRun                   │\n│   • OST_CableTrayRiseDrop, OST_CableTrayDrop                                │\n│   • OST_Conduit, OST_ConduitFitting, OST_ConduitRun                         │\n│   • OST_ConduitRiseDrop, OST_ConduitDrop, OST_ConduitStandards              │\n│   • OST_FabricationContainment, OST_FabricationContainmentRise              │\n│   • OST_SwitchSystem                                                        │\n│   • OST_LightingDevices                                                     │\n│                                                                             │\n│ Typical Works Sequence:                                                     │\n│   1. Containment (m) - cable tray, conduit                                  │\n│   2. Wiring (m) - cable pulling                                             │\n│   3. Equipment (pcs) - panels, switchgear                                   │\n│   4. Devices (pcs) - outlets, switches                                      │\n│   5. Fixtures (pcs) - lighting fixtures                                     │\n│   6. Terminations (pcs)                                                     │\n│   7. Testing (pcs) - circuit testing                                        │\n│                                                                             │\n│ ✅ APPROPRIATE: containment, wiring, equipment, devices, testing            │\n│ ❌ NOT APPROPRIATE: structural concrete, plumbing, roofing                  │\n└─────────────────────────────────────────────────────────────────────────────┘\n\n┌─────────────────────────────────────────────────────────────────────────────┐\n│ GROUP: MEP_DEVICES                                                          │\n│ Phase: ELEC, LOW_VOLTAGE, FIRE_PROT                                         │\n├─────────────────────────────────────────────────────────────────────────────┤\n│ Categories:                                                                 │\n│   • OST_MechanicalEquipment, OST_MechanicalControlDevices                   │\n│   • OST_FireAlarmDevices, OST_SecurityDevices                               │\n│   • OST_CommunicationDevices, OST_DataDevices                               │\n│   • OST_TelephoneDevices, OST_NurseCallDevices                              │\n│                                                                             │\n│ Typical Works Sequence:                                                     │\n│   1. Mounting (pcs) - brackets, backboxes                                   │\n│   2. Device installation (pcs)                                              │\n│   3. Wiring connection (pcs)                                                │\n│   4. Programming (pcs) - for smart devices                                  │\n│   5. Testing (pcs)                                                          │\n│   6. Commissioning (pcs)                                                    │\n│                                                                             │\n│ ✅ APPROPRIATE: mounting, installation, wiring, programming, testing        │\n│ ❌ NOT APPROPRIATE: structural works, masonry, landscaping                  │\n└─────────────────────────────────────────────────────────────────────────────┘\n\n┌─────────────────────────────────────────────────────────────────────────────┐\n│ GROUP: SITE_LANDSCAPE                                                       │\n│ Phase: LANDSCAPE, EARTH, PAVING, SITE_EQUIP                                 │\n├─────────────────────────────────────────────────────────────────────────────┤\n│ Categories:                                                                 │\n│   • OST_Site, OST_Topography, OST_TopographySurface, OST_TopographyContours │\n│   • OST_Planting, OST_Entourage                                             │\n│   • OST_Roads, OST_Parking                                                  │\n│   • OST_SiteProperty, OST_SitePointBoundary                                 │\n│   • OST_ApproachSlabs                                                       │\n│                                                                             │\n│ Typical Works Sequence (earthworks):                                        │\n│   1. Clearing (m²) - site clearing                                          │\n│   2. Excavation (m³) - cut                                                  │\n│   3. Fill (m³) - embankment                                                 │\n│   4. Grading (m²) - leveling                                                │\n│   5. Compaction (m²)                                                        │\n│                                                                             │\n│ Typical Works Sequence (planting):                                          │\n│   1. Pit excavation (m³)                                                    │\n│   2. Soil preparation (m²)                                                  │\n│   3. Planting (pcs) - trees, shrubs                                         │\n│   4. Staking (pcs) - tree supports                                          │\n│   5. Mulching (m²)                                                          │\n│   6. Irrigation (m)                                                         │\n│                                                                             │\n│ Typical Works Sequence (paving):                                            │\n│   1. Subbase (m²)                                                           │\n│   2. Base course (m²)                                                       │\n│   3. Surface (m²) - asphalt, concrete, pavers                               │\n│   4. Edging (m) - kerbs                                                     │\n│   5. Markings (m²)                                                          │\n│                                                                             │\n│ ✅ APPROPRIATE: earthwork, planting, paving, drainage, fencing, irrigation  │\n│ ❌ NOT APPROPRIATE: building structure, interior finishes, building MEP     │\n└─────────────────────────────────────────────────────────────────────────────┘\n\n┌─────────────────────────────────────────────────────────────────────────────┐\n│ GROUP: GENERIC_SPECIALTY                                                    │\n│ Phase: EQUIPMENT, GENERIC                                                   │\n├─────────────────────────────────────────────────────────────────────────────┤\n│ Categories:                                                                 │\n│   • OST_GenericModel                                                        │\n│   • OST_SpecialityEquipment                                                 │\n│   • OST_Parts                                                               │\n│   • OST_ShaftOpening                                                        │\n│                                                                             │\n│ ⚠️ ANALYZE TYPE_NAME to determine appropriate works!                        │\n│                                                                             │\n│ For OST_GenericModel - check type_name for:                                 │\n│   - If contains \"window/door\" → use OPENINGS works                          │\n│   - If contains \"equipment/machine\" → use EQUIPMENT works                   │\n│   - If contains \"furniture\" → use FURNITURE works                           │\n│   - If contains \"structural\" → use STRUCTURAL works                         │\n│                                                                             │\n│ Typical Works Sequence (equipment):                                         │\n│   1. Delivery (pcs)                                                         │\n│   2. Rigging/positioning (pcs)                                              │\n│   3. Installation (pcs)                                                     │\n│   4. Connection (pcs) - utilities                                           │\n│   5. Commissioning (pcs)                                                    │\n│                                                                             │\n│ Typical Works Sequence (shaft opening):                                     │\n│   1. Opening formation (m²)                                                 │\n│   2. Edge protection (m)                                                    │\n│   3. Waterproofing (m²) - if required                                       │\n└─────────────────────────────────────────────────────────────────────────────┘\n\n═══════════════════════════════════════════════════════════════════════════════\nELEMENT TO ANALYZE\n═══════════════════════════════════════════════════════════════════════════════\n\nType: {{ $json.type_name }}\nCategory: {{ $json.category }}\nElement Count: {{ $json.element_count }}\nProject Type: {{ $json.project_type }}\n\nBIM Parameters:\n{{ JSON.stringify($json.quantities, null, 2) }}\n\nMaterial Layers:\n{{ JSON.stringify($json.material_parameters, null, 2) }}\n\n═══════════════════════════════════════════════════════════════════════════════\nCRITICAL RULES\n═══════════════════════════════════════════════════════════════════════════════\n\n1. NEVER return empty work_items array - minimum 2 works per element\n2. Works MUST be DIRECTLY RELEVANT to the element's CATEGORY (see mapping above)\n3. All output in {{ $json.language }} language\n4. search_query must contain specific construction terms for database search\n5. Match CATEGORY to the correct GROUP, then use appropriate works from that group\n\n═══════════════════════════════════════════════════════════════════════════════\nQUANTITY RULES\n═══════════════════════════════════════════════════════════════════════════════\n\nUse actual BIM parameters:\n- AREA works: bim_parameter: \"Area\"\n- VOLUME works: bim_parameter: \"Volume\"  \n- LENGTH works: bim_parameter: \"Length\"\n- COUNT works: bim_parameter: \"element_count\"\n\n═══════════════════════════════════════════════════════════════════════════════\nOUTPUT FORMAT (pure JSON, no markdown!)\n═══════════════════════════════════════════════════════════════════════════════\n\n{\n  \"element_analysis\": {\n    \"category_group\": \"foundations|structural_frame|floors|walls|curtain_walls|roofing|stairs_ramps|windows|doors|ceilings|interior_furniture|reinforcement|mep_hvac|mep_plumbing|mep_electrical|mep_devices|site_landscape|generic_specialty\",\n    \"detected_phase\": \"FOUND|FRAME|FLOORS|WALLS|CURTAIN|ROOF|STAIRS|WINDOWS|DOORS|CEILINGS|INT_FINISH|HVAC|PLUMB|ELEC|LANDSCAPE|EQUIPMENT\",\n    \"complexity\": \"simple|standard|composite|complex\"\n  },\n  \"work_items\": [\n    {\n      \"work_id\": \"W001\",\n      \"work_name\": \"Work name in {{ $json.language }}\",\n      \"search_query\": \"Specific construction search terms in {{ $json.language }}\",\n      \"expected_unit\": \"m²|m³|m|pcs|t|set\",\n      \"quantity_source\": {\n        \"method\": \"direct|formula\",\n        \"bim_parameter\": \"Area|Volume|Length|element_count\",\n        \"coefficient\": 1.0\n      },\n      \"work_sequence\": 1\n    }\n  ]\n}\n\n═══════════════════════════════════════════════════════════════════════════════\nLANGUAGE REQUIREMENT\n═══════════════════════════════════════════════════════════════════════════════\n\nAll work_name and search_query MUST be in {{ $json.language }}!"}]}},"typeVersion":3.4},{"id":"7fe776fc-ab83-4d9f-8a12-1d9ba0a4b13c","name":"STAGE 4 - Decompose Type to Works","type":"@n8n/n8n-nodes-langchain.chainLlm","position":[880,2272],"parameters":{"messages":{"messageValues":[{"message":"={{ $('CONFIG - STAGE 4').item.json.chatInput }}"}]}},"typeVersion":1.4},{"id":"634179c4-6ea4-4d20-8df8-8f7a80dc681e","name":"CONFIG - STAGE 7.5","type":"n8n-nodes-base.set","position":[1600,2784],"parameters":{"options":{},"assignments":{"assignments":[{"id":"c48c44f4-3de2-4d6c-844a-094a20753081","name":"system_prompt","type":"string","value":"You are a Chief Technical Officer reviewing a cost estimate.\n\nCHECK FOR:\n1. COMPLETENESS - All necessary works included?\n2. DUPLICATIONS - Overlapping rates?\n3. LOGICAL SEQUENCE - Correct order?\n4. MISSING WORKS - What should be added?\n5. UNIT CONSISTENCY - Correct measurement units?"},{"id":"97f36ffa-5c2c-4ee6-a46d-9dbf9c71433d","name":"chatInput","type":"string","value":"You are a Chief Technical Officer reviewing a cost estimate.\n\n{{ $json.system_prompt_lang }}\n\n**CHECK FOR:**\n1. COMPLETENESS - All necessary works included?\n2. DUPLICATIONS - Overlapping rates?\n3. LOGICAL SEQUENCE - Correct order?\n4. MISSING WORKS - What should be added?"}]}},"typeVersion":3.4},{"id":"47918aa9-e57f-4f64-b6ea-c5819183ccd3","name":"STAGE 7.5 - Validate Type Works","type":"@n8n/n8n-nodes-langchain.chainLlm","position":[1728,2784],"parameters":{"messages":{"messageValues":[{"message":"={{ $('CONFIG - STAGE 7.5').item.json.chatInput }}"}]}},"typeVersion":1.4},{"id":"cb85bee0-e5f9-4b42-866c-fdc70db63ddd","name":"Write XLS File","type":"n8n-nodes-base.writeBinaryFile","position":[1664,3152],"parameters":{"options":{},"fileName":"={{ $json.xls_path }}","dataPropertyName":"xls"},"typeVersion":1},{"id":"7a3fc3da-4e28-4ad3-8794-af6d7dd3086d","name":"Google Gemini Chat Model","type":"@n8n/n8n-nodes-langchain.lmChatGoogleGemini","disabled":true,"position":[352,3376],"parameters":{"options":{},"modelName":"models/gemini-2.5-pro"},"typeVersion":1},{"id":"03bf88fa-7768-49f9-bdc7-736edd689357","name":"Anthropic Chat Model2","type":"@n8n/n8n-nodes-langchain.lmChatAnthropic","disabled":true,"position":[352,3200],"parameters":{"model":{"__rl":true,"mode":"list","value":"claude-opus-4-20250514","cachedResultName":"Claude Opus 4"},"options":{}},"typeVersion":1.3},{"id":"5acd0b0f-4fb0-4c70-b94f-dc4fc2fb472e","name":"OpenRouter Chat Model1","type":"@n8n/n8n-nodes-langchain.lmChatOpenRouter","disabled":true,"position":[192,3200],"parameters":{"options":{}},"typeVersion":1},{"id":"0d303c9a-12c7-43a7-8242-9eb3d5cea0aa","name":"xAI Grok Chat Model1","type":"@n8n/n8n-nodes-langchain.lmChatXAiGrok","disabled":true,"position":[192,3376],"parameters":{"model":"grok-4-0709","options":{}},"typeVersion":1},{"id":"8c424e84-3e21-4735-97b3-557f19f38c40","name":"Open HTML in Browser","type":"n8n-nodes-base.executeCommand","position":[1824,3296],"parameters":{"command":"={{ 'start \"\" \"' + $json.html_path + '\"' }}"},"typeVersion":1},{"id":"e87484f7-9c19-4537-8dc9-9fc58ee22518","name":"STAGE 5.1 - Embeddings","type":"@n8n/n8n-nodes-langchain.embeddingsOpenAi","position":[1648,2512],"parameters":{"model":"text-embedding-3-large","options":{"batchSize":50,"dimensions":3072,"stripNewLines":true}},"credentials":{"openAiApi":{"id":"credential-id","name":"OpenAi account WS"}},"typeVersion":1.2},{"id":"afb9f11d-87f1-4c36-88b9-90b94a650ced","name":"Sticky Note1","type":"n8n-nodes-base.stickyNote","position":[1920,176],"parameters":{"width":356,"height":132,"content":"⭐ **If you find our tools helpful**, please consider **starring our repository** on [GitHub](https://github.com/datadrivenconstruction/OpenConstructionEstimate-DDC-CWICR). \n\nYour support helps us improve and continue developing open solutions for the community!\n"},"typeVersion":1},{"id":"f30ffe46-34d0-4b9f-accd-eb2e92a3a2eb","name":"Header","type":"n8n-nodes-base.stickyNote","position":[144,0],"parameters":{"color":5,"width":2152,"height":144,"content":"## 🚀 CAD (BIM) Cost Estimation Pipeline with DDC CWICR (for Revit 2015-2026)\n**Automated cost estimation from Revit/BIM models**\nDataDrivenConstruction [GitHub](https://github.com/datadrivenconstruction/OpenConstructionEstimate-DDC-CWICR)\n\n⭐ **Star our repository** if you find this helpful!"},"typeVersion":1},{"id":"399d3cbc-2cd4-48fd-9c94-a6247c329af1","name":"Configuration","type":"n8n-nodes-base.stickyNote","position":[432,160],"parameters":{"color":4,"width":512,"height":804,"content":"## ⚙️ Configuration\n\nConfigure these settings before running:\n\n**Setup - Define file paths:**\n- `path_to_converter` — Path to RvtExporter.exe\n- `project_file` — Path to .rvt file\n\n## language_code\n- **AR** – Arabic. Price level – Dubai\n- **ZH** – Chinese. Price level – Shanghai\n- **DE** – German. Price level – Berlin\n- **EN** – English. Price level – Toronto\n- **ES** – Spanish. Price level – Barcelona\n- **FR** – French. Price level – Paris\n- **HI** – Hindi. Price level – Mumbai\n- **PT** – Portuguese. Price level – São Paulo\n- **RU** – Russian. Price level – St. Petersburg\n\n\n\n\n\n\n\n\n\n\n\n\n\n**Configure Language & Vector DB:**\n- `language` — Output language (German, English, Russian)\n- `search_lang` — Vector DB search language\n- `currency` — EUR, USD, RUB\n- `pricing_level` — City for pricing\n- `qdrant_url` — Vector database URL\n- `collection` — Qdrant collection name\n"},"typeVersion":1},{"id":"26062010-2431-4080-9af5-d53e9ba4832c","name":"Block 1 - Conversion","type":"n8n-nodes-base.stickyNote","position":[960,160],"parameters":{"color":5,"width":1336,"height":796,"content":"## Block 1: Conversion Block\n\nThis block:\n- Checks if Excel file exists from Revit project\n- If not, runs converter to extract BIM data\n- If yes, skips conversion to save time\n\n**Key nodes:**\n- Check - Does Excel file exist?\n- Extract - Run converter\n- Merge - Continue workflow"},"typeVersion":1},{"id":"12edc155-01dd-49da-8e1a-e8cf532eebd6","name":"Block 2 - Data Loading","type":"n8n-nodes-base.stickyNote","position":[144,992],"parameters":{"color":6,"width":280,"height":664,"content":"## Block 2: Data Loading & AI Classification\n\nThis block:\n- Loads Excel data with BIM elements\n- Filters 3D View elements only\n- AI analyzes headers and decides aggregation rules\n- Groups data by Type Name\n- AI classifies building vs non-building elements\n\n**Key nodes:**\n- Read Excel, Parse Excel\n- AI Analyze All Headers\n- AI Classify Categories\n- Apply Classification to Groups"},"typeVersion":1},{"id":"b43cba69-0975-470e-ac4f-7176da2feecf","name":"Block 3 - Stages 0-3","type":"n8n-nodes-base.stickyNote","position":[144,1680],"parameters":{"color":5,"width":280,"height":356,"content":"## Block 3: Project Analysis (Stages 0-3)\n\nThis block:\n- **STAGE 0:** Collects filtered BIM data\n- **STAGE 1:** AI detects project type (Residential/Commercial/Industrial)\n- **STAGE 2:** AI generates construction phases\n- **STAGE 3:** AI assigns element types to phases\n\n**Output:** Structured project with phases and assigned types"},"typeVersion":1},{"id":"ed3e6dc9-a5b7-4e99-8ed6-3bae51864eb6","name":"Block 4 - Decomposition","type":"n8n-nodes-base.stickyNote","position":[432,2048],"parameters":{"color":6,"width":744,"height":684,"content":""},"typeVersion":1},{"id":"6d104b01-8d1a-4e59-ad02-8fd0e9037a2d","name":"Block 5 - Pricing","type":"n8n-nodes-base.stickyNote","position":[1200,2048],"parameters":{"color":5,"width":1092,"height":684,"content":"## Block 5: Pricing & Calculation (Stages 5-7)\n\nThis block:\n- **STAGE 5:** Vector search in Qdrant for pricing rates\n- **STAGE 6:** Maps BIM units → Rate units (m² → 100 m²)\n- **STAGE 7:** Calculates costs (Qty × Unit Price)\n\n**Database:** DDC CWICR with 700,000+ rates"},"typeVersion":1},{"id":"32e11d4f-e58d-4bc3-8a72-7f7aa4799847","name":"Block 6 - Validation","type":"n8n-nodes-base.stickyNote","position":[688,2752],"parameters":{"color":6,"width":1616,"height":332,"content":"## Block 6: Validation & Aggregation\n\nThis block:\n- **STAGE 7.5:** AI validates work completeness\n- Checks for duplications and missing works\n- Aggregates results by type\n- **STAGE 8:** Aggregates all costs by phases\n\n**Output:** Complete cost structure by phases"},"typeVersion":1},{"id":"bf71a213-dbf1-4f2b-975c-0b4aea9c99bd","name":"Block 7 - Reports","type":"n8n-nodes-base.stickyNote","position":[688,3136],"parameters":{"color":5,"width":1288,"height":436,"content":"## Block 7: Report Generation\n\nThis block:\n- **STAGE 9:** Generates professional HTML report\n- Creates Excel-compatible XLS file\n- Saves to project folder\n- Opens HTML in browser\n\n**Features:**\n- Quality indicators (●)\n- Calculation formulas\n- Clickable rate links\n- Multi-language support"},"typeVersion":1},{"id":"8b516af2-adaf-4621-b479-ac32802f5aaf","name":"LLM Models","type":"n8n-nodes-base.stickyNote","position":[144,2752],"parameters":{"width":504,"height":816,"content":"## 🧠 Available AI Models\n\nConnect any model to LLM Chain nodes:\n\n- **OpenAI** — GPT-4o (default)\n- **Anthropic** — Claude 3.5\n- **Google Gemini** — Gemini Pro\n- **OpenRouter** — Multiple models\n- **xAI Grok** — Grok models\n\nModels are used for:\n- Header analysis\n- Category classification\n- Project type detection\n- Phase generation\n- Work decomposition\n- Validation"},"typeVersion":1},{"id":"909e24ad-f23d-493d-8e53-565573349432","name":"Pipeline Overview","type":"n8n-nodes-base.stickyNote","position":[144,160],"parameters":{"color":7,"width":268,"height":300,"content":"## 📊 Pipeline Overview\n\n| Stage | Description |\n|-------|-------------|\n| 0 | Collect BIM data |\n| 1 | Detect project type |\n| 2 | Generate phases |\n| 3 | Assign types to phases |\n| 4 | Decompose to works |\n| 5 | Vector search pricing |\n| 6 | Map units |\n| 7 | Calculate costs |\n| 7.5 | Validate works |\n| 8 | Aggregate by phases |\n| 9 | Generate reports |"},"typeVersion":1},{"id":"c5a7f161-9e5c-4231-8baa-2cf33aad8d6b","name":"Output Files","type":"n8n-nodes-base.stickyNote","position":[1280,3344],"parameters":{"color":7,"width":308,"height":172,"content":"## 📁 Output Files\n\nSaved to project folder:\n```\nproject_2024-12-08.html\nproject_2024-12-08.xls\n```"},"typeVersion":1},{"id":"e1bfa371-8021-4b4c-b4c4-642e90b858e0","name":"Block 2 - Data Loading1","type":"n8n-nodes-base.stickyNote","position":[432,992],"parameters":{"color":6,"width":1864,"height":664,"content":""},"typeVersion":1},{"id":"dd8eb1e6-5f13-4035-b27f-e334d345ca60","name":"Block 3 - Stages 0-","type":"n8n-nodes-base.stickyNote","position":[432,1680],"parameters":{"color":5,"width":1864,"height":356,"content":""},"typeVersion":1},{"id":"a67b0b43-e46f-4943-96ca-a033c715c6f8","name":"Block 4 - Decomposition1","type":"n8n-nodes-base.stickyNote","position":[144,2048],"parameters":{"color":6,"width":280,"height":684,"content":"## Block 4: Work Decomposition Loop\n\nThis block processes each BIM type:\n\n1. **STAGE 4:** AI decomposes type into work items\n2. Loop through each work item\n3. Prepare search queries for pricing\n\n**Example:**\nWindow type → Demolition, Installation, Sealing, Hardware\n\n**Key nodes:**\n- Loop Types for Decomposition\n- STAGE 4 - Decompose Type to Works\n- Has Work Items? (filter)"},"typeVersion":1},{"id":"50429dc7-2570-43d5-8c67-f5c36a82ce58","name":"DeepSeek Chat Model","type":"@n8n/n8n-nodes-langchain.lmChatDeepSeek","disabled":true,"position":[496,3376],"parameters":{"options":{}},"typeVersion":1},{"id":"8888b38c-d023-4881-9440-efa6e64dda1f","name":"OpenAI LLM","type":"@n8n/n8n-nodes-langchain.lmChatOpenAi","position":[496,3200],"parameters":{"model":{"__rl":true,"mode":"list","value":"chatgpt-4o-latest","cachedResultName":"chatgpt-4o-latest"},"options":{"temperature":0}},"credentials":{"openAiApi":{"id":"credential-id","name":"OpenAi account WS"}},"typeVersion":1.2},{"id":"0437d044-9907-4cc8-a8a6-20f6d1d34ebd","name":"Save Type Before LLM","type":"n8n-nodes-base.code","position":[592,2272],"parameters":{"jsCode":"// Save Type Data to staticData BEFORE LLM call\n// FIXED v12.8: Use type_cache[typeKey] to prevent race condition\n\nconst typeData = $input.first().json;\nconst staticData = $getWorkflowStaticData('global');\n\n// Create unique key for this type\nconst typeKey = `${typeData.type_name || 'Unknown'}|||${typeData.category || ''}`;\n\n// Initialize cache if needed\nif (!staticData.type_cache) {\n  staticData.type_cache = {};\n}\n\n// Save type data with typeKey (NOT overwriting other types!)\nstaticData.type_cache[typeKey] = {\n  type_name: typeData.type_name,\n  type_index: typeData.type_index,\n  total_types: typeData.total_types,\n  category: typeData.category,\n  assigned_phase: typeData.assigned_phase,\n  quantities: typeData.quantities || {},\n  element_count: typeData.element_count || 1,\n  qdrant_collection: typeData.qdrant_collection,\n  language: typeData.language,\n  currency: typeData.currency,\n  locale: typeData.locale,\n  system_prompt_lang: typeData.system_prompt_lang,\n  search_lang: typeData.search_lang,\n  _typeKey: typeKey,\n  _saved_at: new Date().toISOString()\n};\n\n// Also save as current (for backwards compatibility)\nstaticData.current_type_key = typeKey;\n\nconsole.log(`Saved type [${typeKey}] to cache`);\nconsole.log(`Cache now has ${Object.keys(staticData.type_cache).length} types`);\n\n// Pass through with typeKey added\nreturn [{ json: { ...typeData, _typeKey: typeKey } }];"},"typeVersion":2},{"id":"56648eeb-40d5-4037-af02-087f6fb63014","name":"Sticky Note3","type":"n8n-nodes-base.stickyNote","position":[1344,1024],"parameters":{"width":336,"height":112,"content":"### 🤖 AI Classification \nof parameters for selecting the aggregation method"},"typeVersion":1},{"id":"200332c3-be2f-440a-96c0-724a7b7a5257","name":"Sticky Note","type":"n8n-nodes-base.stickyNote","position":[768,1408],"parameters":{"width":352,"height":128,"content":"### 🤖  AI classification \n(optional) highlighting of building elements"},"typeVersion":1},{"id":"9e3d1e70-bc93-4d5e-a760-744be8d264bb","name":"Sticky Note2","type":"n8n-nodes-base.stickyNote","position":[1872,1392],"parameters":{"width":336,"height":144,"content":"### 🤖 AI Project Type Detection \n(renovation, construction, demolition)"},"typeVersion":1},{"id":"2e90afd6-2aa3-4b81-ba77-af3cae17b585","name":"Sticky Note4","type":"n8n-nodes-base.stickyNote","position":[656,1696],"parameters":{"width":352,"height":96,"content":"### 🤖 AI Developing \na step-by-step work plan"},"typeVersion":1},{"id":"832c18c0-2f7f-4fd2-be2a-7f90c42a1a08","name":"Sticky Note6","type":"n8n-nodes-base.stickyNote","position":[1488,1696],"parameters":{"width":352,"height":96,"content":"### 🤖 AI Mapping\nElement Types → Work Plan Stages"},"typeVersion":1},{"id":"284a3f0e-e9f8-4140-bb96-e077d9e1dd9a","name":"Export All Outputs","type":"n8n-nodes-base.code","disabled":true,"position":[2016,3296],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════\n// EXPORT ALL NODE OUTPUTS\n// Подключи эту ноду в конец workflow\n// После неё добавь \"Write Binary File\" с Property Name: data\n// ═══════════════════════════════════════════════════════════════\n\nconst ALL_NODES = [\n  \"When clicking 'Execute workflow'\",\n  \"Setup - Define file paths1\",\n  \"Configure Language & Vector DB\",\n  \"Non-3D View Elements Output1\",\n  \"Find Category Fields\",\n  \"Apply Classification to Groups\",\n  \"Non-Building Elements Output1\",\n  \"Is Building Element1\",\n  \"Group Data with AI Rules1\",\n  \"Extract Headers and Data\",\n  \"Read Excel File1\",\n  \"Parse Excel1\",\n  \"Create - Excel filename1\",\n  \"Check - Does Excel file exist?1\",\n  \"If - File exists?1\",\n  \"Extract - Run converter1\",\n  \"Info - Skip conversion1\",\n  \"Check - Did extraction succeed?1\",\n  \"Error - Show what went wrong1\",\n  \"Set xlsx_filename after success1\",\n  \"Merge - Continue workflow1\",\n  \"Set Parameters1\",\n  \"Process AI Response1\",\n  \"On the standard 3D View\",\n  \"STAGE 0 - Collect BIM Data\",\n  \"Parse Stage 1 - Project Type\",\n  \"Parse Stage 2 - Phases\",\n  \"Parse Stage 3 - Final Structure\",\n  \"Prepare Types for Decomposition\",\n  \"Loop Types for Decomposition\",\n  \"Parse Decomposition & Prepare Works\",\n  \"Has Work Items?\",\n  \"Loop Work Items\",\n  \"STAGE 6 - Map Rate Units to BIM\",\n  \"Accumulate Work Results\",\n  \"Aggregate Type Works\",\n  \"Store Type Result\",\n  \"Handle No Works\",\n  \"STAGE 8 - Aggregate by Phases\",\n  \"STAGE 7 - Calculate Costs\",\n  \"STAGE 9 - Generate Cost Estimate\",\n  \"STAGE 7.5 - Parse Validation\",\n  \"STAGE 5.1 - Prepare Search Strategies1\",\n  \"STAGE 5.1 - Vector Search\",\n  \"STAGE 5.2 - Parse Results\",\n  \"Save to Project Folder\",\n  \"Write HTML File\",\n  \"CONFIG - AI Classify\",\n  \"AI Classify Categories\",\n  \"CONFIG - AI Headers\",\n  \"AI Analyze All Headers\",\n  \"CONFIG - STAGE 1\",\n  \"STAGE 1 - Detect Project Type\",\n  \"CONFIG - STAGE 2\",\n  \"STAGE 2 - Generate Construction Phases\",\n  \"CONFIG - STAGE 3\",\n  \"STAGE 3 - Assign Types to Phases\",\n  \"CONFIG - STAGE 4\",\n  \"STAGE 4 - Decompose Type to Works\",\n  \"CONFIG - STAGE 7.5\",\n  \"STAGE 7.5 - Validate Type Works\",\n  \"Write XLS File\",\n  \"Open HTML in Browser\",\n  \"STAGE 5.1 - Embeddings\",\n  \"Limit to 4 Groups1\",\n  \"Save Type Before LLM\"\n];\n\nlet txt = '═══════════════════════════════════════════════════════════════\\n';\ntxt += '              WORKFLOW OUTPUT DUMP\\n';\ntxt += '              ' + new Date().toISOString() + '\\n';\ntxt += '              Execution: ' + $execution.id + '\\n';\ntxt += '═══════════════════════════════════════════════════════════════\\n\\n';\n\nlet ok = 0, skip = 0;\n\nfor (const name of ALL_NODES) {\n  txt += '\\n' + '═'.repeat(60) + '\\n';\n  txt += 'NODE: ' + name + '\\n';\n  txt += '─'.repeat(60) + '\\n';\n  \n  try {\n    const data = $(name).all();\n    if (data && data.length > 0) {\n      txt += '✓ Items: ' + data.length + '\\n\\n';\n      data.forEach((item, i) => {\n        txt += '--- Item ' + (i+1) + ' ---\\n';\n        txt += JSON.stringify(item.json, null, 2) + '\\n';\n      });\n      ok++;\n    } else {\n      txt += '○ No output\\n';\n    }\n  } catch (e) {\n    txt += '✗ Not accessible\\n';\n    skip++;\n  }\n}\n\ntxt += '\\n\\n' + '═'.repeat(60) + '\\n';\ntxt += 'TOTAL: ' + ok + ' nodes exported, ' + skip + ' skipped\\n';\ntxt += '═'.repeat(60) + '\\n';\n\nconst ts = new Date().toISOString().replace(/[:.]/g, '-');\nconst fn = 'output_' + ts + '.txt';\n\nreturn [{\n  json: { filename: fn, exported: ok, skipped: skip },\n  binary: {\n    data: await this.helpers.prepareBinaryData(Buffer.from(txt), fn, 'text/plain')\n  }\n}];"},"typeVersion":2},{"id":"c78c2c17-ed39-439c-b808-9dd542f148b6","name":"Write Output File","type":"n8n-nodes-base.writeBinaryFile","disabled":true,"position":[2160,3296],"parameters":{"options":{},"fileName":"={{ $json.filename }}"},"typeVersion":1},{"id":"05708147-39ff-41a2-b3fc-e711095e6c03","name":"STAGE 2.5 - Compact Types1","type":"n8n-nodes-base.code","position":[1280,1776],"parameters":{"jsCode":"// STAGE 2.5 - Compact Types for Phase Assignment\n// Reduces data size for LLM (~90% token reduction)\n\nconst prevData = $('Parse Stage 2 - Phases').first().json;\nconst rawTypes = prevData.raw_types || [];\n\n// ═══════════════════════════════════════════════════════════════════════════\n// COMPACT TYPES - keep only what LLM needs for phase assignment\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst KEEP_QUANTITIES = [\n  'Element Count', 'Volume', 'Area', 'Length', 'Width', 'Height', \n  'Thickness', 'Perimeter', 'Default Thickness'\n];\n\nconst compactTypes = rawTypes.map((t, idx) => {\n  // Filter quantities - keep only essential ones\n  const quantities = {};\n  if (t.quantities) {\n    for (const key of KEEP_QUANTITIES) {\n      if (t.quantities[key] !== undefined && t.quantities[key] !== null) {\n        quantities[key] = t.quantities[key];\n      }\n    }\n  }\n  \n  // Extract useful context from additional_parameters\n  const params = t.additional_parameters || {};\n  \n  // Build compact object\n  const compact = {\n    type_index: t.type_index ?? idx,\n    type_name: t.type_name,\n    category: t.category,\n    element_count: t.element_count || 1,\n    quantities: quantities\n  };\n  \n  // Add optional fields only if they exist and are useful\n  if (params.Description) compact.description = params.Description;\n  if (params['Assembly Description']) compact.assembly = params['Assembly Description'];\n  if (params['Classification Title']) compact.classification = params['Classification Title'];\n  if (params.Structural === true) compact.is_structural = true;\n  \n  return compact;\n});\n\n// ═══════════════════════════════════════════════════════════════════════════\n// STATISTICS\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst originalSize = JSON.stringify(rawTypes).length;\nconst compactSize = JSON.stringify(compactTypes).length;\nconst reduction = Math.round((1 - compactSize / originalSize) * 100);\n\nconsole.log(`════════════════════════════════════════`);\nconsole.log(`📦 STAGE 2.5 - Compact Types`);\nconsole.log(`════════════════════════════════════════`);\nconsole.log(`   Types count: ${compactTypes.length}`);\nconsole.log(`   Original:    ${(originalSize / 1024).toFixed(1)} KB`);\nconsole.log(`   Compact:     ${(compactSize / 1024).toFixed(1)} KB`);\nconsole.log(`   Reduction:   ${reduction}%`);\nconsole.log(`════════════════════════════════════════\\n`);\n\n// ═══════════════════════════════════════════════════════════════════════════\n// OUTPUT\n// ═══════════════════════════════════════════════════════════════════════════\n\nreturn {\n  json: {\n    // Compact data for LLM (STAGE 3)\n    compact_types: compactTypes,\n    \n    // Original data preserved for later stages (STAGE 4+)\n    raw_types: rawTypes,\n    \n    // Pass through from Stage 2\n    construction_phases: prevData.construction_phases,\n    phase_logic: prevData.phase_logic,\n    total_phases: prevData.total_phases,\n    project_type: prevData.project_type,\n    project_scale: prevData.project_scale,\n    main_categories: prevData.main_categories,\n    types_count: prevData.types_count,\n    parameters_info: prevData.parameters_info,\n    project_description: prevData.project_description,\n    qdrant_collection: prevData.qdrant_collection,\n    pricing_standards: prevData.pricing_standards,\n    language: prevData.language,\n    currency: prevData.currency,\n    locale: prevData.locale,\n    system_prompt_lang: prevData.system_prompt_lang,\n    \n    // Metadata\n    compaction_stats: {\n      original_kb: Math.round(originalSize / 1024 * 10) / 10,\n      compact_kb: Math.round(compactSize / 1024 * 10) / 10,\n      reduction_percent: reduction\n    },\n    \n    stage: 'STAGE_2.5_COMPACTED'\n  }\n};"},"typeVersion":2},{"id":"2eb14aba-76e0-4bc3-93ba-693e6206d998","name":"CONFIG - STAGE 5","type":"n8n-nodes-base.set","position":[1488,1776],"parameters":{"options":{},"assignments":{"assignments":[{"id":"03f629ca-3aab-43b2-b70d-eca7f97a5c87","name":"system_prompt","type":"string","value":"You are a cost estimator. You need to DISTRIBUTE BIM elements across construction phases.\n\nFor each element type:\n1. Determine which PHASE it belongs to (use phase_id from phase list)\n2. Determine ORDER within phase (what is done first)\n3. Consider dependencies between elements"},{"id":"03224333-4d73-4a66-9d3a-0bc7a3e5c000","name":"chatInput","type":"string","value":"=You are a construction project manager. Distribute BIM elements to CORRECT construction phases.\n\n{{ $json.system_prompt_lang }}\n\n═══════════════════════════════════════════════════════════\nPHASE ASSIGNMENT BY CATEGORY (CRITICAL!)\n═══════════════════════════════════════════════════════════\n\nFOUNDATIONS:\n- OST_StructuralFoundation\n\nFRAME:\n- OST_Columns, OST_StructuralColumns\n- OST_StructuralFraming, OST_StructuralFramingSystem\n- OST_Girder, OST_Joist, OST_Purlin\n- OST_HorizontalBracing, OST_VerticalBracing, OST_KickerBracing\n- OST_StructuralStiffener, OST_Truss\n- OST_Stairs, OST_StairsRuns, OST_StairsStringerCarriage\n- OST_StairsRailing, OST_Ramps\n- OST_PierCaps, OST_PierColumns, OST_PierPiles, OST_PierWalls\n\nFLOORS:\n- OST_Floors, OST_FloorsStructure, OST_FloorsInsulation\n- OST_EdgeSlab\n\nWALLS:\n- OST_Walls, OST_WallsStructure, OST_WallsInsulation\n- OST_WallsMembrane, OST_WallsSubstrate, OST_WallsDefault\n- OST_StackedWalls\n- OST_CurtainWallPanels, OST_CurtainWallMullions\n- OST_CurtaSystem, OST_Curtain_Systems\n\nROOFING:\n- OST_Roofs, OST_RoofsStructure, OST_RoofsInsulation\n- OST_RoofsDefault, OST_Fascia, OST_Gutter\n- OST_RoofSoffit, OST_Cornices\n\nWINDOWS AND DOORS:\n- OST_Windows, OST_Doors\n\nINTERIOR:\n- OST_Ceilings, OST_Furniture, OST_FurnitureSystems\n- OST_Casework, OST_Reveals\n\nMEP:\n- HVAC: OST_DuctCurves, OST_DuctFitting, OST_DuctTerminal,\n  OST_DuctSystem, OST_DuctAccessory, OST_DuctInsulations,\n  OST_DuctLinings, OST_HVAC_Zones\n- Plumbing: OST_PlumbingFixtures, OST_PlumbingEquipment,\n  OST_PipeCurves, OST_PipeFitting, OST_PipeAccessory,\n  OST_PipeInsulations, OST_PipingSystem, OST_Sprinklers\n- Electrical: OST_ElectricalEquipment, OST_ElectricalCircuit,\n  OST_Wire, OST_CableTray, OST_CableTrayFitting, OST_Conduit,\n  OST_ConduitFitting, OST_LightingDevices\n- Devices: OST_MechanicalEquipment, OST_FireAlarmDevices,\n  OST_SecurityDevices, OST_CommunicationDevices, OST_DataDevices\n\nLANDSCAPING:\n- OST_Site, OST_Topography, OST_TopographySurface\n- OST_Planting, OST_Entourage, OST_Roads, OST_Parking\n\nREINFORCEMENT (assign to host phase):\n- OST_Rebar, OST_RebarCover, OST_FabricReinforcement\n- OST_Coupler, OST_StructuralTendons, OST_StructConnections\n\nGENERIC (analyze by name):\n- OST_GenericModel, OST_SpecialityEquipment, OST_Parts\n\n═══════════════════════════════════════════════════════════\nIMPORTANT:\n- Match phase by CATEGORY, NOT by element name\n- PREP phase is ONLY for temporary works (scaffolding)\n- CurtainWall → Walls (not Windows)\n- Rebar follows its host element's phase\n═══════════════════════════════════════════════════════════\n\n═══════════════════════════════════════════════════════════\nAVAILABLE PHASES\n═══════════════════════════════════════════════════════════\n\n{{ JSON.stringify($json.construction_phases, null, 2) }}\n\n═══════════════════════════════════════════════════════════\nELEMENTS TO ASSIGN ({{ $json.compact_types.length }} types)\n═══════════════════════════════════════════════════════════\n\n{{ JSON.stringify($json.compact_types, null, 2) }}\n\n═══════════════════════════════════════════════════════════\nOUTPUT FORMAT\n═══════════════════════════════════════════════════════════\n\nReturn JSON:\n{\n  \"types_with_phases\": [\n    {\n      \"type_index\": 0,\n      \"type_name\": \"element name\",\n      \"category\": \"OST_Category\",\n      \"assigned_phase_id\": 7,\n      \"sequence_in_phase\": 1,\n      \"detected_materials\": [\"concrete\"],\n      \"primary_quantity\": {\n        \"parameter\": \"Volume\",\n        \"unit\": \"m³\"\n      },\n      \"element_count\": 1,\n      \"quantities\": {\"Volume\": 18.51, \"Area\": 123.39}\n    }\n  ],\n  \"assignment_summary\": {\n    \"by_phase\": {\"7\": 1, \"9\": 1, \"13\": 1},\n    \"total_assigned\": 3\n  }\n}"}]}},"typeVersion":3.4},{"id":"b929d3f9-ac46-4c93-8a16-aa49600d912a","name":"STAGE 3 - Assign Types to Phases1","type":"@n8n/n8n-nodes-langchain.chainLlm","position":[1616,1776],"parameters":{"messages":{"messageValues":[{"message":"={{ $('CONFIG - STAGE 5').item.json.chatInput }}"}]}},"typeVersion":1.4},{"id":"355183bc-2361-412c-8ba6-0a689dff8295","name":"Rate Limit Wait","type":"n8n-nodes-base.wait","position":[1520,2336],"webhookId":"1c31e2f6-53a8-41ce-98be-215afc8d0740","parameters":{"amount":0.5},"typeVersion":1.1},{"id":"a06f585f-5b3c-4eb5-973b-980992b0a76e","name":"Limit to 10 Groups","type":"n8n-nodes-base.code","disabled":true,"position":[2112,1104],"parameters":{"jsCode":"const items = $input.all();\n// console.log(\\`Input groups: \\${items.length}\\`);\n// Return ALL items - no limit\n// return items;\nreturn items.slice(0, 10);"},"typeVersion":2},{"id":"224486e8-f669-499a-ab5c-467803d9a091","name":"Output Files1","type":"n8n-nodes-base.stickyNote","position":[1984,3136],"parameters":{"color":6,"width":324,"height":428,"content":"## 📁 Debug node`s output "},"typeVersion":1},{"id":"d5394625-dd49-465d-92fd-ee06c1f10c77","name":"Sticky Note7","type":"n8n-nodes-base.stickyNote","position":[768,2208],"parameters":{"width":336,"height":128,"content":"### 🤖 AI work-scope generation\nfrom building element types"},"typeVersion":1},{"id":"112df327-a0a7-4710-9865-202ec9659bcd","name":"Sticky Note8","type":"n8n-nodes-base.stickyNote","position":[1648,2272],"parameters":{"width":224,"height":96,"content":"### 🔍 Search for jobs ✨\n in a vector database "},"typeVersion":1},{"id":"f151627a-8d47-47aa-b5ef-4929dd6d4758","name":"Sticky Note9","type":"n8n-nodes-base.stickyNote","position":[1600,2720],"parameters":{"width":352,"height":96,"content":"### 🤖 AI verification of the quality \nof received data"},"typeVersion":1},{"id":"83a0ea30-510b-45a9-965c-ae9119c26b08","name":"Configuration1","type":"n8n-nodes-base.stickyNote","position":[144,480],"parameters":{"color":4,"width":272,"height":484,"content":"## ⚡️ Execute workflow\n"},"typeVersion":1},{"id":"144e6d2d-8c7b-4a70-8afc-83ebd89218f1","name":"STAGE 5.1 - Vector Search","type":"@n8n/n8n-nodes-langchain.vectorStoreQdrant","position":[1648,2336],"parameters":{"mode":"load","topK":5,"prompt":"={{ $json.search_query || $json.work_name || 'construction work' }}","options":{"contentPayloadKey":"content"},"qdrantCollection":{"__rl":true,"mode":"id","value":"={{ $json.qdrant_collection || 'ENG_TORONTO_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR' }}"}},"credentials":{"qdrantApi":{"id":"credential-id","name":"QdrantApi account 2"}},"typeVersion":1.1},{"id":"750f9888-96df-4131-b3bb-55e6f4b0baa6","name":"Sticky Note10","type":"n8n-nodes-base.stickyNote","position":[1904,2064],"parameters":{"color":3,"width":368,"height":256,"content":"### 📥 To enable vector database search, you need to:\n\n- Install the open-source Qdrant instance on your local machine or a VPS.\n- Fill in the Qdrant credentials in your workflow.\n- Upload the required dataset into Qdrant — either one dataset or all of them (9 total).\n- Choose any collection you want to search. The exact collection names are listed in the GitHub repository"},"typeVersion":1},{"id":"17e47c59-bc66-4a8c-9ac1-95a62c495283","name":"Sticky Note11","type":"n8n-nodes-base.stickyNote","position":[1456,2496],"parameters":{"width":304,"height":128,"content":"### 🤖 AI translate\nText-qery in emmbeding"},"typeVersion":1},{"id":"784cf12e-fa89-42db-b2af-a3de5bcfe575","name":"Sticky Note13","type":"n8n-nodes-base.stickyNote","position":[2320,160],"parameters":{"color":2,"width":384,"height":848,"content":"## ⚠️ n8n 2.0+ Setup Required ⚠️\n\n**Execute Command node is disabled by default in n8n 2.0+**\n\nWithout setup below, this workflow **won't work!**\n---\n\n### 🪟 Windows\n\n**Option 1 — each launch:**\n```\nset NODES_EXCLUDE=[] && npx n8n\n```\n**Option 2 — permanent:**\n\nCreate file `C:\\Users\\YOUR_USER\\.n8n\\.env`:\n```\nNODES_EXCLUDE=[]\n```\n---\n### 🐳 Docker\n```yaml\nenvironment:\n  - NODES_EXCLUDE=[]\n```\n\n---\n### ✅ Verify\n1. Restart n8n\n2. Press **+** → search **\"Execute Command\"**\n3. Found? You're ready! ✅\n\n📚 [n8n 2.0 Breaking Changes](https://docs.n8n.io/2-0-breaking-changes/)"},"typeVersion":1}],"active":false,"pinData":{},"settings":{"executionOrder":"v1"},"versionId":"","connections":{"OpenAI LLM":{"ai_languageModel":[[{"node":"STAGE 7.5 - Validate Type Works","type":"ai_languageModel","index":0},{"node":"STAGE 2 - Generate Construction Phases","type":"ai_languageModel","index":0},{"node":"STAGE 1 - Detect Project Type","type":"ai_languageModel","index":0},{"node":"AI Classify Categories","type":"ai_languageModel","index":0},{"node":"AI Analyze All Headers","type":"ai_languageModel","index":0},{"node":"STAGE 3 - Assign Types to Phases1","type":"ai_languageModel","index":0},{"node":"STAGE 4 - Decompose Type to Works","type":"ai_languageModel","index":0}]]},"Parse Excel1":{"main":[[{"node":"On the standard 3D View","type":"main","index":0}]]},"Handle No Works":{"main":[[{"node":"Store Type Result","type":"main","index":0}]]},"Has Work Items?":{"main":[[{"node":"Loop Work Items","type":"main","index":0}],[{"node":"Handle No Works","type":"main","index":0}]]},"Loop Work Items":{"main":[[{"node":"Aggregate Type Works","type":"main","index":0}],[{"node":"STAGE 5.1 - Prepare Search Strategies1","type":"main","index":0}]]},"Rate Limit Wait":{"main":[[{"node":"STAGE 5.1 - Vector Search","type":"main","index":0}]]},"Set Parameters1":{"main":[[{"node":"Read Excel File1","type":"main","index":0}]]},"Write HTML File":{"main":[[{"node":"Open HTML in Browser","type":"main","index":0}]]},"CONFIG - STAGE 1":{"main":[[{"node":"STAGE 1 - Detect Project Type","type":"main","index":0}]]},"CONFIG - STAGE 2":{"main":[[{"node":"STAGE 2 - Generate Construction Phases","type":"main","index":0}]]},"CONFIG - STAGE 4":{"main":[[{"node":"STAGE 4 - Decompose Type to Works","type":"main","index":0}]]},"CONFIG - STAGE 5":{"main":[[{"node":"STAGE 3 - Assign Types to Phases1","type":"main","index":0}]]},"Read Excel File1":{"main":[[{"node":"Parse Excel1","type":"main","index":0}]]},"Store Type Result":{"main":[[{"node":"Loop Types for Decomposition","type":"main","index":0}]]},"CONFIG - STAGE 7.5":{"main":[[{"node":"STAGE 7.5 - Validate Type Works","type":"main","index":0}]]},"Export All Outputs":{"main":[[{"node":"Write Output File","type":"main","index":0}]]},"If - File exists?1":{"main":[[{"node":"Info - Skip conversion1","type":"main","index":0}],[{"node":"Extract - Run converter1","type":"main","index":0}]]},"CONFIG - AI Headers":{"main":[[{"node":"AI Analyze All Headers","type":"main","index":0}]]},"Aggregate Type Works":{"main":[[{"node":"CONFIG - STAGE 7.5","type":"main","index":0}]]},"CONFIG - AI Classify":{"main":[[{"node":"AI Classify Categories","type":"main","index":0}]]},"Find Category Fields":{"main":[[{"node":"CONFIG - AI Classify","type":"main","index":0}]]},"Is Building Element1":{"main":[[{"node":"STAGE 0 - Collect BIM Data","type":"main","index":0}],[{"node":"Non-Building Elements Output1","type":"main","index":0}]]},"Open HTML in Browser":{"main":[[{"node":"Export All Outputs","type":"main","index":0}]]},"Process AI Response1":{"main":[[{"node":"Group Data with AI Rules1","type":"main","index":0}]]},"Save Type Before LLM":{"main":[[{"node":"CONFIG - STAGE 4","type":"main","index":0}]]},"AI Analyze All Headers":{"main":[[{"node":"Process AI Response1","type":"main","index":0}]]},"AI Classify Categories":{"main":[[{"node":"Apply Classification to Groups","type":"main","index":0}]]},"Parse Stage 2 - Phases":{"main":[[{"node":"STAGE 2.5 - Compact Types1","type":"main","index":0}]]},"STAGE 5.1 - Embeddings":{"ai_embedding":[[{"node":"STAGE 5.1 - Vector Search","type":"ai_embedding","index":0}]]},"Save to Project Folder":{"main":[[{"node":"Write HTML File","type":"main","index":0},{"node":"Write XLS File","type":"main","index":0}]]},"Accumulate Work Results":{"main":[[{"node":"Loop Work Items","type":"main","index":0}]]},"Info - Skip conversion1":{"main":[[{"node":"Merge - Continue workflow1","type":"main","index":0}]]},"On the standard 3D View":{"main":[[{"node":"Extract Headers and Data","type":"main","index":0}],[{"node":"Non-3D View Elements Output1","type":"main","index":0}]]},"Create - Excel filename1":{"main":[[{"node":"Check - Does Excel file exist?1","type":"main","index":0}]]},"Extract - Run converter1":{"main":[[{"node":"Check - Did extraction succeed?1","type":"main","index":0}]]},"Extract Headers and Data":{"main":[[{"node":"CONFIG - AI Headers","type":"main","index":0}]]},"Group Data with AI Rules1":{"main":[[{"node":"Find Category Fields","type":"main","index":0}]]},"STAGE 5.1 - Vector Search":{"main":[[{"node":"STAGE 5.2 - Parse Results","type":"main","index":0}]]},"STAGE 5.2 - Parse Results":{"main":[[{"node":"STAGE 6 - Map Rate Units to BIM","type":"main","index":0}]]},"STAGE 7 - Calculate Costs":{"main":[[{"node":"Accumulate Work Results","type":"main","index":0}]]},"Merge - Continue workflow1":{"main":[[{"node":"Set Parameters1","type":"main","index":0}]]},"STAGE 0 - Collect BIM Data":{"main":[[{"node":"CONFIG - STAGE 1","type":"main","index":0}]]},"STAGE 2.5 - Compact Types1":{"main":[[{"node":"CONFIG - STAGE 5","type":"main","index":0}]]},"Setup - Define file paths1":{"main":[[{"node":"Configure Language & Vector DB","type":"main","index":0}]]},"Loop Types for Decomposition":{"main":[[{"node":"STAGE 8 - Aggregate by Phases","type":"main","index":0}],[{"node":"Save Type Before LLM","type":"main","index":0}]]},"Parse Stage 1 - Project Type":{"main":[[{"node":"CONFIG - STAGE 2","type":"main","index":0}]]},"STAGE 7.5 - Parse Validation":{"main":[[{"node":"Store Type Result","type":"main","index":0}]]},"Error - Show what went wrong1":{"main":[[{"node":"Merge - Continue workflow1","type":"main","index":1}]]},"STAGE 1 - Detect Project Type":{"main":[[{"node":"Parse Stage 1 - Project Type","type":"main","index":0}]]},"STAGE 8 - Aggregate by Phases":{"main":[[{"node":"STAGE 9 - Generate Cost Estimate","type":"main","index":0}]]},"Apply Classification to Groups":{"main":[[{"node":"Is Building Element1","type":"main","index":0}]]},"Configure Language & Vector DB":{"main":[[{"node":"Create - Excel filename1","type":"main","index":0}]]},"Check - Does Excel file exist?1":{"main":[[{"node":"If - File exists?1","type":"main","index":0}]]},"Parse Stage 3 - Final Structure":{"main":[[{"node":"Prepare Types for Decomposition","type":"main","index":0}]]},"Prepare Types for Decomposition":{"main":[[{"node":"Loop Types for Decomposition","type":"main","index":0}]]},"STAGE 6 - Map Rate Units to BIM":{"main":[[{"node":"STAGE 7 - Calculate Costs","type":"main","index":0}]]},"STAGE 7.5 - Validate Type Works":{"main":[[{"node":"STAGE 7.5 - Parse Validation","type":"main","index":0}]]},"Check - Did extraction succeed?1":{"main":[[{"node":"Error - Show what went wrong1","type":"main","index":0}],[{"node":"Set xlsx_filename after success1","type":"main","index":0}]]},"STAGE 9 - Generate Cost Estimate":{"main":[[{"node":"Save to Project Folder","type":"main","index":0}]]},"Set xlsx_filename after success1":{"main":[[{"node":"Merge - Continue workflow1","type":"main","index":1}]]},"When clicking 'Execute workflow'":{"main":[[{"node":"Setup - Define file paths1","type":"main","index":0}]]},"STAGE 3 - Assign Types to Phases1":{"main":[[{"node":"Parse Stage 3 - Final Structure","type":"main","index":0}]]},"STAGE 4 - Decompose Type to Works":{"main":[[{"node":"Parse Decomposition & Prepare Works","type":"main","index":0}]]},"Parse Decomposition & Prepare Works":{"main":[[{"node":"Has Work Items?","type":"main","index":0}]]},"STAGE 2 - Generate Construction Phases":{"main":[[{"node":"Parse Stage 2 - Phases","type":"main","index":0}]]},"STAGE 5.1 - Prepare Search Strategies1":{"main":[[{"node":"Rate Limit Wait","type":"main","index":0}]]}}},"lastUpdatedBy":1,"workflowInfo":{"nodeCount":106,"nodeTypes":{"n8n-nodes-base.if":{"count":5},"n8n-nodes-base.set":{"count":15},"n8n-nodes-base.code":{"count":28},"n8n-nodes-base.wait":{"count":1},"n8n-nodes-base.merge":{"count":1},"n8n-nodes-base.stickyNote":{"count":30},"n8n-nodes-base.manualTrigger":{"count":1},"n8n-nodes-base.executeCommand":{"count":2},"n8n-nodes-base.readBinaryFile":{"count":2},"n8n-nodes-base.splitInBatches":{"count":2},"n8n-nodes-base.spreadsheetFile":{"count":1},"n8n-nodes-base.writeBinaryFile":{"count":3},"@n8n/n8n-nodes-langchain.chainLlm":{"count":7},"@n8n/n8n-nodes-langchain.lmChatOpenAi":{"count":1},"@n8n/n8n-nodes-langchain.lmChatXAiGrok":{"count":1},"@n8n/n8n-nodes-langchain.lmChatDeepSeek":{"count":1},"@n8n/n8n-nodes-langchain.lmChatAnthropic":{"count":1},"@n8n/n8n-nodes-langchain.embeddingsOpenAi":{"count":1},"@n8n/n8n-nodes-langchain.lmChatOpenRouter":{"count":1},"@n8n/n8n-nodes-langchain.vectorStoreQdrant":{"count":1},"@n8n/n8n-nodes-langchain.lmChatGoogleGemini":{"count":1}}},"status":"published","readyToDemo":null,"user":{"name":"Artem Boiko","username":"datadrivenconstruction","bio":"Founder DataDrivenConstruction.io | AEC Tech Consultant & Automation Expert | Bridging Software and Construction","verified":true,"links":["https://datadrivenconstruction.io/"],"avatar":"https://gravatar.com/avatar/96a88b84c9f49338945054d2393a04a29e434a2b60a8937de78e6ef9a6305b5f?r=pg&d=retro&size=200"},"nodes":[{"id":13,"icon":"fa:terminal","name":"n8n-nodes-base.executeCommand","codex":{"data":{"alias":["Shell","Command","OS","Bash"],"details":"Execute command allows you to run terminal commands on the computer/server hosting your n8n instance. Useful for executing a shell script or interacting with your n8n instance programmatically via the CLI.","resources":{"generic":[{"url":"https://n8n.io/blog/how-uproc-scraped-a-multi-page-website-with-a-low-code-workflow/","icon":" 🕸️","label":"How uProc scraped a multi-page website with a low-code workflow"},{"url":"https://n8n.io/blog/why-this-product-manager-loves-workflow-automation-with-n8n/","icon":"🧠","label":"Why this Product Manager loves workflow automation with n8n"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.executecommand/"}]},"categories":["Development","Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Helpers"]}}},"group":"[\"transform\"]","defaults":{"name":"Execute Command","color":"#886644"},"iconData":{"icon":"terminal","type":"icon"},"displayName":"Execute Command","typeVersion":1,"nodeCategories":[{"id":5,"name":"Development"},{"id":9,"name":"Core Nodes"}]},{"id":20,"icon":"fa:map-signs","name":"n8n-nodes-base.if","codex":{"data":{"alias":["Router","Filter","Condition","Logic","Boolean","Branch"],"details":"The IF node can be used to implement binary conditional logic in your workflow. You can set up one-to-many conditions to evaluate each item of data being inputted into the node. That data will either evaluate to TRUE or FALSE and route out of the node accordingly.\n\nThis node has multiple types of conditions: Bool, String, Number, and Date & Time.","resources":{"generic":[{"url":"https://n8n.io/blog/learn-to-automate-your-factorys-incident-reporting-a-step-by-step-guide/","icon":"🏭","label":"Learn to Automate Your Factory's Incident Reporting: A Step by Step Guide"},{"url":"https://n8n.io/blog/2021-the-year-to-automate-the-new-you-with-n8n/","icon":"☀️","label":"2021: The Year to Automate the New You with n8n"},{"url":"https://n8n.io/blog/why-business-process-automation-with-n8n-can-change-your-daily-life/","icon":"🧬","label":"Why business process automation with n8n can change your daily life"},{"url":"https://n8n.io/blog/create-a-toxic-language-detector-for-telegram/","icon":"🤬","label":"Create a toxic language detector for Telegram in 4 step"},{"url":"https://n8n.io/blog/no-code-ecommerce-workflow-automations/","icon":"store","label":"6 e-commerce workflows to power up your Shopify s"},{"url":"https://n8n.io/blog/how-to-build-a-low-code-self-hosted-url-shortener/","icon":"🔗","label":"How to build a low-code, self-hosted URL shortener in 3 steps"},{"url":"https://n8n.io/blog/automate-your-data-processing-pipeline-in-9-steps-with-n8n/","icon":"⚙️","label":"Automate your data processing pipeline in 9 steps"},{"url":"https://n8n.io/blog/how-to-get-started-with-crm-automation-and-no-code-workflow-ideas/","icon":"👥","label":"How to get started with CRM automation (with 3 no-code workflow ideas"},{"url":"https://n8n.io/blog/5-tasks-you-can-automate-with-notion-api/","icon":"⚡️","label":"5 tasks you can automate with the new Notion API "},{"url":"https://n8n.io/blog/automate-google-apps-for-productivity/","icon":"💡","label":"15 Google apps you can combine and automate to increase productivity"},{"url":"https://n8n.io/blog/automation-for-maintainers-of-open-source-projects/","icon":"🏷️","label":"How to automatically manage contributions to open-source projects"},{"url":"https://n8n.io/blog/how-uproc-scraped-a-multi-page-website-with-a-low-code-workflow/","icon":" 🕸️","label":"How uProc scraped a multi-page website with a low-code workflow"},{"url":"https://n8n.io/blog/5-workflow-automations-for-mattermost-that-we-love-at-n8n/","icon":"🤖","label":"5 workflow automations for Mattermost that we love at n8n"},{"url":"https://n8n.io/blog/why-this-product-manager-loves-workflow-automation-with-n8n/","icon":"🧠","label":"Why this Product Manager loves workflow automation with n8n"},{"url":"https://n8n.io/blog/sending-automated-congratulations-with-google-sheets-twilio-and-n8n/","icon":"🙌","label":"Sending Automated Congratulations with Google Sheets, Twilio, and n8n "},{"url":"https://n8n.io/blog/how-to-set-up-a-ci-cd-pipeline-with-no-code/","icon":"🎡","label":"How to set up a no-code CI/CD pipeline with GitHub and TravisCI"},{"url":"https://n8n.io/blog/benefits-of-automation-and-n8n-an-interview-with-hubspots-hugh-durkin/","icon":"🎖","label":"Benefits of automation and n8n: An interview with HubSpot's Hugh Durkin"},{"url":"https://n8n.io/blog/aws-workflow-automation/","label":"7 no-code workflow automations for Amazon Web Services"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.if/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Flow"]}}},"group":"[\"transform\"]","defaults":{"name":"If","color":"#408000"},"iconData":{"icon":"map-signs","type":"icon"},"displayName":"If","typeVersion":2,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":24,"icon":"file:merge.svg","name":"n8n-nodes-base.merge","codex":{"data":{"alias":["Join","Concatenate","Wait"],"resources":{"generic":[{"url":"https://n8n.io/blog/how-to-sync-data-between-two-systems/","icon":"🏬","label":"How to synchronize data between two systems (one-way vs. two-way sync"},{"url":"https://n8n.io/blog/supercharging-your-conference-registration-process-with-n8n/","icon":"🎫","label":"Supercharging your conference registration process with n8n"},{"url":"https://n8n.io/blog/migrating-community-metrics-to-orbit-using-n8n/","icon":"📈","label":"Migrating Community Metrics to Orbit using n8n"},{"url":"https://n8n.io/blog/build-your-own-virtual-assistant-with-n8n-a-step-by-step-guide/","icon":"👦","label":"Build your own virtual assistant with n8n: A step by step guide"},{"url":"https://n8n.io/blog/sending-automated-congratulations-with-google-sheets-twilio-and-n8n/","icon":"🙌","label":"Sending Automated Congratulations with Google Sheets, Twilio, and n8n "},{"url":"https://n8n.io/blog/aws-workflow-automation/","label":"7 no-code workflow automations for Amazon Web Services"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.merge/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Flow","Data Transformation"]}}},"group":"[\"transform\"]","defaults":{"name":"Merge"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF8xMTc3XzUxOCkiPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTAgNDhDMCAyMS40OTAzIDIxLjQ5MDMgMCA0OCAwSDExMkMxMzguNTEgMCAxNjAgMjEuNDkwMyAxNjAgNDhWNTZIMTk2LjI1MkMyNDAuNDM1IDU2IDI3Ni4yNTIgOTEuODE3MiAyNzYuMjUyIDEzNlYxOTJDMjc2LjI1MiAyMTQuMDkxIDI5NC4xNjEgMjMyIDMxNi4yNTIgMjMySDM1MlYyMjRDMzUyIDE5Ny40OSAzNzMuNDkgMTc2IDQwMCAxNzZINDY0QzQ5MC41MSAxNzYgNTEyIDE5Ny40OSA1MTIgMjI0VjI4OEM1MTIgMzE0LjUxIDQ5MC41MSAzMzYgNDY0IDMzNkg0MDBDMzczLjQ5IDMzNiAzNTIgMzE0LjUxIDM1MiAyODhWMjgwSDMxNi4yNTJDMjk0LjE2MSAyODAgMjc2LjI1MiAyOTcuOTA5IDI3Ni4yNTIgMzIwVjM3NkMyNzYuMjUyIDQyMC4xODMgMjQwLjQzNSA0NTYgMTk2LjI1MiA0NTZIMTYwVjQ2NEMxNjAgNDkwLjUxIDEzOC41MSA1MTIgMTEyIDUxMkg0OEMyMS40OTAzIDUxMiAwIDQ5MC41MSAwIDQ2NFY0MDBDMCAzNzMuNDkgMjEuNDkwMyAzNTIgNDggMzUySDExMkMxMzguNTEgMzUyIDE2MCAzNzMuNDkgMTYwIDQwMFY0MDhIMTk2LjI1MkMyMTMuOTI1IDQwOCAyMjguMjUyIDM5My42NzMgMjI4LjI1MiAzNzZWMzIwQzIyOC4yNTIgMjk0Ljc4NCAyMzguODU5IDI3Mi4wNDQgMjU1Ljg1MyAyNTZDMjM4Ljg1OSAyMzkuOTU2IDIyOC4yNTIgMjE3LjIxNiAyMjguMjUyIDE5MlYxMzZDMjI4LjI1MiAxMTguMzI3IDIxMy45MjUgMTA0IDE5Ni4yNTIgMTA0SDE2MFYxMTJDMTYwIDEzOC41MSAxMzguNTEgMTYwIDExMiAxNjBINDhDMjEuNDkwMyAxNjAgMCAxMzguNTEgMCAxMTJWNDhaTTEwNCA0OEMxMDguNDE4IDQ4IDExMiA1MS41ODE3IDExMiA1NlYxMDRDMTEyIDEwOC40MTggMTA4LjQxOCAxMTIgMTA0IDExMkg1NkM1MS41ODE3IDExMiA0OCAxMDguNDE4IDQ4IDEwNFY1NkM0OCA1MS41ODE3IDUxLjU4MTcgNDggNTYgNDhIMTA0Wk00NTYgMjI0QzQ2MC40MTggMjI0IDQ2NCAyMjcuNTgyIDQ2NCAyMzJWMjgwQzQ2NCAyODQuNDE4IDQ2MC40MTggMjg4IDQ1NiAyODhINDA4QzQwMy41ODIgMjg4IDQwMCAyODQuNDE4IDQwMCAyODBWMjMyQzQwMCAyMjcuNTgyIDQwMy41ODIgMjI0IDQwOCAyMjRINDU2Wk0xMTIgNDA4QzExMiA0MDMuNTgyIDEwOC40MTggNDAwIDEwNCA0MDBINTZDNTEuNTgxNyA0MDAgNDggNDAzLjU4MiA0OCA0MDhWNDU2QzQ4IDQ2MC40MTggNTEuNTgxNyA0NjQgNTYgNDY0SDEwNEMxMDguNDE4IDQ2NCAxMTIgNDYwLjQxOCAxMTIgNDU2VjQwOFoiIGZpbGw9IiM1NEI4QzkiLz4KPC9nPgo8ZGVmcz4KPGNsaXBQYXRoIGlkPSJjbGlwMF8xMTc3XzUxOCI+CjxyZWN0IHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiBmaWxsPSJ3aGl0ZSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo="},"displayName":"Merge","typeVersion":3,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":31,"icon":"fa:file-import","name":"n8n-nodes-base.readBinaryFile","codex":{"data":{"alias":["Text","Open","Import"],"resources":{"generic":[{"url":"https://n8n.io/blog/build-your-own-virtual-assistant-with-n8n-a-step-by-step-guide/","icon":"👦","label":"Build your own virtual assistant with n8n: A step by step guide"},{"url":"https://n8n.io/blog/how-uproc-scraped-a-multi-page-website-with-a-low-code-workflow/","icon":" 🕸️","label":"How uProc scraped a multi-page website with a low-code workflow"},{"url":"https://n8n.io/blog/how-to-use-the-http-request-node-the-swiss-army-knife-for-workflow-automation/","icon":"🧰","label":"How to use the HTTP Request Node - The Swiss Army Knife for Workflow Automation"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.readwritefile/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Files"]}}},"group":"[\"input\"]","defaults":{"name":"Read Binary File","color":"#449922"},"iconData":{"icon":"file-import","type":"icon"},"displayName":"Read Binary File","typeVersion":1,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":38,"icon":"fa:pen","name":"n8n-nodes-base.set","codex":{"data":{"alias":["Set","JS","JSON","Filter","Transform","Map"],"resources":{"generic":[{"url":"https://n8n.io/blog/learn-to-automate-your-factorys-incident-reporting-a-step-by-step-guide/","icon":"🏭","label":"Learn to Automate Your Factory's Incident Reporting: A Step by Step Guide"},{"url":"https://n8n.io/blog/2021-the-year-to-automate-the-new-you-with-n8n/","icon":"☀️","label":"2021: The Year to Automate the New You with n8n"},{"url":"https://n8n.io/blog/automatically-pulling-and-visualizing-data-with-n8n/","icon":"📈","label":"Automatically pulling and visualizing data with n8n"},{"url":"https://n8n.io/blog/database-monitoring-and-alerting-with-n8n/","icon":"📡","label":"Database Monitoring and Alerting with n8n"},{"url":"https://n8n.io/blog/automatically-adding-expense-receipts-to-google-sheets-with-telegram-mindee-twilio-and-n8n/","icon":"🧾","label":"Automatically Adding Expense Receipts to Google Sheets with Telegram, Mindee, Twilio, and n8n"},{"url":"https://n8n.io/blog/no-code-ecommerce-workflow-automations/","icon":"store","label":"6 e-commerce workflows to power up your Shopify s"},{"url":"https://n8n.io/blog/how-to-build-a-low-code-self-hosted-url-shortener/","icon":"🔗","label":"How to build a low-code, self-hosted URL shortener in 3 steps"},{"url":"https://n8n.io/blog/automate-your-data-processing-pipeline-in-9-steps-with-n8n/","icon":"⚙️","label":"Automate your data processing pipeline in 9 steps"},{"url":"https://n8n.io/blog/how-to-get-started-with-crm-automation-and-no-code-workflow-ideas/","icon":"👥","label":"How to get started with CRM automation (with 3 no-code workflow ideas"},{"url":"https://n8n.io/blog/5-tasks-you-can-automate-with-notion-api/","icon":"⚡️","label":"5 tasks you can automate with the new Notion API "},{"url":"https://n8n.io/blog/automate-google-apps-for-productivity/","icon":"💡","label":"15 Google apps you can combine and automate to increase productivity"},{"url":"https://n8n.io/blog/how-uproc-scraped-a-multi-page-website-with-a-low-code-workflow/","icon":" 🕸️","label":"How uProc scraped a multi-page website with a low-code workflow"},{"url":"https://n8n.io/blog/building-an-expense-tracking-app-in-10-minutes/","icon":"📱","label":"Building an expense tracking app in 10 minutes"},{"url":"https://n8n.io/blog/the-ultimate-guide-to-automate-your-video-collaboration-with-whereby-mattermost-and-n8n/","icon":"📹","label":"The ultimate guide to automate your video collaboration with Whereby, Mattermost, and n8n"},{"url":"https://n8n.io/blog/5-workflow-automations-for-mattermost-that-we-love-at-n8n/","icon":"🤖","label":"5 workflow automations for Mattermost that we love at n8n"},{"url":"https://n8n.io/blog/learn-to-build-powerful-api-endpoints-using-webhooks/","icon":"🧰","label":"Learn to Build Powerful API Endpoints Using Webhooks"},{"url":"https://n8n.io/blog/how-a-membership-development-manager-automates-his-work-and-investments/","icon":"📈","label":"How a Membership Development Manager automates his work and investments"},{"url":"https://n8n.io/blog/a-low-code-bitcoin-ticker-built-with-questdb-and-n8n-io/","icon":"📈","label":"A low-code bitcoin ticker built with QuestDB and n8n.io"},{"url":"https://n8n.io/blog/how-to-set-up-a-ci-cd-pipeline-with-no-code/","icon":"🎡","label":"How to set up a no-code CI/CD pipeline with GitHub and TravisCI"},{"url":"https://n8n.io/blog/benefits-of-automation-and-n8n-an-interview-with-hubspots-hugh-durkin/","icon":"🎖","label":"Benefits of automation and n8n: An interview with HubSpot's Hugh Durkin"},{"url":"https://n8n.io/blog/how-goomer-automated-their-operations-with-over-200-n8n-workflows/","icon":"🛵","label":"How Goomer automated their operations with over 200 n8n workflows"},{"url":"https://n8n.io/blog/aws-workflow-automation/","label":"7 no-code workflow automations for Amazon Web Services"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.set/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Data Transformation"]}}},"group":"[\"input\"]","defaults":{"name":"Edit Fields"},"iconData":{"icon":"pen","type":"icon"},"displayName":"Edit Fields (Set)","typeVersion":3,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":39,"icon":"fa:sync","name":"n8n-nodes-base.splitInBatches","codex":{"data":{"alias":["Loop","Concatenate","Batch","Split","Split In Batches"],"resources":{"generic":[{"url":"https://n8n.io/blog/how-uproc-scraped-a-multi-page-website-with-a-low-code-workflow/","icon":" 🕸️","label":"How uProc scraped a multi-page website with a low-code workflow"},{"url":"https://n8n.io/blog/benefits-of-automation-and-n8n-an-interview-with-hubspots-hugh-durkin/","icon":"🎖","label":"Benefits of automation and n8n: An interview with HubSpot's Hugh Durkin"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.splitinbatches/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Flow"]}}},"group":"[\"organization\"]","defaults":{"name":"Loop Over Items","color":"#007755"},"iconData":{"icon":"sync","type":"icon"},"displayName":"Loop Over Items (Split in Batches)","typeVersion":3,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":41,"icon":"fa:table","name":"n8n-nodes-base.spreadsheetFile","codex":{"data":{"alias":["_Excel","Excel","CSV","Sheet","Spreadsheet","xls","xlsx","ods"],"resources":{"generic":[{"url":"https://n8n.io/blog/build-your-own-virtual-assistant-with-n8n-a-step-by-step-guide/","icon":"👦","label":"Build your own virtual assistant with n8n: A step by step guide"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.converttofile/"}]},"categories":["Data & Storage","Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Files"]}}},"group":"[\"transform\"]","defaults":{"name":"Spreadsheet File","color":"#2244FF"},"iconData":{"icon":"table","type":"icon"},"displayName":"Spreadsheet File","typeVersion":2,"nodeCategories":[{"id":3,"name":"Data & Storage"},{"id":9,"name":"Core Nodes"}]},{"id":46,"icon":"fa:file-export","name":"n8n-nodes-base.writeBinaryFile","codex":{"data":{"alias":["Text","Save","Export"],"resources":{"generic":[{"url":"https://n8n.io/blog/how-uproc-scraped-a-multi-page-website-with-a-low-code-workflow/","icon":" 🕸️","label":"How uProc scraped a multi-page website with a low-code workflow"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.readwritefile/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Files"]}}},"group":"[\"output\"]","defaults":{"name":"Write Binary File","color":"#CC2233"},"iconData":{"icon":"file-export","type":"icon"},"displayName":"Write Binary File","typeVersion":1,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":514,"icon":"fa:pause-circle","name":"n8n-nodes-base.wait","codex":{"data":{"alias":["pause","sleep","delay","timeout"],"resources":{"generic":[{"url":"https://n8n.io/blog/how-to-get-started-with-crm-automation-and-no-code-workflow-ideas/","icon":"👥","label":"How to get started with CRM automation (with 3 no-code workflow ideas"},{"url":"https://n8n.io/blog/aws-workflow-automation/","label":"7 no-code workflow automations for Amazon Web Services"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.wait/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Helpers","Flow"]}}},"group":"[\"organization\"]","defaults":{"name":"Wait","color":"#804050"},"iconData":{"icon":"pause-circle","type":"icon"},"displayName":"Wait","typeVersion":1,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":565,"icon":"fa:sticky-note","name":"n8n-nodes-base.stickyNote","codex":{"data":{"alias":["Comments","Notes","Sticky"],"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Helpers"]}}},"group":"[\"input\"]","defaults":{"name":"Sticky Note","color":"#FFD233"},"iconData":{"icon":"sticky-note","type":"icon"},"displayName":"Sticky Note","typeVersion":1,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":834,"icon":"file:code.svg","name":"n8n-nodes-base.code","codex":{"data":{"alias":["cpde","Javascript","JS","Python","Script","Custom Code","Function"],"details":"The Code node allows you to execute JavaScript in your workflow.","resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.code/"}]},"categories":["Development","Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Helpers","Data Transformation"]}}},"group":"[\"transform\"]","defaults":{"name":"Code"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF8xMTcxXzQ0MSkiPgo8cGF0aCBkPSJNMTcwLjI4MyA0OEgxOTYuNUMyMDMuMTI3IDQ4IDIwOC41IDQyLjYyNzQgMjA4LjUgMzZWMTJDMjA4LjUgNS4zNzI1OCAyMDMuMTI3IDAgMTk2LjUgMEgxNzAuMjgzQzEyNi4xIDAgOTAuMjgzIDM1LjgxNzIgOTAuMjgzIDgwVjE3NkM5MC4yODMgMjA2LjkyOCA2NS4yMTA5IDIzMiAzNC4yODMgMjMySDIzQzE2LjM3MjYgMjMyIDExIDIzNy4zNzIgMTEgMjQ0VjI2OEMxMSAyNzQuNjI3IDE2LjM3MjQgMjgwIDIyLjk5OTYgMjgwTDM0LjI4MyAyODBDNjUuMjEwOSAyODAgOTAuMjgzIDMwNS4wNzIgOTAuMjgzIDMzNlY0NDBDOTAuMjgzIDQ3OS43NjQgMTIyLjUxOCA1MTIgMTYyLjI4MyA1MTJIMTk2LjVDMjAzLjEyNyA1MTIgMjA4LjUgNTA2LjYyNyAyMDguNSA1MDBWNDc2QzIwOC41IDQ2OS4zNzMgMjAzLjEyNyA0NjQgMTk2LjUgNDY0SDE2Mi4yODNDMTQ5LjAyOCA0NjQgMTM4LjI4MyA0NTMuMjU1IDEzOC4yODMgNDQwVjMzNkMxMzguMjgzIDMwOS4wMjIgMTI4LjAxMSAyODQuNDQzIDExMS4xNjQgMjY1Ljk2MUMxMDYuMTA5IDI2MC40MTYgMTA2LjEwOSAyNTEuNTg0IDExMS4xNjQgMjQ2LjAzOUMxMjguMDExIDIyNy41NTcgMTM4LjI4MyAyMDIuOTc4IDEzOC4yODMgMTc2VjgwQzEzOC4yODMgNjIuMzI2OSAxNTIuNjEgNDggMTcwLjI4MyA0OFoiIGZpbGw9IiNGRjk5MjIiLz4KPHBhdGggZD0iTTMwNSAzNkMzMDUgNDIuNjI3NCAzMTAuMzczIDQ4IDMxNyA0OEgzNDIuOTc5QzM2MC42NTIgNDggMzc0Ljk3OCA2Mi4zMjY5IDM3NC45NzggODBWMTc2QzM3NC45NzggMjAyLjk3OCAzODUuMjUxIDIyNy41NTcgNDAyLjA5OCAyNDYuMDM5QzQwNy4xNTMgMjUxLjU4NCA0MDcuMTUzIDI2MC40MTYgNDAyLjA5OCAyNjUuOTYxQzM4NS4yNTEgMjg0LjQ0MyAzNzQuOTc4IDMwOS4wMjIgMzc0Ljk3OCAzMzZWNDMyQzM3NC45NzggNDQ5LjY3MyAzNjAuNjUyIDQ2NCAzNDIuOTc5IDQ2NEgzMTdDMzEwLjM3MyA0NjQgMzA1IDQ2OS4zNzMgMzA1IDQ3NlY1MDBDMzA1IDUwNi42MjcgMzEwLjM3MyA1MTIgMzE3IDUxMkgzNDIuOTc5QzM4Ny4xNjEgNTEyIDQyMi45NzggNDc2LjE4MyA0MjIuOTc4IDQzMlYzMzZDNDIyLjk3OCAzMDUuMDcyIDQ0OC4wNTEgMjgwIDQ3OC45NzkgMjgwSDQ5MEM0OTYuNjI3IDI4MCA1MDIgMjc0LjYyOCA1MDIgMjY4VjI0NEM1MDIgMjM3LjM3MyA0OTYuNjI4IDIzMiA0OTAgMjMyTDQ3OC45NzkgMjMyQzQ0OC4wNTEgMjMyIDQyMi45NzggMjA2LjkyOCA0MjIuOTc4IDE3NlY4MEM0MjIuOTc4IDM1LjgxNzIgMzg3LjE2MSAwIDM0Mi45NzkgMEgzMTdDMzEwLjM3MyAwIDMwNSA1LjM3MjU4IDMwNSAxMlYzNloiIGZpbGw9IiNGRjk5MjIiLz4KPC9nPgo8ZGVmcz4KPGNsaXBQYXRoIGlkPSJjbGlwMF8xMTcxXzQ0MSI+CjxyZWN0IHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiBmaWxsPSJ3aGl0ZSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo="},"displayName":"Code","typeVersion":2,"nodeCategories":[{"id":5,"name":"Development"},{"id":9,"name":"Core Nodes"}]},{"id":838,"icon":"fa:mouse-pointer","name":"n8n-nodes-base.manualTrigger","codex":{"data":{"resources":{"generic":[],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.manualworkflowtrigger/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0"}},"group":"[\"trigger\"]","defaults":{"name":"When clicking ‘Execute workflow’","color":"#909298"},"iconData":{"icon":"mouse-pointer","type":"icon"},"displayName":"Manual Trigger","typeVersion":1,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":1123,"icon":"fa:link","name":"@n8n/n8n-nodes-langchain.chainLlm","codex":{"data":{"alias":["LangChain"],"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.chainllm/"}]},"categories":["AI","Langchain"],"subcategories":{"AI":["Chains","Root Nodes"]}}},"group":"[\"transform\"]","defaults":{"name":"Basic LLM Chain","color":"#909298"},"iconData":{"icon":"link","type":"icon"},"displayName":"Basic LLM Chain","typeVersion":2,"nodeCategories":[{"id":25,"name":"AI"},{"id":26,"name":"Langchain"}]},{"id":1141,"icon":"file:openAiLight.svg","name":"@n8n/n8n-nodes-langchain.embeddingsOpenAi","codex":{"data":{"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.embeddingsopenai/"}]},"categories":["AI","Langchain"],"subcategories":{"AI":["Embeddings"]}}},"group":"[\"transform\"]","defaults":{"name":"Embeddings OpenAI"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTM2Ljg2NzEgMTYuMzcxOEMzNy43NzQ2IDEzLjY0OCAzNy40NjIxIDEwLjY2NDIgMzYuMDEwOCA4LjE4NjYxQzMzLjgyODIgNC4zODY1MyAyOS40NDA3IDIuNDMxNDkgMjUuMTU1NiAzLjM1MTUxQzIzLjI0OTMgMS4yMDM5NiAyMC41MTA1IC0wLjAxNzMxNDggMTcuNjM5MiAwLjAwMDE4NTUzM0MxMy4yNTkxIC0wLjAwOTgxNDY4IDkuMzcyNzMgMi44MTAyNSA4LjAyNTIgNi45Nzc4M0M1LjIxMTM5IDcuNTU0MSAyLjc4MjU4IDkuMzE1MzggMS4zNjEzIDExLjgxMTdDLTAuODM3NDkzIDE1LjYwMTggLTAuMzM2MjMyIDIwLjM3OTQgMi42MDEzMyAyMy42Mjk0QzEuNjkzODEgMjYuMzUzMiAyLjAwNjMyIDI5LjMzNzEgMy40NTc2IDMxLjgxNDZDNS42NDAxNSAzNS42MTQ3IDEwLjAyNzcgMzcuNTY5NyAxNC4zMTI4IDM2LjY0OTdDMTYuMjE3OSAzOC43OTczIDE4Ljk1NzkgNDAuMDE4NSAyMS44MjkyIDM5Ljk5OThDMjYuMjExOCA0MC4wMTEgMzAuMDk5NCAzNy4xODg1IDMxLjQ0NjkgMzMuMDE3MUMzNC4yNjA4IDMyLjQ0MDkgMzYuNjg5NiAzMC42Nzk2IDM4LjExMDggMjguMTgzM0M0MC4zMDcxIDI0LjM5MzIgMzkuODA0NiAxOS42MTk0IDM2Ljg2ODMgMTYuMzY5M0wzNi44NjcxIDE2LjM3MThaTTIxLjgzMTcgMzcuMzg2QzIwLjA3OCAzNy4zODg1IDE4LjM3OTIgMzYuNzc0NyAxNy4wMzI5IDM1LjY1MDlDMTcuMDk0MSAzNS42MTg0IDE3LjIwMDQgMzUuNTU5NyAxNy4yNjkxIDM1LjUxNzJMMjUuMjM0MyAzMC45MTcxQzI1LjY0MTggMzAuNjg1OCAyNS44OTE4IDMwLjI1MjEgMjUuODg5MyAyOS43ODMzVjE4LjU1NDNMMjkuMjU1NyAyMC40OTgxQzI5LjI5MTkgMjAuNTE1NiAyOS4zMTU3IDIwLjU1MDYgMjkuMzIwNyAyMC41OTA2VjI5Ljg4OTZDMjkuMzE1NyAzNC4wMjQ3IDI1Ljk2NjggMzcuMzc3MiAyMS44MzE3IDM3LjM4NlpNNS43MjY0IDMwLjUwNzFDNC44NDc2MyAyOC45ODk2IDQuNTMxMzcgMjcuMjEwOCA0LjgzMjYzIDI1LjQ4NDVDNC44OTEzOCAyNS41MTk1IDQuOTk1MTMgMjUuNTgzMiA1LjA2ODg4IDI1LjYyNTdMMTMuMDM0MSAzMC4yMjU4QzEzLjQzNzggMzAuNDYyMSAxMy45Mzc4IDMwLjQ2MjEgMTQuMzQyOCAzMC4yMjU4TDI0LjA2NjggMjQuNjEwN1YyOC40OTgzQzI0LjA2OTMgMjguNTM4MyAyNC4wNTA1IDI4LjU3NyAyNC4wMTkzIDI4LjYwMkwxNS45Njc5IDMzLjI1MDlDMTIuMzgxNSAzNS4zMTU5IDcuODAxNDQgMzQuMDg4NCA1LjcyNzY1IDMwLjUwNzFINS43MjY0Wk0zLjYzMDEgMTMuMTIwNUM0LjUwNTEyIDExLjYwMDQgNS44ODY0IDEwLjQzNzkgNy41MzE0NCA5LjgzNDE1QzcuNTMxNDQgOS45MDI5IDcuNTI3NjkgMTAuMDI0MiA3LjUyNzY5IDEwLjEwOTJWMTkuMzEwNkM3LjUyNTE5IDE5Ljc3ODEgNy43NzUxOSAyMC4yMTE5IDguMTgxNDUgMjAuNDQzMUwxNy45MDU0IDI2LjA1N0wxNC41MzkxIDI4LjAwMDhDMTQuNTA1MyAyOC4wMjMzIDE0LjQ2MjggMjguMDI3IDE0LjQyNTMgMjguMDEwOEw2LjM3MjY2IDIzLjM1ODJDMi43OTM4MyAyMS4yODU2IDEuNTY2MzEgMTYuNzA2OCAzLjYyODg1IDEzLjEyMTdMMy42MzAxIDEzLjEyMDVaTTMxLjI4ODIgMTkuNTU2OUwyMS41NjQyIDEzLjk0MTdMMjQuOTMwNiAxMS45OTkyQzI0Ljk2NDMgMTEuOTc2NyAyNS4wMDY4IDExLjk3MjkgMjUuMDQ0MyAxMS45ODkyTDMzLjA5NyAxNi42MzhDMzYuNjgyMSAxOC43MDkzIDM3LjkxMDggMjMuMjk1NyAzNS44Mzk1IDI2Ljg4MDhDMzQuOTYzMyAyOC4zOTgzIDMzLjU4MzIgMjkuNTYwOCAzMS45Mzk1IDMwLjE2NThWMjAuNjg5NEMzMS45NDMyIDIwLjIyMTkgMzEuNjk0NSAxOS43ODk0IDMxLjI4OTQgMTkuNTU2OUgzMS4yODgyWk0zNC42MzgzIDE0LjUxNDJDMzQuNTc5NSAxNC40NzggMzQuNDc1OCAxNC40MTU1IDM0LjQwMiAxNC4zNzNMMjYuNDM2OCA5Ljc3Mjg5QzI2LjAzMzEgOS41MzY2NCAyNS41MzMxIDkuNTM2NjQgMjUuMTI4MSA5Ljc3Mjg5TDE1LjQwNDEgMTUuMzg4VjExLjUwMDRDMTUuNDAxNiAxMS40NjA0IDE1LjQyMDQgMTEuNDIxNyAxNS40NTE2IDExLjM5NjdMMjMuNTAzIDYuNzUxNThDMjcuMDg5NCA0LjY4Mjc5IDMxLjY3NDUgNS45MTQwNiAzMy43NDIgOS41MDE2NEMzNC42MTU4IDExLjAxNjcgMzQuOTMyIDEyLjc5MDUgMzQuNjM1OCAxNC41MTQySDM0LjYzODNaTTEzLjU3NDEgMjEuNDQzMUwxMC4yMDY1IDE5LjQ5OTRDMTAuMTcwMiAxOS40ODE5IDEwLjE0NjUgMTkuNDQ2OCAxMC4xNDE1IDE5LjQwNjhWMTAuMTA3OUMxMC4xNDQgNS45Njc4MSAxMy41MDI4IDIuNjEyNzQgMTcuNjQyOSAyLjYxNTI0QzE5LjM5NDIgMi42MTUyNCAyMS4wODkyIDMuMjMwMjUgMjIuNDM1NSA0LjM1MDI4QzIyLjM3NDMgNC4zODI3OCAyMi4yNjkzIDQuNDQxNTMgMjIuMTk5MiA0LjQ4NDAzTDE0LjIzNDEgOS4wODQxM0MxMy44MjY2IDkuMzE1MzggMTMuNTc2NiA5Ljc0Nzg5IDEzLjU3OTEgMTAuMjE2N0wxMy41NzQxIDIxLjQ0MDZWMjEuNDQzMVpNMTUuNDAyOSAxNy41MDA2TDE5LjczNDIgMTQuOTk5M0wyNC4wNjU1IDE3LjQ5OTNWMjIuNTAwN0wxOS43MzQyIDI1LjAwMDdMMTUuNDAyOSAyMi41MDA3VjE3LjUwMDZaIiBmaWxsPSIjN0Q3RDg3Ii8+Cjwvc3ZnPgo="},"displayName":"Embeddings OpenAI","typeVersion":1,"nodeCategories":[{"id":25,"name":"AI"},{"id":26,"name":"Langchain"}]},{"id":1145,"icon":"file:anthropic.svg","name":"@n8n/n8n-nodes-langchain.lmChatAnthropic","codex":{"data":{"alias":["claude","sonnet","opus"],"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatanthropic/"}]},"categories":["AI","Langchain"],"subcategories":{"AI":["Language Models","Root Nodes"],"Language Models":["Chat Models (Recommended)"]}}},"group":"[\"transform\"]","defaults":{"name":"Anthropic Chat Model"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NiIgaGVpZ2h0PSIzMiIgZmlsbD0ibm9uZSI+PHBhdGggZmlsbD0iIzdEN0Q4NyIgZD0iTTMyLjczIDBoLTYuOTQ1TDM4LjQ1IDMyaDYuOTQ1ek0xMi42NjUgMCAwIDMyaDcuMDgybDIuNTktNi43MmgxMy4yNWwyLjU5IDYuNzJoNy4wODJMMTkuOTI5IDB6bS0uNzAyIDE5LjMzNyA0LjMzNC0xMS4yNDYgNC4zMzQgMTEuMjQ2eiIvPjwvc3ZnPg=="},"displayName":"Anthropic Chat Model","typeVersion":1,"nodeCategories":[{"id":25,"name":"AI"},{"id":26,"name":"Langchain"}]},{"id":1153,"icon":"file:openAiLight.svg","name":"@n8n/n8n-nodes-langchain.lmChatOpenAi","codex":{"data":{"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatopenai/"}]},"categories":["AI","Langchain"],"subcategories":{"AI":["Language Models","Root Nodes"],"Language Models":["Chat Models (Recommended)"]}}},"group":"[\"transform\"]","defaults":{"name":"OpenAI Chat Model"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTM2Ljg2NzEgMTYuMzcxOEMzNy43NzQ2IDEzLjY0OCAzNy40NjIxIDEwLjY2NDIgMzYuMDEwOCA4LjE4NjYxQzMzLjgyODIgNC4zODY1MyAyOS40NDA3IDIuNDMxNDkgMjUuMTU1NiAzLjM1MTUxQzIzLjI0OTMgMS4yMDM5NiAyMC41MTA1IC0wLjAxNzMxNDggMTcuNjM5MiAwLjAwMDE4NTUzM0MxMy4yNTkxIC0wLjAwOTgxNDY4IDkuMzcyNzMgMi44MTAyNSA4LjAyNTIgNi45Nzc4M0M1LjIxMTM5IDcuNTU0MSAyLjc4MjU4IDkuMzE1MzggMS4zNjEzIDExLjgxMTdDLTAuODM3NDkzIDE1LjYwMTggLTAuMzM2MjMyIDIwLjM3OTQgMi42MDEzMyAyMy42Mjk0QzEuNjkzODEgMjYuMzUzMiAyLjAwNjMyIDI5LjMzNzEgMy40NTc2IDMxLjgxNDZDNS42NDAxNSAzNS42MTQ3IDEwLjAyNzcgMzcuNTY5NyAxNC4zMTI4IDM2LjY0OTdDMTYuMjE3OSAzOC43OTczIDE4Ljk1NzkgNDAuMDE4NSAyMS44MjkyIDM5Ljk5OThDMjYuMjExOCA0MC4wMTEgMzAuMDk5NCAzNy4xODg1IDMxLjQ0NjkgMzMuMDE3MUMzNC4yNjA4IDMyLjQ0MDkgMzYuNjg5NiAzMC42Nzk2IDM4LjExMDggMjguMTgzM0M0MC4zMDcxIDI0LjM5MzIgMzkuODA0NiAxOS42MTk0IDM2Ljg2ODMgMTYuMzY5M0wzNi44NjcxIDE2LjM3MThaTTIxLjgzMTcgMzcuMzg2QzIwLjA3OCAzNy4zODg1IDE4LjM3OTIgMzYuNzc0NyAxNy4wMzI5IDM1LjY1MDlDMTcuMDk0MSAzNS42MTg0IDE3LjIwMDQgMzUuNTU5NyAxNy4yNjkxIDM1LjUxNzJMMjUuMjM0MyAzMC45MTcxQzI1LjY0MTggMzAuNjg1OCAyNS44OTE4IDMwLjI1MjEgMjUuODg5MyAyOS43ODMzVjE4LjU1NDNMMjkuMjU1NyAyMC40OTgxQzI5LjI5MTkgMjAuNTE1NiAyOS4zMTU3IDIwLjU1MDYgMjkuMzIwNyAyMC41OTA2VjI5Ljg4OTZDMjkuMzE1NyAzNC4wMjQ3IDI1Ljk2NjggMzcuMzc3MiAyMS44MzE3IDM3LjM4NlpNNS43MjY0IDMwLjUwNzFDNC44NDc2MyAyOC45ODk2IDQuNTMxMzcgMjcuMjEwOCA0LjgzMjYzIDI1LjQ4NDVDNC44OTEzOCAyNS41MTk1IDQuOTk1MTMgMjUuNTgzMiA1LjA2ODg4IDI1LjYyNTdMMTMuMDM0MSAzMC4yMjU4QzEzLjQzNzggMzAuNDYyMSAxMy45Mzc4IDMwLjQ2MjEgMTQuMzQyOCAzMC4yMjU4TDI0LjA2NjggMjQuNjEwN1YyOC40OTgzQzI0LjA2OTMgMjguNTM4MyAyNC4wNTA1IDI4LjU3NyAyNC4wMTkzIDI4LjYwMkwxNS45Njc5IDMzLjI1MDlDMTIuMzgxNSAzNS4zMTU5IDcuODAxNDQgMzQuMDg4NCA1LjcyNzY1IDMwLjUwNzFINS43MjY0Wk0zLjYzMDEgMTMuMTIwNUM0LjUwNTEyIDExLjYwMDQgNS44ODY0IDEwLjQzNzkgNy41MzE0NCA5LjgzNDE1QzcuNTMxNDQgOS45MDI5IDcuNTI3NjkgMTAuMDI0MiA3LjUyNzY5IDEwLjEwOTJWMTkuMzEwNkM3LjUyNTE5IDE5Ljc3ODEgNy43NzUxOSAyMC4yMTE5IDguMTgxNDUgMjAuNDQzMUwxNy45MDU0IDI2LjA1N0wxNC41MzkxIDI4LjAwMDhDMTQuNTA1MyAyOC4wMjMzIDE0LjQ2MjggMjguMDI3IDE0LjQyNTMgMjguMDEwOEw2LjM3MjY2IDIzLjM1ODJDMi43OTM4MyAyMS4yODU2IDEuNTY2MzEgMTYuNzA2OCAzLjYyODg1IDEzLjEyMTdMMy42MzAxIDEzLjEyMDVaTTMxLjI4ODIgMTkuNTU2OUwyMS41NjQyIDEzLjk0MTdMMjQuOTMwNiAxMS45OTkyQzI0Ljk2NDMgMTEuOTc2NyAyNS4wMDY4IDExLjk3MjkgMjUuMDQ0MyAxMS45ODkyTDMzLjA5NyAxNi42MzhDMzYuNjgyMSAxOC43MDkzIDM3LjkxMDggMjMuMjk1NyAzNS44Mzk1IDI2Ljg4MDhDMzQuOTYzMyAyOC4zOTgzIDMzLjU4MzIgMjkuNTYwOCAzMS45Mzk1IDMwLjE2NThWMjAuNjg5NEMzMS45NDMyIDIwLjIyMTkgMzEuNjk0NSAxOS43ODk0IDMxLjI4OTQgMTkuNTU2OUgzMS4yODgyWk0zNC42MzgzIDE0LjUxNDJDMzQuNTc5NSAxNC40NzggMzQuNDc1OCAxNC40MTU1IDM0LjQwMiAxNC4zNzNMMjYuNDM2OCA5Ljc3Mjg5QzI2LjAzMzEgOS41MzY2NCAyNS41MzMxIDkuNTM2NjQgMjUuMTI4MSA5Ljc3Mjg5TDE1LjQwNDEgMTUuMzg4VjExLjUwMDRDMTUuNDAxNiAxMS40NjA0IDE1LjQyMDQgMTEuNDIxNyAxNS40NTE2IDExLjM5NjdMMjMuNTAzIDYuNzUxNThDMjcuMDg5NCA0LjY4Mjc5IDMxLjY3NDUgNS45MTQwNiAzMy43NDIgOS41MDE2NEMzNC42MTU4IDExLjAxNjcgMzQuOTMyIDEyLjc5MDUgMzQuNjM1OCAxNC41MTQySDM0LjYzODNaTTEzLjU3NDEgMjEuNDQzMUwxMC4yMDY1IDE5LjQ5OTRDMTAuMTcwMiAxOS40ODE5IDEwLjE0NjUgMTkuNDQ2OCAxMC4xNDE1IDE5LjQwNjhWMTAuMTA3OUMxMC4xNDQgNS45Njc4MSAxMy41MDI4IDIuNjEyNzQgMTcuNjQyOSAyLjYxNTI0QzE5LjM5NDIgMi42MTUyNCAyMS4wODkyIDMuMjMwMjUgMjIuNDM1NSA0LjM1MDI4QzIyLjM3NDMgNC4zODI3OCAyMi4yNjkzIDQuNDQxNTMgMjIuMTk5MiA0LjQ4NDAzTDE0LjIzNDEgOS4wODQxM0MxMy44MjY2IDkuMzE1MzggMTMuNTc2NiA5Ljc0Nzg5IDEzLjU3OTEgMTAuMjE2N0wxMy41NzQxIDIxLjQ0MDZWMjEuNDQzMVpNMTUuNDAyOSAxNy41MDA2TDE5LjczNDIgMTQuOTk5M0wyNC4wNjU1IDE3LjQ5OTNWMjIuNTAwN0wxOS43MzQyIDI1LjAwMDdMMTUuNDAyOSAyMi41MDA3VjE3LjUwMDZaIiBmaWxsPSIjN0Q3RDg3Ii8+Cjwvc3ZnPgo="},"displayName":"OpenAI Chat Model","typeVersion":1,"nodeCategories":[{"id":25,"name":"AI"},{"id":26,"name":"Langchain"}]},{"id":1248,"icon":"file:qdrant.svg","name":"@n8n/n8n-nodes-langchain.vectorStoreQdrant","codex":{"data":{"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.vectorstoreqdrant/"}]},"categories":["AI","Langchain"],"subcategories":{"AI":["Vector Stores","Tools","Root Nodes"],"Tools":["Other Tools"],"Vector Stores":["Other Vector Stores"]}}},"group":"[\"transform\"]","defaults":{"name":"Qdrant Vector Store"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBkYXRhLW5hbWU9IkNhcGEgMiIgdmlld0JveD0iMCAwIDM0Ni40MiA0MDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxkZWZzPgo8c3R5bGU+LmNscy0xIHsKICAgICAgICBmaWxsOiAjOWUwZDM4OwogICAgICB9CgogICAgICAuY2xzLTIgewogICAgICAgIGZpbGw6ICNkYzI0NGM7CiAgICAgIH0KCiAgICAgIC5jbHMtMyB7CiAgICAgICAgZmlsbDogI2ZmNTE2YjsKICAgICAgfTwvc3R5bGU+CjwvZGVmcz4KPHBvbHlnb24gY2xhc3M9ImNscy0yIiBwb2ludHM9IjE3My4yMSAwIDAgMTAwIDAgMzAwIDE3My4yMSA0MDAgMjM4LjE2IDM2Mi41IDIzOC4xNiAyODcuNSAxNzMuMjEgMzI1IDY0Ljk2IDI2Mi41IDY0Ljk2IDEzNy41IDE3My4yMSA3NSAyODEuNDYgMTM3LjUgMjgxLjQ2IDM4Ny41IDM0Ni40MiAzNTAgMzQ2LjQyIDEwMCIvPgo8cG9seWdvbiBjbGFzcz0iY2xzLTIiIHBvaW50cz0iMTA4LjI2IDE2Mi41IDEwOC4yNiAyMzcuNSAxNzMuMjEgMjc1IDIzOC4xNiAyMzcuNSAyMzguMTYgMTYyLjUgMTczLjIxIDEyNSIvPgo8cG9seWdvbiBjbGFzcz0iY2xzLTEiIHBvaW50cz0iMjM4LjE2IDI4Ny41IDIzOC4xNiAzNjIuNSAxNzMuMjEgNDAwIDE3My4yMSAzMjUiLz4KPHBvbHlnb24gY2xhc3M9ImNscy0xIiBwb2ludHM9IjM0Ni40MiAxMDAgMzQ2LjQyIDM1MCAyODEuNDYgMzg3LjUgMjgxLjQ2IDEzNy41Ii8+Cjxwb2x5Z29uIGNsYXNzPSJjbHMtMyIgcG9pbnRzPSIzNDYuNDIgMTAwIDI4MS40NiAxMzcuNSAxNzMuMjEgNzUgNjQuOTYgMTM3LjUgMCAxMDAgMTczLjIxIDAiLz4KPHBvbHlnb24gY2xhc3M9ImNscy0yIiBwb2ludHM9IjE3My4yMSAzMjUgMTczLjIxIDQwMCAwIDMwMCAwIDEwMCA2NC45NiAxMzcuNSA2NC45NiAyNjIuNSIvPgo8cG9seWdvbiBjbGFzcz0iY2xzLTMiIHBvaW50cz0iMjM4LjE2IDE2Mi41IDE3My4yMSAyMDAgMTA4LjI2IDE2Mi41IDE3My4yMSAxMjUiLz4KPHBvbHlnb24gY2xhc3M9ImNscy0yIiBwb2ludHM9IjE3My4yMSAyMDAgMTczLjIxIDI3NSAxMDguMjYgMjM3LjUgMTA4LjI2IDE2Mi41Ii8+Cjxwb2x5Z29uIGNsYXNzPSJjbHMtMSIgcG9pbnRzPSIyMzguMTYgMTYyLjUgMjM4LjE2IDIzNy41IDE3My4yMSAyNzUgMTczLjIxIDIwMCIvPgo8L3N2Zz4K"},"displayName":"Qdrant Vector Store","typeVersion":1,"nodeCategories":[{"id":25,"name":"AI"},{"id":26,"name":"Langchain"}]},{"id":1262,"icon":"file:google.svg","name":"@n8n/n8n-nodes-langchain.lmChatGoogleGemini","codex":{"data":{"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatgooglegemini/"}]},"categories":["AI","Langchain"],"subcategories":{"AI":["Language Models","Root Nodes"],"Language Models":["Chat Models (Recommended)"]}}},"group":"[\"transform\"]","defaults":{"name":"Google Gemini Chat Model"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNDggNDgiPjxkZWZzPjxwYXRoIGlkPSJhIiBkPSJNNDQuNSAyMEgyNHY4LjVoMTEuOEMzNC43IDMzLjkgMzAuMSAzNyAyNCAzN2MtNy4yIDAtMTMtNS44LTEzLTEzczUuOC0xMyAxMy0xM2MzLjEgMCA1LjkgMS4xIDguMSAyLjlsNi40LTYuNEMzNC42IDQuMSAyOS42IDIgMjQgMiAxMS44IDIgMiAxMS44IDIgMjRzOS44IDIyIDIyIDIyYzExIDAgMjEtOCAyMS0yMiAwLTEuMy0uMi0yLjctLjUtNCIvPjwvZGVmcz48Y2xpcFBhdGggaWQ9ImIiPjx1c2UgeGxpbms6aHJlZj0iI2EiIG92ZXJmbG93PSJ2aXNpYmxlIi8+PC9jbGlwUGF0aD48cGF0aCBmaWxsPSIjRkJCQzA1IiBkPSJNMCAzN1YxMWwxNyAxM3oiIGNsaXAtcGF0aD0idXJsKCNiKSIvPjxwYXRoIGZpbGw9IiNFQTQzMzUiIGQ9Im0wIDExIDE3IDEzIDctNi4xTDQ4IDE0VjBIMHoiIGNsaXAtcGF0aD0idXJsKCNiKSIvPjxwYXRoIGZpbGw9IiMzNEE4NTMiIGQ9Im0wIDM3IDMwLTIzIDcuOSAxTDQ4IDB2NDhIMHoiIGNsaXAtcGF0aD0idXJsKCNiKSIvPjxwYXRoIGZpbGw9IiM0Mjg1RjQiIGQ9Ik00OCA0OCAxNyAyNGwtNC0zIDM1LTEweiIgY2xpcC1wYXRoPSJ1cmwoI2IpIi8+PC9zdmc+"},"displayName":"Google Gemini Chat Model","typeVersion":1,"nodeCategories":[{"id":25,"name":"AI"},{"id":26,"name":"Langchain"}]},{"id":1280,"icon":"file:deepseek.svg","name":"@n8n/n8n-nodes-langchain.lmChatDeepSeek","codex":{"data":{"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatdeepseek/"}]},"categories":["AI","Langchain"],"subcategories":{"AI":["Language Models","Root Nodes"],"Language Models":["Chat Models (Recommended)"]}}},"group":"[\"transform\"]","defaults":{"name":"DeepSeek Chat Model"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+RGVlcFNlZWs8L3RpdGxlPjxwYXRoIGQ9Ik0yMy43NDggNC40ODJjLS4yNTQtLjEyNC0uMzY0LjExMy0uNTEyLjIzNC0uMDUxLjAzOS0uMDk0LjA5LS4xMzcuMTM2LS4zNzIuMzk3LS44MDYuNjU3LTEuMzczLjYyNi0uODI5LS4wNDYtMS41MzcuMjE0LTIuMTYzLjg0OC0uMTMzLS43ODItLjU3NS0xLjI0OC0xLjI0Ny0xLjU0OC0uMzUyLS4xNTYtLjcwOC0uMzExLS45NTUtLjY1LS4xNzItLjI0MS0uMjE5LS41MS0uMzA1LS43NzQtLjA1NS0uMTYtLjExLS4zMjMtLjI5My0uMzUtLjItLjAzMS0uMjc4LjEzNi0uMzU2LjI3Ni0uMzEzLjU3Mi0uNDM0IDEuMjAyLS40MjIgMS44NC4wMjcgMS40MzYuNjMzIDIuNTggMS44MzggMy4zOTMuMTM3LjA5My4xNzIuMTg3LjEyOS4zMjMtLjA4Mi4yOC0uMTguNTUyLS4yNjYuODMzLS4wNTUuMTc5LS4xMzcuMjE3LS4zMjkuMTRhNS41MjYgNS41MjYgMCAwMS0xLjczNi0xLjE4Yy0uODU3LS44MjgtMS42MzEtMS43NDItMi41OTctMi40NThhMTEuMzY1IDExLjM2NSAwIDAwLS42ODktLjQ3MWMtLjk4NS0uOTU3LjEzLTEuNzQzLjM4OC0xLjgzNi4yNy0uMDk4LjA5My0uNDMyLS43NzktLjQyOC0uODcyLjAwNC0xLjY3LjI5NS0yLjY4Ny42ODRhMy4wNTUgMy4wNTUgMCAwMS0uNDY1LjEzNyA5LjU5NyA5LjU5NyAwIDAwLTIuODgzLS4xMDJjLTEuODg1LjIxLTMuMzkgMS4xMDItNC40OTcgMi42MjNDLjA4MiA4LjYwNi0uMjMxIDEwLjY4NC4xNTIgMTIuODVjLjQwMyAyLjI4NCAxLjU2OSA0LjE3NSAzLjM2IDUuNjUzIDEuODU4IDEuNTMzIDMuOTk3IDIuMjg0IDYuNDM4IDIuMTQgMS40ODItLjA4NSAzLjEzMy0uMjg0IDQuOTk0LTEuODYuNDcuMjM0Ljk2Mi4zMjcgMS43OC4zOTcuNjMuMDU5IDEuMjM2LS4wMyAxLjcwNS0uMTI4LjczNS0uMTU2LjY4NC0uODM3LjQxOS0uOTYxLTIuMTU1LTEuMDA0LTEuNjgyLS41OTUtMi4xMTMtLjkyNiAxLjA5Ni0xLjI5NiAyLjc0Ni0yLjY0MiAzLjM5Mi03LjAwMy4wNS0uMzQ3LjAwNy0uNTY1IDAtLjg0NS0uMDA0LS4xNy4wMzUtLjIzNy4yMy0uMjU2YTQuMTczIDQuMTczIDAgMDAxLjU0NS0uNDc1YzEuMzk2LS43NjMgMS45Ni0yLjAxNSAyLjA5My0zLjUxNy4wMi0uMjMtLjAwNC0uNDY3LS4yNDctLjU4OHpNMTEuNTgxIDE4Yy0yLjA4OS0xLjY0Mi0zLjEwMi0yLjE4My0zLjUyLTIuMTYtLjM5Mi4wMjQtLjMyMS40NzEtLjIzNS43NjMuMDkuMjg4LjIwNy40ODYuMzcxLjczOS4xMTQuMTY3LjE5Mi40MTYtLjExMy42MDMtLjY3My40MTYtMS44NDItLjE0LTEuODk3LS4xNjctMS4zNjEtLjgwMi0yLjUtMS44Ni0zLjMwMS0zLjMwNy0uNzc0LTEuMzkzLTEuMjI0LTIuODg3LTEuMjk4LTQuNDgyLS4wMi0uMzg2LjA5My0uNTIyLjQ3Ny0uNTkyYTQuNjk2IDQuNjk2IDAgMDExLjUyOS0uMDM5YzIuMTMyLjMxMiAzLjk0NiAxLjI2NSA1LjQ2OCAyLjc3NC44NjguODYgMS41MjUgMS44ODcgMi4yMDIgMi44OTEuNzIgMS4wNjYgMS40OTQgMi4wODIgMi40OCAyLjkxNC4zNDguMjkyLjYyNS41MTQuODkxLjY3Ny0uODAyLjA5LTIuMTQuMTEtMy4wNTQtLjYxNHptMS02LjQ0YS4zMDYuMzA2IDAgMDEuNDE1LS4yODcuMzAyLjMwMiAwIDAxLjIuMjg4LjMwNi4zMDYgMCAwMS0uMzEuMzA3LjMwMy4zMDMgMCAwMS0uMzA0LS4zMDh6bTMuMTEgMS41OTZjLS4yLjA4MS0uMzk5LjE1MS0uNTkuMTZhMS4yNDUgMS4yNDUgMCAwMS0uNzk4LS4yNTRjLS4yNzQtLjIzLS40Ny0uMzU4LS41NTItLjc1OGExLjczIDEuNzMgMCAwMS4wMTYtLjU4OGMuMDctLjMyNy0uMDA4LS41MzctLjIzOS0uNzI3LS4xODctLjE1Ni0uNDI2LS4xOTktLjY4OC0uMTk5YS41NTkuNTU5IDAgMDEtLjI1NC0uMDc4Yy0uMTEtLjA1NC0uMi0uMTktLjExNC0uMzU4LjAyOC0uMDU0LjE2LS4xODYuMTkyLS4yMS4zNTYtLjIwMi43NjctLjEzNiAxLjE0Ni4wMTYuMzUyLjE0NC42MTguNDA4IDEuMDAxLjc4Mi4zOTEuNDUxLjQ2Mi41NzYuNjg1LjkxNC4xNzYuMjY1LjMzNi41MzcuNDQ1Ljg0OC4wNjcuMTk1LS4wMTkuMzU0LS4yNS40NTJ6IiBmaWxsPSIjNEQ2QkZFIj48L3BhdGg+PC9zdmc+Cg=="},"displayName":"DeepSeek Chat Model","typeVersion":1,"nodeCategories":[{"id":25,"name":"AI"},{"id":26,"name":"Langchain"}]},{"id":1281,"icon":"file:openrouter.svg","name":"@n8n/n8n-nodes-langchain.lmChatOpenRouter","codex":{"data":{"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatopenrouter/"}]},"categories":["AI","Langchain"],"subcategories":{"AI":["Language Models","Root Nodes"],"Language Models":["Chat Models (Recommended)"]}}},"group":"[\"transform\"]","defaults":{"name":"OpenRouter Chat Model"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjOTRBM0I4IiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMCAwIDI0IDI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjx0aXRsZT5PcGVuUm91dGVyPC90aXRsZT48cGF0aCBkPSJNMTYuODA0IDEuOTU3bDcuMjIgNC4xMDV2LjA4N0wxNi43MyAxMC4yMWwuMDE3LTIuMTE3LS44MjEtLjAzYy0xLjA1OS0uMDI4LTEuNjExLjAwMi0yLjI2OC4xMS0xLjA2NC4xNzUtMi4wMzguNTc3LTMuMTQ3IDEuMzUyTDguMzQ1IDExLjAzYy0uMjg0LjE5NS0uNDk1LjMzNi0uNjguNDU1bC0uNTE1LjMyMi0uMzk3LjIzNC4zODUuMjMuNTMuMzM4Yy40NzYuMzE0IDEuMTcuNzk2IDIuNzAxIDEuODY2IDEuMTEuNzc1IDIuMDgzIDEuMTc3IDMuMTQ3IDEuMzUybC4zLjA0NWMuNjk0LjA5MSAxLjM3NS4wOTQgMi44MjUuMDMzbC4wMjItMi4xNTkgNy4yMiA0LjEwNXYuMDg3TDE2LjU4OSAyMmwuMDE0LTEuODYyLS42MzUuMDIyYy0xLjM4Ni4wNDItMi4xMzcuMDAyLTMuMTM4LS4xNjItMS42OTQtLjI4LTMuMjYtLjkyNi00Ljg4MS0yLjA1OWwtMi4xNTgtMS41YTIxLjk5NyAyMS45OTcgMCAwMC0uNzU1LS40OThsLS40NjctLjI4YTU1LjkyNyA1NS45MjcgMCAwMC0uNzYtLjQzQzIuOTA4IDE0LjczLjU2MyAxNC4xMTYgMCAxNC4xMTZWOS44ODhsLjE0LjAwNGMuNTY0LS4wMDcgMi45MS0uNjIyIDMuODA5LTEuMTI0bDEuMDE2LS41OC40MzgtLjI3NGMuNDI4LS4yOCAxLjA3Mi0uNzI2IDIuNjg2LTEuODUzIDEuNjIxLTEuMTMzIDMuMTg2LTEuNzggNC44ODEtMi4wNTkgMS4xNTItLjE5IDEuOTc0LS4yMTMgMy44MTQtLjEzOGwuMDItMS45MDd6Ij48L3BhdGg+PC9zdmc+Cg=="},"displayName":"OpenRouter Chat Model","typeVersion":1,"nodeCategories":[{"id":25,"name":"AI"},{"id":26,"name":"Langchain"}]},{"id":1287,"icon":"file:logo.dark.svg","name":"@n8n/n8n-nodes-langchain.lmChatXAiGrok","codex":{"data":{"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatxaigrok/"}]},"categories":["AI","Langchain"],"subcategories":{"AI":["Language Models","Root Nodes"],"Language Models":["Chat Models (Recommended)"]}}},"group":"[\"transform\"]","defaults":{"name":"xAI Grok Chat Model"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9ImN1cnJlbnRDb2xvciIgdmlld0JveD0iMCAwIDI0IDI0IiBhcmlhLWhpZGRlbj0idHJ1ZSIgY2xhc3M9IiIgZm9jdXNhYmxlPSJmYWxzZSIgc3R5bGU9ImZpbGw6IGN1cnJlbnRjb2xvcjsgaGVpZ2h0OiAyOHB4OyB3aWR0aDogMjhweDsiPjxwYXRoIGQ9Im0zLjAwNSA4Ljg1OCA4Ljc4MyAxMi41NDRoMy45MDRMNi45MDggOC44NTh6TTYuOTA1IDE1LjgyNSAzIDIxLjQwMmgzLjkwN2wxLjk1MS0yLjc4OHpNMTYuNTg1IDJsLTYuNzUgOS42NCAxLjk1MyAyLjc5TDIwLjQ5MiAyek0xNy4yOTIgNy45NjV2MTMuNDM3aDMuMlYzLjM5NXoiPjwvcGF0aD48L3N2Zz4K"},"displayName":"xAI Grok Chat Model","typeVersion":1,"nodeCategories":[{"id":25,"name":"AI"},{"id":26,"name":"Langchain"}]}],"categories":[{"id":35,"name":"Document Extraction"},{"id":48,"name":"AI RAG"}],"image":[]}}