{"workflow":{"id":12175,"name":"Generate photo-based construction cost estimates with GPT-4 Vision and DDC CWICR","views":75,"recentViews":0,"totalViews":75,"createdAt":"2025-12-26T12:50:21.978Z","description":"\nUpload a **construction photo** via web form → get a detailed **cost estimate** with work breakdown, resource costs, and professional HTML report. Powered by **GPT-4 Vision** and the open-source **DDC CWICR** database (55,000+ work items).\n\n## Who's it for\n\n- **Site managers** who need quick estimates from mobile photos\n- **Renovation contractors** evaluating project scope from initial site visit\n- **Real estate inspectors** estimating repair costs\n- **Construction consultants** providing rapid ballpark figures\n- **DIY enthusiasts** planning home improvement budgets\n\n## What it does\n\n1. **Collects** photo + region/language via n8n Form\n2. **Analyzes** photo with GPT-4 Vision (room type, elements, dimensions)\n3. **Decomposes** visible elements into construction work items\n4. **Searches** DDC CWICR vector database for matching rates\n5. **Generates** professional HTML report with cost breakdown\n\n**Supports 9 regions:** 🇩🇪 Berlin · 🇬🇧 Toronto · 🇷🇺 St. Petersburg · 🇪🇸 Barcelona · 🇫🇷 Paris · 🇧🇷 São Paulo · 🇨🇳 Shanghai · 🇦🇪 Dubai · 🇮🇳 Mumbai\n\n## How it works\n\n```\n┌──────────────┐    ┌───────────────┐    ┌───────────────┐    ┌──────────────┐\n│  Web Form    │ →  │  STAGE 1      │ →  │  STAGE 4      │ →  │  Loop Works  │\n│  Photo+Lang  │    │  GPT-4 Vision │    │  Decompose    │    │  per item    │\n└──────────────┘    └───────────────┘    └───────────────┘    └──────────────┘\n                           ↓                     ↓                    ↓\n                    ┌─────────────────────────────────────────────────────┐\n                    │  Identify room, elements, fixtures, dimensions      │\n                    │  → Break down into 15-40 construction work items    │\n                    └─────────────────────────────────────────────────────┘\n                                                                     ↓\n┌──────────────┐    ┌───────────────┐    ┌───────────────┐    ┌──────────────┐\n│  HTML Report │ ←  │  STAGE 7.5    │ ←  │  STAGE 5      │ ←  │  Qdrant      │\n│  Response    │    │  Aggregate    │    │  Parse+Score  │    │  Vector DB   │\n└──────────────┘    └───────────────┘    └───────────────┘    └──────────────┘\n```\n\n**Pipeline stages:**\n\n| Stage | Node | Description |\n|-------|------|-------------|\n| 1 | GPT-4 Vision | Analyzes photo: room type, elements, materials, dimensions |\n| 4 | GPT-4 Decompose | Breaks elements into work items with quantities |\n| 5 | Vector Search + Score | Finds matching rates in DDC CWICR, quality scoring |\n| 7.5 | Aggregate & Validate | Sums costs, groups by phase, validates results |\n| 9 | HTML Report | Generates professional estimate document |\n\n## Prerequisites\n\n| Component | Requirement |\n|-----------|-------------|\n| **n8n** | v1.30+ with Form Trigger support |\n| **OpenAI API** | GPT-4 Vision + Embeddings access |\n| **Qdrant** | Vector DB with DDC CWICR collections |\n| **DDC CWICR Data** | [github.com/datadrivenconstruction/DDC-CWICR](https://github.com/datadrivenconstruction/OpenConstructionEstimate-DDC-CWICR) |\n\n## Setup\n\n### 1. n8n Credentials (Settings → Credentials)\n- **OpenAI API** — required (GPT-4 Vision + text-embedding-3-large)\n- **Qdrant API** — your Qdrant instance connection\n\n### 2. Qdrant Collections\nLoad DDC CWICR embeddings for your target regions:\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\nES_BARCELONA_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR\nFR_PARIS_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR\nPT_SAOPAULO_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR\nZH_SHANGHAI_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR\nAR_DUBAI_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR\nHI_MUMBAI_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR\n```\n\n### 3. Activate Workflow\n1. Import JSON into n8n\n2. Link OpenAI + Qdrant credentials to respective nodes\n3. Activate workflow\n4. Access form at: `https://your-n8n/form/photo-estimate-pro-v3`\n\n## Features\n\n| Feature | Description |\n|---------|-------------|\n| 📸 **Photo Analysis** | GPT-4 Vision identifies room type, elements, fixtures |\n| 📏 **Dimension Estimation** | Uses reference objects (doors, tiles) for sizing |\n| 🔧 **Work Decomposition** | Breaks down to 15-40 specific work items |\n| 🎯 **Quality Scoring** | Rates match quality (high/medium/low/not_found) |\n| 📊 **Phase Grouping** | PREPARATION → MAIN → FINISHING → MEP |\n| 💰 **Cost Breakdown** | Labor, materials, machines per item |\n| ✅ **Validation** | Warns if &lt;50% rates found or missing demolition |\n| 🌍 **9 Languages** | Full localization + regional pricing |\n\n## Form Fields\n\n| Field | Type | Options |\n|-------|------|---------|\n| 📷 Upload Photo | File | .jpg, .png, .webp |\n| 🌍 Region & Language | Dropdown | 9 regions with currencies |\n| 🏗️ Work Type | Dropdown | New / Renovation / Repair / Auto |\n| 📝 Description | Textarea | Optional context |\n\n## Example Output\n\n**Input:** Bathroom photo (renovation)  \n**Region:** 🇩🇪 German - Berlin (EUR €)\n\n**Generated Work Items:**\n```\nPREPARATION (3 items)\n├── Demolition of wall tiles — 12 m² — €180\n├── Demolition of floor tiles — 4.5 m² — €95\n└── Disposal of construction waste — 0.8 m³ — €120\n\nMAIN (8 items)\n├── Floor waterproofing — 4.5 m² — €225\n├── Wall waterproofing wet zone — 8 m² — €280\n├── Floor screed — 4.5 m² — €135\n├── Wall tiling — 22 m² — €880\n├── Floor tiling — 4.5 m² — €225\n├── Toilet installation — 1 pcs — €320\n├── Sink installation — 1 pcs — €185\n└── Shower cabin installation — 1 pcs — €450\n\nFINISHING (3 items)\n├── Ceiling painting — 4.5 m² — €68\n├── Grouting — 26.5 m² — €133\n└── Silicone sealing — 8 m — €48\n\nMEP (4 items)\n├── Socket installation — 2 pcs — €90\n├── Light point installation — 2 pcs — €120\n├── Mixer/faucet installation — 2 pcs — €160\n└── Ventilation installation — 1 pcs — €85\n\n─────────────────────────────────────\nTOTAL: €3,799.00\nLabor: €1,520 · Materials: €1,900 · Machines: €379\nQuality: 78% high match · 18 work items\n```\n\n## Quality Scoring System\n\n| Score | Level | Meaning |\n|-------|-------|---------|\n| 60-100 | 🟢 High | Exact match with resources |\n| 40-59 | 🟡 Medium | Good match, minor differences |\n| 20-39 | 🟠 Low | Partial match, review needed |\n| 0-19 | 🔴 Not Found | No suitable rate found |\n\n**Scoring factors:**\n- Has price in database (+30)\n- Has resources breakdown (+25)\n- Unit matches expected (+20)\n- Material keywords match (+15)\n- Work type keywords match (+10)\n- Vector similarity &gt;0.5 (+10)\n\n## Notes & Tips\n\n- **Best photo angles:** Capture full room, include reference objects (doors, sockets)\n- **Renovation mode:** AI automatically adds demolition works\n- **Validation warnings:** Check if &lt;50% rates found — may need manual additions\n- **Rate accuracy:** Depends on DDC CWICR coverage for your region\n- **Extend:** Chain with PDF generation, email delivery, or CRM integration\n\n## Categories\n\n`AI` · `Data Extraction` · `Document Ops` · `Files & Storage`\n\n## Tags\n\n`photo-analysis`, `gpt-4-vision`, `construction`, `cost-estimation`, `qdrant`, `vector-search`, `form-trigger`, `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 construction, engineering, and technology firms implement:\n- AI-powered visual estimation systems\n- CAD/BIM data processing pipelines\n- Vector database integration for construction data\n- Multilingual cost database solutions\n\n**Contact us** to test with your data or adapt to your project requirements.\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- **n8n Form Trigger:** [docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.formtrigger](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.formtrigger/)\n\n---\n\n⭐ **Star us on GitHub!** [github.com/datadrivenconstruction/DDC-CWICR](https://github.com/datadrivenconstruction/OpenConstructionEstimate-DDC-CWICR)","workflow":{"id":"OvYK6agG8RmwF6Gq","meta":{"instanceId":"faa70e11b7175129a74fd834d3451fdc1862589b16d68ded03f91ca7b1ecca12"},"name":"Photo Cost Estimate Pro v2.0","tags":[],"nodes":[{"id":"0f4cbe14-7ba4-46c5-81d2-6b1d4482efcc","name":"Photo Upload Form","type":"n8n-nodes-base.formTrigger","position":[29104,400],"webhookId":"photo-estimate-pro-v3","parameters":{"options":{},"formTitle":"📸 Photo Cost Estimate Pro v2","formFields":{"values":[{"fieldType":"file","fieldLabel":"📷 Upload Photo","multipleFiles":false,"requiredField":true,"acceptFileTypes":".jpg,.jpeg,.png,.webp"},{"fieldType":"dropdown","fieldLabel":"🌍 Region & Language","fieldOptions":{"values":[{"option":"🇩🇪 German - Berlin (EUR €)"},{"option":"🇬🇧 English - Toronto (CAD $)"},{"option":"🇷🇺 Russian - St. Petersburg (RUB ₽)"},{"option":"🇪🇸 Spanish - Barcelona (EUR €)"},{"option":"🇫🇷 French - Paris (EUR €)"},{"option":"🇧🇷 Portuguese - São Paulo (BRL R$)"},{"option":"🇨🇳 Chinese - Shanghai (CNY ¥)"},{"option":"🇦🇪 Arabic - Dubai (AED د.إ)"},{"option":"🇮🇳 Hindi - Mumbai (INR ₹)"}]},"requiredField":true},{"fieldType":"dropdown","fieldLabel":"🏗️ Work Type","fieldOptions":{"values":[{"option":"🔨 New Construction"},{"option":"🔄 Renovation / Remodel"},{"option":"🔧 Repair"},{"option":"❓ Auto-detect"}]},"requiredField":true},{"fieldType":"textarea","fieldLabel":"📝 Description (optional)","placeholder":"Describe what's in the photo, specify dimensions, or add context..."}]},"responseMode":"lastNode","formDescription":"Upload a construction photo for automatic cost estimation.\nIMPROVED: Multi-stage AI decomposition for accurate work identification.\nPrices based on DDC CWICR regional databases (9 languages)."},"typeVersion":2.2},{"id":"fae54f1a-9225-48c1-a755-83dca9f15033","name":"Extract Input","type":"n8n-nodes-base.code","position":[29328,400],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════\n// EXTRACT INPUT FROM FORM - 9 LANGUAGES + WORK TYPE\n// ═══════════════════════════════════════════════════════════\n\nconst input = $input.first();\nconst formData = input.json || {};\n\nconst regionField = formData['🌍 Region & Language'] || formData['Region & Language'] || '';\nconst workTypeField = formData['🏗️ Work Type'] || formData['Work Type'] || 'Auto-detect';\n\n// 9 languages mapping\nconst langMap = {\n  'German': 'DE',\n  'English': 'EN',\n  'Russian': 'RU',\n  'Spanish': 'ES',\n  'French': 'FR',\n  'Portuguese': 'PT',\n  'Chinese': 'ZH',\n  'Arabic': 'AR',\n  'Hindi': 'HI'\n};\n\nlet languageCode = 'EN';\nfor (const [lang, code] of Object.entries(langMap)) {\n  if (regionField.includes(lang)) {\n    languageCode = code;\n    break;\n  }\n}\n\n// Work type detection\nlet workType = 'auto';\nif (workTypeField.includes('New')) workType = 'new_construction';\nelse if (workTypeField.includes('Renovation')) workType = 'renovation';\nelse if (workTypeField.includes('Repair')) workType = 'repair';\n\nconst userDescription = formData['📝 Description (optional)'] || formData['Description (optional)'] || '';\n\nlet photoBase64 = '';\nlet photoMimeType = 'image/jpeg';\n\nif (input.binary) {\n  for (const key of Object.keys(input.binary)) {\n    const bin = input.binary[key];\n    if (bin.mimeType && bin.mimeType.startsWith('image/')) {\n      photoBase64 = bin.data;\n      photoMimeType = bin.mimeType;\n      break;\n    }\n  }\n}\n\nreturn {\n  json: {\n    language_code: languageCode,\n    photo_base64: photoBase64,\n    photo_mime_type: photoMimeType,\n    has_photo: photoBase64.length > 100,\n    user_description: userDescription,\n    selected_region: regionField,\n    work_type: workType\n  }\n};"},"typeVersion":2},{"id":"d5b99499-be6d-4da7-a381-94cdf5e9f7d4","name":"Configure Language","type":"n8n-nodes-base.code","position":[29552,400],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════\n// LANGUAGE & VECTOR DB CONFIGURATION - 9 LANGUAGES\n// ═══════════════════════════════════════════════════════════\n\nconst input = $input.first().json;\nconst languageCode = (input.language_code || 'EN').toUpperCase();\n\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  '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  '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  '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  '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  '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};\n\nconst config = languageConfig[languageCode] || languageConfig['EN'];\n\nreturn {\n  json: {\n    ...input,\n    language_code: languageCode,\n    language_config: config,\n    qdrant_collection: config.vectorDb,\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    pricing_level: config.city\n  }\n};"},"typeVersion":2},{"id":"2ad831a9-2893-4a0c-992e-337eb4a4bc36","name":"Has Photo?","type":"n8n-nodes-base.if","position":[29776,400],"parameters":{"options":{},"conditions":{"options":{"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"operator":{"type":"boolean","operation":"equals"},"leftValue":"={{ $json.has_photo }}","rightValue":true}]}},"typeVersion":2},{"id":"97d8463c-6e3b-4b16-b44f-6147d7b17f91","name":"Error No Photo","type":"n8n-nodes-base.code","position":[29984,560],"parameters":{"jsCode":"return {\n  json: {\n    success: false,\n    error: true,\n    message: '❌ No photo provided',\n    html_content: '<!DOCTYPE html><html><head><meta charset=\"UTF-8\"></head><body style=\"font-family:system-ui;padding:40px;text-align:center;\"><h1>❌ Error</h1><p>No photo was uploaded. Please go back and upload a construction photo.</p></body></html>'\n  }\n};"},"typeVersion":2},{"id":"b9545c52-79e0-447f-bb2c-fe81fc38a6bf","name":"STAGE 1 Vision Prompt","type":"n8n-nodes-base.set","position":[29984,384],"parameters":{"options":{},"assignments":{"assignments":[{"id":"prompt","name":"chatInput","type":"string","value":"=You are an expert construction surveyor. {{ $json.system_prompt_lang }}\n\n## STAGE 1: IDENTIFY ELEMENTS FROM PHOTO\n\nAnalyze this construction photo and identify ALL visible elements.\n\n### IDENTIFICATION RULES:\n1. **Identify ROOM TYPE** first (bathroom, kitchen, bedroom, living room, hallway, exterior, facade, roof, basement, utility)\n2. **List ALL visible construction elements** with their materials\n3. **Estimate dimensions** using reference objects:\n   - Standard door: 2.0m height, 0.9m width\n   - Standard window: 1.2m × 1.4m\n   - Ceiling height: typically 2.5-2.7m\n   - Brick: 25cm × 12cm × 6.5cm\n   - Tile: 30cm × 30cm or 60cm × 60cm\n   - Socket/switch: 8cm × 8cm\n\n{{ $json.user_description ? 'User note: ' + $json.user_description : '' }}\n\nReturn ONLY valid JSON (no markdown, no code blocks):\n{\n  \"room_type\": \"bathroom|kitchen|bedroom|living_room|hallway|exterior|facade|roof|basement|utility|other\",\n  \"room_description\": \"Brief description of what you see\",\n  \"estimated_dimensions\": {\n    \"floor_area_m2\": 0,\n    \"wall_area_m2\": 0,\n    \"ceiling_height_m\": 2.5,\n    \"perimeter_m\": 0\n  },\n  \"elements\": [\n    {\n      \"element_type\": \"wall|floor|ceiling|window|door|fixture|furniture|mep\",\n      \"element_name\": \"Descriptive name\",\n      \"material\": \"concrete|brick|drywall|tile|wood|glass|metal|plastic\",\n      \"surface_finish\": \"painted|tiled|plastered|wallpaper|raw|laminate\",\n      \"quantity\": 1,\n      \"unit\": \"m²|m|pcs\",\n      \"estimated_size\": \"e.g. 3.5 m² or 2.1 m\",\n      \"condition\": \"new|good|worn|damaged\",\n      \"notes\": \"Any additional observations\"\n    }\n  ],\n  \"fixtures\": [\n    {\n      \"fixture_type\": \"toilet|sink|bathtub|shower|faucet|radiator|socket|switch|light|vent\",\n      \"quantity\": 1,\n      \"notes\": \"Description\"\n    }\n  ],\n  \"work_type_detected\": \"new_construction|renovation|repair\",\n  \"confidence\": \"high|medium|low\"\n}"}]}},"typeVersion":3.4},{"id":"0fb1af6a-fa06-44e6-9181-2f0c3073f1f6","name":"STAGE 1 Analyze Photo","type":"@n8n/n8n-nodes-langchain.chainLlm","position":[30208,384],"parameters":{"messages":{"messageValues":[{"message":"={{ $json.chatInput }}"}]}},"typeVersion":1.4},{"id":"7a62ea97-a203-4ce0-b693-90d385aa542c","name":"GPT-4 Vision","type":"@n8n/n8n-nodes-langchain.lmChatOpenAi","position":[30208,592],"parameters":{"model":{"__rl":true,"mode":"list","value":"chatgpt-4o-latest","cachedResultName":"chatgpt-4o-latest"},"options":{"maxTokens":4000,"temperature":0.2}},"credentials":{"openAiApi":{"id":"credential-id","name":"OpenAi account WS"}},"typeVersion":1.2},{"id":"f4168e15-aa61-4878-a583-79df493e8104","name":"Parse STAGE 1","type":"n8n-nodes-base.code","position":[30480,384],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════\n// PARSE STAGE 1 VISION RESPONSE\n// ═══════════════════════════════════════════════════════════\n\nconst aiResponse = $input.first().json;\nconst configData = $('Configure Language').first().json;\n\nlet parsed = {\n  room_type: 'unknown',\n  room_description: 'Analysis failed',\n  elements: [],\n  fixtures: [],\n  estimated_dimensions: { floor_area_m2: 10, wall_area_m2: 30, ceiling_height_m: 2.5, perimeter_m: 12 }\n};\n\ntry {\n  const content = aiResponse.text || aiResponse.content || aiResponse.response || '';\n  let jsonStr = content;\n  \n  const jsonMatch = content.match(/```json\\s*([\\s\\S]*?)\\s*```/);\n  if (jsonMatch) {\n    jsonStr = jsonMatch[1];\n  } else {\n    const codeMatch = content.match(/```\\s*([\\s\\S]*?)\\s*```/);\n    if (codeMatch) {\n      jsonStr = codeMatch[1];\n    } else {\n      const objMatch = content.match(/\\{[\\s\\S]*\\}/);\n      if (objMatch) {\n        jsonStr = objMatch[0];\n      }\n    }\n  }\n  \n  parsed = JSON.parse(jsonStr);\n} catch (error) {\n  console.error('Parse error:', error.message);\n}\n\nreturn {\n  json: {\n    ...configData,\n    stage1_result: parsed,\n    room_type: parsed.room_type || 'unknown',\n    room_description: parsed.room_description || 'Photo analysis',\n    elements: parsed.elements || [],\n    fixtures: parsed.fixtures || [],\n    dimensions: parsed.estimated_dimensions || {},\n    work_type_detected: parsed.work_type_detected || configData.work_type || 'renovation',\n    confidence: parsed.confidence || 'medium'\n  }\n};"},"typeVersion":2},{"id":"c733ceb2-3357-42b1-b585-0cf2f8d3f2d1","name":"STAGE 4 Decompose Prompt","type":"n8n-nodes-base.set","position":[30656,384],"parameters":{"options":{},"assignments":{"assignments":[{"id":"prompt2","name":"chatInput","type":"string","value":"={{ $json.system_prompt_lang }}\n\n## STAGE 4: DECOMPOSE ELEMENTS TO CONSTRUCTION WORKS\n\nYou are a construction cost estimator. Based on the photo analysis, decompose each element into specific construction work items.\n\n### PHOTO ANALYSIS RESULTS:\n- Room Type: {{ $json.room_type }}\n- Description: {{ $json.room_description }}\n- Work Type: {{ $json.work_type_detected }}\n- Dimensions: Floor {{ $json.dimensions.floor_area_m2 || 10 }} m², Walls {{ $json.dimensions.wall_area_m2 || 30 }} m²\n\n### ELEMENTS DETECTED:\n{{ JSON.stringify($json.elements, null, 2) }}\n\n### FIXTURES DETECTED:\n{{ JSON.stringify($json.fixtures, null, 2) }}\n\n═══════════════════════════════════════════════════════════════\n## CATEGORY → WORK ITEMS MAPPING (USE THIS!)\n═══════════════════════════════════════════════════════════════\n\n### BATHROOM:\n1. Demolition of old finishes (m²) - if renovation\n2. Waterproofing floor (m²)\n3. Waterproofing walls wet zone (m²)\n4. Floor screed (m²)\n5. Wall tiling (m²)\n6. Floor tiling (m²)\n7. Ceiling finishing (m²)\n8. Toilet installation (pcs)\n9. Sink/washbasin installation (pcs)\n10. Bathtub installation (pcs) - if present\n11. Shower cabin installation (pcs) - if present\n12. Faucet/mixer installation (pcs)\n13. Towel rail installation (pcs)\n14. Mirror installation (pcs)\n15. Ventilation installation (pcs)\n16. Electrical: sockets, switches, lighting (pcs)\n17. Plumbing pipes (m)\n18. Door installation (pcs)\n\n### KITCHEN:\n1. Demolition of old finishes (m²) - if renovation\n2. Wall preparation/plastering (m²)\n3. Wall tiling (backsplash area) (m²)\n4. Floor finishing (m²)\n5. Ceiling finishing (m²)\n6. Kitchen cabinets installation (m)\n7. Countertop installation (m)\n8. Sink installation (pcs)\n9. Faucet installation (pcs)\n10. Appliance connections (pcs)\n11. Electrical: sockets, switches (pcs)\n12. Lighting installation (pcs)\n13. Ventilation hood installation (pcs)\n14. Plumbing pipes (m)\n\n### LIVING ROOM / BEDROOM:\n1. Demolition of old finishes (m²) - if renovation\n2. Wall preparation (m²)\n3. Wall painting OR wallpaper (m²)\n4. Floor preparation/screed (m²)\n5. Floor covering - laminate/parquet/carpet (m²)\n6. Baseboard/skirting installation (m)\n7. Ceiling finishing - paint/stretch/drywall (m²)\n8. Electrical: sockets, switches (pcs)\n9. Lighting installation (pcs)\n10. Window sill finishing (m)\n11. Door installation (pcs)\n12. Radiator installation (pcs) - if visible\n\n### FLOOR WORKS:\n1. Old floor demolition (m²) - if renovation\n2. Floor leveling/screed (m²)\n3. Insulation (m²) - if ground floor\n4. Underfloor heating (m²) - if applicable\n5. Primer/preparation (m²)\n6. Floor covering (m²)\n7. Baseboard installation (m)\n\n### WALL WORKS:\n- Drywall: Metal framing (m²) → Insulation (m²) → Boarding (m²) → Jointing (m²) → Painting (m²)\n- Masonry: Brickwork (m²) → Plastering (m²) → Painting (m²)\n- Tiling: Wall preparation (m²) → Tiling (m²) → Grouting (m²)\n\n### WINDOW WORKS:\n1. Old window demolition (pcs) - if renovation\n2. Window frame installation (pcs)\n3. Glazing (m²)\n4. Internal sill (m)\n5. External sill (m)\n6. Sealing/foam (m)\n7. Trim/architrave (m)\n8. Painting/finishing (m)\n\n### DOOR WORKS:\n1. Old door demolition (pcs) - if renovation\n2. Door frame installation (pcs)\n3. Door leaf hanging (pcs)\n4. Hardware installation (pcs)\n5. Trim/architrave (m)\n6. Painting/finishing (m²)\n\n### ELECTRICAL WORKS:\n1. Cable routing (m)\n2. Socket installation (pcs)\n3. Switch installation (pcs)\n4. Junction box (pcs)\n5. Lighting point (pcs)\n6. Panel/breaker installation (pcs)\n\n### PLUMBING WORKS:\n1. Pipe installation - supply (m)\n2. Pipe installation - drain (m)\n3. Valve installation (pcs)\n4. Connection to fixture (pcs)\n5. Pressure testing (pcs)\n\n═══════════════════════════════════════════════════════════════\n## CRITICAL RULES:\n═══════════════════════════════════════════════════════════════\n\n1. **NEVER return empty work_items** - minimum 3 works per element\n2. **Include PREPARATION works** - demolition, priming, leveling\n3. **Include FINISHING works** - painting, sealing, cleaning\n4. **Match units correctly**: areas→m², lengths→m, items→pcs\n5. **For RENOVATION**: always include demolition/removal works first\n6. **Scale quantities** from dimensions provided\n7. **search_query must be in {{ $json.search_lang }}** for vector database\n\n═══════════════════════════════════════════════════════════════\n\nReturn ONLY valid JSON:\n{\n  \"work_items\": [\n    {\n      \"work_sequence\": 1,\n      \"work_category\": \"PREPARATION|MAIN|FINISHING|MEP\",\n      \"work_name\": \"Work name in {{ $json.language }}\",\n      \"search_query\": \"Search terms in {{ $json.search_lang }} for DDC CWICR database - be specific!\",\n      \"quantity\": 12.5,\n      \"unit\": \"m²|m|pcs\",\n      \"calculation_basis\": \"floor_area × 1.0 = 12.5 m²\",\n      \"source_element\": \"Which element this work is for\",\n      \"is_demolition\": false\n    }\n  ],\n  \"total_works_count\": 15,\n  \"phases\": [\"PREPARATION\", \"MAIN\", \"FINISHING\", \"MEP\"]\n}"}]}},"typeVersion":3.4},{"id":"b7550960-96b9-400a-82cd-57fcae2813ec","name":"STAGE 4 Decompose LLM","type":"@n8n/n8n-nodes-langchain.chainLlm","position":[30880,384],"parameters":{"messages":{"messageValues":[{"message":"={{ $json.chatInput }}"}]}},"typeVersion":1.4},{"id":"b973f4c7-7800-48ac-bd3f-f6a7f1e4d278","name":"GPT-4 Decompose","type":"@n8n/n8n-nodes-langchain.lmChatOpenAi","position":[30880,592],"parameters":{"model":{"__rl":true,"mode":"list","value":"chatgpt-4o-latest","cachedResultName":"chatgpt-4o-latest"},"options":{"maxTokens":8000,"temperature":0.3}},"credentials":{"openAiApi":{"id":"credential-id","name":"OpenAi account WS"}},"typeVersion":1.2},{"id":"ae0ff40d-08ce-4c80-8cdc-69d7a343b1a7","name":"Parse STAGE 4","type":"n8n-nodes-base.code","position":[31168,384],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════\n// PARSE STAGE 4 DECOMPOSITION RESPONSE\n// ═══════════════════════════════════════════════════════════\n\nconst aiResponse = $input.first().json;\nconst configData = $('Parse STAGE 1').first().json;\n\nlet parsed = { work_items: [] };\n\ntry {\n  const content = aiResponse.text || aiResponse.content || aiResponse.response || '';\n  let jsonStr = content;\n  \n  const jsonMatch = content.match(/```json\\s*([\\s\\S]*?)\\s*```/);\n  if (jsonMatch) {\n    jsonStr = jsonMatch[1];\n  } else {\n    const objMatch = content.match(/\\{[\\s\\S]*\\}/);\n    if (objMatch) {\n      jsonStr = objMatch[0];\n    }\n  }\n  \n  parsed = JSON.parse(jsonStr);\n} catch (error) {\n  console.error('STAGE 4 Parse error:', error.message);\n}\n\n// Validate and enrich work items\nconst workItems = (parsed.work_items || []).map((work, idx) => {\n  // Ensure search_query exists and is meaningful\n  let searchQuery = work.search_query || work.work_name || '';\n  \n  // Add language-specific terms if needed\n  if (searchQuery.length < 5) {\n    searchQuery = work.work_name + ' ' + (work.source_element || '');\n  }\n  \n  return {\n    work_id: `W${String(idx + 1).padStart(3, '0')}`,\n    work_sequence: work.work_sequence || idx + 1,\n    work_category: work.work_category || 'MAIN',\n    work_name: work.work_name || 'Unnamed work',\n    search_query: searchQuery.trim(),\n    project_quantity: parseFloat(work.quantity) || 1,\n    unit: work.unit || 'm²',\n    calculation_basis: work.calculation_basis || '',\n    source_element: work.source_element || '',\n    is_demolition: work.is_demolition || false\n  };\n});\n\n// Sort by sequence\nworkItems.sort((a, b) => a.work_sequence - b.work_sequence);\n\nreturn {\n  json: {\n    ...configData,\n    work_items: workItems,\n    works_count: workItems.length,\n    phases: parsed.phases || ['PREPARATION', 'MAIN', 'FINISHING', 'MEP'],\n    stage4_success: workItems.length >= 3\n  }\n};"},"typeVersion":2},{"id":"70e62b94-292e-4f2e-b3bc-ca607618d772","name":"Prepare Works","type":"n8n-nodes-base.code","position":[31344,384],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════\n// PREPARE WORKS FOR LOOP\n// ═══════════════════════════════════════════════════════════\n\nconst data = $input.first().json;\nconst works = data.work_items || [];\n\nconst staticData = $getWorkflowStaticData('global');\nstaticData.photo_config = {\n  language_code: data.language_code,\n  language: data.language,\n  currency: data.currency,\n  currency_symbol: data.currency_symbol,\n  locale: data.locale,\n  city: data.city,\n  qdrant_collection: data.qdrant_collection,\n  room_type: data.room_type,\n  room_description: data.room_description,\n  dimensions: data.dimensions,\n  work_type_detected: data.work_type_detected,\n  phases: data.phases,\n  elements: data.elements,\n  fixtures: data.fixtures\n};\nstaticData.work_results = [];\n\nif (works.length === 0) {\n  return [{ json: { _no_works: true, message: 'No work items generated' } }];\n}\n\nreturn works.map((work) => ({\n  json: {\n    ...work,\n    expected_unit: work.unit,\n    type_name: work.work_name,\n    category: work.work_category,\n    assigned_phase: work.work_category,\n    qdrant_collection: data.qdrant_collection,\n    language: data.language,\n    currency: data.currency,\n    currency_symbol: data.currency_symbol,\n    locale: data.locale\n  }\n}));"},"typeVersion":2},{"id":"10b9b98b-f782-499f-9c12-e254af13bd0a","name":"Loop Works","type":"n8n-nodes-base.splitInBatches","position":[31536,384],"parameters":{"options":{"reset":false}},"typeVersion":3},{"id":"79f735d7-6de7-4a05-88e7-12d08b2fd6df","name":"Store Work Data","type":"n8n-nodes-base.code","position":[32016,592],"parameters":{"jsCode":"// Store current work item for Vector Search\nconst work = $input.first().json;\nconst staticData = $getWorkflowStaticData('global');\nstaticData.currentWork = work;\nreturn { json: work };"},"typeVersion":2},{"id":"60873726-0ece-4e69-91dc-73c77ad796b2","name":"Wait","type":"n8n-nodes-base.wait","position":[32160,592],"webhookId":"wait-photo-v3","parameters":{"amount":0.3},"typeVersion":1.1},{"id":"c5e35231-575a-4d53-95c4-ef2875363906","name":"Restore Work Data","type":"n8n-nodes-base.code","position":[32304,592],"parameters":{"jsCode":"// Restore work data from staticData after Wait\nconst staticData = $getWorkflowStaticData('global');\nconst work = staticData.currentWork || {};\nreturn { json: work };"},"typeVersion":2},{"id":"595e380e-4b57-4063-a594-fb89e010f81f","name":"Vector Search","type":"@n8n/n8n-nodes-langchain.vectorStoreQdrant","position":[32464,592],"parameters":{"mode":"load","topK":5,"prompt":"={{ $json.search_query || $json.work_name }}","options":{"contentPayloadKey":"content"},"qdrantCollection":{"__rl":true,"mode":"id","value":"={{ $json.qdrant_collection }}"}},"credentials":{"qdrantApi":{"id":"credential-id","name":"QdrantApi account 2"}},"typeVersion":1.1},{"id":"782d04ee-61c6-40a0-85bf-c5554533f3b5","name":"Embeddings","type":"@n8n/n8n-nodes-langchain.embeddingsOpenAi","position":[32448,752],"parameters":{"model":"text-embedding-3-large","options":{"dimensions":3072}},"credentials":{"openAiApi":{"id":"credential-id","name":"OpenAi account WS"}},"typeVersion":1.2},{"id":"e527cbd6-8841-4425-902a-ee4a28706c2f","name":"STAGE 5 Parse & Score","type":"n8n-nodes-base.code","position":[32720,592],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════\n// STAGE 5: PARSE & VALIDATE VECTOR SEARCH RESULTS\n// FIXED: Correct document/pageContent extraction\n// Quality scoring v2.0 + Resource extraction\n// ═══════════════════════════════════════════════════════════\n\nconst searchResults = $input.all();\nconst staticData = $getWorkflowStaticData('global');\nconst workData = staticData.currentWork || {};\n\n// MACHINE/LABOR PATTERNS (9 languages)\nconst MACHINE_PATTERNS = ['masch', 'maschinenstunde', 'gerät', 'machine', 'mach-h', 'equipment', 'heure machine', 'engin', 'máquina', 'equipo', 'equipamento', 'машин', 'маш-ч', 'маш.ч', 'маш.-ч', 'механизм', '机器', '机械', '设备', '台班', 'آلة', 'معدة', 'मशीन', 'यंत्र'];\nconst LABOR_PATTERNS = ['std', 'stunde', 'arbeiter', 'hour', 'man-hour', 'labor', 'worker', 'heure', 'ouvrier', 'hora', 'obrero', 'operário', 'ч-ч', 'чел-ч', 'человеко-час', 'труд', 'рабочий', 'разряд', ' ч', '工时', '人工', '工人', 'ساعة', 'عامل', 'घंटा', 'श्रम', 'मजदूर'];\n\nfunction detectResourceType(unit, name, code) {\n  const combined = ((unit || '') + ' ' + (name || '') + ' ' + (code || '')).toLowerCase();\n  if (MACHINE_PATTERNS.some(p => combined.includes(p.toLowerCase()))) return 'machine';\n  if (LABOR_PATTERNS.some(p => combined.includes(p.toLowerCase()))) return 'labor';\n  return 'material';\n}\n\nfunction normalizeUnit(unit) {\n  if (!unit) return '';\n  const u = unit.toLowerCase().trim();\n  const unitGroups = {\n    'm2': ['m²', 'm2', 'qm', 'квм', 'кв.м', 'кв. м', 'sq.m', 'sqm', '平方米'],\n    'm3': ['m³', 'm3', 'cbm', 'куб.м', 'куб. м', 'cu.m', '立方米'],\n    'm': ['m', 'м', 'lm', 'lfm', 'п.м', 'п. м', 'lin.m', '米'],\n    'stk': ['stk', 'stück', 'st', 'шт', 'шт.', 'pcs', 'ea', 'each', 'unit', '个', '件'],\n    '100m2': ['100 m²', '100m²', '100 m2', '100m2', '100 qm', '100 кв.м']\n  };\n  for (const [normalized, variants] of Object.entries(unitGroups)) {\n    if (variants.some(v => u === v || u.includes(v))) return normalized;\n  }\n  return u;\n}\n\n// MATERIAL KEYWORDS for matching\nconst MATERIAL_KEYWORDS = [\n  'aluminum', 'aluminium', 'alu', 'алюмин',\n  'wood', 'holz', 'дерев', 'древес', '木',\n  'plastic', 'pvc', 'kunststoff', 'пластик', 'пвх',\n  'steel', 'stahl', 'стал', 'металл', '钢',\n  'concrete', 'beton', 'бетон', '混凝土',\n  'glass', 'glas', 'стекл', '玻璃',\n  'brick', 'ziegel', 'кирпич', '砖',\n  'tile', 'fliese', 'плитк', 'кафель', '瓷砖',\n  'ceramic', 'keramik', 'керамик', '陶瓷',\n  'gypsum', 'gips', 'гипс', '石膏',\n  'drywall', 'гипсокартон', 'гкл',\n  'paint', 'farbe', 'краск', '油漆',\n  'wallpaper', 'tapete', 'обо', '壁纸',\n  'laminate', 'laminat', 'ламинат',\n  'parquet', 'parkett', 'паркет'\n];\n\n// WORK TYPE KEYWORDS for matching\nconst WORK_TYPE_KEYWORDS = [\n  'installation', 'montage', 'установк', 'монтаж', '安装',\n  'demolition', 'abbruch', 'демонтаж', 'снос', 'разбор', '拆除',\n  'preparation', 'vorbereitung', 'подготов', '准备',\n  'sealing', 'abdichtung', 'герметиз', 'гидроизол', '密封',\n  'insulation', 'dämmung', 'изоляц', 'утепл', '保温',\n  'finishing', 'отделк', 'finish', '装修',\n  'excavation', 'aushub', 'выемк', 'копан', '开挖',\n  'concrete', 'beton', 'бетонир', '混凝土',\n  'reinforcement', 'bewehrung', 'армир', '钢筋',\n  'plastering', 'putz', 'штукатур', '抹灰',\n  'painting', 'malen', 'anstrich', 'окраск', 'покраск', '油漆',\n  'tiling', 'fliesen', 'облицов', 'укладк плитк', '贴砖',\n  'flooring', 'boden', 'напольн', 'пол', '地板',\n  'roofing', 'dach', 'кровл', '屋面',\n  'plumbing', 'sanitär', 'сантехн', '管道',\n  'electrical', 'elektro', 'электр', '电气'\n];\n\n// NOT FOUND FALLBACK\nif (!searchResults || searchResults.length === 0) {\n  return [{\n    json: {\n      ...workData,\n      rate_code: 'NOT_FOUND',\n      rate_name: '[Not Found] ' + (workData.work_name || 'Unknown'),\n      rate_unit: workData.expected_unit || 'm²',\n      unit_cost: 0,\n      total_cost: 0,\n      estimated_labor_hours: 0,\n      quality_level: 'not_found',\n      quality_score: 0,\n      quality_reason: 'No search results',\n      resources_all: [],\n      cost_breakdown: { workers: 0, machines: 0, materials: 0 }\n    }\n  }];\n}\n\n// ═══════════════════════════════════════════════════════════\n// FIX: CORRECT EXTRACTION OF document.pageContent\n// ═══════════════════════════════════════════════════════════\nconst parsedResults = searchResults.map((item) => {\n  const data = item.json || {};\n  \n  // FIX: Handle nested document structure from Vector Search\n  const doc = data.document || {};\n  const content = String(doc.pageContent || doc.content || data.pageContent || data.content || '');\n  const metadata = doc.metadata || data.metadata || {};\n  const score = data.score || 0;\n  \n  // ═══════════════════════════════════════════════════════════\n  // EXTRACT TOTAL COST from \"Total cost: 3127.16 EUR\"\n  // ═══════════════════════════════════════════════════════════\n  let totalCost = 0;\n  const costPatterns = [\n    /Total\\s*cost:\\s*([\\d.,]+)\\s*(?:EUR|USD|RUB|CAD|CNY|AED|INR|BRL)/i,\n    /Resources?\\s*cost:\\s*([\\d.,]+)/i,\n    /(?:Gesamt|ИТОГО|Всего|Стоимость)[^\\d]*([\\d.,]+)/i,\n    /(?:EUR|USD|RUB|CAD|\\$|€|₽)\\s*([\\d.,]+)/i\n  ];\n  for (const pattern of costPatterns) {\n    const match = content.match(pattern);\n    if (match) {\n      totalCost = parseFloat(match[1].replace(/,/g, '.').replace(/\\s/g, '')) || 0;\n      if (totalCost > 0) break;\n    }\n  }\n  \n  // ═══════════════════════════════════════════════════════════\n  // EXTRACT CODE, NAME, UNIT from content if not in metadata\n  // ═══════════════════════════════════════════════════════════\n  let rateCode = metadata.rsts || metadata.code || '';\n  let rateName = metadata.names || metadata.name || '';\n  let rateUnit = metadata.unit || '';\n  \n  // Fallback: extract from content\n  if (!rateCode) {\n    const codeMatch = content.match(/CODE:\\s*([A-ZА-Яa-zа-я0-9_-]+)/i);\n    if (codeMatch) rateCode = codeMatch[1];\n  }\n  if (!rateName) {\n    const nameMatch = content.match(/NAME:\\s*(.+?)(?:\\n|UNIT:|CATEGORY:)/i);\n    if (nameMatch) rateName = nameMatch[1].trim();\n  }\n  if (!rateUnit) {\n    const unitMatch = content.match(/UNIT:\\s*(.+?)(?:\\n|CATEGORY:)/i);\n    if (unitMatch) rateUnit = unitMatch[1].trim();\n  }\n  \n  // ═══════════════════════════════════════════════════════════\n  // EXTRACT LABOR HOURS\n  // ═══════════════════════════════════════════════════════════\n  let laborHours = 0;\n  const laborMatch = content.match(/(?:разряд|worker|arbeiter|labor)[^—\\n]*—\\s*([\\d.,]+)\\s*(?:ч|час|std|hour|h)/i);\n  if (laborMatch) {\n    laborHours = parseFloat(laborMatch[1].replace(',', '.')) || 0;\n  }\n  \n  // ═══════════════════════════════════════════════════════════\n  // EXTRACT RESOURCES from RESOURCES: section\n  // ═══════════════════════════════════════════════════════════\n  const resources = [];\n  const resourcesSection = content.match(/RESOURCES:\\s*([\\s\\S]*?)(?:$|MACHINES:|SCOPE|CLASSIFICATION:)/i);\n  if (resourcesSection) {\n    const resourcesText = resourcesSection[1];\n    const resourceLines = resourcesText.split('\\n').filter(line => line.trim().startsWith('-'));\n    \n    for (const line of resourceLines) {\n      const resMatch = line.match(/^-\\s*([A-ZА-Яa-zа-я0-9_-]+)\\s*[—–-]\\s*(.+?)\\s*[—–-]\\s*([\\d.,]+)\\s*(.+?)$/i);\n      if (resMatch) {\n        const resCode = resMatch[1].trim();\n        const resName = resMatch[2].trim();\n        const resQty = parseFloat(resMatch[3].replace(',', '.')) || 0;\n        const resUnit = resMatch[4].trim();\n        const resType = detectResourceType(resUnit, resName, resCode);\n        \n        resources.push({\n          resource_code: resCode,\n          resource_name: resName,\n          resource_quantity: resQty,\n          resource_unit: resUnit,\n          resource_cost: 0,\n          resource_type: resType\n        });\n      }\n    }\n  }\n  \n  return {\n    rate_code: rateCode,\n    rate_name: rateName,\n    rate_unit: rateUnit,\n    total_cost_position: totalCost,\n    worker_labor_hours: laborHours,\n    hierarchy: metadata.hierarchy || '',\n    resources: resources,\n    resources_count: resources.length,\n    vector_score: score,\n    content_preview: content.substring(0, 300)\n  };\n});\n\n// ═══════════════════════════════════════════════════════════\n// QUALITY SCORING v2.0\n// ═══════════════════════════════════════════════════════════\nconst expectedUnit = normalizeUnit(workData.expected_unit || workData.unit || '');\nconst searchQuery = (workData.search_query || workData.work_name || '').toLowerCase();\nconst workName = (workData.work_name || '').toLowerCase();\n\nconst scoredResults = parsedResults.map(result => {\n  let score = 0;\n  const factors = [];\n  const resultUnit = normalizeUnit(result.rate_unit);\n  const rateName = (result.rate_name || '').toLowerCase();\n  const rateContent = (result.content_preview || '').toLowerCase();\n  const combined = rateName + ' ' + rateContent;\n  \n  if (result.total_cost_position > 0) { \n    score += 30; \n    factors.push('has_price:' + result.total_cost_position.toFixed(0)); \n  }\n  \n  if (result.resources_count > 0) { \n    score += Math.min(25, result.resources_count * 5); \n    factors.push('resources:' + result.resources_count); \n  }\n  \n  if (resultUnit && expectedUnit) {\n    if (resultUnit === expectedUnit) { \n      score += 20; \n      factors.push('unit_exact'); \n    } else if (resultUnit.includes(expectedUnit) || expectedUnit.includes(resultUnit)) { \n      score += 10; \n      factors.push('unit_partial'); \n    }\n  }\n  \n  const materialMatches = MATERIAL_KEYWORDS.filter(kw => \n    (workName.includes(kw) || searchQuery.includes(kw)) && combined.includes(kw)\n  );\n  if (materialMatches.length > 0) {\n    score += 15;\n    factors.push('material:' + materialMatches[0]);\n  }\n  \n  const workTypeMatches = WORK_TYPE_KEYWORDS.filter(kw => \n    (workName.includes(kw) || searchQuery.includes(kw)) && combined.includes(kw)\n  );\n  if (workTypeMatches.length > 0) {\n    score += 10;\n    factors.push('work_type:' + workTypeMatches[0]);\n  }\n  \n  const queryWords = searchQuery.split(/\\s+/).filter(w => w.length > 3);\n  const matchedWords = queryWords.filter(w => rateName.includes(w) || rateContent.includes(w));\n  if (matchedWords.length > 0) { \n    score += Math.min(15, matchedWords.length * 5); \n    factors.push('words:' + matchedWords.length); \n  }\n  \n  if (result.vector_score > 0.5) {\n    score += 10;\n    factors.push('vscore:' + result.vector_score.toFixed(2));\n  } else if (result.vector_score > 0.4) {\n    score += 5;\n    factors.push('vscore:' + result.vector_score.toFixed(2));\n  }\n  \n  const hasLabor = result.resources.some(r => r.resource_type === 'labor');\n  const hasMaterial = result.resources.some(r => r.resource_type === 'material');\n  if (hasLabor && hasMaterial) {\n    score += 5;\n    factors.push('complete_rate');\n  }\n  \n  return { ...result, quality_score: score, quality_factors: factors };\n});\n\nconst sorted = scoredResults.sort((a, b) => b.quality_score - a.quality_score);\nconst best = sorted[0] || { quality_score: 0, quality_factors: [], resources: [] };\n\nlet qualityLevel = 'not_found';\nconst s = best.quality_score;\nif (s >= 60) { qualityLevel = 'high'; }\nelse if (s >= 40) { qualityLevel = 'medium'; }\nelse if (s >= 20) { qualityLevel = 'low'; }\n\n// ═══════════════════════════════════════════════════════════\n// CALCULATE COSTS\n// ═══════════════════════════════════════════════════════════\nconst projectQuantity = workData.project_quantity || 0;\nconst unitCost = best.total_cost_position || 0;\n\nlet unitDivisor = 1;\nconst rateUnit = (best.rate_unit || '').toLowerCase();\nif (rateUnit.includes('100')) unitDivisor = 100;\nelse if (rateUnit.includes('10 ')) unitDivisor = 10;\n\nconst quantityInRateUnits = projectQuantity / unitDivisor;\nconst totalCost = quantityInRateUnits * unitCost;\nconst estimatedLaborHours = (best.worker_labor_hours || 0) * quantityInRateUnits;\n\n// ═══════════════════════════════════════════════════════════\n// SCALE & CATEGORIZE RESOURCES\n// ═══════════════════════════════════════════════════════════\nlet laborCost = 0, machineCost = 0, materialCost = 0;\nconst scaledResources = (best.resources || []).map(r => {\n  const scaledQty = (r.resource_quantity || 0) * quantityInRateUnits;\n  const scaledCost = (r.resource_cost || 0) * quantityInRateUnits;\n  \n  if (r.resource_type === 'labor') laborCost += scaledCost;\n  else if (r.resource_type === 'machine') machineCost += scaledCost;\n  else materialCost += scaledCost;\n  \n  return { ...r, scaled_quantity: scaledQty, scaled_cost: scaledCost };\n});\n\nif (laborCost === 0 && materialCost === 0 && totalCost > 0) {\n  laborCost = totalCost * 0.35;\n  materialCost = totalCost * 0.55;\n  machineCost = totalCost * 0.10;\n}\n\nreturn [{\n  json: {\n    work_id: workData.work_id,\n    work_sequence: workData.work_sequence,\n    work_category: workData.work_category,\n    work_name: workData.work_name,\n    source_element: workData.source_element,\n    calculation_basis: workData.calculation_basis,\n    is_demolition: workData.is_demolition,\n    rate_code: best.rate_code || 'NOT_FOUND',\n    rate_name: best.rate_name || workData.work_name || 'Not found',\n    rate_unit: best.rate_unit || workData.expected_unit,\n    project_quantity: projectQuantity,\n    project_unit: workData.unit || workData.expected_unit,\n    quantity_in_rate_units: quantityInRateUnits,\n    calculated_quantity: quantityInRateUnits,\n    unit_divisor: unitDivisor,\n    unit_cost: unitCost,\n    total_cost: totalCost,\n    estimated_labor_hours: estimatedLaborHours,\n    quality_level: qualityLevel,\n    quality_score: s,\n    quality_reason: 'Score ' + s + '/100: ' + best.quality_factors.join(', '),\n    currency: workData.currency,\n    currency_symbol: workData.currency_symbol,\n    locale: workData.locale,\n    type_name: workData.type_name,\n    category: workData.category,\n    assigned_phase: workData.assigned_phase,\n    resources_all: scaledResources,\n    cost_breakdown: {\n      workers: laborCost,\n      machines: machineCost,\n      materials: materialCost\n    },\n    calculation_details: {\n      method: 'photo_analysis',\n      calculation_basis: workData.calculation_basis,\n      raw_value: projectQuantity,\n      unit_divisor: unitDivisor,\n      formula_display: (workData.unit || 'Qty') + ' = ' + projectQuantity\n    },\n    search_debug: {\n      query_used: workData.search_query,\n      results_count: searchResults.length,\n      best_match_score: s,\n      best_vector_score: best.vector_score || 0,\n      best_rate_found: best.rate_name || 'none'\n    }\n  }\n}];"},"typeVersion":2},{"id":"577e8350-ff19-4b73-a53f-80605484a493","name":"Accumulate","type":"n8n-nodes-base.code","position":[32864,592],"parameters":{"jsCode":"// ACCUMULATE RESULTS\nconst staticData = $getWorkflowStaticData('global');\nconst work = $input.first().json;\nif (!staticData.work_results) staticData.work_results = [];\nstaticData.work_results.push(work);\nreturn { json: work };"},"typeVersion":2},{"id":"9b8dd6f9-e36b-4fc0-bac8-5a0857890e32","name":"STAGE 7.5 Aggregate & Validate","type":"n8n-nodes-base.code","position":[31824,352],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════\n// STAGE 7.5: AGGREGATE & VALIDATE RESULTS\n// Build by_phase structure with validation\n// ═══════════════════════════════════════════════════════════\n\nconst staticData = $getWorkflowStaticData('global');\nconst config = staticData.photo_config || {};\nconst works = staticData.work_results || [];\n\n// Calculate totals\nlet grandTotal = 0, grandHours = 0;\nlet grandLaborCost = 0, grandMaterialCost = 0, grandMachineCost = 0;\nlet qHigh = 0, qMedium = 0, qLow = 0, qNotFound = 0;\n\nworks.forEach(w => {\n  grandTotal += w.total_cost || 0;\n  grandHours += w.estimated_labor_hours || 0;\n  \n  const cb = w.cost_breakdown || {};\n  grandLaborCost += cb.workers || 0;\n  grandMaterialCost += cb.materials || 0;\n  grandMachineCost += cb.machines || 0;\n  \n  if (w.quality_level === 'high') qHigh++;\n  else if (w.quality_level === 'medium') qMedium++;\n  else if (w.quality_level === 'low') qLow++;\n  else qNotFound++;\n});\n\n// Sort by sequence\nworks.sort((a, b) => (a.work_sequence || 0) - (b.work_sequence || 0));\n\n// Group by category (PREPARATION, MAIN, FINISHING, MEP)\nconst categories = {};\nworks.forEach(w => {\n  const cat = w.work_category || 'MAIN';\n  if (!categories[cat]) {\n    categories[cat] = {\n      works: [],\n      total_cost: 0,\n      labor_hours: 0\n    };\n  }\n  categories[cat].works.push(w);\n  categories[cat].total_cost += w.total_cost || 0;\n  categories[cat].labor_hours += w.estimated_labor_hours || 0;\n});\n\n// Build by_phase structure\nconst phaseOrder = ['PREPARATION', 'MAIN', 'FINISHING', 'MEP'];\nconst byPhase = phaseOrder\n  .filter(phase => categories[phase])\n  .map((phase, idx) => {\n    const cat = categories[phase];\n    const phaseName = {\n      'PREPARATION': config.language_code === 'RU' ? 'Подготовительные работы' : \n                     config.language_code === 'DE' ? 'Vorbereitungsarbeiten' : 'Preparation Works',\n      'MAIN': config.language_code === 'RU' ? 'Основные работы' : \n              config.language_code === 'DE' ? 'Hauptarbeiten' : 'Main Works',\n      'FINISHING': config.language_code === 'RU' ? 'Отделочные работы' : \n                   config.language_code === 'DE' ? 'Ausbauarbeiten' : 'Finishing Works',\n      'MEP': config.language_code === 'RU' ? 'Инженерные системы' : \n             config.language_code === 'DE' ? 'Haustechnik' : 'MEP Systems'\n    }[phase] || phase;\n    \n    return {\n      phase_name: phaseName,\n      phase_code: phase,\n      phase_total_cost: cat.total_cost,\n      phase_labor_hours: cat.labor_hours,\n      types: [{\n        type_name: config.room_description || 'Photo Analysis',\n        category: config.room_type || 'General',\n        element_count: cat.works.length,\n        type_total_cost: cat.total_cost,\n        type_total_labor_hours: cat.labor_hours,\n        works: cat.works\n      }]\n    };\n  });\n\n// VALIDATION CHECKS\nconst validationIssues = [];\n\nif (works.length < 3) {\n  validationIssues.push('⚠️ Less than 3 work items generated');\n}\n\nconst foundPercent = works.length > 0 ? Math.round((qHigh + qMedium + qLow) / works.length * 100) : 0;\nif (foundPercent < 50) {\n  validationIssues.push('⚠️ Less than 50% rates found - manual review needed');\n}\n\nconst zeroCostCount = works.filter(w => w.total_cost === 0).length;\nif (zeroCostCount > works.length * 0.3) {\n  validationIssues.push('⚠️ Many items with zero cost - database coverage issue');\n}\n\nif (config.work_type_detected === 'renovation') {\n  const hasDemolition = works.some(w => w.is_demolition || \n    (w.work_name || '').toLowerCase().match(/демонтаж|снос|разбор|demolition|abbruch|removal/));\n  if (!hasDemolition) {\n    validationIssues.push('💡 Renovation detected but no demolition works - consider adding');\n  }\n}\n\nconst photoConfig = staticData.photo_config;\nstaticData.work_results = [];\nstaticData.photo_config = null;\n\nreturn {\n  json: {\n    by_phase: byPhase,\n    cost_summary: {\n      grand_total_cost: grandTotal,\n      grand_labor_hours: grandHours,\n      grand_resource_cost: grandLaborCost,\n      grand_material_cost: grandMaterialCost,\n      grand_machine_cost: grandMachineCost\n    },\n    photo_analysis: {\n      description: photoConfig?.room_description || 'Photo Analysis',\n      room_type: photoConfig?.room_type || 'unknown',\n      location_type: photoConfig?.room_type || 'interior',\n      room_size: (photoConfig?.dimensions?.floor_area_m2 || 'N/A') + ' m²',\n      work_phase: photoConfig?.work_type_detected || 'general',\n      materials: photoConfig?.elements?.map(e => e.material).filter(Boolean) || [],\n      elements_count: photoConfig?.elements?.length || 0,\n      fixtures_count: photoConfig?.fixtures?.length || 0\n    },\n    language_code: photoConfig?.language_code || 'EN',\n    currency: photoConfig?.currency || 'EUR',\n    currency_symbol: photoConfig?.currency_symbol || '€',\n    locale: photoConfig?.locale || 'en-US',\n    city: photoConfig?.city || 'Toronto',\n    language: photoConfig?.language || 'English',\n    pricing_level: photoConfig?.city || 'Toronto',\n    quality_stats: {\n      total: works.length,\n      high: qHigh,\n      medium: qMedium,\n      low: qLow,\n      not_found: qNotFound,\n      found_percent: foundPercent\n    },\n    validation: {\n      issues: validationIssues,\n      issues_count: validationIssues.length,\n      passed: validationIssues.length === 0\n    }\n  }\n};"},"typeVersion":2},{"id":"d3fe33db-d2a6-4217-a976-10aeab400af9","name":"STAGE 9 HTML Report","type":"n8n-nodes-base.code","position":[32240,352],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════════\n// STAGE 9: PROFESSIONAL HTML REPORT - 9 LANGUAGES\n// Photo Cost Estimate Pro v2 - with validation section\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst input = $input.first().json;\n\nconst langCode = (input.language_code || 'EN').toUpperCase();\nconst currency = input.currency || 'EUR';\nconst currencySymbol = input.currency_symbol || '€';\nconst locale = input.locale || 'en-US';\nconst pricingLevel = input.pricing_level || input.city || 'Unknown';\nconst photoAnalysis = input.photo_analysis || {};\nconst validation = input.validation || {};\nconst projectName = photoAnalysis.description || 'Photo Cost Estimate';\n\nconst RATES_LINK = 'https://openconstructionestimate.com/all-estimates/?utm=OCE';\n\n// TRANSLATIONS - 9 LANGUAGES\nconst translations = {\n  'DE': { doc_title: 'KOSTENVORANSCHLAG', project: 'Projekt', pricing_level: 'Preisniveau', date: 'Datum', col_pos: 'Pos.', col_code: 'Kennziffer', col_description: 'Bezeichnung', col_calc: 'Berechnung', col_unit: 'Einheit', col_qty: 'Menge', col_price: 'EP', col_total: 'GP', col_labor: 'Std.', col_quality: 'Q', subtotal: 'Zwischensumme', grand_total: 'GESAMTSUMME', labor_cost: 'Lohnkosten', material_cost: 'Materialkosten', labor_days: 'Arbeitstage', found_rates: 'Gefunden', manual_check: 'prüfen', kpi_total: 'Gesamtkosten', kpi_hours: 'Arbeitsstunden', kpi_days: 'Arbeitstage', chart_cost_structure: 'Kostenstruktur', chart_by_phase: 'Nach Phase', chart_labor: 'Lohn', chart_material: 'Material', chart_machines: 'Maschinen', chart_timeline: 'Zeitplan', chart_hierarchy: 'Kostenhierarchie', collapse_all: 'Alles einklappen', expand_all: 'Alles ausklappen', res_labor: 'Lohn', res_material: 'Mat.', res_machine: 'Masch.', photo_analysis: 'Fotoanalyse', location: 'Raumtyp', room_size: 'Raumgröße', work_phase: 'Arbeitstyp', materials: 'Materialien', validation: 'Validierung', validation_passed: 'Alle Prüfungen bestanden', validation_issues: 'Hinweise' },\n  'EN': { doc_title: 'COST ESTIMATE', project: 'Project', pricing_level: 'Pricing Level', date: 'Date', col_pos: 'Pos.', col_code: 'Code', col_description: 'Description', col_calc: 'Calculation', col_unit: 'Unit', col_qty: 'Qty', col_price: 'UP', col_total: 'Total', col_labor: 'Hrs', col_quality: 'Q', subtotal: 'Subtotal', grand_total: 'GRAND TOTAL', labor_cost: 'Labor Cost', material_cost: 'Material Cost', labor_days: 'Work Days', found_rates: 'Found', manual_check: 'check', kpi_total: 'Total Cost', kpi_hours: 'Work Hours', kpi_days: 'Work Days', chart_cost_structure: 'Cost Structure', chart_by_phase: 'By Phase', chart_labor: 'Labor', chart_material: 'Material', chart_machines: 'Machines', chart_timeline: 'Timeline', chart_hierarchy: 'Cost Hierarchy', collapse_all: 'Collapse All', expand_all: 'Expand All', res_labor: 'Labor', res_material: 'Mat.', res_machine: 'Mach.', photo_analysis: 'Photo Analysis', location: 'Room Type', room_size: 'Room Size', work_phase: 'Work Type', materials: 'Materials', validation: 'Validation', validation_passed: 'All checks passed', validation_issues: 'Issues' },\n  'RU': { doc_title: 'СМЕТА', project: 'Проект', pricing_level: 'Уровень цен', date: 'Дата', col_pos: 'N', col_code: 'Шифр', col_description: 'Наименование', col_calc: 'Расчёт', col_unit: 'Ед.', col_qty: 'Кол-во', col_price: 'Цена', col_total: 'Сумма', col_labor: 'Ч/ч', col_quality: 'К', subtotal: 'Итого', grand_total: 'ВСЕГО', labor_cost: 'Труд', material_cost: 'Материалы', labor_days: 'Дней', found_rates: 'Найдено', manual_check: 'проверить', kpi_total: 'Общая стоимость', kpi_hours: 'Часы', kpi_days: 'Дни', chart_cost_structure: 'Структура затрат', chart_by_phase: 'По этапам', chart_labor: 'Труд', chart_material: 'Материалы', chart_machines: 'Машины', chart_timeline: 'График', chart_hierarchy: 'Иерархия затрат', collapse_all: 'Свернуть всё', expand_all: 'Развернуть всё', res_labor: 'Труд', res_material: 'Мат.', res_machine: 'Маш.', photo_analysis: 'Анализ фото', location: 'Тип помещения', room_size: 'Площадь', work_phase: 'Тип работ', materials: 'Материалы', validation: 'Проверка', validation_passed: 'Все проверки пройдены', validation_issues: 'Замечания' },\n  'FR': { doc_title: 'DEVIS ESTIMATIF', project: 'Projet', pricing_level: 'Niveau de prix', date: 'Date', col_pos: 'Pos.', col_code: 'Code', col_description: 'Désignation', col_calc: 'Calcul', col_unit: 'Unité', col_qty: 'Qté', col_price: 'PU', col_total: 'Total', col_labor: 'Hrs', col_quality: 'Q', subtotal: 'Sous-total', grand_total: 'TOTAL GÉNÉRAL', labor_cost: 'Main-d-oeuvre', material_cost: 'Matériaux', labor_days: 'Jours', found_rates: 'Trouvés', manual_check: 'vérifier', kpi_total: 'Coût Total', kpi_hours: 'Heures', kpi_days: 'Jours', chart_cost_structure: 'Structure des coûts', chart_by_phase: 'Par phase', chart_labor: 'Main-d-oeuvre', chart_material: 'Matériaux', chart_machines: 'Machines', chart_timeline: 'Planning', chart_hierarchy: 'Hiérarchie', collapse_all: 'Tout réduire', expand_all: 'Tout développer', res_labor: 'M.O.', res_material: 'Mat.', res_machine: 'Mach.', photo_analysis: 'Analyse photo', location: 'Type de pièce', room_size: 'Surface', work_phase: 'Type de travaux', materials: 'Matériaux', validation: 'Validation', validation_passed: 'Toutes les vérifications passées', validation_issues: 'Remarques' },\n  'ES': { doc_title: 'PRESUPUESTO', project: 'Proyecto', pricing_level: 'Nivel de precios', date: 'Fecha', col_pos: 'Pos.', col_code: 'Código', col_description: 'Descripción', col_calc: 'Cálculo', col_unit: 'Unidad', col_qty: 'Cant.', col_price: 'P.U.', col_total: 'Total', col_labor: 'Hrs', col_quality: 'Q', subtotal: 'Subtotal', grand_total: 'TOTAL GENERAL', labor_cost: 'Mano obra', material_cost: 'Materiales', labor_days: 'Días', found_rates: 'Encontrados', manual_check: 'verificar', kpi_total: 'Coste Total', kpi_hours: 'Horas', kpi_days: 'Días', chart_cost_structure: 'Estructura de costes', chart_by_phase: 'Por fase', chart_labor: 'Mano obra', chart_material: 'Material', chart_machines: 'Máquinas', chart_timeline: 'Cronograma', chart_hierarchy: 'Jerarquía', collapse_all: 'Contraer todo', expand_all: 'Expandir todo', res_labor: 'M.O.', res_material: 'Mat.', res_machine: 'Máq.', photo_analysis: 'Análisis foto', location: 'Tipo de espacio', room_size: 'Superficie', work_phase: 'Tipo de obra', materials: 'Materiales', validation: 'Validación', validation_passed: 'Todas las verificaciones pasaron', validation_issues: 'Observaciones' },\n  'PT': { doc_title: 'ORÇAMENTO', project: 'Projeto', pricing_level: 'Nível de preços', date: 'Data', col_pos: 'Pos.', col_code: 'Código', col_description: 'Descrição', col_calc: 'Cálculo', col_unit: 'Unidade', col_qty: 'Qtd.', col_price: 'P.U.', col_total: 'Total', col_labor: 'Hrs', col_quality: 'Q', subtotal: 'Subtotal', grand_total: 'TOTAL GERAL', labor_cost: 'Mão obra', material_cost: 'Materiais', labor_days: 'Dias', found_rates: 'Encontrados', manual_check: 'verificar', kpi_total: 'Custo Total', kpi_hours: 'Horas', kpi_days: 'Dias', chart_cost_structure: 'Estrutura de custos', chart_by_phase: 'Por fase', chart_labor: 'Mão obra', chart_material: 'Material', chart_machines: 'Máquinas', chart_timeline: 'Cronograma', chart_hierarchy: 'Hierarquia', collapse_all: 'Recolher tudo', expand_all: 'Expandir tudo', res_labor: 'M.O.', res_material: 'Mat.', res_machine: 'Máq.', photo_analysis: 'Análise foto', location: 'Tipo de espaço', room_size: 'Área', work_phase: 'Tipo de obra', materials: 'Materiais', validation: 'Validação', validation_passed: 'Todas as verificações passaram', validation_issues: 'Observações' },\n  'ZH': { doc_title: '工程预算', project: '项目', pricing_level: '价格水平', date: '日期', col_pos: '序号', col_code: '编码', col_description: '名称', col_calc: '计算', col_unit: '单位', col_qty: '数量', col_price: '单价', col_total: '合计', col_labor: '工时', col_quality: '质', subtotal: '小计', grand_total: '总计', labor_cost: '人工费', material_cost: '材料费', labor_days: '工作日', found_rates: '已找到', manual_check: '核查', kpi_total: '总成本', kpi_hours: '工时', kpi_days: '工作日', chart_cost_structure: '成本结构', chart_by_phase: '按阶段', chart_labor: '人工', chart_material: '材料', chart_machines: '机械', chart_timeline: '进度', chart_hierarchy: '层次', collapse_all: '全部折叠', expand_all: '全部展开', res_labor: '人工', res_material: '材料', res_machine: '机械', photo_analysis: '照片分析', location: '房间类型', room_size: '面积', work_phase: '施工类型', materials: '材料', validation: '验证', validation_passed: '所有检查通过', validation_issues: '问题' },\n  'AR': { doc_title: 'تقدير التكلفة', project: 'المشروع', pricing_level: 'مستوى الأسعار', date: 'التاريخ', col_pos: 'رقم', col_code: 'الرمز', col_description: 'الوصف', col_calc: 'الحساب', col_unit: 'الوحدة', col_qty: 'الكمية', col_price: 'سعر الوحدة', col_total: 'المجموع', col_labor: 'ساعات', col_quality: 'ج', subtotal: 'المجموع الفرعي', grand_total: 'المجموع الكلي', labor_cost: 'تكلفة العمالة', material_cost: 'تكلفة المواد', labor_days: 'أيام العمل', found_rates: 'تم العثور', manual_check: 'تحقق', kpi_total: 'التكلفة الإجمالية', kpi_hours: 'ساعات العمل', kpi_days: 'أيام العمل', chart_cost_structure: 'هيكل التكلفة', chart_by_phase: 'حسب المرحلة', chart_labor: 'العمالة', chart_material: 'المواد', chart_machines: 'المعدات', chart_timeline: 'الجدول الزمني', chart_hierarchy: 'تسلسل التكاليف', collapse_all: 'طي الكل', expand_all: 'توسيع الكل', res_labor: 'عمالة', res_material: 'مواد', res_machine: 'معدات', photo_analysis: 'تحليل الصورة', location: 'نوع الغرفة', room_size: 'المساحة', work_phase: 'نوع العمل', materials: 'المواد', validation: 'التحقق', validation_passed: 'جميع الفحوصات ناجحة', validation_issues: 'ملاحظات' },\n  'HI': { doc_title: 'लागत अनुमान', project: 'परियोजना', pricing_level: 'मूल्य स्तर', date: 'तारीख', col_pos: 'क्रम', col_code: 'कोड', col_description: 'विवरण', col_calc: 'गणना', col_unit: 'इकाई', col_qty: 'मात्रा', col_price: 'दर', col_total: 'कुल', col_labor: 'घंटे', col_quality: 'गु', subtotal: 'उप-योग', grand_total: 'कुल योग', labor_cost: 'श्रम लागत', material_cost: 'सामग्री लागत', labor_days: 'कार्य दिवस', found_rates: 'मिला', manual_check: 'जाँचें', kpi_total: 'कुल लागत', kpi_hours: 'कार्य घंटे', kpi_days: 'कार्य दिवस', chart_cost_structure: 'लागत संरचना', chart_by_phase: 'चरण अनुसार', chart_labor: 'श्रम', chart_material: 'सामग्री', chart_machines: 'मशीनें', chart_timeline: 'समयरेखा', chart_hierarchy: 'पदानुक्रम', collapse_all: 'सभी संक्षिप्त करें', expand_all: 'सभी विस्तृत करें', res_labor: 'श्रम', res_material: 'सामग्री', res_machine: 'मशीन', photo_analysis: 'फोटो विश्लेषण', location: 'कमरे का प्रकार', room_size: 'क्षेत्रफल', work_phase: 'कार्य प्रकार', materials: 'सामग्री', validation: 'सत्यापन', validation_passed: 'सभी जाँच पास', validation_issues: 'मुद्दे' }\n};\nconst t = translations[langCode] || translations['EN'];\n\n// HELPERS\nfunction formatCurrency(value) {\n  if (value === null || value === undefined || isNaN(value)) return '—';\n  try { return new Intl.NumberFormat(locale, { style: 'currency', currency: currency, minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value); }\n  catch (e) { return currencySymbol + value.toFixed(2); }\n}\nfunction formatNumber(value, decimals) {\n  if (decimals === undefined) decimals = 3;\n  if (value === null || value === undefined || isNaN(value)) return '—';\n  try { return new Intl.NumberFormat(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }).format(value); }\n  catch (e) { return value.toFixed(decimals); }\n}\nfunction formatDateTime() {\n  try { return new Intl.DateTimeFormat(locale, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }).format(new Date()); }\n  catch (e) { return new Date().toISOString().split('T')[0]; }\n}\nfunction escapeHtml(text) {\n  if (!text) return '';\n  return String(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n}\n\n// DATA\nconst phases = input.by_phase || [];\nconst costSummary = input.cost_summary || {};\nconst qualityStats = input.quality_stats || {};\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;\nconst grandMachineCost = costSummary.grand_machine_cost || 0;\n\nconst totalWorks = qualityStats.total || 0;\nconst qualityHigh = qualityStats.high || 0;\nconst qualityMedium = qualityStats.medium || 0;\nconst qualityLow = qualityStats.low || 0;\nconst qualityNotFound = qualityStats.not_found || 0;\nconst foundRates = qualityHigh + qualityMedium + qualityLow;\nconst foundPercent = qualityStats.found_percent || 0;\n\nconst laborDays = Math.ceil(grandLaborHours / 8);\nconst laborPercent = grandTotal > 0 ? Math.round(grandResourceCost / grandTotal * 100) : 35;\nconst materialPercent = grandTotal > 0 ? Math.round(grandMaterialCost / grandTotal * 100) : 55;\nconst machinesPercent = grandTotal > 0 ? Math.round(grandMachineCost / grandTotal * 100) : 10;\n\nconst dateTimeStr = formatDateTime();\n\nconst phaseChartData = phases.map(function(phase) { return { name: phase.phase_name || 'Phase', cost: phase.phase_total_cost || 0, hours: phase.phase_labor_hours || 0 }; });\n\n// HTML GENERATION\nvar html = `<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<title>${t.doc_title} - ${escapeHtml(projectName)}</title>\n<style>\n:root { --primary: #007AFF; --primary-light: #5AC8FA; --primary-dark: #0051D5; --text-primary: #1D1D1F; --text-secondary: #86868B; --text-muted: #AEAEB2; --bg-white: #FFFFFF; --bg-light: #F5F5F7; --bg-medium: #E8E8ED; --border: #D2D2D7; --success: #34C759; --warning: #FF9500; --error: #FF3B30; }\nbody { font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Arial, sans-serif; margin: 0; padding: 20px; background: #F5F5F7; color: var(--text-primary); line-height: 1.5; font-size: 11px; }\n.container { background: var(--bg-white); max-width: 1350px; margin: 0 auto; box-shadow: 0 4px 20px rgba(0,0,0,0.08); border-radius: 20px; overflow: hidden; }\ntable { border-collapse: collapse; width: 100%; }\ntd, th { padding: 8px 10px; text-align: left; vertical-align: middle; }\n.header-new { background: var(--bg-white); color: var(--text-primary); display: flex; justify-content: space-between; align-items: center; padding: 14px 20px; border-bottom: 1px solid var(--border); }\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 { display: inline-flex; align-items: center; gap: 5px; padding: 7px 12px; background: var(--bg-light); border: 1px solid var(--border); border-radius: 16px; color: var(--text-secondary); text-decoration: none; font-size: 11px; font-weight: 500; transition: all 0.2s; }\n.header-btn:hover { background: var(--bg-medium); color: var(--text-primary); }\n.header-btn svg { width: 14px; height: 14px; }\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.row-1 { display: grid; grid-template-columns: auto 1fr; gap: 12px; margin-bottom: 12px; }\n.kpi-row { display: flex; gap: 6px; }\n.kpi-card { background: var(--bg-white); border-radius: 8px; padding: 10px 14px; display: flex; align-items: center; gap: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); border: 1px solid var(--border); white-space: nowrap; }\n.kpi-icon { font-size: 16px; opacity: 0.5; }\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.row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; }\n.row-3 { display: grid; grid-template-columns: 1fr; gap: 12px; }\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.treemap { display: flex; gap: 6px; height: 60px; }\n.treemap-item { border-radius: 8px; padding: 8px 10px; background: var(--bg-light); color: var(--text-primary); display: flex; flex-direction: column; justify-content: space-between; border: 1px solid var(--border); min-width: 0; cursor: pointer; transition: all 0.15s; }\n.treemap-item:hover { background: var(--bg-medium); border-color: var(--text-secondary); transform: translateY(-1px); }\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.timeline-row { display: flex; align-items: center; margin-bottom: 8px; }\n.timeline-label { width: 120px; 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.quality-bar { background: var(--bg-medium); padding: 8px 16px; font-size: 10px; color: var(--text-secondary); display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }\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.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.col-header { background: var(--bg-medium); color: var(--text-secondary); font-weight: 600; font-size: 9px; text-transform: uppercase; letter-spacing: 0.5px; text-align: center; padding: 8px 6px; border-bottom: 2px solid var(--border); }\n.phase { background: var(--primary); color: #FFFFFF; font-weight: 600; font-size: 11px; cursor: pointer; }\n.phase:hover { opacity: 0.95; }\n.type { background: #EFF6FF; color: var(--text-primary); font-weight: 600; font-size: 10px; border-left: 3px solid var(--primary); cursor: pointer; }\n.type:hover { opacity: 0.95; }\n.work { background: var(--bg-white); color: var(--text-primary); font-size: 10px; border-bottom: 1px solid var(--bg-medium); cursor: pointer; }\n.work:hover { background: #FAFAFA; }\n.work-demolition { background: #FEF3C7; }\n.work-demolition:hover { background: #FDE68A; }\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.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.right { text-align: right; }\n.center { text-align: center; }\n.num { font-variant-numeric: tabular-nums; }\n.link { color: var(--primary); text-decoration: none; }\n.link:hover { text-decoration: underline; }\n.toggle-icon { display: inline-block; width: 16px; transition: transform 0.2s; cursor: pointer; }\ntr.hidden { display: none; }\n.calc-cell { font-size: 9px; color: var(--text-muted); line-height: 1.4; max-width: 200px; }\n.calc-type { color: var(--primary); font-weight: 600; font-size: 9px; display: block; margin-bottom: 2px; }\n.calc-formula { font-family: monospace; font-size: 8px; color: var(--text-secondary); background: #F0F9FF; border: 1px solid #BAE6FD; padding: 3px 6px; border-radius: 4px; display: block; }\n.res-tag { display: inline-block; padding: 2px 6px; border-radius: 4px; font-size: 8px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.3px; margin-left: 4px; }\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.photo-info { padding: 12px 20px; background: #EFF6FF; border-bottom: 1px solid var(--border); font-size: 11px; }\n.photo-info-title { font-weight: 600; color: var(--primary-dark); margin-bottom: 6px; }\n.photo-info-item { display: inline-block; margin-right: 16px; color: var(--text-secondary); }\n.photo-info-item strong { color: var(--text-primary); }\n.validation-bar { padding: 10px 20px; font-size: 11px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }\n.validation-passed { background: #D1FAE5; color: #065F46; }\n.validation-issues { background: #FEF3C7; color: #92400E; }\n.validation-icon { font-size: 14px; }\n.validation-text { font-weight: 500; }\n.validation-issue { background: rgba(0,0,0,0.05); padding: 4px 8px; border-radius: 4px; font-size: 10px; }\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@media print { body { padding: 0; background: white; } .container { box-shadow: none; } }\n</style>\n</head>\n<body>\n<div class=\"container\">\n`;\n\n// HEADER\nhtml += '<div class=\"header-new\"><div class=\"header-left\"><div class=\"header-title-new\">' + t.doc_title + ' v2</div><div class=\"header-project-new\">' + escapeHtml(projectName) + '</div><div class=\"header-info-new\">' + t.pricing_level + ': ' + pricingLevel + ' | ' + dateTimeStr + '</div></div><div class=\"header-right\"><a href=\"https://github.com/datadrivenconstruction\" target=\"_blank\" class=\"header-btn\"><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>GitHub</a><a href=\"https://datadrivenconstruction.io/\" target=\"_blank\"><img src=\"https://datadrivenconstruction.io/wp-content/uploads/2023/07/DataDrivenConstruction-1-1.png\" alt=\"DDC\" class=\"header-logo\"></a></div></div>\\n';\n\n// PHOTO INFO\nhtml += '<div class=\"photo-info\"><div class=\"photo-info-title\">📷 ' + t.photo_analysis + '</div><span class=\"photo-info-item\"><strong>' + t.location + ':</strong> ' + escapeHtml(photoAnalysis.room_type || 'N/A') + '</span><span class=\"photo-info-item\"><strong>' + t.room_size + ':</strong> ' + escapeHtml(photoAnalysis.room_size || 'N/A') + '</span><span class=\"photo-info-item\"><strong>' + t.work_phase + ':</strong> ' + escapeHtml(photoAnalysis.work_phase || 'N/A') + '</span><span class=\"photo-info-item\"><strong>Elements:</strong> ' + (photoAnalysis.elements_count || 0) + '</span><span class=\"photo-info-item\"><strong>Fixtures:</strong> ' + (photoAnalysis.fixtures_count || 0) + '</span>';\nif (photoAnalysis.materials && photoAnalysis.materials.length > 0) {\n  html += '<span class=\"photo-info-item\"><strong>' + t.materials + ':</strong> ' + escapeHtml(photoAnalysis.materials.slice(0, 5).join(', ')) + '</span>';\n}\nhtml += '</div>\\n';\n\n// VALIDATION BAR\nif (validation.passed) {\n  html += '<div class=\"validation-bar validation-passed\"><span class=\"validation-icon\">✅</span><span class=\"validation-text\">' + t.validation_passed + '</span></div>\\n';\n} else if (validation.issues && validation.issues.length > 0) {\n  html += '<div class=\"validation-bar validation-issues\"><span class=\"validation-icon\">⚠️</span><span class=\"validation-text\">' + t.validation_issues + ':</span>';\n  validation.issues.forEach(function(issue) {\n    html += '<span class=\"validation-issue\">' + escapeHtml(issue) + '</span>';\n  });\n  html += '</div>\\n';\n}\n\n// CHARTS SECTION\nhtml += '<div class=\"charts-section\"><div class=\"row-1\"><div class=\"kpi-row\"><div class=\"kpi-card\"><div class=\"kpi-icon\">💰</div><div class=\"kpi-content\"><div class=\"kpi-value\">' + formatCurrency(grandTotal) + '</div><div class=\"kpi-label\">' + t.kpi_total + '</div></div></div><div class=\"kpi-card\"><div class=\"kpi-icon\">⏱️</div><div class=\"kpi-content\"><div class=\"kpi-value\">' + formatNumber(grandLaborHours, 0) + ' h</div><div class=\"kpi-label\">' + t.kpi_hours + '</div></div></div><div class=\"kpi-card\"><div class=\"kpi-icon\">📅</div><div class=\"kpi-content\"><div class=\"kpi-value\">' + laborDays + '</div><div class=\"kpi-label\">' + t.kpi_days + '</div></div></div></div><div class=\"chart-card\"><div class=\"chart-title\">' + t.chart_cost_structure + '</div><div class=\"h-bar\" style=\"margin-bottom:10px;\"><div class=\"h-bar-track\" style=\"height:8px;\"><div class=\"h-bar-fill\" style=\"width:' + laborPercent + '%;background:#1D1D1F;\"></div><div class=\"h-bar-fill\" style=\"width:' + materialPercent + '%;background:#86868B;\"></div><div class=\"h-bar-fill\" style=\"width:' + machinesPercent + '%;background:#AEAEB2;\"></div></div></div><div style=\"display:flex;flex-direction:column;gap:5px;font-size:10px;\"><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 + '</span><span style=\"font-weight:600;\">' + formatCurrency(grandResourceCost) + ' (' + laborPercent + '%)</span></div><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 + '</span><span style=\"font-weight:600;\">' + formatCurrency(grandMaterialCost) + ' (' + materialPercent + '%)</span></div><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 + '</span><span style=\"font-weight:600;\">' + formatCurrency(grandMachineCost) + ' (' + machinesPercent + '%)</span></div></div></div></div>';\n\n// Phase chart\nvar phaseHtml = '';\nfor (var i = 0; i < phaseChartData.length; i++) {\n  var p = phaseChartData[i];\n  var pct = grandTotal > 0 ? Math.round(p.cost / grandTotal * 100) : 0;\n  phaseHtml += '<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}\n\n// Timeline chart\nvar timelineHtml = '';\nfor (var i = 0; i < phaseChartData.length; i++) {\n  var p = phaseChartData[i];\n  var phaseDays = Math.ceil(p.hours / 8);\n  var widthPct = laborDays > 0 ? Math.min(phaseDays / laborDays * 100, 100) : 100;\n  timelineHtml += '<div class=\"timeline-row\"><div class=\"timeline-label\">' + escapeHtml(p.name) + '</div><div class=\"timeline-track\"><div class=\"timeline-bar\" style=\"left:0;width:' + widthPct + '%;\">' + phaseDays + 'd</div></div></div>';\n}\n\nhtml += '<div class=\"row-2\"><div class=\"chart-card\"><div class=\"chart-title\">' + t.chart_by_phase + '</div>' + phaseHtml + '</div><div class=\"chart-card\"><div class=\"chart-title\">' + t.chart_timeline + '</div>' + timelineHtml + '</div></div>';\n\n// Treemap\nvar treemapHtml = '';\nfor (var i = 0; i < phaseChartData.length; i++) {\n  var p = phaseChartData[i];\n  var pct = grandTotal > 0 ? Math.round(p.cost / grandTotal * 100) : 0;\n  treemapHtml += '<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}\nhtml += '<div class=\"row-3\"><div class=\"chart-card\"><div class=\"chart-title\">' + t.chart_hierarchy + '</div><div class=\"treemap\">' + treemapHtml + '</div></div></div></div>\\n';\n\n// TABLE CONTROLS\nhtml += '<div class=\"table-controls\"><button class=\"control-btn\" onclick=\"collapseAll()\">' + t.collapse_all + '</button><button class=\"control-btn\" onclick=\"expandAll()\">' + t.expand_all + '</button></div>\\n';\n\n// QUALITY BAR\nhtml += '<div class=\"quality-bar\"><div class=\"quality-item\"><span>' + t.found_rates + ':</span><span class=\"quality-percent\">' + foundPercent + '%</span><span>(' + foundRates + '/' + totalWorks + ')</span></div><div class=\"quality-item\"><span class=\"quality-dot dot-high\"></span><span>' + qualityHigh + '</span></div><div class=\"quality-item\"><span class=\"quality-dot dot-medium\"></span><span>' + qualityMedium + '</span></div><div class=\"quality-item\"><span class=\"quality-dot dot-low\"></span><span>' + qualityLow + '</span></div><div class=\"quality-item\"><span class=\"quality-dot dot-notfound\"></span><span>' + qualityNotFound + '</span></div>';\nif (qualityNotFound > 0) {\n  html += '<div class=\"quality-item\" style=\"color:var(--warning)\">⚠ ' + qualityNotFound + ' ' + t.manual_check + '</div>';\n}\nhtml += '</div>\\n';\n\n// TABLE\nhtml += '<table><tr class=\"col-header\"><td style=\"width:4%\">' + t.col_pos + '</td><td style=\"width:9%\">' + t.col_code + '</td><td style=\"width:24%\">' + t.col_description + '</td><td style=\"width:16%\">' + t.col_calc + '</td><td style=\"width:6%\">' + t.col_unit + '</td><td style=\"width:7%\" class=\"right\">' + t.col_qty + '</td><td style=\"width:9%\" class=\"right\">' + t.col_price + '</td><td style=\"width:9%\" class=\"right\">' + t.col_total + '</td><td style=\"width:5%\" class=\"right\">' + t.col_labor + '</td><td style=\"width:4%\" class=\"center\">' + t.col_quality + '</td></tr>';\n\n// DATA ROWS\nvar globalWorkIndex = 0;\nfor (var phaseIdx = 0; phaseIdx < phases.length; phaseIdx++) {\n  var phase = phases[phaseIdx];\n  var phaseNum = phaseIdx + 1;\n  var phasePercent = grandTotal > 0 ? ((phase.phase_total_cost || 0) / grandTotal * 100).toFixed(0) : '0';\n  \n  html += '<tr class=\"phase\" id=\"phase-' + phaseNum + '\" data-phase=\"' + phaseNum + '\"><td>' + phaseNum + '</td><td></td><td colspan=\"2\"><span class=\"toggle-icon\">▼</span> ' + escapeHtml(phase.phase_name || 'Phase') + '</td><td></td><td></td><td></td><td class=\"right num\">' + formatCurrency(phase.phase_total_cost || 0) + '</td><td class=\"right num\">' + formatNumber(phase.phase_labor_hours || 0, 1) + '</td><td class=\"center\">' + phasePercent + '%</td></tr>';\n\n  var types = phase.types || [];\n  for (var typeIdx = 0; typeIdx < types.length; typeIdx++) {\n    var type = types[typeIdx];\n    var typeNum = typeIdx + 1;\n    var typeName = type.type_name || 'Unknown Type';\n    var elementCount = type.element_count || 1;\n    var category = type.category || '';\n    \n    html += '<tr class=\"type\" data-phase=\"' + phaseNum + '\" data-type=\"' + typeNum + '\"><td>' + phaseNum + '.' + typeNum + '</td><td></td><td><span class=\"toggle-icon\">▽</span> ' + escapeHtml(typeName) + ' <span style=\"color:var(--text-muted)\">(×' + elementCount + ')</span></td><td class=\"calc-cell\"><span class=\"calc-type\">' + escapeHtml(category) + '</span></td><td></td><td></td><td></td><td class=\"right num\">' + formatCurrency(type.type_total_cost || 0) + '</td><td class=\"right num\">' + formatNumber(type.type_total_labor_hours || 0, 1) + '</td><td></td></tr>';\n\n    var works = type.works || [];\n    for (var workIdx = 0; workIdx < works.length; workIdx++) {\n      var work = works[workIdx];\n      var workNum = workIdx + 1;\n      globalWorkIndex++;\n      var level = work.quality_level || 'not_found';\n      var 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      var rateCode = work.rate_code || '';\n      var codeLink = (rateCode && rateCode !== 'NOT_FOUND') ? '<a href=\"' + RATES_LINK + '\" class=\"link\" target=\"_blank\">' + escapeHtml(rateCode) + '</a>' : '—';\n      \n      var calcDetails = work.calculation_details || {};\n      var calcDisplay = '<span class=\"calc-type\">' + escapeHtml(work.calculation_basis || work.source_element || '') + '</span>';\n      if (calcDetails.formula_display) {\n        calcDisplay += '<span class=\"calc-formula\">' + escapeHtml(calcDetails.formula_display) + '</span>';\n      }\n      \n      var workRowClass = 'work';\n      if (work.is_demolition) workRowClass += ' work-demolition';\n      \n      html += '<tr class=\"' + workRowClass + '\" data-phase=\"' + phaseNum + '\" data-type=\"' + typeNum + '\" data-work=\"' + globalWorkIndex + '\"><td style=\"padding-left:16px\">' + phaseNum + '.' + typeNum + '.' + workNum + '</td><td>' + codeLink + '</td><td style=\"padding-left:24px\"><span class=\"toggle-icon\">▿</span> ' + (work.is_demolition ? '🔨 ' : '') + escapeHtml(work.rate_name || work.work_name || 'Work') + '</td><td class=\"calc-cell\">' + calcDisplay + '</td><td>' + escapeHtml(work.rate_unit || work.project_unit || '—') + '</td><td class=\"right num\">' + formatNumber(work.calculated_quantity || work.quantity_in_rate_units || work.project_quantity || 0, 4) + '</td><td class=\"right num\">' + formatCurrency(work.unit_cost || 0) + '</td><td class=\"right num\">' + formatCurrency(work.total_cost || 0) + '</td><td class=\"right num\">' + formatNumber(work.estimated_labor_hours || 0, 1) + '</td><td class=\"center\"><span class=\"quality-dot ' + dotClass + '\"></span></td></tr>';\n\n      var resources = work.resources_all || [];\n      for (var resIdx = 0; resIdx < resources.length; resIdx++) {\n        var res = resources[resIdx];\n        var isLast = resIdx === resources.length - 1;\n        var prefix = isLast ? '└' : '├';\n        var resType = res.resource_type || 'material';\n        var tagLabels = { labor: t.res_labor, material: t.res_material, machine: t.res_machine };\n        var tagLabel = tagLabels[resType] || t.res_material;\n        var tagClass = 'res-tag res-tag-' + resType;\n        \n        html += '<tr class=\"resource\" data-phase=\"' + phaseNum + '\" data-type=\"' + typeNum + '\" data-work=\"' + globalWorkIndex + '\"><td></td><td style=\"font-size:9px;\"><span style=\"opacity:0.5;\">' + escapeHtml(res.resource_code || '') + '</span><span class=\"' + tagClass + '\">' + tagLabel + '</span></td><td style=\"padding-left:36px\">' + prefix + ' ' + escapeHtml(res.resource_name || 'Resource') + '</td><td></td><td>' + escapeHtml(res.resource_unit || '') + '</td><td class=\"right num\">' + formatNumber(res.scaled_quantity || res.resource_quantity || 0, 4) + '</td><td class=\"right num\">' + formatCurrency(res.price_per_unit || 0) + '</td><td class=\"right num\">' + formatCurrency(res.scaled_cost || res.resource_cost || 0) + '</td><td></td><td></td></tr>';\n      }\n    }\n  }\n  \n  html += '<tr class=\"subtotal\" data-phase=\"' + phaseNum + '\"><td></td><td></td><td colspan=\"2\">' + t.subtotal + ' ' + phaseNum + '</td><td></td><td></td><td></td><td class=\"right num\">' + formatCurrency(phase.phase_total_cost || 0) + '</td><td class=\"right num\">' + formatNumber(phase.phase_labor_hours || 0, 1) + '</td><td class=\"center\">' + phasePercent + '%</td></tr>';\n}\n\n// GRAND TOTAL\nhtml += '<tr class=\"grand-total\"><td colspan=\"4\" style=\"padding-left:16px\">' + t.grand_total + '</td><td></td><td></td><td></td><td class=\"right num\">' + formatCurrency(grandTotal) + '</td><td class=\"right num\">' + formatNumber(grandLaborHours, 1) + '</td><td class=\"center\">100%</td></tr>';\nhtml += '<tr class=\"grand-total-info\"><td colspan=\"4\" style=\"padding-left:16px\">' + t.labor_cost + ': <span class=\"highlight\">' + formatCurrency(grandResourceCost) + '</span> (' + laborPercent + '%)</td><td colspan=\"3\">' + t.material_cost + ': <span class=\"highlight\">' + formatCurrency(grandMaterialCost) + '</span></td><td colspan=\"3\" class=\"right\" style=\"padding-right:16px\"><span class=\"highlight\">' + laborDays + '</span> ' + t.labor_days + '</td></tr>';\nhtml += '</table>\\n';\n\n// FOOTER\nhtml += '<div class=\"footer\"><div class=\"footer-main\"><div class=\"footer-brand\">OPEN CONSTRUCTION ESTIMATE v2</div><div class=\"footer-info\">Professional cost estimation powered by DDC CWICR database | Multi-stage AI decomposition</div><div class=\"footer-links\"><a href=\"' + RATES_LINK + '\" target=\"_blank\">openconstructionestimate.com</a><a href=\"https://github.com/datadrivenconstruction/OpenConstructionEstimate-DDC-CWICR\" target=\"_blank\">GitHub</a><a href=\"https://datadrivenconstruction.io\" target=\"_blank\">datadrivenconstruction.io</a></div></div><div class=\"footer-db\">DDC CWICR — Construction Work Items, Costs & Resources Database | Generated: ' + dateTimeStr + '</div></div>\\n';\n\nhtml += '</div>\\n';\n\n// SCRIPT\nhtml += `<script>\nfunction collapseAll() {\n  document.querySelectorAll(\"tr.phase\").forEach(function(row) {\n    row.classList.add(\"collapsed\");\n    var icon = row.querySelector(\".toggle-icon\");\n    if (icon) icon.textContent = \"▶\";\n    var 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\nfunction expandAll() {\n  document.querySelectorAll(\"tr.phase\").forEach(function(row) {\n    row.classList.remove(\"collapsed\");\n    var icon = row.querySelector(\".toggle-icon\");\n    if (icon) icon.textContent = \"▼\";\n    var 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    var icon = row.querySelector(\".toggle-icon\");\n    if (icon) icon.textContent = \"▽\";\n  });\n  document.querySelectorAll(\"tr.work\").forEach(function(row) {\n    row.classList.remove(\"collapsed\");\n    var icon = row.querySelector(\".toggle-icon\");\n    if (icon) icon.textContent = \"▿\";\n  });\n}\n\ndocument.addEventListener(\"DOMContentLoaded\", function() {\n  document.querySelectorAll(\"tr.phase\").forEach(function(row) {\n    row.addEventListener(\"click\", function(e) {\n      if (e.target.tagName === \"A\") return;\n      var phaseId = this.dataset.phase;\n      this.classList.toggle(\"collapsed\");\n      var icon = this.querySelector(\".toggle-icon\");\n      var isCollapsed = this.classList.contains(\"collapsed\");\n      if (icon) icon.textContent = isCollapsed ? \"▶\" : \"▼\";\n      document.querySelectorAll('tr[data-phase=\"' + phaseId + '\"]:not(.phase)').forEach(function(child) {\n        if (isCollapsed) {\n          child.classList.add(\"hidden\");\n        } else {\n          if (child.classList.contains(\"resource\")) {\n            var workId = child.dataset.work;\n            var parentWork = document.querySelector('tr.work[data-work=\"' + workId + '\"]');\n            if (parentWork && !parentWork.classList.contains(\"collapsed\")) {\n              child.classList.remove(\"hidden\");\n            }\n          } else if (child.classList.contains(\"work\")) {\n            var typeId = child.dataset.type;\n            var parentType = document.querySelector('tr.type[data-phase=\"' + phaseId + '\"][data-type=\"' + typeId + '\"]');\n            if (parentType && !parentType.classList.contains(\"collapsed\")) {\n              child.classList.remove(\"hidden\");\n            }\n          } else {\n            child.classList.remove(\"hidden\");\n          }\n        }\n      });\n    });\n  });\n  \n  document.querySelectorAll(\"tr.type\").forEach(function(row) {\n    row.addEventListener(\"click\", function(e) {\n      e.stopPropagation();\n      if (e.target.tagName === \"A\") return;\n      var phaseId = this.dataset.phase;\n      var typeId = this.dataset.type;\n      this.classList.toggle(\"collapsed\");\n      var icon = this.querySelector(\".toggle-icon\");\n      var isCollapsed = this.classList.contains(\"collapsed\");\n      if (icon) icon.textContent = isCollapsed ? \"▷\" : \"▽\";\n      \n      document.querySelectorAll('tr.work[data-phase=\"' + phaseId + '\"][data-type=\"' + typeId + '\"]').forEach(function(work) {\n        if (isCollapsed) {\n          work.classList.add(\"hidden\");\n        } else {\n          work.classList.remove(\"hidden\");\n        }\n      });\n      \n      document.querySelectorAll('tr.resource[data-phase=\"' + phaseId + '\"][data-type=\"' + typeId + '\"]').forEach(function(res) {\n        if (isCollapsed) {\n          res.classList.add(\"hidden\");\n        } else {\n          var workId = res.dataset.work;\n          var parentWork = document.querySelector('tr.work[data-work=\"' + workId + '\"]');\n          if (parentWork && !parentWork.classList.contains(\"collapsed\")) {\n            res.classList.remove(\"hidden\");\n          }\n        }\n      });\n    });\n  });\n  \n  document.querySelectorAll(\"tr.work\").forEach(function(row) {\n    row.addEventListener(\"click\", function(e) {\n      e.stopPropagation();\n      if (e.target.tagName === \"A\") return;\n      var workId = this.dataset.work;\n      this.classList.toggle(\"collapsed\");\n      var icon = this.querySelector(\".toggle-icon\");\n      var isCollapsed = this.classList.contains(\"collapsed\");\n      if (icon) icon.textContent = isCollapsed ? \"▹\" : \"▿\";\n      \n      document.querySelectorAll('tr.resource[data-work=\"' + workId + '\"]').forEach(function(res) {\n        if (isCollapsed) {\n          res.classList.add(\"hidden\");\n        } else {\n          res.classList.remove(\"hidden\");\n        }\n      });\n    });\n  });\n  \n  document.querySelectorAll(\".treemap-item\").forEach(function(item) {\n    item.addEventListener(\"click\", function() {\n      var phaseId = this.dataset.phase;\n      var targetRow = document.getElementById(\"phase-\" + phaseId);\n      if (targetRow) {\n        if (targetRow.classList.contains(\"collapsed\")) targetRow.click();\n        targetRow.scrollIntoView({ behavior: \"smooth\", block: \"start\" });\n        targetRow.style.background = \"#FFFBEB\";\n        setTimeout(function() { targetRow.style.background = \"\"; }, 1500);\n      }\n    });\n  });\n  \n  // Default: hide resources\n  document.querySelectorAll(\"tr.resource\").forEach(function(row) {\n    row.classList.add(\"hidden\");\n  });\n  document.querySelectorAll(\"tr.work\").forEach(function(row) {\n    row.classList.add(\"collapsed\");\n    var icon = row.querySelector(\".toggle-icon\");\n    if (icon) icon.textContent = \"▹\";\n  });\n});\n</script>\n`;\n\nhtml += '</body>\\n</html>';\n\nreturn {\n  json: {\n    success: true,\n    html_content: html,\n    summary: {\n      total_cost: grandTotal,\n      total_cost_formatted: formatCurrency(grandTotal),\n      labor_hours: grandLaborHours,\n      labor_days: laborDays,\n      works_count: totalWorks,\n      found_percent: foundPercent,\n      pricing_city: pricingLevel,\n      currency: currency\n    },\n    photo_analysis: photoAnalysis,\n    quality_stats: qualityStats,\n    validation: validation,\n    message: '✅ ' + t.doc_title + ' v2! ' + totalWorks + ' works → ' + formatCurrency(grandTotal) + ' (' + pricingLevel + ')'\n  }\n};"},"typeVersion":2},{"id":"286a9e9b-d24d-49b1-aaf1-eaeea6fb079a","name":"Final HTML Output","type":"n8n-nodes-base.code","position":[32448,352],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════\n// FINAL HTML OUTPUT - Returns HTML directly to Form\n// ═══════════════════════════════════════════════════════════\n\nconst input = $input.first().json;\nconst htmlContent = input.html_content || '<!DOCTYPE html><html><head><meta charset=\"UTF-8\"></head><body><h1>Error</h1><p>No content generated</p></body></html>';\n\n// Form Trigger with responseMode: lastNode will return this\nreturn {\n  json: {\n    success: true,\n    message: input.message || 'Estimate complete!',\n    summary: input.summary || {},\n    validation: input.validation || {}\n  },\n  binary: {\n    data: {\n      data: Buffer.from(htmlContent, 'utf-8').toString('base64'),\n      mimeType: 'text/html',\n      fileName: 'PhotoEstimate_v2.html'\n    }\n  }\n};"},"typeVersion":2},{"id":"b32c35fe-7284-4c29-818b-c8fed8b42ee5","name":"Validation Note","type":"n8n-nodes-base.stickyNote","position":[31696,48],"parameters":{"color":5,"width":260,"height":464,"content":"## ✅ STAGE 7.5: Validation\n\n**Checks performed:**\n1. Minimum 3 work items\n2. Found rate % > 50%\n3. Zero cost items < 30%\n4. Renovation has demolition\n\nGroups works by category:\n- PREPARATION\n- MAIN\n- FINISHING\n- MEP"},"typeVersion":1},{"id":"fec19cc3-f61f-48bd-86a2-d919d0b36b3f","name":"Pipeline Overview","type":"n8n-nodes-base.stickyNote","position":[28688,640],"parameters":{"color":7,"width":328,"height":240,"content":"## 📊 Pipeline Overview\n\n| Stage | Description |\n|-------|-------------|\n| 1 | Vision analyzes photo |\n| 4 | Decompose to works |\n| 5 | Vector search pricing |\n| 5.2 | Parse & Score results |\n| 7 | Calculate costs |\n| 7.5 | Validate works |\n| 9 | Generate HTML report |"},"typeVersion":1},{"id":"3623a3d0-55cd-4741-bf4e-58b49c7e2bc2","name":"Block 1 Note","type":"n8n-nodes-base.stickyNote","position":[29040,48],"parameters":{"color":5,"width":1080,"height":824,"content":"## Block 1: Photo Upload & Config\n\nThis block:\n- Receives photo from web form\n- Extracts language/region settings\n- Configures Vector DB collection\n- Validates photo presence\n\n**Supported formats:** JPG, PNG, WebP"},"typeVersion":1},{"id":"a8f67a11-ec59-401c-bf06-429aec72f28a","name":"Block 2 Note","type":"n8n-nodes-base.stickyNote","position":[30144,48],"parameters":{"color":6,"width":464,"height":820,"content":"## Block 2: STAGE 1 - Vision Analysis\n\n### 🤖 AI Photo Analysis\n\nGPT-4 Vision analyzes photo:\n- Detects room type (bathroom, kitchen, etc.)\n- Identifies construction elements\n- Estimates dimensions from references\n- Lists fixtures and materials\n\n**Output:** Structured JSON with elements"},"typeVersion":1},{"id":"fb2c4ced-db96-4bd2-9032-22ccd960fec4","name":"Block 3 Note","type":"n8n-nodes-base.stickyNote","position":[30624,48],"parameters":{"color":6,"width":656,"height":816,"content":"## Block 3: STAGE 4 - Work Decomposition\n\n### 🤖 AI Decomposition\n\nDecomposes elements into construction works:\n- BATHROOM → waterproofing, tiling, plumbing\n- KITCHEN → cabinets, countertops, backsplash\n- FLOOR → screed, covering, baseboard\n- WALL → prep, finish, paint\n- MEP → electrical, plumbing\n\n**Rules:**\n- Minimum 3 works per element\n- Include PREP + MAIN + FINISHING\n- For renovation: add demolition first"},"typeVersion":1},{"id":"9db962cb-16a4-4368-961d-f00fb59edca9","name":"Block 4 Note","type":"n8n-nodes-base.stickyNote","position":[31296,48],"parameters":{"color":5,"width":380,"height":816,"content":"## Block 4: STAGE 5 - Pricing Loop\n\nProcesses each work item:\n\n1. **Vector Search** - Find rates in Qdrant\n2. **Parse Results** - Extract costs & resources\n3. **Quality Scoring** - Rate match quality\n4. **Calculate** - Qty × Unit Price\n\n**Database:** DDC CWICR\n700,000+ construction rates"},"typeVersion":1},{"id":"d34124bf-ae65-49e3-b801-4d80f553964d","name":"Vector Search Note","type":"n8n-nodes-base.stickyNote","position":[32768,48],"parameters":{"width":276,"height":176,"content":"### 🔍 Vector Search\n\nSearches DDC CWICR database:\n- 3072-dim embeddings\n- Top 5 matches per query\n- Multilingual support"},"typeVersion":1},{"id":"581a9ed1-67e0-4677-be4a-921a38abc68e","name":"Parse Score Note","type":"n8n-nodes-base.stickyNote","position":[31696,528],"parameters":{"color":4,"width":1356,"height":336,"content":"### ⚡ STAGE 5.2 Parse & Score\n\n**FIXED:** Correct document extraction\n\nExtracts from Qdrant results:\n- Total cost from content\n- Rate code, name, unit\n- Resources (labor/material/machine)\n"},"typeVersion":1},{"id":"77b90b95-27a5-4703-90d7-7cc6364d559f","name":"Block 6 Note","type":"n8n-nodes-base.stickyNote","position":[31968,48],"parameters":{"color":5,"width":776,"height":460,"content":"## Block 6: Report Generation\n\n### 📊 STAGE 9 HTML Report\n**Features:**\n- Professional design\n- Quality indicators (● ○)\n- Calculation formulas\n- Clickable rate links\n- Cost structure charts\n- Phase timeline\n- Treemap visualization\n- 9 language support\n\n**Output:** HTML + XLS files"},"typeVersion":1},{"id":"d260439b-2562-4319-b022-cc7954dacb71","name":"AI Models Note","type":"n8n-nodes-base.stickyNote","position":[28688,48],"parameters":{"width":328,"height":232,"content":"## 🧠 AI Models Used\n\n**GPT-4 Vision** — Photo analysis\n**GPT-4** — Work decomposition\n\nCan be replaced with:\n- Anthropic Claude 3.5\n- Google Gemini Pro\n- OpenRouter models"},"typeVersion":1},{"id":"6ddf3397-f636-42cd-aac2-ade61036c5e4","name":"Qdrant Setup Note","type":"n8n-nodes-base.stickyNote","position":[32768,256],"parameters":{"width":272,"height":248,"content":"### 📥 Vector Database Setup\n\nTo enable search:\n1. Install Qdrant (local or VPS)\n2. Add Qdrant credentials\n3. Upload DDC CWICR dataset\n4. Select collection for your language\n\nCollections available on [GitHub](https://github.com/datadrivenconstruction)"},"typeVersion":1},{"id":"60030e9d-0323-425b-a99f-973e6ec0e077","name":"Info1","type":"n8n-nodes-base.stickyNote","position":[29040,-112],"parameters":{"width":560,"height":144,"content":"# 📸 Photo Cost Estimate Pro v2\n\n## Multi-stage AI decomposition pipeline\n\n"},"typeVersion":1},{"id":"809715a7-2395-4fe1-a987-7e8df7c0cdb8","name":"AI Models Note1","type":"n8n-nodes-base.stickyNote","position":[28688,304],"parameters":{"width":328,"height":312,"content":"### Pipeline:\n1. 📷 GPT-4 Vision → Elements\n2. 🔄 STAGE 4: Decompose → Works  \n3. 🔍 Vector Search DDC CWICR\n4. ✅ Validation Stage\n5. 📊 HTML Report\n\n### Regions (9):\n🇩🇪 Berlin | 🇬🇧 Toronto | 🇷🇺 St. Petersburg\n🇪🇸 Barcelona | 🇫🇷 Paris | 🇧🇷 São Paulo\n🇨🇳 Shanghai | 🇦🇪 Dubai | 🇮🇳 Mumbai\n\n⭐ **Star our repository** on [GitHub](https://github.com/datadrivenconstruction/OpenConstructionEstimate-DDC-CWICR)"},"typeVersion":1},{"id":"b4e01f61-6023-4851-b7ae-7d555c5059de","name":"Sticky Note1","type":"n8n-nodes-base.stickyNote","position":[32368,-112],"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}],"active":false,"pinData":{},"settings":{"executionOrder":"v1"},"versionId":"56ea10a3-8e5f-4545-95b5-d107d41cad75","connections":{"Wait":{"main":[[{"node":"Restore Work Data","type":"main","index":0}]]},"Accumulate":{"main":[[{"node":"Loop Works","type":"main","index":0}]]},"Embeddings":{"ai_embedding":[[{"node":"Vector Search","type":"ai_embedding","index":0}]]},"Has Photo?":{"main":[[{"node":"STAGE 1 Vision Prompt","type":"main","index":0}],[{"node":"Error No Photo","type":"main","index":0}]]},"Loop Works":{"main":[[{"node":"STAGE 7.5 Aggregate & Validate","type":"main","index":0}],[{"node":"Store Work Data","type":"main","index":0}]]},"GPT-4 Vision":{"ai_languageModel":[[{"node":"STAGE 1 Analyze Photo","type":"ai_languageModel","index":0}]]},"Extract Input":{"main":[[{"node":"Configure Language","type":"main","index":0}]]},"Parse STAGE 1":{"main":[[{"node":"STAGE 4 Decompose Prompt","type":"main","index":0}]]},"Parse STAGE 4":{"main":[[{"node":"Prepare Works","type":"main","index":0}]]},"Prepare Works":{"main":[[{"node":"Loop Works","type":"main","index":0}]]},"Vector Search":{"main":[[{"node":"STAGE 5 Parse & Score","type":"main","index":0}]]},"GPT-4 Decompose":{"ai_languageModel":[[{"node":"STAGE 4 Decompose LLM","type":"ai_languageModel","index":0}]]},"Store Work Data":{"main":[[{"node":"Wait","type":"main","index":0}]]},"Photo Upload Form":{"main":[[{"node":"Extract Input","type":"main","index":0}]]},"Restore Work Data":{"main":[[{"node":"Vector Search","type":"main","index":0}]]},"Configure Language":{"main":[[{"node":"Has Photo?","type":"main","index":0}]]},"STAGE 9 HTML Report":{"main":[[{"node":"Final HTML Output","type":"main","index":0}]]},"STAGE 1 Analyze Photo":{"main":[[{"node":"Parse STAGE 1","type":"main","index":0}]]},"STAGE 1 Vision Prompt":{"main":[[{"node":"STAGE 1 Analyze Photo","type":"main","index":0}]]},"STAGE 4 Decompose LLM":{"main":[[{"node":"Parse STAGE 4","type":"main","index":0}]]},"STAGE 5 Parse & Score":{"main":[[{"node":"Accumulate","type":"main","index":0}]]},"STAGE 4 Decompose Prompt":{"main":[[{"node":"STAGE 4 Decompose LLM","type":"main","index":0}]]},"STAGE 7.5 Aggregate & Validate":{"main":[[{"node":"STAGE 9 HTML Report","type":"main","index":0}]]}}},"lastUpdatedBy":1,"workflowInfo":{"nodeCount":39,"nodeTypes":{"n8n-nodes-base.if":{"count":1},"n8n-nodes-base.set":{"count":2},"n8n-nodes-base.code":{"count":13},"n8n-nodes-base.wait":{"count":1},"n8n-nodes-base.stickyNote":{"count":14},"n8n-nodes-base.formTrigger":{"count":1},"n8n-nodes-base.splitInBatches":{"count":1},"@n8n/n8n-nodes-langchain.chainLlm":{"count":2},"@n8n/n8n-nodes-langchain.lmChatOpenAi":{"count":2},"@n8n/n8n-nodes-langchain.embeddingsOpenAi":{"count":1},"@n8n/n8n-nodes-langchain.vectorStoreQdrant":{"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":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":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":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":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":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":1225,"icon":"file:form.svg","name":"n8n-nodes-base.formTrigger","codex":{"data":{"alias":["table","submit","post"],"resources":{"generic":[],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.formtrigger/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Other Trigger Nodes"]}}},"group":"[\"trigger\"]","defaults":{"name":"On form submission"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NiIgaGVpZ2h0PSI0MCIgZmlsbD0ibm9uZSI+PHBhdGggZmlsbD0iIzAwQjdCQyIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzQuOTc4IDM3LjczMmExLjU2IDEuNTYgMCAwIDEtMS41NjIgMS41NjNINi4yNmExLjU2IDEuNTYgMCAwIDEtMS41NjMtMS41NjNWOS42MDdjMC0uNDA1LjE1Ny0uNzk0LjQzOC0xLjA4Nmw2LjMwNC02LjUzMXY1LjM0NEg4LjIxM2ExLjE3MiAxLjE3MiAwIDEgMCAwIDIuMzQzaDQuNDNhMS4xNyAxLjE3IDAgMCAwIDEuMTcxLTEuMTcxVi4yMzJoMTkuNjAyYTEuNTYgMS41NiAwIDAgMSAxLjU2MiAxLjU2M3YxMC4zMjdsLTIuODYgMi44Ni04LjI1MiA4LjI3NmE0MTMuMDA2IDQxMy4wMDYgMCAwIDEtMS42NTQgMS42NjJsLS4zMzcuMzM3YTIgMiAwIDAgMC0uNTU3IDEuMDhMMjAuMyAzMS45MjJjLS4xMDguNjM4LS4yMTUgMS4wNzkuMjExIDEuNDE4LjQwMy4zMi45LjE3NCAxLjU0LjA2Nmw1LjQwOC0uOTI4YTIgMiAwIDAgMCAxLjA4LS41NTZsNi40NC02LjQyOXptLTI0LjAzLTIxLjI2NWExLjE4IDEuMTggMCAwIDAgMS4xNzEgMS4xNzJoMTMuMTYzYTEuMTcyIDEuMTcyIDAgMSAwIDAtMi4zNDRIMTIuMTE5YTEuMTcgMS4xNyAwIDAgMC0xLjE3MiAxLjE3Mm03LjI5NCAxNC43NjZhMS4xNyAxLjE3IDAgMCAwLTEuMTcyLTEuMTcySDEyLjEyYTEuMTcyIDEuMTcyIDAgMSAwIDAgMi4zNDNoNC45NTFhMS4xNyAxLjE3IDAgMCAwIDEuMTcyLTEuMTcybS44Ni03LjM5MWExLjE3IDEuMTcgMCAwIDAtMS4xNzItMS4xNzJoLTUuODExYTEuMTcyIDEuMTcyIDAgMSAwIDAgMi4zNDNoNS44MWExLjE2NCAxLjE2NCAwIDAgMCAxLjE3My0xLjE3MSIgY2xpcC1ydWxlPSJldmVub2RkIi8+PHBhdGggZmlsbD0iIzAwQjdCQyIgZD0ibTMzLjUzMiAxNi4zOTcgNC4yODktNC4yODkgMy43NTggMy43MSAxLjYxNy0xLjYxNiAyLjI1OCAyLjI1N2MuMjE4LjIxOC4zNC41MTMuMzQzLjgyLS4wMDIuMzExLS4xMjUuNjA4LS4zNDQuODNsLTYuODA0IDYuNzk2YTEuMTMgMS4xMyAwIDAgMS0uODI4LjM0MyAxLjE1IDEuMTUgMCAwIDEtLjgyOC0uMzQzIDEuMTggMS4xOCAwIDAgMSAwLTEuNjU3bDUuOTc2LTUuOTY4LTEuMzEyLTEuMzEzLTEuMzgzIDEuNDE0LTEzLjE0OSAxMy4xMjUtNC42MTcuNzgyLjc4Mi00LjYxNy4zMzYtLjMzNyAyLjU2MiAyLjU1NWExLjEgMS4xIDAgMCAwIC44MjguMzQ0Yy4zMTIuMDA1LjYxMi0uMTIuODI4LS4zNDRhMS4xOCAxLjE4IDAgMCAwIDAtMS42NTZsLTIuNTYyLTIuNTYyek00NC43MzYgMTIuMjRjMCAuNDE0LS4xNjMuODEtLjQ1NCAxLjEwMmwtLjkyMi45MTQtMy44NTItMy44MjguOTMtLjkzYTEuNTYzIDEuNTYzIDAgMCAxIDIuMjAzIDBsMS42NCAxLjY0MWMuMjkxLjI5My40NTUuNjkuNDU1IDEuMTAyIi8+PC9zdmc+"},"displayName":"n8n Form Trigger","typeVersion":3,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"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"}]}],"categories":[{"id":35,"name":"Document Extraction"},{"id":48,"name":"AI RAG"}],"image":[]}}