{"workflow":{"id":12174,"name":"Estimate construction costs from text with Telegram, OpenAI and DDC CWICR","views":136,"recentViews":0,"totalViews":136,"createdAt":"2025-12-26T12:46:39.960Z","description":"\nA **Telegram bot** that converts natural-language work descriptions into detailed **cost estimates** using AI parsing, vector search, and the open-source **DDC CWICR** database with 55,000+ construction work items.\n\n## Who's it for\n\n- **Contractors & Estimators** who need quick ballpark figures from verbal/text descriptions\n- **Construction managers** doing feasibility checks on-site via mobile\n- **BIM/CAD professionals** integrating text-based estimation into workflows\n- **Developers** building construction cost APIs or chatbots\n\n## What it does\n\n1. **Receives** text messages in Telegram (work lists, specifications, notes)\n2. **Parses** input with AI (OpenAI/Claude/Gemini) into structured work items\n3. **Searches** DDC CWICR vector database via Qdrant for matching rates\n4. **Calculates** costs with full breakdown (labor, materials, machines)\n5. **Exports** results as HTML report, Excel, or PDF\n\n**Supports 9 languages:** 🇩🇪 DE · 🇬🇧 EN · 🇷🇺 RU · 🇪🇸 ES · 🇫🇷 FR · 🇧🇷 PT · 🇨🇳 ZH · 🇦🇪 AR · 🇮🇳 HI\n\n## How it works\n\n```\n┌─────────────┐    ┌──────────────┐    ┌─────────────┐    ┌──────────────┐\n│  Telegram   │ →  │  AI Parse    │ →  │  Embeddings │ →  │   Qdrant     │\n│  Text Input │    │  (GPT/Claude)│    │  (OpenAI)   │    │   Search     │\n└─────────────┘    └──────────────┘    └─────────────┘    └──────────────┘\n                                                                  ↓\n┌─────────────┐    ┌──────────────┐    ┌─────────────┐    ┌──────────────┐\n│   Export    │ ←  │  Aggregate   │ ←  │  Calculate  │ ←  │  AI Rerank   │\n│ HTML/XLS/PDF│    │   Results    │    │    Costs    │    │   Results    │\n└─────────────┘    └──────────────┘    └─────────────┘    └──────────────┘\n```\n\n**Step-by-step:**\n1. User sends `/start` → selects language → enters work description\n2. **AI Parse** extracts work items: name, quantity, unit, room\n3. **Query Transform** optimizes search terms for construction domain\n4. **Embeddings API** converts query to vector (OpenAI `text-embedding-3-small`)\n5. **Qdrant Search** finds top-10 matching rates from DDC CWICR\n6. **AI Rerank** selects best match considering context and units\n7. **Calculate** applies quantities, sums labor/materials/machines\n8. **Report** sends Telegram message + optional Excel/PDF export\n\n## Prerequisites\n\n| Component | Requirement |\n|-----------|-------------|\n| **n8n** | v1.30+ (AI nodes support) |\n| **Telegram Bot** | Token from @BotFather |\n| **OpenAI API** | For embeddings + LLM parsing |\n| **Qdrant** | Vector DB with DDC CWICR collections loaded |\n| **DDC CWICR Data** | [github.com/datadrivenconstruction/DDC-CWICR](https://github.com/datadrivenconstruction/OpenConstructionEstimate-DDC-CWICR) |\n\n## Setup\n\n### 1. Credentials (n8n Settings → Credentials)\n- **OpenAI API** — required for embeddings and text parsing\n- **Anthropic API** — optional, for Claude models\n- **Google Gemini API** — optional, for Gemini models\n\n### 2. Configuration (🔑 TOKEN node)\n```\nbot_token     = YOUR_TELEGRAM_BOT_TOKEN\nQDRANT_URL    = http://localhost:6333\nQDRANT_API_KEY = (if using Qdrant Cloud)\n```\n\n### 3. Qdrant Setup\nLoad DDC CWICR collections for your target languages:\n- `DE_construction_rates` — German (STLB-Bau based)\n- `EN_construction_rates` — English\n- `RU_construction_rates` — Russian (GESN/FER based)\n- ... (see DDC CWICR docs for all 9 languages)\n\n### 4. Link AI Model Nodes\n1. Open **OpenAI Model** nodes\n2. Select your OpenAI credential\n3. (Optional) Enable Claude/Gemini nodes for alternative models\n\n### 5. Telegram Webhook\n1. Activate workflow\n2. Telegram Trigger auto-registers webhook\n3. Test with `/start` in your bot\n\n## Features\n\n| Feature | Description |\n|---------|-------------|\n| 🤖 **Multi-LLM** | Swap between OpenAI, Claude, Gemini |\n| 🌍 **9 Languages** | Full UI + database localization |\n| 📝 **Smart Parsing** | Handles lists, tables, free-form text |\n| 🔍 **Semantic Search** | Vector similarity + AI reranking |\n| 📊 **Cost Breakdown** | Labor, materials, machines, hours |\n| ✏️ **Inline Edit** | Modify quantities, delete items |\n| 📤 **Export** | HTML report, Excel, PDF |\n| 💾 **Session State** | Multi-turn conversation support |\n\n## Example Input/Output\n\n**Input (Telegram message):**\n```\nLiving room renovation:\n- Laminate flooring 25 m²\n- Wall painting 60 m²\n- Ceiling plasterboard 25 m²\n- 3 electrical outlets\n```\n\n**Output:**\n```\n✅ Estimate Ready — 4 items found\n\n1. Laminate flooring ✓\n   25 m² × €18.50 = €462.50\n   └ Labor: €125 · Materials: €337.50\n\n2. Wall painting ✓\n   60 m² × €8.20 = €492.00\n   └ Labor: €312 · Materials: €180\n\n3. Ceiling plasterboard ✓\n   25 m² × €32.00 = €800.00\n   └ Labor: €425 · Materials: €375\n\n4. Electrical outlets ✓\n   3 pcs × €45.00 = €135.00\n   └ Labor: €95 · Materials: €40\n\n─────────────────────\nTotal: €1,889.50\n\n[↓ Excel] [↓ PDF] [↻ Restart]\n```\n\n## Notes & Tips\n\n- **First run:** Ensure Qdrant has DDC CWICR data loaded before testing\n- **Rate accuracy:** Results depend on query quality; AI reranking improves matching\n- **Large lists:** Bot handles 50+ items; progress shown per-item\n- **Customization:** Edit `Config` node for UI text, currencies, database mapping\n- **Extend:** Chain with your CRM, project management, or reporting tools\n\n## Categories\n\n`AI` · `Data Extraction` · `Communication` · `Files & Storage`\n\n## Tags\n\n`telegram-bot`, `construction`, `cost-estimation`, `qdrant`, `vector-search`, `openai`, `multilingual`, `bim`, `cad`\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- Open data principles for construction\n- CAD/BIM processing automation\n- AI-powered estimation pipelines\n- ETL workflows for construction databases\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 Setup Guide:** [qdrant.tech/documentation](https://qdrant.tech/documentation/)\n- **n8n AI Nodes:** [docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain](https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain/)\n\n---\n\n⭐ **Star us on GitHub!** [github.com/datadrivenconstruction/DDC-CWICR](https://github.com/datadrivenconstruction/OpenConstructionEstimate-DDC-CWICR)","workflow":{"id":"Y4vZ0z2b1xAcFQxy","meta":{"instanceId":"1d7b1da65c471e434188555a2959bd7f825cdfb684701b1bb9481fb29d0bd489","templateCredsSetupCompleted":true},"name":"DDC CWICR - Text Estimator v11 (AI Nodes)","tags":[],"nodes":[{"id":"5c6b56be-8107-4d2a-99c6-fe01de0dcd7b","name":"Sticky Note1","type":"n8n-nodes-base.stickyNote","position":[-5600,-2176],"parameters":{"width":404,"height":132,"content":"⭐ **Star us on GitHub!**\n\n[github.com/datadrivenconstruction/DDC-CWICR](https://github.com/datadrivenconstruction/OpenConstructionEstimate-DDC-CWICR)\n\n**DDC CWICR** — Open Source Construction Cost Database\n- 55,000+ work items\n- 9 languages\n- Free forever"},"typeVersion":1},{"id":"00370c25-67e4-4789-9abf-e994473af278","name":"🔐 Credentials Setup","type":"n8n-nodes-base.stickyNote","position":[-5600,-2032],"parameters":{"color":5,"width":412,"height":936,"content":"## 🔐 Credentials Setup\n\n### 🔑 TOKEN node:\n- `bot_token` - Telegram Bot API\n- `QDRANT_URL` - Qdrant server\n- `QDRANT_API_KEY` - Qdrant auth\n\n### n8n Credentials (Settings → Credentials):\n- **OpenAI API** - for LLM + Embeddings\n- **Anthropic API** - (optional) for Claude\n- **Google Gemini API** - (optional) for Gemini\n\n### To switch AI models:\n1. Disable current model node\n2. Enable alternative model\n3. Models auto-connect to chain"},"typeVersion":1},{"id":"828e259a-6e85-479c-a60e-76931f116b73","name":"Checklist","type":"n8n-nodes-base.stickyNote","position":[-5936,-1712],"parameters":{"width":324,"height":460,"content":"## ✅ SETUP CHECKLIST\n\n**1. Telegram Bot**\n- [ ] Create via @BotFather\n- [ ] Token in 🔑 TOKEN\n\n**2. n8n Credentials**\n- [ ] Add OpenAI credential\n- [ ] Link to Model nodes\n\n**3. Qdrant**\n- [ ] Install/connect Qdrant\n- [ ] Load DDC collections\n- [ ] Set QDRANT_URL\n\n**4. Test**\n- [ ] /start → language\n- [ ] Enter works\n- [ ] Get estimate"},"typeVersion":1},{"id":"1044a0e4-96af-46fb-87e0-f5b7011b82ad","name":"Intro","type":"n8n-nodes-base.stickyNote","position":[-5936,-2176],"parameters":{"width":318,"height":440,"content":"## 🚀 DDC CWICR Text Estimator\n### Construction Cost Estimation Bot\n\n**Version:** 11.0 AI Nodes\n**Author:** DataDrivenConstruction.io\n\n**All AI via n8n Credentials:**\n- 🤖 Parse Text (OpenAI/Claude/Gemini)\n- 🤖 Transform Query\n- 🤖 Rerank Results\n- 📊 Embeddings (OpenAI)\n\n**Features:**\n- 💬 Text input\n- 🌍 9 languages\n- 🔍 Vector search\n- 📊 HTML/Excel/PDF\n\n**No API keys in code!**\nUse n8n Credentials only."},"typeVersion":1},{"id":"349fcf16-f55a-4300-aa2a-2bb9ed806716","name":"🔑 TOKEN","type":"n8n-nodes-base.set","position":[-5344,-1568],"parameters":{"options":{},"assignments":{"assignments":[{"id":"e7b8f8af-ca88-4515-a8ce-3d2c50b815b6","name":"bot_token","type":"string","value":"YOUR_TELEGRAM_BOT_TOKEN"},{"id":"bd298b9a-45b6-4b99-9e50-0346cc402a30","name":"OPENAI_API_KEY","type":"string","value":"YOUR_OPENAI_API_KEY"},{"id":"9c4cfdd2-d46e-465b-ac08-cea275128c20","name":"QDRANT_URL","type":"string","value":"http://localhost:6333"},{"id":"835c6846-26c6-4a0f-ad75-f352010fe4a8","name":"QDRANT_API_KEY","type":"string","value":""}]}},"typeVersion":3.4},{"id":"c576122d-0243-4772-99d0-01cf54898118","name":"UI Messages","type":"n8n-nodes-base.stickyNote","position":[-4304,-2784],"parameters":{"color":6,"width":464,"height":804,"content":"## 🌍 UI Messages\n\nTelegram interface elements:\n- Language selection menu\n- Text input prompts\n- Edit options\n- Help text\n- Error messages\n- Progress indicators\n\nAll localized in Config node.\n\n**Customization:**\nEdit LANG object in Config\nto modify any UI text."},"typeVersion":1},{"id":"3385b0fc-beba-4867-8cd8-e1f5c9c210ea","name":"Route Switch","type":"n8n-nodes-base.stickyNote","position":[-4640,-2176],"parameters":{"color":4,"width":308,"height":1072,"content":"## 🔀 Route Switch\n\n**11 Actions:**\n\n| # | Action | Description |\n|---|--------|-------------|\n| 0 | show_lang | Language menu |\n| 1 | lang_selected | Confirm & prompt |\n| 2 | works_updated | After edit |\n| 3 | calculate | Start calculation |\n| 4 | edit | Show edit menu |\n| 5 | export_excel | CSV download |\n| 6 | export_pdf | PDF download |\n| 7 | view_details | Resource details |\n| 8 | help | Help message |\n| 9 | restart | New session |\n| 10 | fallback | Error handler |"},"typeVersion":1},{"id":"c095dbb9-7ce0-4173-8c45-d6de272c43b9","name":"Config & Localization","type":"n8n-nodes-base.stickyNote","position":[-4880,-2176],"parameters":{"color":5,"width":220,"height":808,"content":"## 🌐 Config & Localization\n\n**9 Languages:**\nDE, EN, RU, ES, FR, PT, ZH, AR, HI\n\n**Contains:**\n- UI messages (buttons, prompts)\n- Currency symbols (€/$₽¥)\n- Database mapping\n- Search language names\n\n**Auto-selects:**\n- Qdrant collection by language\n- Currency by region\n- UI text localization\n\n**Customizable:**\n- Add new languages in LANG object\n- Modify button labels\n- Change currency defaults"},"typeVersion":1},{"id":"4fe52156-022b-49e3-97f6-2da1f363a635","name":"Main Router","type":"n8n-nodes-base.stickyNote","position":[-5168,-2176],"parameters":{"color":5,"width":268,"height":808,"content":"## 🧠 Main Router\n\nCentral message handler.\n\n**Input:** Telegram Update\n\n**Processing:**\n1. Parse message/callback\n2. Manage user sessions\n3. Detect content type\n4. Route to action\n\n**Output:** `action` code (0-10)\n\n**Session Storage:**\n- User language\n- Work items list\n- Calculation results\n- State machine"},"typeVersion":1},{"id":"a5e21659-7430-412a-82a5-24b3f153118b","name":"Agg","type":"n8n-nodes-base.code","position":[-1920,-784],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// AGG - Final aggregation of calculation results + DDC CWICR info\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst cfg = $('Config').first().json;\nconst cid = String(cfg.chatId);\nconst sd = $getWorkflowStaticData('global');\nconst session = sd.sess?.[cid] || {};\n\nconst L = cfg.L || {};\nlet total = 0, workers_sum = 0, materials_sum = 0, machines_sum = 0, labor_hours_sum = 0;\nlet found_count = 0;\n\n// Get accumulated results\nconst storedResults = sd.res?.[cid] || [];\n\nconsole.log('=== AGG ===');\nconsole.log('Results count:', storedResults.length);\n\n// Process each result\nconst works = storedResults.map(w => {\n  const uc = w.uc || 0;\n  const tc = w.tc || 0;\n\n  total += tc;\n  workers_sum += (w.workers_total || 0);\n  materials_sum += (w.materials_total || 0);\n  machines_sum += (w.machines_total || 0);\n  labor_hours_sum += (w.labor_hours || 0);\n\n  if (w._found) found_count++;\n\n  return {\n    id: w.id,\n    name: w.name || w.sq,\n    query: w.sq || w.query,\n    qty: w.qty,\n    unit: w.unit,\n    room: w.room,\n    uc: uc,\n    tc: tc,\n    rate_code: w.rate_code || '',\n    rate_name: w.rate_name || '',\n    resources: w.resources || [],\n    workers_total: w.workers_total || 0,\n    materials_total: w.materials_total || 0,\n    machines_total: w.machines_total || 0,\n    labor_hours: w.labor_hours || 0,\n    found: w._found || false,\n    scope_of_work: w.scope_of_work || [],\n    original_query: w.original_query || w.name\n  };\n});\n\nconst pct = works.length > 0 ? Math.round(found_count / works.length * 100) : 0;\n\n// DDC CWICR info\nconst isLimited = false; // No limit\nconst originalTotal = session.totalWorks || works.length;\nconst skippedWorks = originalTotal - works.length;\n\nconsole.log('Calculated:', works.length, '/', originalTotal);\nconsole.log('Found:', found_count, '/', works.length);\nconsole.log('Total cost:', total.toFixed(2));\n// No limit mode\n\n// Cleanup staticData\nif (sd.res?.[cid]) delete sd.res[cid];\nif (sd.calcProgress?.[cid]) delete sd.calcProgress[cid];\nif (sd.progress?.[cid]) delete sd.progress[cid];\n\n// Save for report generation\nsd.lastResults = { works, total, workers_sum, materials_sum, machines_sum, labor_hours_sum, found_count, pct, L };\n\nreturn {\n  json: {\n    chatId: cid,\n    bot_token: cfg.bot_token,\n    works: works,\n    total: Math.round(total * 100) / 100,\n    workers_sum: Math.round(workers_sum * 100) / 100,\n    materials_sum: Math.round(materials_sum * 100) / 100,\n    machines_sum: Math.round(machines_sum * 100) / 100,\n    labor_hours_sum: Math.round(labor_hours_sum * 100) / 100,\n    found_count: found_count,\n    total_count: works.length,\n    pct: pct,\n    L: L,\n    currency: L.sym || '€',\n    description: session.description || '',\n    // DDC CWICR\n    _is_limited: isLimited,\n    _total_works: originalTotal,\n    _skipped_works: skippedWorks\n  }\n};"},"typeVersion":2},{"id":"f6f6aa53-b07e-4a09-b740-7db971f3f7c9","name":"🗑️ Delete Progress Msg","type":"n8n-nodes-base.httpRequest","position":[-2096,-784],"parameters":{"url":"=https://api.telegram.org/bot{{ $('🔑 TOKEN').first().json.bot_token }}/deleteMessage","method":"POST","options":{"response":{"response":{"neverError":true}}},"jsonBody":"={\n  \"chat_id\": {{ $('🧹 Prep Cleanup').first().json.chatId }},\n  \"message_id\": {{ $('🧹 Prep Cleanup').first().json._delete_progress_msg || 0 }}\n}","sendBody":true,"specifyBody":"json"},"typeVersion":4.2},{"id":"602ec477-8f11-47ec-8cfc-aaf8c9183a09","name":"🗑️ Delete Work Msg","type":"n8n-nodes-base.httpRequest","position":[-2288,-784],"parameters":{"url":"=https://api.telegram.org/bot{{ $('🔑 TOKEN').first().json.bot_token }}/deleteMessage","method":"POST","options":{"response":{"response":{"neverError":true}}},"jsonBody":"={\n  \"chat_id\": {{ $('🧹 Prep Cleanup').first().json.chatId }},\n  \"message_id\": {{ $('🧹 Prep Cleanup').first().json._delete_work_msg || 0 }}\n}","sendBody":true,"specifyBody":"json"},"typeVersion":4.2},{"id":"7a275264-fb92-4cf2-8d35-dbbbbdf5d2a3","name":"🧹 Prep Cleanup","type":"n8n-nodes-base.code","position":[-2464,-784],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// PREP CLEANUP - Prepare message IDs for deletion after loop completes\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst cfg = $('Config').first().json;\nconst sd = $getWorkflowStaticData('global');\nconst cid = String(cfg.chatId);\n\n// Get message IDs to delete\nconst lastMsgId = sd.calcProgress?.[cid]?.lastMsgId || null;\nconst progressMsgId = sd.progress?.[cid]?.message_id || null;\n\nconsole.log('=== PREP CLEANUP ===');\nconsole.log('ChatId:', cid);\nconsole.log('Work msg:', lastMsgId);\nconsole.log('Progress msg:', progressMsgId);\n\nreturn {\n  json: {\n    chatId: cfg.chatId,\n    bot_token: cfg.bot_token,\n    L: cfg.L,\n    _delete_work_msg: lastMsgId,\n    _delete_progress_msg: progressMsgId\n  }\n};"},"typeVersion":2},{"id":"db70fc98-d9ab-4c81-8992-9e4b844c705a","name":"Acc","type":"n8n-nodes-base.code","position":[-1920,0],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// ACC - Accumulate calculation results for final aggregation\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst sd = $getWorkflowStaticData('global');\nconst w = $('📊 Update Result').first().json;\nconst cid = String(w.chatId);\n\nif (!sd.res) sd.res = {};\nif (!sd.res[cid]) sd.res[cid] = [];\nsd.res[cid].push(w);\n\nconsole.log('Accumulated:', sd.res[cid].length, '/', w.total_works);\n\nreturn { json: w };"},"typeVersion":2},{"id":"e6214da3-91f1-4065-8f60-a73a8fcd8609","name":"📤 Edit Result","type":"n8n-nodes-base.httpRequest","position":[-2096,0],"parameters":{"url":"=https://api.telegram.org/bot{{ $('🔑 TOKEN').first().json.bot_token }}/editMessageText","method":"POST","options":{"response":{"response":{"neverError":true}}},"jsonBody":"={\"chat_id\": {{ $json.chatId }}, \"message_id\": {{ $json._edit_msg_id || 0 }}, \"text\": {{ JSON.stringify($json._result_text) }}, \"parse_mode\": \"Markdown\"}","sendBody":true,"specifyBody":"json"},"typeVersion":4.2},{"id":"f78f0fc1-011c-46c4-a4ae-92088c88589c","name":"📊 Update Result","type":"n8n-nodes-base.code","position":[-2256,0],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// UPDATE RESULT - Format result message (✓ Found / Not found)\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst calcData = $input.first().json;\nconst sd = $getWorkflowStaticData('global');\nconst cid = String(calcData.chatId);\nconst L = calcData.L || {};\n\nconst current = calcData.work_index || 1;\nconst total = calcData.total_works || 1;\nconst name = calcData.name || calcData.sq || 'Work';\n\nlet shortName = name.length > 22 ? name.substring(0, 19) + '...' : name;\n\nconst tc = parseFloat(calcData.tc) || 0;\nconst uc = parseFloat(calcData.uc) || 0;\nconst rateCode = String(calcData.rate_code || '');\nconst rateName = String(calcData.rate_name || '');\nconst currency = calcData.currency || L.sym || '€';\n\nconst hasValidRate = rateCode && \n  !rateCode.includes('NOT_FOUND') && \n  !rateCode.includes('PAYLOAD_NOT_FOUND');\n\nconst found = tc > 0 || uc > 0 || hasValidRate || (rateName && rateName.length > 0);\n\nconsole.log('Result:', shortName, found ? '✓' : '✗', tc.toFixed(0), currency);\n\nlet text = '';\nif (found) {\n  text = current + '/' + total + ' ' + shortName + ' ✓\\n';\n  text += tc.toFixed(0) + ' ' + currency;\n  if (rateCode && hasValidRate) text += ' · ' + rateCode.substring(0, 20);\n} else {\n  text = current + '/' + total + ' ' + shortName + '\\n';\n  text += (L.not_found || 'Не найдено');\n}\n\nconst msgId = sd.calcProgress?.[cid]?.lastMsgId || null;\n\nreturn { json: { ...calcData, _result_text: text, _edit_msg_id: msgId, _found: found } };"},"typeVersion":2},{"id":"86c8dc46-d8e3-4aaa-9aff-986535232720","name":"1️⃣ Prep Query","type":"n8n-nodes-base.code","position":[-1920,-400],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// PREP QUERY - Prepare search query with Qdrant credentials from TOKEN\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst loopItem = $input.first().json;\nconst tokenData = $('🔑 TOKEN').first().json;\n\nconst originalQuery = loopItem.sq || loopItem.query || loopItem.name || '';\nconst collectionName = loopItem.db || '';\nconst L = loopItem.L || {};\nconst workUnit = loopItem.unit || 'm²';\nconst workQty = loopItem.qty || 1;\n\n// Get Qdrant credentials from TOKEN node\nconst QDRANT_URL = tokenData.QDRANT_URL || 'http://localhost:6333';\nconst QDRANT_API_KEY = tokenData.QDRANT_API_KEY || '';\n\nconsole.log('=== PREP QUERY ===');\nconsole.log('Query:', originalQuery);\nconsole.log('Collection:', collectionName);\nconsole.log('Qdrant URL:', QDRANT_URL);\n\nif (!originalQuery || !collectionName) {\n  console.log('ERROR: Missing query or collection');\n  return [{ json: { ...loopItem, _error: 'Missing data', _skip: true } }];\n}\n\nconst searchLang = L.search_lang || 'Russian';\n\n// Detect database language from collection name\nconst isRussianDB = collectionName.includes('RU_') || collectionName.includes('_RU');\nconst isGermanDB = collectionName.includes('DE_');\nconst dbLang = isRussianDB ? 'Russian' : (isGermanDB ? 'German' : 'English');\n\nconst transformPrompt = `You are a construction cost database search expert for ${dbLang} construction rates database.\n\nTASK: Transform user query into optimal SEARCH KEYWORDS that will match entries in a vector database of construction work rates.\n\nDATABASE CONTEXT:\n- Contains standardized construction work rates with codes, names, units, resources\n- Each rate has: rate_code, rate_name, rate_unit, scope_of_work, resources\n- Language: ${dbLang}\n\nTRANSFORMATION RULES:\n1. EXPAND abbreviations to full professional terms\n2. ADD synonyms and related construction terms\n3. INCLUDE work action verbs (install, apply, lay, mount)\n4. Keep original query terms + expanded terms\n5. Output in ${dbLang} language only\n\nUSER QUERY: ${originalQuery}\nUNIT: ${workUnit}\n\nReply with ONLY the optimized search keywords (one line, no explanations):`;\n\nreturn [{ json: {\n  ...loopItem,\n  _original_query: originalQuery,\n  _transform_prompt: transformPrompt,\n  _collection: collectionName,\n  _db_lang: dbLang,\n  _qdrant_url: QDRANT_URL,\n  _qdrant_key: QDRANT_API_KEY\n}}];"},"typeVersion":2},{"id":"38cc18f0-26f4-4fbf-a12d-13eb6154bb94","name":"💾 Save Work Msg","type":"n8n-nodes-base.code","position":[-2096,-400],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// SAVE WORK MSG - Store message ID for later editing/deletion\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst loopItem = $('📝 Prep Work Msg').first().json;\nconst tgResp = $input.first().json;\nconst sd = $getWorkflowStaticData('global');\nconst cid = String(loopItem.chatId);\n\nif (!sd.calcProgress) sd.calcProgress = {};\nif (!sd.calcProgress[cid]) sd.calcProgress[cid] = {};\nsd.calcProgress[cid].lastMsgId = tgResp.result?.message_id || null;\n\nconsole.log('Saved msg ID:', sd.calcProgress[cid].lastMsgId);\n\nreturn { json: loopItem };"},"typeVersion":2},{"id":"78681325-44aa-43d9-960d-276444c26b1e","name":"📤 Send Work","type":"n8n-nodes-base.httpRequest","position":[-2272,-400],"parameters":{"url":"=https://api.telegram.org/bot{{ $('🔑 TOKEN').first().json.bot_token }}/sendMessage","method":"POST","options":{},"jsonBody":"={\"chat_id\": {{ $(\"📝 Prep Work Msg\").item.json.chatId }}, \"text\": {{ JSON.stringify($(\"📝 Prep Work Msg\").item.json._work_text) }}, \"parse_mode\": \"Markdown\"}","sendBody":true,"specifyBody":"json"},"typeVersion":4.2},{"id":"ac19ffbe-1ef7-4730-bc8b-5e6fa86b36d2","name":"🗑️ Delete Prev","type":"n8n-nodes-base.httpRequest","position":[-2464,-400],"parameters":{"url":"=https://api.telegram.org/bot{{ $('🔑 TOKEN').first().json.bot_token }}/deleteMessage","method":"POST","options":{"response":{"response":{"neverError":true}}},"jsonBody":"={\"chat_id\": {{ $json.chatId }}, \"message_id\": {{ $json._prev_msg_id || 0 }}}","sendBody":true,"specifyBody":"json"},"typeVersion":4.2},{"id":"2601173f-9e00-41d0-b242-5e21532e2ac1","name":"📝 Prep Work Msg","type":"n8n-nodes-base.code","position":[-2640,-400],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// PREP WORK MSG - Create localized \"Searching...\" message for current work\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst loopItem = $('Loop').first().json;\nconst sd = $getWorkflowStaticData('global');\nconst cid = String(loopItem.chatId);\nconst L = loopItem.L || {};\nconst lang = L.search_lang || 'English';\n\nconst current = loopItem.work_index || 1;\nconst total = loopItem.total_works || 1;\nconst name = loopItem.name || loopItem.sq || 'Work';\nconst qty = loopItem.qty || 1;\nconst unit = loopItem.unit || 'm²';\n\nlet shortName = name.length > 30 ? name.substring(0, 27) + '...' : name;\n\n// Localized search messages\nconst SEARCH_WORD = {\n  'German': '🔍 Suche',\n  'English': '🔍 Searching',\n  'Russian': '🔍 Поиск',\n  'Spanish': '🔍 Buscando',\n  'French': '🔍 Recherche',\n  'Portuguese': '🔍 Buscando',\n  'Chinese': '🔍 搜索中',\n  'Arabic': '🔍 بحث',\n  'Hindi': '🔍 खोज'\n};\n\nconst searchWord = SEARCH_WORD[lang] || '🔍 Searching';\n\n// Format: \"🔍 Поиск 3/26\\nГипсокартон 25m²\"\nlet text = `${searchWord} ${current}/${total}\\n`;\ntext += `*${shortName}*\\n`;\ntext += `${qty} ${unit}`;\n\nconst prevMsgId = sd.calcProgress?.[cid]?.lastMsgId || null;\n\nconsole.log('Work', current, '/', total, '-', shortName);\n\nreturn { json: { ...loopItem, _work_text: text, _prev_msg_id: prevMsgId } };"},"typeVersion":2},{"id":"d676b99b-6ed0-4f16-b00a-a9182175d754","name":"Loop","type":"n8n-nodes-base.splitInBatches","position":[-2816,-768],"parameters":{"options":{"reset":false}},"typeVersion":3},{"id":"b7c5ad07-b6f8-488b-8a1a-b398336545b5","name":"Prep Works","type":"n8n-nodes-base.code","position":[-3104,-1024],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// PREP WORKS - Prepare work items for calculation loop (: 5 items)\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst cfg = $input.first().json;\nconst cid = String(cfg.chatId);\nconst sd = $getWorkflowStaticData('global');\nconst session = sd.sess?.[cid] || {};\n\n// Get all works (no limit)\nconst works = session.works || [];\nconst db = session.db || cfg.db;\nconst L = session.L || cfg.L;\n\n// Initialize results accumulator\nif (!sd.res) sd.res = {};\nsd.res[cid] = [];\n\nconst totalWorks = works.length;\n\nconsole.log('=== PREP WORKS ===');\nconsole.log('Processing:', totalWorks, 'items');\n\n// Return array for loop processing\nreturn works.map((w, idx) => ({ \n  json: { \n    ...w, \n    db, \n    L, \n    currency: L?.sym || '€',\n    bot_token: cfg.bot_token, \n    chatId: cid, \n    sq: w.query || w.name,\n    original_query: w.query || w.name,\n    work_index: idx + 1, \n    total_works: totalWorks,\n    _is_limited: cfg._is_limited,\n    _original_total: cfg._total_works\n  } \n}));"},"typeVersion":2},{"id":"0ffb3b95-d9e3-4c3d-83e6-1f909019b6a1","name":"Save Progress ID","type":"n8n-nodes-base.code","position":[-3280,-1024],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// SAVE PROGRESS ID - Store initial progress message ID for cleanup\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst cfg = $('Config').first().json;\nconst prepData = $('📝 Prep Progress').first().json;\nconst telegramResponse = $input.first().json;\nconst sd = $getWorkflowStaticData('global');\nconst cid = String(cfg.chatId);\n\n// Store progress message info\nif (!sd.progress) sd.progress = {};\nsd.progress[cid] = {\n  message_id: telegramResponse.result?.message_id || 0,\n  chat_id: cfg.chatId,\n  bot_token: cfg.bot_token\n};\n\n// Initialize calculation progress tracker\nif (!sd.calcProgress) sd.calcProgress = {};\nsd.calcProgress[cid] = { lastMsgId: null };\n\nconsole.log('Progress msg ID:', sd.progress[cid].message_id);\n\nreturn { \n  json: { \n    ...cfg,\n    _total_works: prepData._total_works\n  } \n};"},"typeVersion":2},{"id":"76047230-a1cf-46b9-8eb4-9c85e1982969","name":"📤 Send Progress","type":"n8n-nodes-base.httpRequest","position":[-3488,-1024],"parameters":{"url":"=https://api.telegram.org/bot{{ $('🔑 TOKEN').first().json.bot_token }}/sendMessage","method":"POST","options":{},"jsonBody":"={\"chat_id\": {{ $json._progress_chat_id }}, \"text\": {{ JSON.stringify($json._progress_text) }}, \"parse_mode\": \"Markdown\"}","sendBody":true,"specifyBody":"json"},"typeVersion":4.2},{"id":"d1cec9fe-6d9a-468d-8ba1-1f8b4112de53","name":"📝 Prep Progress","type":"n8n-nodes-base.code","position":[-3728,-1024],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// PREP PROGRESS - Show calculation progress message (localized)\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst cfg = $('Config').first().json;\nconst sd = $getWorkflowStaticData('global');\nconst cid = String(cfg.chatId);\nconst session = sd.sess?.[cid] || {};\n\nconst allWorks = session.works || [];\nconst L = cfg.L || session.L || {};\nconst lang = cfg.lang || 'EN';\n\nconsole.log('=== PREP PROGRESS ===');\nconsole.log('Works:', allWorks.length);\nconsole.log('Language:', lang);\n\nconst totalWorks = allWorks.length;\nconst estimatedMinutes = Math.max(1, Math.ceil(totalWorks * 8 / 60));\n\n// Localized messages for \"Searching prices...\"\nconst SEARCH_MESSAGES = {\n  DE: `🔍 *Preissuche läuft...*\\n\\n${totalWorks} Positionen\\n⏱ ~${estimatedMinutes} Min`,\n  EN: `🔍 *Searching prices...*\\n\\n${totalWorks} items\\n⏱ ~${estimatedMinutes} min`,\n  RU: `🔍 *Поиск расценок...*\\n\\n${totalWorks} позиций\\n⏱ ~${estimatedMinutes} мин`,\n  ES: `🔍 *Buscando precios...*\\n\\n${totalWorks} elementos\\n⏱ ~${estimatedMinutes} min`,\n  FR: `🔍 *Recherche des prix...*\\n\\n${totalWorks} éléments\\n⏱ ~${estimatedMinutes} min`,\n  PT: `🔍 *Buscando preços...*\\n\\n${totalWorks} itens\\n⏱ ~${estimatedMinutes} min`,\n  ZH: `🔍 *正在搜索价格...*\\n\\n${totalWorks} 项目\\n⏱ ~${estimatedMinutes} 分钟`,\n  AR: `🔍 *جاري البحث عن الأسعار...*\\n\\n${totalWorks} عناصر\\n⏱ ~${estimatedMinutes} دقيقة`,\n  HI: `🔍 *कीमतें खोज रहे हैं...*\\n\\n${totalWorks} आइटम\\n⏱ ~${estimatedMinutes} मिनट`\n};\n\nconst text = SEARCH_MESSAGES[lang] || SEARCH_MESSAGES['EN'];\n\n// Save to session\nsession.totalWorks = totalWorks;\n\nreturn {\n  json: {\n    ...cfg,\n    _progress_chat_id: cfg.chatId,\n    _progress_text: text,\n    _total_works: totalWorks\n  }\n};"},"typeVersion":2},{"id":"067f5f6a-7a4a-43fc-90ca-46e7976d0f1e","name":"📤 Send Works","type":"n8n-nodes-base.httpRequest","position":[-3328,-1696],"parameters":{"url":"=https://api.telegram.org/bot{{ $('🔑 TOKEN').first().json.bot_token }}/sendMessage","method":"POST","options":{},"jsonBody":"={\"chat_id\": {{ $json.chatId }}, \"text\": {{ JSON.stringify($json.msg) }}, \"parse_mode\": \"Markdown\", \"reply_markup\": {\"inline_keyboard\": {{ JSON.stringify($json.keyboard) }}}}","sendBody":true,"specifyBody":"json"},"typeVersion":4.2},{"id":"d0f90abb-6cb7-4180-ba96-9a2e94b46062","name":"📊 Show Works","type":"n8n-nodes-base.code","position":[-3472,-1696],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// SHOW WORKS - Display extracted work items with edit buttons\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst cfgNode = $('Config').first().json;\nconst inputData = $input.first().json;\nconst sd = $getWorkflowStaticData('global');\n\nconst chatId = inputData.chatId || cfgNode.chatId;\nconst bot_token = inputData.bot_token || cfgNode.bot_token;\nconst cid = String(chatId);\n\n// Get L from multiple sources (session is most reliable)\nconst session = sd.sess?.[cid] || {};\nlet L = cfgNode.L || session.L || inputData.L || {};\n\n// Get works from input (from Deduplicate) or session\nconst works = inputData.works || session.works || [];\nconst rooms = inputData.rooms || session.rooms || [];\n\nconsole.log('=== SHOW WORKS ===');\nconsole.log('Works:', works.length);\nconsole.log('L.search_lang:', L.search_lang);\n\n// Save to session\nif (!sd.sess) sd.sess = {};\nif (!sd.sess[cid]) sd.sess[cid] = {};\nif (works.length > 0) {\n  sd.sess[cid].works = works;\n  sd.sess[cid].rooms = rooms;\n  sd.sess[cid].L = L;\n  sd.sess[cid].db = cfgNode.db;\n  sd.sess[cid].state = 'wait_edit';\n}\n\nfunction short(str, len) {\n  str = String(str || '');\n  return str.length > len ? str.substring(0, len - 1) + '…' : str;\n}\n\nconst totalArea = rooms.reduce((sum, r) => sum + (r.area_m2 || 0), 0);\n\n// Use localized labels with fallback\nconst roomWord = L.rooms || 'комнат';\nconst workWord = L.works_identified || 'позиций';\nconst generalWord = L.general || 'Общее';\n\nlet msg = '*' + rooms.length + ' ' + roomWord;\nif (totalArea > 0) msg += ' · ' + (Math.round(totalArea * 10) / 10) + ' m²';\nmsg += '*\\n';\nmsg += '_' + works.length + ' ' + workWord + '_\\n\\n';\n\nif (works.length === 0) {\n  msg += '_' + (L.no_works || 'Работы не найдены') + '_\\n';\n} else {\n  const worksByRoom = new Map();\n  const noRoom = [];\n\n  for (const w of works) {\n    if (w.room && w.room.length > 0) {\n      if (!worksByRoom.has(w.room)) worksByRoom.set(w.room, []);\n      worksByRoom.get(w.room).push(w);\n    } else {\n      noRoom.push(w);\n    }\n  }\n\n  let workNum = 1;\n\n  for (const [roomName, roomWorks] of worksByRoom) {\n    const room = rooms.find(r => r.name === roomName);\n    const areaStr = room?.area_m2 ? ' · ' + room.area_m2 + ' m²' : '';\n    msg += '*' + short(roomName, 20) + '*' + areaStr + '\\n';\n\n    for (const w of roomWorks) {\n      const wname = short(w.name || 'Work', 25);\n      const qty = typeof w.qty === 'number' ? Math.round(w.qty * 100) / 100 : w.qty;\n      msg += workNum + '. ' + wname + ' — ' + qty + ' ' + (w.unit || '') + '\\n';\n      workNum++;\n    }\n    msg += '\\n';\n  }\n\n  if (noRoom.length > 0) {\n    msg += '*' + generalWord + '*\\n';\n    for (const w of noRoom) {\n      const wname = short(w.name || 'Work', 25);\n      const qty = typeof w.qty === 'number' ? Math.round(w.qty * 100) / 100 : w.qty;\n      msg += workNum + '. ' + wname + ' — ' + qty + ' ' + (w.unit || '') + '\\n';\n      workNum++;\n    }\n  }\n}\n\n// Build keyboard\nconst keyboard = [];\nconst maxBtns = works.length;\nif (maxBtns > 0) {\n  for (let i = 0; i < maxBtns; i += 5) {\n    const row = [];\n    for (let j = 0; j < 5 && i + j < maxBtns; j++) {\n      row.push({ text: '✏' + (i + j + 1), callback_data: 'edit_work_' + (i + j) });\n    }\n    keyboard.push(row);\n  }\n}\n\nkeyboard.push([\n  { text: L.btn_add_work || '+ Позиция', callback_data: 'add_work' },\n  { text: L.btn_calc || '▶ Расчёт', callback_data: 'calculate' }\n]);\nkeyboard.push([\n  { text: L.btn_new || '🔄 Заново', callback_data: 'restart' }\n]);\n\nreturn { json: { chatId, bot_token, L, msg, keyboard, works, rooms } };"},"typeVersion":2},{"id":"420db4a8-19cf-43db-8289-bc932bee2a54","name":"9️⃣ Calculate","type":"n8n-nodes-base.code","position":[-2464,0],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// STEP 8: Calculate - FIXED VERSION v3\n// Универсальный поиск данных в разных структурах\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst inputData = $input.first().json;\n\nconsole.log('═══════════════════════════════════════════════════════════════');\nconsole.log('STEP 8: CALCULATE v3');\nconsole.log('═══════════════════════════════════════════════════════════════');\n\n// ═══════════════════════════════════════════════════════════════════════════\n// УМНЫЙ ПОИСК PAYLOAD - ищем во всех возможных местах\n// ═══════════════════════════════════════════════════════════════════════════\n\nlet payload = null;\nlet payloadSource = 'not_found';\n\n// Вариант 1: _best_result.payload\nif (inputData._best_result?.payload?.rate_code) {\n  payload = inputData._best_result.payload;\n  payloadSource = '_best_result.payload';\n}\n// Вариант 2: _best_payload напрямую\nelse if (inputData._best_payload?.rate_code) {\n  payload = inputData._best_payload;\n  payloadSource = '_best_payload';\n}\n// Вариант 3: данные лежат прямо в _best_result (без вложенного payload)\nelse if (inputData._best_result?.rate_code) {\n  payload = inputData._best_result;\n  payloadSource = '_best_result (direct)';\n}\n// Вариант 4: payload внутри payload (двойная вложенность)\nelse if (inputData._best_result?.payload?.payload?.rate_code) {\n  payload = inputData._best_result.payload.payload;\n  payloadSource = '_best_result.payload.payload';\n}\n// Вариант 5: ищем rate_code где угодно в _best_result\nelse if (inputData._best_result) {\n  const br = inputData._best_result;\n  // Рекурсивный поиск\n  const findPayload = (obj, depth = 0) => {\n    if (!obj || depth > 3) return null;\n    if (obj.rate_code && obj.resources) return obj;\n    for (const key of Object.keys(obj)) {\n      if (typeof obj[key] === 'object' && obj[key] !== null) {\n        const found = findPayload(obj[key], depth + 1);\n        if (found) return found;\n      }\n    }\n    return null;\n  };\n  payload = findPayload(br);\n  if (payload) payloadSource = '_best_result (deep search)';\n}\n\nconsole.log('Payload source:', payloadSource);\nconsole.log('Payload found:', !!payload);\n\nif (payload) {\n  console.log('Payload keys:', Object.keys(payload));\n  console.log('rate_code:', payload.rate_code);\n  console.log('rate_name:', payload.rate_name?.substring(0, 50));\n  console.log('resources count:', (payload.resources || []).length);\n  console.log('cost_summary:', JSON.stringify(payload.cost_summary || {}).substring(0, 200));\n}\n\n// Если payload не найден - выводим структуру для диагностики\nif (!payload) {\n  console.log('');\n  console.log('❌ PAYLOAD NOT FOUND! Debug info:');\n  console.log('inputData keys:', Object.keys(inputData));\n  if (inputData._best_result) {\n    console.log('_best_result keys:', Object.keys(inputData._best_result));\n    console.log('_best_result.payload:', typeof inputData._best_result.payload);\n    if (inputData._best_result.payload) {\n      console.log('_best_result.payload keys:', Object.keys(inputData._best_result.payload));\n    }\n  }\n  \n  // Возвращаем ошибку с диагностикой\n  return [{ json: { \n    ...inputData,\n    rate_code: 'PAYLOAD_NOT_FOUND',\n    rate_name: inputData.name || inputData.sq || 'Unknown',\n    uc: 0, tc: 0,\n    resources: [],\n    _debug_keys: Object.keys(inputData),\n    _debug_best_result_keys: Object.keys(inputData._best_result || {}),\n    _debug_payload_source: payloadSource\n  }}];\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Теперь payload найден - извлекаем данные\n// ═══════════════════════════════════════════════════════════════════════════\n\n// Базовые данные работы\nconst workName = inputData.name || inputData.sq || '';\nconst workQty = inputData.qty || 1;\nconst workUnit = inputData.unit || 'm²';\n\nconsole.log('');\nconsole.log('Work:', workName);\nconsole.log('Qty:', workQty, workUnit);\n\n// Данные расценки\nconst rateCode = payload.rate_code || 'NOT_FOUND';\nconst rateName = payload.rate_name || workName;\nconst rateUnit = payload.rate_unit || '';\n\nconsole.log('Rate code:', rateCode);\nconsole.log('Rate name:', rateName?.substring(0, 50));\nconsole.log('Rate unit:', rateUnit);\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Получаем стоимость\n// ═══════════════════════════════════════════════════════════════════════════\nconst costSummary = payload.cost_summary || {};\nlet totalCost = parseFloat(costSummary.total_cost_position || costSummary.total_cost || 0);\n\n// Если cost_summary пустой, вычисляем из ресурсов\nconst rawResources = payload.resources || [];\nif (totalCost === 0 && rawResources.length > 0) {\n  totalCost = rawResources.reduce((sum, r) => {\n    return sum + parseFloat(r.resource_cost_eur || 0);\n  }, 0);\n  console.log('Total cost calculated from resources:', totalCost);\n}\n\nconsole.log('Total cost:', totalCost);\nconsole.log('Resources count:', rawResources.length);\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Определяем делитель единицы измерения\n// ═══════════════════════════════════════════════════════════════════════════\nlet unitDivisor = 1;\nconst rateUnitLower = (rateUnit || '').toLowerCase();\n\nif (rateUnitLower.includes('100 ') || rateUnitLower === '100 м' || rateUnitLower === '100 м²' || rateUnitLower === '100 м2') {\n  unitDivisor = 100;\n} else if (rateUnitLower.match(/^10\\s/) || rateUnitLower.includes('10 м')) {\n  unitDivisor = 10;\n}\n\nconsole.log('Unit divisor:', unitDivisor);\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Price calculation\n// ═══════════════════════════════════════════════════════════════════════════\nconst uc = unitDivisor > 0 ? totalCost / unitDivisor : 0;\nconst tc = workQty * uc;\nconst scaleFactor = unitDivisor > 0 ? workQty / unitDivisor : workQty;\n\nconsole.log('');\nconsole.log('=== PRICE CALCULATION ===');\nconsole.log('UC (price per unit):', uc.toFixed(2), 'EUR');\nconsole.log('TC (total cost):', tc.toFixed(2), 'EUR');\nconsole.log('Scale factor:', scaleFactor);\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Обработка ресурсов\n// ═══════════════════════════════════════════════════════════════════════════\nlet workersTotal = 0;\nlet materialsTotal = 0;\nlet machinesTotal = 0;\nlet laborHoursTotal = 0;\n\nconst resources = rawResources.map(r => {\n  const code = r.resource_code || '';\n  const name = r.resource_name || '';\n  const unit = r.resource_unit || '';\n  const rowType = r.row_type || '';\n  \n  const originalQty = r.resource_quantity !== null && r.resource_quantity !== undefined \n    ? parseFloat(r.resource_quantity) \n    : null;\n  const pricePerUnit = parseFloat(r.resource_price_per_unit_eur_current || 0);\n  const originalCost = parseFloat(r.resource_cost_eur || 0);\n  \n  // Определяем тип ресурса\n  let resourceType = 'material';\n  const rowTypeLower = (rowType || '').toLowerCase();\n  const codeUpper = (code || '').toUpperCase();\n  \n  if (rowTypeLower === 'машинист' || rowTypeLower.includes('машинист')) {\n    resourceType = 'labor';\n  } else if (rowTypeLower === 'электричество' || rowTypeLower.includes('электричеств')) {\n    resourceType = 'machine';\n  } else if (codeUpper.startsWith('DXME') || codeUpper.startsWith('DX')) {\n    resourceType = 'machine';\n  } else if (codeUpper.startsWith('ME_') || codeUpper.startsWith('PU_') || codeUpper.startsWith('RI_')) {\n    resourceType = 'labor';\n  }\n  \n  // Масштабирование\n  let scaledQty = null;\n  let scaledCost = 0;\n  \n  if (originalQty !== null) {\n    scaledQty = originalQty * scaleFactor;\n    scaledCost = scaledQty * pricePerUnit;\n  } else {\n    scaledCost = originalCost * scaleFactor;\n  }\n  \n  // Суммируем по категориям\n  if (resourceType === 'labor') {\n    workersTotal += scaledCost;\n    if (unit === 'ч' || unit === 'чел.-ч' || unit === 'чел-ч' || unit === 'маш.-ч') {\n      laborHoursTotal += scaledQty || 0;\n    }\n  } else if (resourceType === 'machine') {\n    machinesTotal += scaledCost;\n  } else {\n    materialsTotal += scaledCost;\n  }\n  \n  return {\n    resource_code: code,\n    resource_name: name,\n    resource_unit: unit,\n    resource_type: resourceType,\n    row_type: rowType,\n    resource_price: pricePerUnit,\n    original_quantity: originalQty,\n    original_cost: originalCost,\n    scaled_quantity: scaledQty,\n    scaled_cost: scaledCost\n  };\n});\n\nconsole.log('');\nconsole.log('=== RESOURCES BREAKDOWN ===');\nconsole.log('Resources processed:', resources.length);\nconsole.log('Workers total:', workersTotal.toFixed(2), 'EUR');\nconsole.log('Materials total:', materialsTotal.toFixed(2), 'EUR');\nconsole.log('Machines total:', machinesTotal.toFixed(2), 'EUR');\nconsole.log('Labor hours:', laborHoursTotal.toFixed(1), 'h');\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Scope of work\n// ═══════════════════════════════════════════════════════════════════════════\nconst workSteps = payload.work_steps || [];\nconst scopeOfWork = workSteps.map(s => s.text || '').filter(t => t.length > 0);\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Quality\n// ═══════════════════════════════════════════════════════════════════════════\nconst llmScore = inputData._llm_score || 0;\nconst qdrantScore = inputData._qdrant_score || 0;\n\nconst qualityLevel = llmScore >= 75 ? 'high' : \n                     llmScore >= 50 ? 'medium' : \n                     llmScore >= 25 ? 'low' : 'not_found';\n\nconsole.log('');\nconsole.log('═══════════════════════════════════════════════════════════════');\nconsole.log('✅ CALCULATION COMPLETE');\nconsole.log('Rate:', rateCode, '-', rateName?.substring(0, 40));\nconsole.log('Total:', tc.toFixed(2), 'EUR');\nconsole.log('Resources:', resources.length);\nconsole.log('═══════════════════════════════════════════════════════════════');\n\n// ═══════════════════════════════════════════════════════════════════════════\n// ФИНАЛЬНЫЙ РЕЗУЛЬТАТ\n// ═══════════════════════════════════════════════════════════════════════════\nreturn [{ json: { \n  // Original data\n  ...inputData,\n  name: workName,\n  qty: workQty,\n  unit: workUnit,\n  \n  // Rate\n  rate_code: rateCode,\n  rate_name: rateName,\n  rate_unit: workUnit,\n  original_rate_unit: rateUnit,\n  \n  // Prices\n  uc,\n  tc,\n  \n  // Labor\n  labor_hours: laborHoursTotal,\n  workers_total: workersTotal,\n  machines_total: machinesTotal,\n  materials_total: materialsTotal,\n  \n  // Quality\n  ql: qualityLevel,\n  quality_score: llmScore,\n  qdrant_score: qdrantScore,\n  llm_score: llmScore,\n  llm_reason: inputData._llm_reason || '',\n  \n  // Resources\n  resources,\n  \n  // Scope of work\n  scope_of_work: scopeOfWork,\n  \n  // Hierarchy\n  hierarchy: payload.hierarchy || {},\n  \n  // Breakdown\n  cost_breakdown: { \n    workers: workersTotal, \n    machines: machinesTotal, \n    materials: materialsTotal \n  },\n  \n  // Debug\n  _payload_source: payloadSource,\n  _scale_factor: scaleFactor,\n  _unit_divisor: unitDivisor,\n  _original_total_cost: totalCost\n}}];"},"typeVersion":2},{"id":"aaf03f21-01b2-4da4-80d7-81a012957d5b","name":"8️⃣ Apply Rerank","type":"n8n-nodes-base.code","position":[-1264,-208],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// 8️⃣ APPLY RERANK v5 - Fixed empty results handling\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst prepData = $('6️⃣ Prep Rerank').first().json;\nconst llmResponse = $input.first().json;\n\nconsole.log('=== APPLY RERANK v5 ===');\n\nconst qdrantResults = prepData._qdrant_results || [];\n\n// Early exit if no results\nif (qdrantResults.length === 0) {\n  console.log('No Qdrant results to rerank');\n  return [{ json: {\n    ...prepData,\n    _best_result: null,\n    _best_payload: {},\n    _llm_score: 0,\n    _llm_reason: 'no results',\n    _qdrant_score: 0,\n    _quality_level: 'not_found',\n    ql: 'not_found',\n    _step: 'rerank_done'\n  }}];\n}\n\n// Helper: extract actual data from various payload formats\nfunction getPayloadData(rawPayload) {\n  // Format 1: LangChain/n8n format - data in payload_full\n  if (rawPayload.payload_full) {\n    return rawPayload.payload_full;\n  }\n  // Format 2: data in metadata\n  if (rawPayload.metadata?.rate_code || rawPayload.metadata?.hierarchy) {\n    return rawPayload.metadata;\n  }\n  // Format 3: direct format - data at root level\n  if (rawPayload.rate_code || rawPayload.hierarchy) {\n    return rawPayload;\n  }\n  return rawPayload;\n}\n\nlet rankings = [];\ntry {\n  // AI node returns text directly, HTTP returns choices[0].message.content\n  let text = llmResponse.text || llmResponse.choices?.[0]?.message?.content || '';\n  \n  // Clear от markdown и лишних символов\n  text = text.replace(/```json\\n?/g, '').replace(/```/g, '').trim();\n  \n  console.log('Raw LLM response:', text.substring(0, 200));\n  \n  // Пытаемся найти JSON\n  const jsonMatch = text.match(/\\{[\\s\\S]*\\}/);\n  if (jsonMatch) {\n    const parsed = JSON.parse(jsonMatch[0]);\n    rankings = parsed.rankings || [];\n    console.log('Parsed rankings:', rankings.length);\n  }\n} catch(e) {\n  console.log('Parse error:', e.message);\n  // Fallback - используем Qdrant scores\n  rankings = qdrantResults.map((r, i) => ({\n    index: i,\n    score: Math.round((r.score || 0) * 100),\n    reason: 'qdrant score'\n  }));\n}\n\n// Комбинируем LLM и Qdrant scores\nconst scored = qdrantResults.map((r, i) => {\n  const rawPayload = r.payload || {};\n  const p = getPayloadData(rawPayload);\n  const h = p.hierarchy || {};\n  \n  // Normalize payload with extracted data\n  const normalizedPayload = {\n    ...p,\n    rate_code: p.rate_code || h.subsection_code || h.section_code || h.justification_nr || '',\n    rate_name: p.rate_name || h.subsection_name || h.section_name || '',\n    rate_unit: p.rate_unit || h.unit || '',\n    resources: p.resources || [],\n    work_steps: p.work_steps || [],\n    hierarchy: h\n  };\n  \n  const rank = rankings.find(x => x.index === i);\n  const llmScore = rank?.score || 0;\n  const reason = rank?.reason || '';\n  const qdrantScore = (r.score || 0) * 100;\n  \n  // Weighted combination: LLM важнее если есть, иначе Qdrant\n  let combinedScore;\n  if (llmScore > 0) {\n    combinedScore = llmScore * 0.7 + qdrantScore * 0.3;\n  } else {\n    combinedScore = qdrantScore;\n  }\n  \n  console.log('[' + i + '] LLM=' + llmScore + ' Qdrant=' + qdrantScore.toFixed(0) + ' Combined=' + combinedScore.toFixed(0) + ' - ' + (normalizedPayload.rate_code || 'N/A'));\n  \n  return {\n    ...r,\n    payload: normalizedPayload,\n    llm_score: llmScore,\n    llm_reason: reason,\n    qdrant_score: r.score || 0,\n    combined_score: combinedScore\n  };\n});\n\n// Sort по комбинированному score\nscored.sort((a, b) => b.combined_score - a.combined_score);\n\nconst best = scored[0];\nconst bestPayload = best?.payload || {};\nconst combinedScore = best?.combined_score || 0;\n\n// Determine quality результата\nlet qualityLevel = 'not_found';\nif (combinedScore >= 80) qualityLevel = 'high';\nelse if (combinedScore >= 60) qualityLevel = 'medium';\nelse if (combinedScore >= 40) qualityLevel = 'low';\n\nconsole.log('');\nconsole.log('✅ BEST: ' + (bestPayload.rate_code || 'N/A') + ' - ' + (bestPayload.rate_name || '').substring(0, 50));\nconsole.log('   Combined: ' + combinedScore.toFixed(0) + ' | Quality: ' + qualityLevel);\nconsole.log('   Reason: ' + (best?.llm_reason || 'N/A'));\n\nreturn [{ json: {\n  ...prepData,\n  _best_result: best || null,\n  _best_payload: bestPayload,\n  _llm_score: best?.llm_score || 0,\n  _llm_reason: best?.llm_reason || '',\n  _qdrant_score: best?.qdrant_score || 0,\n  _quality_level: qualityLevel,\n  ql: qualityLevel,\n  _step: 'rerank_done'\n}}];"},"typeVersion":2},{"id":"51b2c91b-f306-4b76-ab25-3cd8f7bbbd85","name":"6️⃣ Prep Rerank","type":"n8n-nodes-base.code","position":[-1920,-208],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// 6️⃣ PREP RERANK v4 - Fixed for LangChain/n8n vector store format\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst prepData = $('4️⃣ Extract Embedding').first().json;\nconst qdrantResponse = $input.first().json;\n\nconsole.log('=== PREP RERANK v4 ===');\n\nif (qdrantResponse.status?.error) {\n  return [{ json: { ...prepData, rate_code: 'QDRANT_ERROR', uc: 0, tc: 0, ql: 'not_found', resources: [] }}];\n}\n\nconst results = qdrantResponse.result || [];\nconsole.log('Qdrant results:', results.length);\n\nif (results.length === 0) {\n  return [{ json: { ...prepData, rate_code: 'NOT_FOUND', rate_name: prepData._original_query, uc: 0, tc: 0, ql: 'not_found', resources: [] }}];\n}\n\nconst originalQuery = prepData._original_query || prepData.name || '';\nconst workUnit = prepData.unit || 'm²';\nconst workQty = prepData.qty || 1;\n\n// Helper: extract actual data from various payload formats\nfunction getPayloadData(rawPayload) {\n  // Format 1: LangChain/n8n format - data in payload_full\n  if (rawPayload.payload_full) {\n    return rawPayload.payload_full;\n  }\n  // Format 2: data in metadata\n  if (rawPayload.metadata?.rate_code || rawPayload.metadata?.hierarchy) {\n    return rawPayload.metadata;\n  }\n  // Format 3: direct format - data at root level\n  if (rawPayload.rate_code || rawPayload.hierarchy) {\n    return rawPayload;\n  }\n  // Return as-is\n  return rawPayload;\n}\n\n// Format компактное описание кандидатов\nconst candidates = results.slice(0, 5).map((r, i) => {\n  const rawPayload = r.payload || {};\n  const p = getPayloadData(rawPayload);\n  const h = p.hierarchy || {};\n  \n  // Extract rate info (support both formats)\n  const code = p.rate_code || h.subsection_code || h.section_code || h.justification_nr || '';\n  const name = p.rate_name || h.subsection_name || h.section_name || '';\n  const unit = p.rate_unit || h.unit || '';\n  \n  // Scope of work\n  const workSteps = p.work_steps || [];\n  const scopeText = workSteps.slice(0, 3).map(s => s.text || s).join('; ');\n  \n  // Key materials\n  const resources = p.resources || [];\n  const materials = resources\n    .filter(res => !res.resource_code?.match(/^(DXME|ME_|PU_|RI_)/))\n    .slice(0, 3)\n    .map(res => res.resource_name)\n    .join(', ');\n  \n  const qdrantScore = (r.score * 100).toFixed(0);\n  \n  console.log('[' + i + '] ' + code + ' - ' + (name || '').substring(0, 40) + ' | ' + qdrantScore + '%');\n  \n  return i + '. [' + code + '] ' + name + '\\n   Unit: ' + unit + ' | Scope: ' + (scopeText || 'n/a').substring(0, 100) + '\\n   Materials: ' + (materials || 'n/a');\n}).join('\\n\\n');\n\nconst rerankPrompt = `TASK: Score construction rate candidates (0-100) for matching user's work request.\n\nSCORING GUIDE:\n95-100: EXACT MATCH - Same work type, method, materials, unit compatible\n80-94: VERY GOOD - Same work, minor spec differences (thickness, brand)\n65-79: GOOD - Same category, different specification\n50-64: PARTIAL - Related work, different scope\n30-49: WEAK - Same trade, different work type\n0-29: WRONG - Different trade or unrelated\n\nCRITICAL DISTINCTIONS:\n• ГКЛ (гипсокартон) ≠ ГВЛ (гипсоволокно) - DIFFERENT materials, max 60\n• Стены ≠ Потолок - check work location matches\n• Монтаж ≠ Демонтаж - opposite operations\n• 1 слой ≠ 2 слоя - check layer count\n• Profile types: ПП60 = ceiling, ПС = wall stud, ПН = guide\n\nUNIT MATCHING:\n• Query unit: ${workUnit}\n• Rate unit should be compatible or convertible\n• m² work → m² rate (ideal)\n• m² work → 100m² rate (ok, will scale)\n\nUSER REQUEST: \"${originalQuery}\" (${workQty} ${workUnit})\n\nCANDIDATES:\n${candidates}\n\nReturn ONLY valid JSON (no markdown):\n{\"rankings\":[{\"index\":0,\"score\":N,\"reason\":\"brief reason\"},{\"index\":1,\"score\":N,\"reason\":\"brief reason\"}]}`;\n\nconsole.log('Prompt length:', rerankPrompt.length);\n\nreturn [{ json: {\n  ...prepData,\n  _qdrant_results: results,\n  _rerank_prompt: rerankPrompt,\n  _step: 'prep_rerank_done'\n}}];"},"typeVersion":2},{"id":"d2c1dc15-e8fd-4b20-87bf-05eadde6dda4","name":"5️⃣ Qdrant Search","type":"n8n-nodes-base.httpRequest","position":[-2096,-208],"parameters":{"url":"={{ $json._qdrant_url }}/collections/{{ $json._collection }}/points/search","method":"POST","options":{"timeout":30000,"response":{"response":{"neverError":true,"responseFormat":"json"}}},"jsonBody":"={\n  \"vector\": {{ JSON.stringify($json._embedding) }},\n  \"limit\": 10,\n  \"with_payload\": true,\n  \"with_vector\": false\n}","sendBody":true,"sendHeaders":true,"specifyBody":"json","headerParameters":{"parameters":[{"name":"api-key","value":"={{ $json._qdrant_key }}"},{"name":"Content-Type","value":"application/json"}]}},"typeVersion":4.2},{"id":"a0c96cf7-02e7-43dd-b626-ad10568f48ad","name":"4️⃣ Extract Embedding","type":"n8n-nodes-base.code","position":[-2256,-208],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// EXTRACT EMBEDDING - Get vector from OpenAI response\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst prepData = $('2️⃣ Extract Transform').first().json;\nconst embResponse = $input.first().json;\n\nconsole.log('=== EXTRACT EMBEDDING ===');\n\nif (embResponse.error) {\n  console.log('OpenAI Error:', embResponse.error.message);\n  return [{ json: { ...prepData, _error: embResponse.error.message, _embedding: [] }}];\n}\n\nconst embedding = embResponse.data?.[0]?.embedding || [];\nconsole.log('Embedding length:', embedding.length);\n\n// Accept various embedding sizes\nif (embedding.length < 256) {\n  console.log('WARNING: Embedding too short:', embedding.length);\n}\n\n// Pass all data including Qdrant credentials\nreturn [{ json: {\n  ...prepData,\n  _embedding: embedding,\n  _collection: prepData._collection,\n  _qdrant_url: prepData._qdrant_url,\n  _qdrant_key: prepData._qdrant_key,\n  _step: 'embedding_done'\n}}];"},"typeVersion":2},{"id":"68ea3a8d-ec4f-4dc7-84ba-69b0193667ed","name":"2️⃣ Extract Transform","type":"n8n-nodes-base.code","position":[-1264,-400],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// EXTRACT TRANSFORM - Clean AI response and combine queries\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst prepData = $('1️⃣ Prep Query').first().json;\nconst aiResponse = $input.first().json;\n\nconsole.log('=== EXTRACT TRANSFORM ===');\n\nlet transformedQuery = prepData._original_query;\n\ntry {\n  // n8n AI node returns text in different places\n  let content = '';\n  if (aiResponse.text) {\n    content = aiResponse.text;\n  } else if (aiResponse.response?.text) {\n    content = aiResponse.response.text;\n  } else if (aiResponse.output) {\n    content = aiResponse.output;\n  } else if (aiResponse.choices?.[0]?.message?.content) {\n    // Fallback for raw API response\n    content = aiResponse.choices[0].message.content;\n  }\n  \n  if (content && content.length > 5) {\n    transformedQuery = content\n      .replace(/^(keywords?:|search:|query:|result:)/i, '')\n      .replace(/[\\n\\r]+/g, ' ')\n      .trim();\n    console.log('Transformed OK');\n  }\n} catch(e) {\n  console.log('Transform failed:', e.message);\n}\n\nconsole.log('Original:', prepData._original_query);\nconsole.log('Transformed:', transformedQuery.substring(0, 80));\n\n// Smart combination - original is more important\nconst originalWords = prepData._original_query.toLowerCase().split(/\\s+/);\nconst transformedWords = transformedQuery.toLowerCase().split(/\\s+/);\n\n// Add only new words from transformation\nconst newWords = transformedWords.filter(w => \n  w.length > 2 && !originalWords.some(ow => ow.includes(w) || w.includes(ow))\n);\n\nconst combinedQuery = prepData._original_query + ' ' + newWords.slice(0, 10).join(' ');\n\nconsole.log('Combined:', combinedQuery.substring(0, 100));\n\nreturn [{ json: {\n  ...prepData,\n  _query: combinedQuery.trim(),\n  _transformed_query: transformedQuery,\n  _collection: prepData._collection,\n  _qdrant_url: prepData._qdrant_url,\n  _qdrant_key: prepData._qdrant_key,\n  _step: 'transform_done'\n}}];"},"typeVersion":2},{"id":"4dc79df3-d1c1-441f-9a78-bc019de5bbbf","name":"📤 Details","type":"n8n-nodes-base.httpRequest","position":[-3728,-720],"parameters":{"url":"=https://api.telegram.org/bot{{ $('🔑 TOKEN').first().json.bot_token }}/sendMessage","method":"POST","options":{},"jsonBody":"={\n  \"chat_id\": {{ $json.chatId }},\n  \"text\": {{ JSON.stringify($json.msg) }},\n  \"parse_mode\": \"Markdown\",\n  \"reply_markup\": {\n    \"inline_keyboard\": [\n      [{\"text\": \"{{ $json.L.btn_export_excel || '↓ Excel' }}\", \"callback_data\": \"export_excel\"}, {\"text\": \"{{ $json.L.btn_export_pdf || '↓ PDF' }}\", \"callback_data\": \"export_pdf\"}],\n      [{\"text\": \"{{ $json.L.btn_restart || '↻ Restart' }}\", \"callback_data\": \"restart\"}]\n    ]\n  }\n}","sendBody":true,"specifyBody":"json"},"typeVersion":4.2},{"id":"afb04e6f-e242-4434-9458-6dfe4de368a2","name":"View Details","type":"n8n-nodes-base.code","position":[-3920,-720],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// DDC CWICR - Data Driven Construction Cost Estimator\n// https://DataDrivenConstruction.io\n// Open source construction cost database with 55,000+ work items\n// ═══════════════════════════════════════════════════════════════════════════\n\n// Detailed view with resource prices\nconst cfg = $('Config').first().json;\nconst cid = String(cfg.chatId);\nconst sd = $getWorkflowStaticData('global');\nconst data = sd.lastResults || {};\nconst L = data.L || cfg.L || {};\nconst works = data.works || [];\nconst sym = L.sym || '€';\n\nfunction fmtCur(v) { \n  if (!v || v === 0) return sym + ' 0';\n  return sym + ' ' + v.toFixed(2); \n}\n\nlet msg = `*${L.ready} — ${L.resources}*\\n\\n`;\n\nworks.forEach((w, i) => {\n  const qi = { 'high': '●', 'medium': '○', 'low': '◌', 'not_found': '✕' }[w.ql] || '○';\n  \n  msg += `*${i+1}. ${w.name}*\\n`;\n  if (w.rate_code && w.rate_code !== 'NOT_FOUND') {\n    msg += `${qi} \\`${w.rate_code}\\`\\n`;\n    if (w.rate_name && w.rate_name !== w.name) {\n      const rateName = (w.rate_name || '').substring(0, 45);\n      msg += `_${rateName}_\\n`;\n    }\n  } else {\n    msg += `${qi} _${L.not_found}_\\n`;\n  }\n  msg += `${w.qty} ${w.unit} × ${fmtCur(w.uc)} = *${fmtCur(w.tc)}*\\n`;\n  \n  // Cost breakdown\n  const parts = [];\n  if (w.workers_total > 0) parts.push(`${L.workers}: ${fmtCur(w.workers_total)}`);\n  if (w.materials_total > 0) parts.push(`${L.materials}: ${fmtCur(w.materials_total)}`);\n  if (w.machines_total > 0) parts.push(`${L.machines}: ${fmtCur(w.machines_total)}`);\n  if (parts.length > 0) msg += `_${parts.join(' · ')}_\\n`;\n  \n  // Resources with prices\n  const resources = w.resources || [];\n  if (resources.length > 0) {\n    msg += `\\n`;\n    const showCount = Math.min(5, resources.length);\n    resources.slice(0, showCount).forEach((r, ri) => {\n      const isLast = ri === showCount - 1 && resources.length <= 5;\n      const prefix = isLast ? '└' : '├';\n      const typeTag = r.resource_type === 'labor' ? L.res_labor : r.resource_type === 'machine' ? L.res_machine : L.res_material;\n      const resName = (r.resource_name || '').substring(0, 26);\n      msg += `${prefix} *${typeTag}*: ${resName}\\n`;\n      \n      // Show quantity and cost\n      const qtyVal = r.scaled_quantity || r.resource_quantity || 0;\n      const costVal = r.scaled_cost || r.resource_cost || 0;\n      if (costVal > 0) {\n        msg += `   ${qtyVal.toFixed(2)} ${r.resource_unit || ''} = ${fmtCur(costVal)}\\n`;\n      } else {\n        msg += `   ${qtyVal.toFixed(2)} ${r.resource_unit || ''}\\n`;\n      }\n    });\n    if (resources.length > 5) {\n      msg += `└ _...+${resources.length - 5} ${L.resources}_\\n`;\n    }\n  }\n  \n  // Scope of work\n  const scope = w.scope_of_work || [];\n  if (scope.length > 0) {\n    msg += `\\n📋 *${L.scope_title || 'Scope of Work'}:*\\n`;\n    scope.forEach((s, si) => {\n      const prefix = si === scope.length - 1 ? '└' : '├';\n      const sText = (s || '').substring(0, 45);\n      msg += `${prefix} ${sText}\\n`;\n    });\n  }\n  msg += `\\n`;\n});\n\n// Summary\nmsg += `─────────────────────\\n`;\nconst totalParts = [];\nif (data.workers_sum > 0) totalParts.push(`${L.workers}: ${fmtCur(data.workers_sum)}`);\nif (data.materials_sum > 0) totalParts.push(`${L.materials}: ${fmtCur(data.materials_sum)}`);\nif (data.machines_sum > 0) totalParts.push(`${L.machines}: ${fmtCur(data.machines_sum)}`);\nif (totalParts.length > 0) msg += totalParts.join('\\n') + '\\n\\n';\n\nmsg += `*${L.total}: ${fmtCur(data.total || 0)}*\\n`;\n\nreturn { json: { ...cfg, msg, chatId: cid, bot_token: cfg.bot_token, L } };"},"typeVersion":2},{"id":"c3aad83b-e6bf-4ec8-b283-daa45871eab9","name":"📤 Fallback","type":"n8n-nodes-base.httpRequest","position":[-4272,-1232],"parameters":{"url":"=https://api.telegram.org/bot{{ $('🔑 TOKEN').first().json.bot_token }}/sendMessage","method":"POST","options":{},"jsonBody":"={\n  \"chat_id\": {{ $json.chatId }},\n  \"text\": {{ JSON.stringify(($json.L && $json.L.fallback_start) || \"Use /start to begin\") }},\n  \"parse_mode\": \"Markdown\"\n}","sendBody":true,"specifyBody":"json"},"typeVersion":4.2},{"id":"1b9da534-f7af-4bb6-b8ab-7b43749584db","name":"📤 Help","type":"n8n-nodes-base.httpRequest","position":[-3920,-720],"parameters":{"url":"=https://api.telegram.org/bot{{ $('🔑 TOKEN').first().json.bot_token }}/sendMessage","method":"POST","options":{},"jsonBody":"={\n  \"chat_id\": {{ $json.chatId }},\n  \"text\": \"*DDC CWICR - Help*\\n\\n*What is DDC CWICR?*\\nOpen source construction cost database\\nhttps://DataDrivenConstruction.io\\n\\n*Features:*\\n📸 Photo analysis with AI\\n📝 Text input with work lists\\n🌍 9 languages supported\\n📊 55,000+ work items\\n💰 Regional pricing (EUR, USD, RUB, etc.)\\n📄 Excel & PDF export\\n\\n*How to use:*\\n1. Select your language\\n2. Send photo OR text description\\n3. Edit detected works if needed\\n4. Get cost estimate\\n\\n*Commands:*\\n/start - New estimate\\n\\n*Contact:*\\nGitHub: github.com/datadrivenconstruction/OpenConstructionEstimate-DDC-CWICR\",\n  \"parse_mode\": \"Markdown\",\n  \"reply_markup\": {\n    \"inline_keyboard\": [[{\"text\": \"◀️ Back\", \"callback_data\": \"back_to_lang\"}]]\n  }\n}","sendBody":true,"specifyBody":"json"},"typeVersion":4.2},{"id":"db60dd77-e822-4331-bd7c-3aa78e4da29c","name":"📤 Send PDF","type":"n8n-nodes-base.telegram","position":[-3488,-880],"webhookId":"0cfa0dd8-bdc3-4031-9a7a-1d3948d2c347","parameters":{"chatId":"={{ $json.chatId }}","operation":"sendDocument","binaryData":true,"additionalFields":{"caption":"={{ $json.L?.export_pdf_msg || '📄 PDF Export (HTML)' }}","fileName":"={{ $json.filename }}"},"binaryPropertyName":"pdf"},"credentials":{"telegramApi":{"id":"","name":""}},"typeVersion":1.2},{"id":"8d1a58e4-a704-4624-8180-0b529806f3c3","name":"IF PDF","type":"n8n-nodes-base.if","position":[-3728,-864],"parameters":{"options":{},"conditions":{"options":{"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"operator":{"type":"boolean","operation":"notEquals"},"leftValue":"={{ $json.skip }}","rightValue":true}]}},"typeVersion":2.2},{"id":"4b92eb93-933b-4f56-8e21-687d11a0e0c1","name":"Generate PDF","type":"n8n-nodes-base.code","position":[-3920,-864],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// DDC CWICR - Data Driven Construction Cost Estimator\n// https://DataDrivenConstruction.io\n// Open source construction cost database with 55,000+ work items\n// ═══════════════════════════════════════════════════════════════════════════\n\n// Generate PDF (using HTML-to-PDF service or return HTML)\nconst cfg = $('Config').first().json;\nconst cid = String(cfg.chatId);\nconst sd = $getWorkflowStaticData('global');\nconst html = sd.html_report || '';\nconst L = sd.lastResults?.L || cfg.L || {};\n\nif (!html) {\n  // No HTML report yet - send message\n  try {\n    await $http.request({\n      method: 'POST',\n      url: `https://api.telegram.org/bot${cfg.bot_token}/sendMessage`,\n      body: { chat_id: parseInt(cid), text: '❌ No report to export. Please calculate first.' },\n      json: true\n    });\n  } catch(e) {}\n  return { json: { skip: true } };\n}\n\n// For now, send HTML file as \"PDF alternative\"\nconst filename = `Estimate_${(L.region || 'Report').replace(/[^a-zA-Z0-9]/g, '_')}_${new Date().toISOString().substring(0, 10)}.html`;\n\nreturn { json: { chatId: cid, L, bot_token: cfg.bot_token, filename }, binary: { pdf: { data: Buffer.from(html, 'utf-8').toString('base64'), mimeType: 'text/html', fileName: filename } } };"},"typeVersion":2},{"id":"46040b41-3270-4544-a7f3-1fc4fef3827c","name":"📤 Send Excel","type":"n8n-nodes-base.telegram","position":[-3728,-560],"webhookId":"944e581c-0095-4205-9487-8551328862ea","parameters":{"chatId":"={{ $json.chatId }}","operation":"sendDocument","binaryData":true,"additionalFields":{"caption":"={{ $json.L?.export_excel_msg || '📊 Excel Export (CSV)' }}","fileName":"={{ $json.filename }}"},"binaryPropertyName":"excel"},"credentials":{"telegramApi":{"id":"","name":""}},"typeVersion":1.2},{"id":"f2db6463-97c3-48e2-84e3-0a8f5fb1708e","name":"Generate Excel","type":"n8n-nodes-base.code","position":[-3920,-560],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// DDC CWICR - Data Driven Construction Cost Estimator\n// https://DataDrivenConstruction.io\n// Open source construction cost database with 55,000+ work items\n// ═══════════════════════════════════════════════════════════════════════════\n\n// Generate Excel file\nconst cfg = $('Config').first().json;\nconst cid = String(cfg.chatId);\nconst sd = $getWorkflowStaticData('global');\nconst data = sd.lastResults || {};\nconst L = data.L || cfg.L || {};\nconst works = data.works || [];\n\n// Create CSV content (Excel-compatible)\nlet csv = '\\uFEFF'; // BOM for UTF-8\ncsv += `${L.doc_title || 'ESTIMATE'} - ${data.description || 'Estimate'}\\n`;\ncsv += `${L.region || 'Region'} | ${new Date().toLocaleDateString()}\\n\\n`;\n\n// Headers\ncsv += `${L.col_pos || 'Pos'};${L.col_code || 'Code'};${L.col_desc || 'Description'};${L.col_unit || 'Unit'};${L.col_qty || 'Qty'};${L.col_price || 'Price'};${L.col_total || 'Total'}\\n`;\n\n// Data rows\nworks.forEach((w, i) => {\n  const name = (w.rate_name || w.name || '').replace(/;/g, ',').replace(/\\n/g, ' ');\n  csv += `${i+1};${w.rate_code || ''};\"${name}\";${w.rate_unit || w.unit || ''};${(w.qty || 0).toFixed(2)};${(w.uc || 0).toFixed(2)};${(w.tc || 0).toFixed(2)}\\n`;\n  \n  // Resources\n  (w.resources || []).forEach(r => {\n    const resName = (r.resource_name || '').replace(/;/g, ',').replace(/\\n/g, ' ');\n    csv += `;${r.resource_code || ''};\"  ${resName}\";${r.resource_unit || ''};${(r.scaled_quantity || 0).toFixed(3)};${(r.resource_price || 0).toFixed(2)};${(r.scaled_cost || 0).toFixed(2)}\\n`;\n  });\n});\n\n// Totals\ncsv += `\\n;;;;;${L.total || 'TOTAL'};${(data.total || 0).toFixed(2)}\\n`;\ncsv += `;;;;;${L.workers || 'Labor'};${(data.workers_sum || 0).toFixed(2)}\\n`;\ncsv += `;;;;;${L.materials || 'Materials'};${(data.materials_sum || 0).toFixed(2)}\\n`;\ncsv += `;;;;;${L.machines || 'Equipment'};${(data.machines_sum || 0).toFixed(2)}\\n`;\n\nconst filename = `Estimate_${(L.region || 'Report').replace(/[^a-zA-Z0-9]/g, '_')}_${new Date().toISOString().substring(0, 10)}.csv`;\n\nreturn { json: { chatId: cid, L, bot_token: cfg.bot_token, filename }, binary: { excel: { data: Buffer.from(csv, 'utf-8').toString('base64'), mimeType: 'text/csv', fileName: filename } } };"},"typeVersion":2},{"id":"50c3a592-49e4-4c66-a26a-fd48ad24894c","name":"📤 Send HTML","type":"n8n-nodes-base.telegram","position":[-1152,-784],"webhookId":"2d59d493-4108-40d3-b74e-77fd2b869592","parameters":{"chatId":"={{ $json.chatId }}","operation":"sendDocument","binaryData":true,"additionalFields":{"caption":"📊 Professional HTML Report","fileName":"={{ $json.filename }}"},"binaryPropertyName":"html"},"credentials":{"telegramApi":{"id":"","name":""}},"typeVersion":1.2},{"id":"547ccce6-8b5c-415d-9c20-6e2ccd30bf91","name":"Prep HTML File","type":"n8n-nodes-base.code","position":[-1376,-784],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// DDC CWICR - Data Driven Construction Cost Estimator\n// https://DataDrivenConstruction.io\n// Open source construction cost database with 55,000+ work items\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst d = $input.first().json;\nconst html = d.html_content || '';\nconst L = d.L || {};\nconst now = new Date();\nconst ts = now.toISOString().replace(/[:.]/g, '-').substring(0, 16);\nconst filename = `Estimate_${(L.region || 'Report').replace(/[^a-zA-Z0-9]/g, '_')}_${ts}.html`;\n\nreturn { json: { chatId: d.chatId, bot_token: d.bot_token, filename }, binary: { html: { data: Buffer.from(html, 'utf-8').toString('base64'), mimeType: 'text/html', fileName: filename } } };"},"typeVersion":2},{"id":"36335c56-6c71-43aa-b12c-19f182a8edca","name":"📤 Final","type":"n8n-nodes-base.httpRequest","position":[-1376,-928],"parameters":{"url":"=https://api.telegram.org/bot{{ $('🔑 TOKEN').first().json.bot_token }}/sendMessage","method":"POST","options":{},"jsonBody":"={\"chat_id\": {{ $json.chatId }}, \"text\": {{ JSON.stringify($json.msg) }}, \"parse_mode\": \"Markdown\", \"reply_markup\": {\"inline_keyboard\": [[{\"text\": \"{{ $json.L.resources || 'Resources' }}\", \"callback_data\": \"view_details\"}], [{\"text\": \"{{ $json.L.btn_export_excel || 'Excel' }}\", \"callback_data\": \"export_excel\"}, {\"text\": \"{{ $json.L.btn_export_pdf || 'PDF' }}\", \"callback_data\": \"export_pdf\"}], [{\"text\": \"{{ $json.L.btn_restart || 'New' }}\", \"callback_data\": \"restart\"}]]}}","sendBody":true,"specifyBody":"json"},"typeVersion":4.2},{"id":"beab9359-66f2-4fe3-b554-336be6767f3b","name":"Final","type":"n8n-nodes-base.code","position":[-1584,-784],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// DDC CWICR - Final message (ultra-compact for Telegram 4096 limit)\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst d = $('Generate HTML').first().json;\nconst cid = d.chatId;\nconst sd = $getWorkflowStaticData('global');\nconst L = d.L || {};\n\nif (sd.res?.[cid]) delete sd.res[cid];\nif (sd.sess?.[cid]) sd.sess[cid].state = 'done';\n\nfunction fmt(v) { \n  try { return new Intl.NumberFormat(L.loc || 'en', { maximumFractionDigits: 0 }).format(v || 0); } \n  catch(e) { return String(Math.round(v || 0)); } \n}\n\nfunction fmtCur(v) { \n  const sym = L.sym || '$';\n  const num = Math.round(v || 0);\n  return sym + ' ' + fmt(num);\n}\n\nfunction esc(s) {\n  // Remove ALL markdown special chars to avoid Telegram parse errors\n  return String(s || '').replace(/[_*`\\[\\]()~>#+\\-=|{}.!\\\\]/g, '');\n}\n\nfunction shortName(str, len) {\n  str = esc(str);\n  if (str.length > len) return str.substring(0, len - 1) + '…';\n  return str;\n}\n\nconst works = d.works || [];\nconst total = d.total || 0;\nconst now = new Date();\nconst dateStr = now.toLocaleDateString(L.loc || 'en', { day: '2-digit', month: '2-digit', year: 'numeric' });\n\n// Build compact message\nlet lines = [];\n\n// Header\nlines.push('*' + esc(L.doc_title || 'COST ESTIMATE') + '*');\nlines.push(dateStr + ' · ' + esc(L.region || ''));\nlines.push(works.length + ' ' + esc(L.items || 'items'));\nlines.push('');\n\n// Works list - super compact\nconst MAX_ITEMS_SHOWN = 30;\nconst showWorks = works.slice(0, MAX_ITEMS_SHOWN);\n\nshowWorks.forEach((w, i) => {\n  const name = shortName(w.rate_name || w.name || '', 20);\n  lines.push((i+1) + '. ' + name + ' ' + fmtCur(w.tc));\n});\n\nif (works.length > MAX_ITEMS_SHOWN) {\n  lines.push('... +' + (works.length - MAX_ITEMS_SHOWN) + ' ' + esc(L.more_resources || 'more'));\n}\n\n// Total\nlines.push('');\nlines.push('*' + esc(L.total || 'TOTAL') + ': ' + fmtCur(total) + '*');\nlines.push('');\n\n// Breakdown (one line)\nconst parts = [];\nif (d.workers_sum > 0) parts.push(fmtCur(d.workers_sum));\nif (d.materials_sum > 0) parts.push(fmtCur(d.materials_sum));\nif (d.machines_sum > 0) parts.push(fmtCur(d.machines_sum));\nif (parts.length > 0) lines.push(parts.join(' · '));\n\n// Hours\nif (d.labor_hours_sum > 0) {\n  const days = Math.ceil(d.labor_hours_sum / 8);\n  lines.push(fmt(d.labor_hours_sum) + 'h · ' + days + ' ' + esc(L.days || 'd'));\n}\n\n// Build message\nlet msg = lines.join('\\n');\n\n// Hard truncate at 3800 chars to be safe\nif (msg.length > 3800) {\n  msg = msg.substring(0, 3700);\n  msg += '\\n\\n...\\n*' + esc(L.total || 'TOTAL') + ': ' + fmtCur(total) + '*';\n}\n\nconsole.log('Final msg length:', msg.length);\n\nreturn { json: { ...d, msg } };"},"typeVersion":2},{"id":"15e86a97-6806-49e5-82dd-d5546f164475","name":"Generate HTML","type":"n8n-nodes-base.code","position":[-1744,-784],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// DDC CWICR - Generate HTML Report with expandable resources\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst d = $('Agg').first().json;\nconst L = d.L || {};\nconst cur = L.cur || 'USD'; \nconst loc = L.loc || 'en'; \nconst sym = L.sym || '$';\nconst works = d.works || []; \nconst total = d.total || 0;\n\nfunction fmt(v) { \n  try { \n    return new Intl.NumberFormat(loc, { style: 'currency', currency: cur, minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(v || 0); \n  } catch(e) { \n    return sym + ' ' + (v || 0).toFixed(2); \n  } \n}\nfunction fmtNum(v, dec) { \n  return new Intl.NumberFormat(loc, { minimumFractionDigits: dec || 2, maximumFractionDigits: dec || 2 }).format(v || 0); \n}\nfunction esc(t) { \n  return String(t || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); \n}\n\nconst now = new Date();\nconst dateStr = now.toLocaleDateString(loc, { day: '2-digit', month: '2-digit', year: 'numeric' });\nconst timeStr = now.toLocaleTimeString(loc, { hour: '2-digit', minute: '2-digit' });\n\nconst laborPct = total > 0 ? Math.round(d.workers_sum / total * 100) : 0;\nconst materialPct = total > 0 ? Math.round(d.materials_sum / total * 100) : 0;\nconst machinePct = total > 0 ? Math.round(d.machines_sum / total * 100) : 0;\nconst laborDays = Math.ceil((d.labor_hours_sum || 0) / 8);\n\nlet html = `<!DOCTYPE html>\n<html><head><meta charset=\"UTF-8\">\n<title>${esc(L.doc_title || 'Cost Estimate')} - ${esc(d.description || '')}</title>\n<style>\n:root{--primary:#007AFF;--text:#1D1D1F;--text2:#86868B;--text3:#AEAEB2;--bg:#FFF;--bg2:#F5F5F7;--bg3:#E8E8ED;--border:#D2D2D7;--labor:#E3F2FD;--labor-text:#1565C0;--material:#FFF3E0;--material-text:#E65100;--machine:#F3E5F5;--machine-text:#7B1FA2}\n*{box-sizing:border-box}\nbody{font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif;margin:0;padding:16px;background:var(--bg2);color:var(--text);font-size:12px;line-height:1.4}\n.container{background:var(--bg);max-width:1200px;margin:0 auto;border-radius:16px;box-shadow:0 4px 20px rgba(0,0,0,.08);overflow:hidden}\n.header{padding:16px 20px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px}\n.header h1{margin:0;font-size:18px;font-weight:600}\n.header-info{font-size:11px;color:var(--text3)}\n.toolbar{padding:10px 20px;background:var(--bg2);border-bottom:1px solid var(--border);display:flex;gap:8px;flex-wrap:wrap}\n.btn{padding:6px 12px;border:1px solid var(--border);border-radius:6px;background:var(--bg);cursor:pointer;font-size:11px}\n.btn:hover{background:var(--bg3)}\n.kpi{display:flex;gap:8px;padding:12px 20px;background:var(--bg2);flex-wrap:wrap}\n.kpi-card{background:var(--bg);border-radius:8px;padding:10px 14px;border:1px solid var(--border);min-width:100px}\n.kpi-value{font-size:16px;font-weight:600}\n.kpi-label{font-size:9px;color:var(--text3);text-transform:uppercase}\ntable{width:100%;border-collapse:collapse}\nth,td{padding:8px 10px;text-align:left;border-bottom:1px solid var(--border)}\nth{background:var(--bg2);font-weight:500;font-size:10px;text-transform:uppercase;color:var(--text2)}\n.work-row{cursor:pointer;transition:background 0.15s}\n.work-row:hover{background:var(--bg2)}\n.work-row td:first-child{font-weight:500}\n.toggle{width:20px;color:var(--text3);font-size:10px}\n.res-row{background:#FAFAFA;font-size:11px}\n.res-row.hidden{display:none}\n.scope-row{background:#F0FFF0;font-size:11px}\n.scope-row.hidden{display:none}\n.scope-row td{padding:6px 10px 6px 30px;border-bottom:1px dashed #D0E8D0}\n.scope-toggle{color:var(--primary);cursor:pointer;font-size:10px;margin-left:8px}\n.res-row td{padding:6px 10px 6px 30px;border-bottom:1px dashed var(--border)}\n.res-tag{display:inline-block;padding:2px 6px;border-radius:4px;font-size:9px;font-weight:500;margin-right:6px}\n.res-labor{background:var(--labor);color:var(--labor-text)}\n.res-material{background:var(--material);color:var(--material-text)}\n.res-machine{background:var(--machine);color:var(--machine-text)}\n.summary-row{background:linear-gradient(90deg,var(--bg2),var(--bg));font-size:10px}\n.summary-row.hidden{display:none}\n.summary-row td{padding:6px 10px 6px 30px;border-top:1px solid var(--border)}\n.dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:4px}\n.dot-high{background:#34C759}\n.dot-medium{background:#FF9500}\n.dot-low{background:#FF3B30}\n.dot-none{background:#8E8E93}\n.total-row{background:var(--bg2);font-weight:600}\n.total-row td{padding:12px 10px}\n.right{text-align:right}\n.footer{padding:16px 20px;text-align:center;font-size:10px;color:var(--text3);border-top:1px solid var(--border)}\n.footer a{color:var(--primary);text-decoration:none}\n@media(max-width:600px){\n  body{padding:8px;font-size:11px}\n  th,td{padding:6px}\n  .kpi-card{min-width:80px;padding:8px}\n  .kpi-value{font-size:14px}\n}\n</style>\n</head><body>\n<div class=\"container\">\n<div class=\"header\">\n  <h1>${esc(L.doc_title || 'Cost Estimate')}</h1>\n  <div class=\"header-info\">${dateStr} ${timeStr} · ${esc(L.region || '')}</div>\n</div>\n<div class=\"toolbar\">\n  <button class=\"btn\" onclick=\"expandAll()\">${esc(L.expand_all || 'Expand All')}</button>\n  <button class=\"btn\" onclick=\"collapseAll()\">${esc(L.collapse_all || 'Collapse All')}</button>\n</div>\n<div class=\"kpi\">\n  <div class=\"kpi-card\"><div class=\"kpi-value\">${fmt(total)}</div><div class=\"kpi-label\">${esc(L.kpi_total || 'Total')}</div></div>\n  <div class=\"kpi-card\"><div class=\"kpi-value\">${works.length}</div><div class=\"kpi-label\">${esc(L.kpi_items || 'Items')}</div></div>\n  <div class=\"kpi-card\"><div class=\"kpi-value\">${laborDays}d</div><div class=\"kpi-label\">${esc(L.kpi_days || 'Days')}</div></div>\n  <div class=\"kpi-card\"><div class=\"kpi-value\">${laborPct}%</div><div class=\"kpi-label\">${esc(L.workers || 'Labor')}</div></div>\n  <div class=\"kpi-card\"><div class=\"kpi-value\">${materialPct}%</div><div class=\"kpi-label\">${esc(L.materials || 'Materials')}</div></div>\n</div>\n<table>\n<tr>\n  <th style=\"width:30px\"></th>\n  <th>${esc(L.col_code || 'Code')}</th>\n  <th>${esc(L.col_desc || 'Description')}</th>\n  <th>${esc(L.col_unit || 'Unit')}</th>\n  <th class=\"right\">${esc(L.col_qty || 'Qty')}</th>\n  <th class=\"right\">${esc(L.col_price || 'Price')}</th>\n  <th class=\"right\">${esc(L.col_total || 'Total')}</th>\n  <th style=\"width:30px\"></th>\n</tr>`;\n\nworks.forEach((w, i) => {\n  console.log('Work ' + (i+1) + ': ' + (w.rate_name || w.name) + ' - Resources: ' + (w.resources || []).length);\n  const hasRes = (w.resources || []).length > 0;\n  const isFirst = i === 0;\n  const dotClass = w.ql === 'high' ? 'dot-high' : w.ql === 'medium' ? 'dot-medium' : w.ql === 'low' ? 'dot-low' : 'dot-none';\n  const code = w.rate_code && w.rate_code !== 'NOT_FOUND' ? w.rate_code : '—';\n  \n  const hasScope = (w.scope_of_work || []).length > 0;\n  const scopeBtn = hasScope ? '<span class=\"scope-toggle\" onclick=\"event.stopPropagation();toggleScope(' + i + ')\">📋</span>' : '';\n  \n  html += `<tr class=\"work-row\" data-work=\"${i}\" onclick=\"toggleWork(${i})\">\n    <td class=\"toggle\"><span id=\"icon-${i}\">${isFirst ? '▼' : '▶'}</span></td>\n    <td style=\"font-size:10px;color:var(--text2)\">${esc(code)}</td>\n    <td><strong>${esc(w.rate_name || w.name)}</strong>${scopeBtn}</td>\n    <td>${esc(w.rate_unit || w.unit)}</td>\n    <td class=\"right\">${fmtNum(w.qty)}</td>\n    <td class=\"right\">${fmt(w.uc)}</td>\n    <td class=\"right\"><strong>${fmt(w.tc)}</strong></td>\n    <td><span class=\"dot ${dotClass}\" title=\"${w.llm_match || ''}\"></span>${w.llm_score ? '<span style=\"font-size:9px;color:var(--text3);margin-left:2px\">' + w.llm_score + '</span>' : ''}</td>\n  </tr>`;\n  \n  // Scope of work rows\n  const scopeItems = w.scope_of_work || [];\n  if (scopeItems.length > 0) {\n    html += `<tr class=\"scope-row hidden\" data-scope=\"${i}\">\n      <td></td>\n      <td colspan=\"6\" style=\"background:#F8FFF8\">\n        <strong style=\"color:#2E7D32\">📋 ${esc(L.scope_title || 'Scope of Work')}:</strong><br>\n        ${scopeItems.map((s, si) => '<span style=\"color:#555\">• ' + esc(s) + '</span>').join('<br>')}\n      </td>\n      <td></td>\n    </tr>`;\n  }\n  \n  // Resources\n  const resources = w.resources || [];\n  resources.forEach((r, ri) => {\n    const tagClass = r.resource_type === 'labor' ? 'res-labor' : r.resource_type === 'machine' ? 'res-machine' : 'res-material';\n    const tagLabel = r.resource_type === 'labor' ? (L.res_labor || 'Labor') : r.resource_type === 'machine' ? (L.res_machine || 'Equip') : (L.res_material || 'Mat');\n    const hiddenClass = isFirst ? '' : 'hidden';\n    const resQty = r.scaled_quantity || r.resource_quantity || 0;\n    const resPrice = r.resource_price || 0;\n    const resCost = r.scaled_cost || 0;\n    const norm = r.resource_quantity || r.norma || 0;\n    const pct = w.tc > 0 ? Math.round(resCost / w.tc * 100) : 0;\n    \n    html += `<tr class=\"res-row ${hiddenClass}\" data-work=\"${i}\">\n      <td></td>\n      <td><span style=\"font-size:9px;color:var(--text3)\">${esc(r.resource_code || '')}</span></td>\n      <td>\n        <span class=\"res-tag ${tagClass}\">${esc(tagLabel)}</span>\n        ${esc(r.resource_name || '')}\n        ${norm ? '<span style=\"color:var(--text3);font-size:9px;margin-left:4px\">×' + fmtNum(norm, 4) + '</span>' : ''}\n      </td>\n      <td style=\"font-size:10px\">${esc(r.resource_unit || '')}</td>\n      <td class=\"right\" style=\"font-size:10px\">${fmtNum(resQty, 3)}</td>\n      <td class=\"right\" style=\"font-size:10px\">${fmt(resPrice)}</td>\n      <td class=\"right\">${fmt(resCost)} <span style=\"font-size:9px;color:var(--text3)\">${pct}%</span></td>\n      <td></td>\n    </tr>`;\n  });\n  \n  // Summary row for work\n  if (resources.length > 0) {\n    const laborSum = resources.filter(r => r.resource_type === 'labor').reduce((s, r) => s + (r.scaled_cost || 0), 0);\n    const matSum = resources.filter(r => r.resource_type === 'material').reduce((s, r) => s + (r.scaled_cost || 0), 0);\n    const machSum = resources.filter(r => r.resource_type === 'machine').reduce((s, r) => s + (r.scaled_cost || 0), 0);\n    const hiddenClass = isFirst ? '' : 'hidden';\n    \n    html += `<tr class=\"summary-row ${hiddenClass}\" data-work=\"${i}\">\n      <td colspan=\"2\"></td>\n      <td colspan=\"4\">\n        <span style=\"color:var(--labor-text)\">${L.workers || 'Labor'}: ${fmt(laborSum)}</span> · \n        <span style=\"color:var(--material-text)\">${L.materials || 'Mat'}: ${fmt(matSum)}</span> · \n        <span style=\"color:var(--machine-text)\">${L.machines || 'Equip'}: ${fmt(machSum)}</span>\n      </td>\n      <td class=\"right\"><strong>${fmt(w.tc)}</strong></td>\n      <td></td>\n    </tr>`;\n  }\n});\n\nhtml += `<tr class=\"total-row\">\n  <td colspan=\"6\" style=\"text-align:right\">${esc(L.grand_total || 'TOTAL')}</td>\n  <td class=\"right\">${fmt(total)}</td>\n  <td></td>\n</tr>\n</table>\n<div class=\"footer\">\n  <a href=\"https://DataDrivenConstruction.io\" target=\"_blank\">DDC CWICR</a> · \n  Open Source Construction Cost Database · ${dateStr}\n</div>\n</div>\n<script>\nfunction toggleWork(idx) {\n  const icon = document.getElementById('icon-' + idx);\n  const rows = document.querySelectorAll('tr[data-work=\"' + idx + '\"]:not(.work-row)');\n  const isHidden = rows.length > 0 && rows[0].classList.contains('hidden');\n  rows.forEach(r => r.classList.toggle('hidden', !isHidden));\n  if (icon) icon.textContent = isHidden ? '▼' : '▶';\n}\nfunction expandAll() {\n  document.querySelectorAll('tr[data-work]').forEach(r => r.classList.remove('hidden'));\n  document.querySelectorAll('[id^=\"icon-\"]').forEach(i => i.textContent = '▼');\n}\nfunction collapseAll() {\n  document.querySelectorAll('tr.res-row, tr.summary-row, tr.scope-row').forEach(r => r.classList.add('hidden'));\n  document.querySelectorAll('[id^=\"icon-\"]').forEach(i => i.textContent = '▶');\n}\nfunction toggleScope(idx) {\n  const rows = document.querySelectorAll('tr.scope-row[data-scope=\"' + idx + '\"]');\n  rows.forEach(r => r.classList.toggle('hidden'));\n}\n</script>\n</body></html>`;\n\nconst sd = $getWorkflowStaticData('global');\nsd.html_report = html;\n\nreturn { json: { ...d, html_content: html } };"},"typeVersion":2},{"id":"652200d3-b872-4e16-8a78-81f62cfed6f1","name":"Answer Calc CB","type":"n8n-nodes-base.httpRequest","onError":"continueRegularOutput","position":[-3920,-1024],"parameters":{"url":"=https://api.telegram.org/bot{{ $('🔑 TOKEN').first().json.bot_token }}/answerCallbackQuery","method":"POST","options":{"timeout":5000},"jsonBody":"={\n  \"callback_query_id\": \"{{ $('Config').item.json.callbackQueryId }}\",\n  \"text\": \"{{ $('Config').item.json.L.loading }}\",\n  \"show_alert\": false\n}","sendBody":true,"specifyBody":"json"},"typeVersion":4.2},{"id":"06108977-5e6c-4ded-bc87-481a1f213469","name":"📤 Works Updated","type":"n8n-nodes-base.httpRequest","position":[-4128,-1376],"parameters":{"url":"=https://api.telegram.org/bot{{ $('🔑 TOKEN').first().json.bot_token }}/sendMessage","method":"POST","options":{},"jsonBody":"={\n  \"chat_id\": {{ $json.chatId }},\n  \"text\": {{ JSON.stringify($json.msg) }},\n  \"parse_mode\": \"Markdown\",\n  \"reply_markup\": { \"inline_keyboard\": {{ JSON.stringify($json.keyboard) }} }\n}","sendBody":true,"specifyBody":"json"},"typeVersion":4.2},{"id":"6e8b2b3d-e435-4a4f-acdd-8dfc3fb7948e","name":"Works Updated","type":"n8n-nodes-base.code","position":[-4272,-1376],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// WORKS UPDATED - Show updated works list after quantity change\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst cfg = $('Config').first().json;\nconst cid = String(cfg.chatId);\nconst sd = $getWorkflowStaticData('global');\nconst session = sd.sess?.[cid] || {};\nconst works = session.works || [];\nconst L = cfg.L || session.L || {};\n\nconsole.log('=== WORKS UPDATED ===');\nconsole.log('Works:', works.length);\nconsole.log('L.native:', L.native);\n\nlet msg = '✅ ' + (L.work_added || 'Обновлено') + '\\n\\n';\nmsg += '*' + works.length + ' ' + (L.items || 'позиций') + '*\\n\\n';\n\nfor (let i = 0; i < works.length; i++) {\n  const w = works[i];\n  const name = w.name.length > 25 ? w.name.substring(0, 22) + '...' : w.name;\n  msg += (i + 1) + '. ' + name + ' — ' + w.qty + ' ' + (w.unit || 'm²') + '\\n';\n}\n\n// No limit\n\nconst keyboard = [];\nconst maxBtns = works.length;\nfor (let i = 0; i < maxBtns; i += 5) {\n  const row = [];\n  for (let j = 0; j < 5 && i + j < maxBtns; j++) {\n    row.push({ text: '✏️' + (i + j + 1), callback_data: 'edit_work_' + (i + j) });\n  }\n  keyboard.push(row);\n}\n\nkeyboard.push([\n  { text: L.btn_add_work || '+ Позиция', callback_data: 'add_work' },\n  { text: L.btn_calc || '▶ Расчёт', callback_data: 'calculate' }\n]);\nkeyboard.push([{ text: L.btn_new || '🔄 Заново', callback_data: 'restart' }]);\n\nreturn { json: { ...cfg, msg, keyboard } };"},"typeVersion":2},{"id":"b4972bc3-4ad6-4fc9-ba80-c2b54fa94ae8","name":"📤 Ask New Work","type":"n8n-nodes-base.httpRequest","position":[-4272,-2128],"parameters":{"url":"=https://api.telegram.org/bot{{ $('🔑 TOKEN').first().json.bot_token }}/sendMessage","method":"POST","options":{},"jsonBody":"={\n  \"chat_id\": {{ $('Config').item.json.chatId }},\n  \"text\": {{ JSON.stringify($('Config').item.json.L.enter_work) }},\n  \"parse_mode\": \"Markdown\"\n}","sendBody":true,"specifyBody":"json"},"typeVersion":4.2},{"id":"22ebe40e-e174-4a2c-917c-9cd3836c8a68","name":"Edit Menu","type":"n8n-nodes-base.code","position":[-4272,-1520],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// EDIT MENU - Show edit buttons for selected work item\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst cfg = $('Config').first().json;\nconst cid = String(cfg.chatId);\nconst sd = $getWorkflowStaticData('global');\nconst session = sd.sess?.[cid] || {};\nconst works = session.works || [];\nconst L = cfg.L || session.L || {};\n\nconst idx = session.editingWorkIndex || cfg.editingWorkIndex || 0;\nconst work = works[idx];\n\nconsole.log('=== EDIT MENU ===');\nconsole.log('Editing work index:', idx);\nconsole.log('Work:', work?.name);\n\nif (!work) {\n  return { json: { ...cfg, _skip: true } };\n}\n\nconst name = work.name.length > 30 ? work.name.substring(0, 27) + '...' : work.name;\nlet msg = '✏️ *' + (L.btn_edit || 'Редактирование') + '*\\n\\n';\nmsg += '*' + (idx + 1) + '. ' + name + '*\\n';\nmsg += '📏 ' + work.qty + ' ' + (work.unit || 'm²') + '\\n\\n';\nmsg += (L.edit_hint || 'Изменить количество:');\n\n// Quantity edit buttons\nconst keyboard = [\n  [\n    { text: '-10', callback_data: 'qty_work_' + idx + '_minus10' },\n    { text: '-1', callback_data: 'qty_work_' + idx + '_minus1' },\n    { text: '+1', callback_data: 'qty_work_' + idx + '_plus1' },\n    { text: '+10', callback_data: 'qty_work_' + idx + '_plus10' }\n  ],\n  [\n    { text: '÷2', callback_data: 'qty_work_' + idx + '_half' },\n    { text: '×2', callback_data: 'qty_work_' + idx + '_double' }\n  ],\n  [\n    { text: '🗑 ' + (L.btn_delete || 'Удалить'), callback_data: 'delete_work_' + idx }\n  ],\n  [\n    { text: '◀️ ' + (L.btn_done || 'Назад'), callback_data: 'done_editing' }\n  ]\n];\n\nreturn { json: { ...cfg, msg, keyboard, chatId: cid } };"},"typeVersion":2},{"id":"e1cb2be1-216e-4826-9438-004b65c3fa57","name":"📤 Lang OK","type":"n8n-nodes-base.httpRequest","position":[-3968,-2288],"parameters":{"url":"=https://api.telegram.org/bot{{ $('🔑 TOKEN').first().json.bot_token }}/sendMessage","method":"POST","options":{},"jsonBody":"={{ JSON.stringify($json._body) }}","sendBody":true,"specifyBody":"json"},"typeVersion":4.2},{"id":"948a9e11-d8f5-43bb-93c9-d83a198a86a6","name":"Answer Lang CB","type":"n8n-nodes-base.httpRequest","onError":"continueRegularOutput","position":[-4272,-2288],"parameters":{"url":"=https://api.telegram.org/bot{{ $('🔑 TOKEN').first().json.bot_token }}/answerCallbackQuery","method":"POST","options":{"timeout":5000},"jsonBody":"={\n  \"callback_query_id\": \"{{ $('Config').item.json.callbackQueryId }}\"\n}","sendBody":true,"specifyBody":"json"},"typeVersion":4.2},{"id":"dc1f5a37-6590-433d-adba-e59843e63aa0","name":"📤 Lang Menu","type":"n8n-nodes-base.httpRequest","position":[-4272,-2432],"parameters":{"url":"=https://api.telegram.org/bot{{ $('🔑 TOKEN').first().json.bot_token }}/sendMessage","method":"POST","options":{},"jsonBody":"={\n  \"chat_id\": {{ $json.chatId }},\n  \"text\": \"*DDC CWICR Cost Estimator*\\nhttps://DataDrivenConstruction.io\\n\\n▸ Photo analysis (up to 4)\\n▸ Text description\\n▸ 9 languages · 55,000+ work items\\n▸ Excel & PDF export\\n\\nSelect language:\",\n  \"parse_mode\": \"Markdown\",\n  \"reply_markup\": {\n    \"inline_keyboard\": [\n      [{\"text\": \"🇩🇪 Deutsch\", \"callback_data\": \"lang_DE\"}, {\"text\": \"🇬🇧 English\", \"callback_data\": \"lang_EN\"}, {\"text\": \"🇷🇺 Русский\", \"callback_data\": \"lang_RU\"}],\n      [{\"text\": \"🇪🇸 Español\", \"callback_data\": \"lang_ES\"}, {\"text\": \"🇫🇷 Français\", \"callback_data\": \"lang_FR\"}, {\"text\": \"🇧🇷 Português\", \"callback_data\": \"lang_PT\"}],\n      [{\"text\": \"🇨🇳 中文\", \"callback_data\": \"lang_ZH\"}, {\"text\": \"🇦🇪 العربية\", \"callback_data\": \"lang_AR\"}, {\"text\": \"🇮🇳 हिन्दी\", \"callback_data\": \"lang_HI\"}],\n      [{\"text\": \"❓ Help\", \"callback_data\": \"show_help\"}]\n    ]\n  }\n}","sendBody":true,"specifyBody":"json"},"typeVersion":4.2},{"id":"2bb17fad-5bfa-4332-a937-fb7c88c4fef5","name":"Route","type":"n8n-nodes-base.switch","position":[-4544,-1728],"parameters":{"rules":{"values":[{"outputKey":"LANG","conditions":{"options":{"version":2,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"a3c7266e-9deb-4abe-a856-0e835757cdc8","operator":{"type":"string","operation":"equals"},"leftValue":"={{ $json.action }}","rightValue":"show_lang"}]},"renameOutput":true},{"outputKey":"LANG_OK","conditions":{"options":{"version":2,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"abc9388b-156f-4a27-8b6b-16d0e18e9749","operator":{"type":"string","operation":"equals"},"leftValue":"={{ $json.action }}","rightValue":"lang_selected"}]},"renameOutput":true},{"outputKey":"WORKS_UPD","conditions":{"options":{"version":2,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"320b81af-ef5e-4776-814d-3aead2e0a38a","operator":{"type":"string","operation":"equals"},"leftValue":"={{ $json.action }}","rightValue":"works_updated"}]},"renameOutput":true},{"outputKey":"EDIT_MENU","conditions":{"options":{"version":2,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"def6b912-0ac9-4066-b421-59fd67b596c8","operator":{"type":"string","operation":"equals"},"leftValue":"={{ $json.action }}","rightValue":"show_edit_menu"}]},"renameOutput":true},{"outputKey":"ADD_WORK","conditions":{"options":{"version":2,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"4ca5650d-0486-456a-b17c-34ec088923d1","operator":{"type":"string","operation":"equals"},"leftValue":"={{ $json.action }}","rightValue":"ask_new_work"}]},"renameOutput":true},{"outputKey":"CALC","conditions":{"options":{"version":2,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"8f7bceab-ef7c-43b4-8afa-a8c309a74645","operator":{"type":"string","operation":"equals"},"leftValue":"={{ $json.action }}","rightValue":"start_calc"}]},"renameOutput":true},{"outputKey":"EXCEL","conditions":{"options":{"version":2,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"e1037d8b-0e5f-46c3-a008-ad16d97689c2","operator":{"type":"string","operation":"equals"},"leftValue":"={{ $json.action }}","rightValue":"export_excel"}]},"renameOutput":true},{"outputKey":"PDF","conditions":{"options":{"version":2,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"6fff52ee-58d7-4073-b42d-cb15298d5788","operator":{"type":"string","operation":"equals"},"leftValue":"={{ $json.action }}","rightValue":"export_pdf"}]},"renameOutput":true},{"outputKey":"HELP","conditions":{"options":{"version":2,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"fdf49046-6eaf-4183-8ff8-209eff4fefc4","operator":{"type":"string","operation":"equals"},"leftValue":"={{ $json.action }}","rightValue":"show_help"}]},"renameOutput":true},{"outputKey":"DETAILS","conditions":{"options":{"version":2,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"6b143832-1fbb-40ee-9465-ff85f59b0328","operator":{"type":"string","operation":"equals"},"leftValue":"={{ $json.action }}","rightValue":"view_details"}]},"renameOutput":true},{"outputKey":"ANALYZE_TEXT","conditions":{"options":{"version":2,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"244cd331-efdc-4e0e-b267-f51d8a6c6f3d","operator":{"type":"string","operation":"equals"},"leftValue":"={{ $json.action }}","rightValue":"analyze_text"}]},"renameOutput":true}]},"options":{"fallbackOutput":"extra"}},"typeVersion":3.2},{"id":"9ed6eb3e-b254-466f-8ff1-c3f015caae2d","name":"Config","type":"n8n-nodes-base.code","position":[-4832,-1568],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// DDC CWICR - Data Driven Construction Cost Estimator\n// https://DataDrivenConstruction.io\n// Open source construction cost database with 55,000+ work items\n// ═══════════════════════════════════════════════════════════════════════════\n\n// CONFIG v8.5 PRO - Professional style localization + PDF SUPPORT\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst input = $input.first().json;\nconst lang = (input.lang || 'EN').toUpperCase();\nconst chatId = String(input.chatId);\nconst sd = $getWorkflowStaticData('global');\n\nconst LANGS = {\n  'DE': { \n    fallback_start: 'Drücken Sie /start um zu beginnen', \n    rooms: 'Räume', works_identified: 'Positionen', general: 'Allgemein', no_works: 'Keine Arbeiten', items: 'Positionen', min: 'Min', \n    db: 'DE_BERLIN_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR', \n    name: 'German', flag: '🇩🇪', native: 'Deutsch', cur: 'EUR', sym: '€', loc: 'de-DE', region: 'Berlin', search_lang: 'German',\n    // Professional welcome with PDF support\n    ok: '✅ *Deutsch* · Berlin · EUR',\n    text_prompt: `*Beschreiben Sie die Arbeiten:*\\n\\n_Beispiel:_\\n\\`\\`\\`\\nGipskarton 2-lagig 25m2\\nFliesen Bad 15m2\\nMalerarbeiten 120m2\\n\\`\\`\\``,\n    photo: `*Foto, PDF oder Beschreibung senden*\n\n📄 *PDF-Zeichnungen* — Grundriss oder Bauplan (max. 3 Seiten)\n📷 *Foto* — Raum oder Objekt fotografieren\n✏️ *Text* — Arbeiten als Liste beschreiben\n\n_Beispiel zum Kopieren:_\n\\`\\`\\`\nGipskarton 2-lagig Metallprofil CW75 25m2\nFliesen Feinsteinzeug 60x60 Bad 15m2\nSpachteln Q3 Decken und Waende 120m2\nElektro Steckdosen UP 20 Stueck\n\\`\\`\\`\n\nOder Foto/PDF senden 📷📄`,\n    // PDF specific\n    pdf_received: '📄 *PDF erhalten*',\n    pdf_processing: '⏳ Analysiere Zeichnung...',\n    pdf_pages: 'Seiten',\n    pdf_page_limit: '⚠️ Nur erste 3 Seiten werden verarbeitet',\n    pdf_rooms_found: '🏠 Räume gefunden',\n    pdf_elements_found: '🧱 Elemente gefunden',\n    pdf_works_generated: '📝 Arbeiten generiert',\n    pdf_analyzing_page: '🔍 Analysiere Seite',\n    pdf_of: 'von',\n    pdf_complete: '✅ Analyse abgeschlossen',\n    pdf_error: '❌ PDF-Verarbeitungsfehler',\n    // Rest of translations\n    photo_added: '✅ Foto hinzugefügt',\n    photos_count: 'Fotos',\n    add_more: '+ Weitere Fotos',\n    analyze_now: '▶ Analyse starten',\n    analyzing: 'Bildanalyse läuft...',\n    found: '*Erkannte Leistungen:*',\n    edit_hint: 'Zur Bearbeitung antippen',\n    calc: 'Preisermittlung',\n    ready: '*KOSTENVORANSCHLAG*',\n    total: 'GESAMT',\n    days: 'Tage',\n    pct: 'Trefferquote',\n    workers: 'Lohn',\n    machines: 'Geräte',\n    materials: 'Material',\n    subtotal: 'Zusammenfassung',\n    searching: 'Suche',\n    of: 'von',\n    not_found: 'Keine Übereinstimmung',\n    low_conf: 'Prüfung empfohlen',\n    price_note: 'Preisbasis: Berlin 2025',\n    btn_calc: '▶ Berechnen',\n    btn_new: '+ Neues Projekt',\n    btn_lang: '⚙ Sprache',\n    btn_edit: '✎ Ändern',\n    btn_delete: '✕ Entfernen',\n    btn_add_work: '+ Position',\n    btn_done: '✅ Fertig',\n    btn_export_excel: '↓ Excel',\n    btn_export_pdf: '↓ PDF',\n    btn_restart: '↻ Neu starten',\n    btn_help: '? Hilfe',\n    loading: 'Preisermittlung läuft...',\n    more_in_html: 'Detaillierter Bericht verfügbar',\n    resources: 'Details: Ressourcen & Leistungsumfang',\n    enter_work: '*Neue Position hinzufügen*\\nFormat: Bezeichnung, Menge Einheit\\nBeispiel: Gipskartonwand, 15 m²',\n    work_added: '✅ Position hinzugefügt',\n    what_next: '*Weitere Optionen:*',\n    categories: 'Kategorien',\n    cat_demolition: 'Abbruch',\n    cat_rough: 'Rohbau',\n    cat_finishing: 'Ausbau',\n    cat_mep: 'TGA',\n    help_title: '*Benutzerhandbuch*',\n    help_text: `*Benutzerhandbuch*\n\n*1. Dokumentation*\nSenden Sie Fotos, PDF-Pläne oder Beschreibung.\nPDF: max. 3 Seiten werden analysiert.\n\n*2. Prüfung*\nÜberprüfen Sie die erkannten Leistungen.\n\n*3. Kalkulation*\nPreisermittlung aus DDC CWICR Datenbank.\n\n*4. Export*\nErgebnisse als Excel oder PDF.\n\n*Befehle:*\n/start — Neues Projekt\n/help — Diese Hilfe`,\n    doc_title: 'KOSTENVORANSCHLAG',\n    col_pos: 'Pos', col_code: 'Kennziffer', col_desc: 'Bezeichnung', col_unit: 'Einh.', col_qty: 'Menge', col_price: 'EP', col_total: 'GP', col_labor: 'Std', col_quality: 'Q',\n    grand_total: 'GESAMTSUMME', labor_cost: 'Lohnkosten', material_cost: 'Materialkosten', labor_days: 'Arbeitstage',\n    kpi_total: 'Gesamtkosten', kpi_hours: 'Arbeitsstunden', kpi_days: 'Arbeitstage',\n    chart_cost_structure: 'Kostenstruktur', chart_labor: 'Lohn', chart_material: 'Material', chart_machines: 'Geräte',\n    res_labor: 'Lohn', res_material: 'Mat', res_machine: 'Ger',\n    collapse_all: 'Alle einklappen', expand_all: 'Alle ausklappen',\n    quality_high: 'Hohe Übereinstimmung', quality_medium: 'Mittlere Übereinstimmung', quality_low: 'Geringe Übereinstimmung',\n    export_excel_msg: 'Excel-Export (CSV)', export_pdf_msg: 'PDF-Export', btn_refine: 'Genauer analysieren', found_pct: 'gefunden', more_resources: 'weitere', kpi_items: 'Positionen', scope_title: 'Leistungsumfang', show_scope: 'Leistungen anzeigen'\n  },\n\n  'EN': { \n    fallback_start: 'Use /start to begin', \n    rooms: 'комнат', works_identified: 'works', general: 'Общее', no_works: 'Работы не найдены', items: 'позиций', min: 'мин', \n    db: 'ENG_TORONTO_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR', \n    name: 'English', flag: '🇬🇧', native: 'English', cur: 'CAD', sym: '$', loc: 'en-CA', region: 'Toronto', search_lang: 'English',\n    ok: '✅ *English* · Toronto · CAD',\n    text_prompt: `*Describe the works:*\\n\\n_Example:_\\n\\`\\`\\`\\nDrywall 2-layer 25m2\\nFloor tiles bathroom 15m2\\nPainting walls 120m2\\n\\`\\`\\``,\n    photo: `*Send photo, PDF or description*\n\n📄 *PDF drawings* — floor plan or blueprint (max 3 pages)\n📷 *Photo* — photograph room or object\n✏️ *Text* — describe work as a list\n\n_Example to copy:_\n\\`\\`\\`\nDrywall 2-layer metal stud CW75 25m2\nPorcelain tiles 60x60 bathroom 15m2\nPlastering level 3 ceiling walls 120m2\nElectrical outlets flush mount 20 pcs\n\\`\\`\\`\n\nOr send photo/PDF 📷📄`,\n    // PDF specific\n    pdf_received: '📄 *PDF received*',\n    pdf_processing: '⏳ Analyzing drawing...',\n    pdf_pages: 'pages',\n    pdf_page_limit: '⚠️ Processing first 3 pages only',\n    pdf_rooms_found: '🏠 Rooms found',\n    pdf_elements_found: '🧱 Elements found',\n    pdf_works_generated: '📝 Works generated',\n    pdf_analyzing_page: '🔍 Analyzing page',\n    pdf_of: 'of',\n    pdf_complete: '✅ Analysis complete',\n    pdf_error: '❌ PDF processing error',\n    // Rest\n    photo_added: '✅ Photo added',\n    photos_count: 'photos',\n    add_more: '+ Add more',\n    analyze_now: '▶ Start analysis',\n    analyzing: 'Analyzing images...',\n    found: '*Identified Work Items:*',\n    edit_hint: 'Tap to edit',\n    calc: 'Pricing lookup',\n    ready: '*COST ESTIMATE*',\n    total: 'TOTAL',\n    days: 'days',\n    pct: 'Match rate',\n    workers: 'Labor',\n    machines: 'Equipment',\n    materials: 'Materials',\n    subtotal: 'Summary',\n    searching: 'Searching',\n    of: 'of',\n    not_found: 'No match found',\n    low_conf: 'Review recommended',\n    price_note: 'Price basis: Toronto 2025',\n    btn_calc: '▶ Calculate',\n    btn_new: '+ New Project',\n    btn_lang: '⚙ Language',\n    btn_edit: '✎ Edit',\n    btn_delete: '✕ Remove',\n    btn_add_work: '+ Add Item',\n    btn_done: '✅ Done',\n    btn_export_excel: '↓ Excel',\n    btn_export_pdf: '↓ PDF',\n    btn_restart: '↻ Start Over',\n    btn_help: '? Help',\n    loading: 'Calculating prices...',\n    more_in_html: 'Detailed report available',\n    resources: 'Details: Resources & Scope of Work',\n    enter_work: '*Add New Item*\\nFormat: Description, Quantity Unit\\nExample: Drywall installation, 15 m²',\n    work_added: '✅ Item added',\n    what_next: '*Options:*',\n    categories: 'Categories',\n    cat_demolition: 'Demolition',\n    cat_rough: 'Structure',\n    cat_finishing: 'Finishes',\n    cat_mep: 'MEP',\n    help_title: '*User Guide*',\n    help_text: `*User Guide*\n\n*1. Documentation*\nSend photos, PDF plans or description.\nPDF: max 3 pages will be analyzed.\n\n*2. Review*\nCheck identified work items.\n\n*3. Расчёт*\nPricing from DDC CWICR database.\n\n*4. Export*\nExport as Excel or PDF.\n\n*Commands:*\n/start — New project\n/help — This guide`,\n    doc_title: 'COST ESTIMATE',\n    col_pos: 'No', col_code: 'Code', col_desc: 'Description', col_unit: 'Unit', col_qty: 'Qty', col_price: 'Rate', col_total: 'Total', col_labor: 'Hrs', col_quality: 'Q',\n    grand_total: 'GRAND TOTAL', labor_cost: 'Labor Cost', material_cost: 'Material Cost', labor_days: 'Work Days',\n    kpi_total: 'Total Cost', kpi_hours: 'Work Hours', kpi_days: 'Work Days',\n    chart_cost_structure: 'Cost Structure', chart_labor: 'Labor', chart_material: 'Material', chart_machines: 'Equipment',\n    res_labor: 'Labor', res_material: 'Mat', res_machine: 'Equip',\n    collapse_all: 'Collapse all', expand_all: 'Expand all',\n    quality_high: 'High confidence', quality_medium: 'Medium confidence', quality_low: 'Low confidence',\n    export_excel_msg: 'Excel Export (CSV)', export_pdf_msg: 'PDF Export', btn_refine: 'Refine Analysis', found_pct: 'found', more_resources: 'more', kpi_items: 'Items', scope_title: 'Scope of Work', show_scope: 'Show scope'\n  },\n\n  'RU': { \n    fallback_start: 'Нажмите /start для начала', \n    rooms: 'комнат', works_identified: 'позиций', general: 'Общее', no_works: 'Работы не найдены', items: 'позиций', min: 'мин', \n    db: 'RU_STPETERSBURG_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR', \n    name: 'Russian', flag: '🇷🇺', native: 'Русский', cur: 'RUB', sym: '₽', loc: 'ru-RU', region: 'Санкт-Петербург', search_lang: 'Russian',\n    ok: '✅ *Русский* · СПб · RUB',\n    text_prompt: `*Опишите работы:*\\n\\n_Пример:_\\n\\`\\`\\`\\nГипсокартон 2 слоя 25м2\\nПлитка ванная 15м2\\nПокраска стен 120м2\\n\\`\\`\\``,\n    photo: `*Отправьте фото, PDF или описание*\n\n📄 *PDF чертежи* — план этажа или чертёж (до 3 стр.)\n📷 *Фото* — сфотографируйте помещение\n✏️ *Текст* — опишите работы списком\n\n_Пример для копирования:_\n\\`\\`\\`\nГипсокартон 2 слоя профиль ПП60 25м2\nПлитка керамогранит 60x60 ванная 15м2\nШпаклевка под покраску потолки стены 120м2\nРозетки скрытый монтаж 20шт\n\\`\\`\\`\n\nИли отправьте фото/PDF 📷📄`,\n    // PDF specific\n    pdf_received: '📄 *PDF получен*',\n    pdf_processing: 'Анализирую чертёж...',\n    pdf_pages: 'стр.',\n    pdf_page_limit: '⚠️ Обрабатываю первые 3 страницы',\n    pdf_rooms_found: '🏠 Найдено помещений',\n    pdf_elements_found: '🧱 Найдено элементов',\n    pdf_works_generated: '📝 Сформировано работ',\n    pdf_analyzing_page: 'Анализирую страницу',\n    pdf_of: 'из',\n    pdf_complete: '✅ Анализ завершён',\n    pdf_error: '❌ Ошибка обработки PDF',\n    // Rest\n    photo_added: '✅ Фото добавлено',\n    photos_count: 'фото',\n    add_more: '+ Ещё фото',\n    analyze_now: '▶ Начать анализ',\n    analyzing: 'Анализ изображений...',\n    found: '*Определённые работы:*',\n    edit_hint: 'Нажмите для редактирования',\n    calc: 'Поиск расценок',\n    ready: '*СМЕТА*',\n    total: 'ИТОГО',\n    days: 'дн.',\n    pct: 'Точность',\n    workers: 'Труд',\n    machines: 'Механизмы',\n    materials: 'Материалы',\n    subtotal: 'Сводка',\n    searching: 'Поиск',\n    of: 'из',\n    not_found: 'Не найдено',\n    low_conf: 'Требует проверки',\n    price_note: 'База цен: Санкт-Петербург 2025',\n    btn_calc: '▶ Рассчитать',\n    btn_new: '+ Новый проект',\n    btn_lang: '⚙ Язык',\n    btn_edit: '✎ Изменить',\n    btn_delete: '✕ Удалить',\n    btn_add_work: '+ Позиция',\n    btn_done: '✅ Готово',\n    btn_export_excel: '↓ Excel',\n    btn_export_pdf: '↓ PDF',\n    btn_restart: '↻ Заново',\n    btn_help: '? Справка',\n    loading: 'Расчёт...',\n    more_in_html: 'Подробный отчёт доступен',\n    resources: 'Подробнее: ресурсы и составы работ',\n    enter_work: '*Добавить позицию*\\nФормат: Наименование, Количество Единица\\nПример: Монтаж ГКЛ, 15 м²',\n    work_added: '✅ Позиция добавлена',\n    what_next: '*Действия:*',\n    categories: 'Категории',\n    cat_demolition: 'Демонтаж',\n    cat_rough: 'Черновые',\n    cat_finishing: 'Отделка',\n    cat_mep: 'Инженерия',\n    help_title: '*Руководство*',\n    help_text: `*Руководство пользователя*\n\n*1. Документация*\nОтправьте фото, PDF-план или описание.\nPDF: анализируются до 3 страниц.\n\n*2. Проверка*\nПроверьте определённые работы.\n\n*3. Расчёт*\nЦены из базы DDC CWICR.\n\n*4. Экспорт*\nВыгрузка в Excel или PDF.\n\n*Команды:*\n/start — Новый проект\n/help — Справка`,\n    doc_title: 'СМЕТА',\n    col_pos: '№', col_code: 'Шифр', col_desc: 'Наименование', col_unit: 'Ед.', col_qty: 'Кол.', col_price: 'Цена', col_total: 'Сумма', col_labor: 'Ч/ч', col_quality: 'К',\n    grand_total: 'ВСЕГО', labor_cost: 'ФОТ', material_cost: 'Материалы', labor_days: 'Дней',\n    kpi_total: 'Стоимость', kpi_hours: 'Трудозатраты', kpi_days: 'Срок',\n    chart_cost_structure: 'Структура затрат', chart_labor: 'Труд', chart_material: 'Материалы', chart_machines: 'Механизмы',\n    res_labor: 'Труд', res_material: 'Мат', res_machine: 'Мех',\n    collapse_all: 'Свернуть', expand_all: 'Развернуть',\n    quality_high: 'Высокая точность', quality_medium: 'Средняя точность', quality_low: 'Низкая точность',\n    export_excel_msg: 'Экспорт Excel (CSV)', export_pdf_msg: 'Экспорт PDF', btn_refine: 'Уточнить анализ', found_pct: 'найдено', more_resources: 'ещё', kpi_items: 'Позиции', scope_title: 'Состав работ', show_scope: 'Показать состав'\n  },\n\n  'ES': { \n    fallback_start: 'Pulse /start para comenzar', \n    rooms: 'habitaciones', works_identified: 'trabajos', general: 'Общее', no_works: 'Sin trabajos', items: 'elementos', min: 'мин', \n    db: 'ES_BARCELONA_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR', \n    name: 'Spanish', flag: '🇪🇸', native: 'Español', cur: 'EUR', sym: '€', loc: 'es-ES', region: 'Barcelona', search_lang: 'Spanish',\n    ok: '✅ *Español* · Barcelona · EUR',\n    text_prompt: `*Describa los trabajos:*\\n\\n_Ejemplo:_\\n\\`\\`\\`\\nPladur 2 capas 25m2\\nAzulejos baño 15m2\\nPintura paredes 120m2\\n\\`\\`\\``,\n    photo: `*Envíe foto, PDF o descripción*\n\n📄 *Planos PDF* — plano de planta o dibujo (máx. 3 págs.)\n📷 *Foto* — fotografíe el espacio\n✏️ *Texto* — describa trabajos en lista\n\n_Ejemplo para copiar:_\n\\`\\`\\`\nPladur 2 capas perfil metálico 25m2\nAzulejos porcelánico 60x60 baño 15m2\nAlisado para pintura techos paredes 120m2\nEnchufes empotrados 20uds\n\\`\\`\\`\n\nO envíe foto/PDF 📷📄`,\n    pdf_received: '📄 *PDF recibido*',\n    pdf_processing: '⏳ Analizando plano...',\n    pdf_pages: 'páginas',\n    pdf_page_limit: '⚠️ Solo se procesan las primeras 3 páginas',\n    pdf_rooms_found: '🏠 Habitaciones encontradas',\n    pdf_elements_found: '🧱 Elementos encontrados',\n    pdf_works_generated: '📝 Trabajos generados',\n    pdf_analyzing_page: '🔍 Analizando página',\n    pdf_of: 'de',\n    pdf_complete: '✅ Análisis completado',\n    pdf_error: '❌ Error al procesar PDF',\n    photo_added: '✅ Foto añadida',\n    photos_count: 'fotos',\n    add_more: '+ Más fotos',\n    analyze_now: '▶ Analizar',\n    analyzing: 'Analizando...',\n    found: '*Trabajos identificados:*',\n    edit_hint: 'Toque para editar',\n    calc: 'Búsqueda de precios',\n    ready: '*PRESUPUESTO*',\n    total: 'TOTAL',\n    days: 'días',\n    pct: 'Precisión',\n    workers: 'M.O.',\n    machines: 'Equipos',\n    materials: 'Materiales',\n    subtotal: 'Resumen',\n    searching: 'Buscando',\n    of: 'de',\n    not_found: 'Sin coincidencia',\n    low_conf: 'Revisar',\n    price_note: 'Base: Barcelona 2025',\n    btn_calc: '▶ Calcular',\n    btn_new: '+ Nuevo',\n    btn_lang: '⚙ Idioma',\n    btn_edit: '✎ Editar',\n    btn_delete: '✕ Eliminar',\n    btn_add_work: '+ Partida',\n    btn_done: '✅ Listo',\n    btn_export_excel: '↓ Excel',\n    btn_export_pdf: '↓ PDF',\n    btn_restart: '↻ Reiniciar',\n    btn_help: '? Ayuda',\n    loading: 'Calculando...',\n    more_in_html: 'Informe detallado disponible',\n    resources: 'Detalles: recursos y alcance',\n    enter_work: '*Añadir partida*\\nFormato: Descripción, Cantidad Unidad',\n    work_added: '✅ Añadido',\n    what_next: '*Opciones:*',\n    categories: 'Categorías',\n    cat_demolition: 'Demolición',\n    cat_rough: 'Estructura',\n    cat_finishing: 'Acabados',\n    cat_mep: 'Instalaciones',\n    help_title: '*Guía*',\n    help_text: `*Guía de uso*\n\n*1.* Envíe fotos, PDF o descripción\nPDF: máx. 3 páginas\n*2.* Revise los trabajos\n*3.* Calcule precios\n*4.* Exporte\n\n/start — Nuevo\n/help — Ayuda`,\n    doc_title: 'PRESUPUESTO',\n    col_pos: 'Nº', col_code: 'Código', col_desc: 'Descripción', col_unit: 'Ud', col_qty: 'Cant', col_price: 'P.U.', col_total: 'Total', col_labor: 'Hrs', col_quality: 'Q',\n    grand_total: 'TOTAL', labor_cost: 'M.O.', material_cost: 'Materiales', labor_days: 'Días',\n    kpi_total: 'Coste Total', kpi_hours: 'Horas', kpi_days: 'Días',\n    chart_cost_structure: 'Estructura', chart_labor: 'M.O.', chart_material: 'Mat', chart_machines: 'Eq',\n    res_labor: 'MO', res_material: 'Mat', res_machine: 'Eq',\n    collapse_all: 'Contraer', expand_all: 'Expandir',\n    quality_high: 'Alta', quality_medium: 'Media', quality_low: 'Baja',\n    export_excel_msg: 'Exportar Excel', export_pdf_msg: 'Exportar PDF', btn_refine: 'Refinar', found_pct: 'encontrado', more_resources: 'más', kpi_items: 'Artículos', scope_title: 'Alcance', show_scope: 'Ver alcance'\n  },\n\n  'FR': { \n    fallback_start: 'Appuyez sur /start pour commencer', \n    rooms: 'pièces', works_identified: 'travaux', general: 'Général', no_works: 'Aucun travail', items: 'éléments', min: 'мин', \n    db: 'FR_PARIS_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR', \n    name: 'French', flag: '🇫🇷', native: 'Français', cur: 'EUR', sym: '€', loc: 'fr-FR', region: 'Paris', search_lang: 'French',\n    ok: '✅ *Français* · Paris · EUR',\n    text_prompt: `*Décrivez les travaux:*\\n\\n_Exemple:_\\n\\`\\`\\`\\nPlaco 2 couches 25m2\\nCarrelage sdb 15m2\\nPeinture murs 120m2\\n\\`\\`\\``,\n    photo: `*Envoyez photo, PDF ou description*\n\n📄 *Plans PDF* — plan d'étage ou dessin (max 3 pages)\n📷 *Photo* — photographiez l'espace\n✏️ *Texte* — décrivez les travaux en liste\n\n_Exemple à copier:_\n\\`\\`\\`\nPlaco 2 couches ossature métallique 25m2\nCarrelage grès cérame 60x60 sdb 15m2\nEnduit plafonds murs 120m2\nPrises encastrées 20pcs\n\\`\\`\\`\n\nOu envoyez photo/PDF 📷📄`,\n    pdf_received: '📄 *PDF reçu*',\n    pdf_processing: '⏳ Analyse du plan...',\n    pdf_pages: 'pages',\n    pdf_page_limit: '⚠️ Seules les 3 premières pages sont traitées',\n    pdf_rooms_found: '🏠 Pièces trouvées',\n    pdf_elements_found: '🧱 Éléments trouvés',\n    pdf_works_generated: '📝 Travaux générés',\n    pdf_analyzing_page: '🔍 Analyse de la page',\n    pdf_of: 'sur',\n    pdf_complete: '✅ Analyse terminée',\n    pdf_error: '❌ Erreur de traitement PDF',\n    photo_added: '✅ Photo ajoutée',\n    photos_count: 'photos',\n    add_more: '+ Autres photos',\n    analyze_now: '▶ Analyser',\n    analyzing: 'Analyse en cours...',\n    found: '*Ouvrages identifiés:*',\n    edit_hint: 'Appuyez pour modifier',\n    calc: 'Recherche des prix',\n    ready: '*DEVIS*',\n    total: 'TOTAL',\n    days: 'jours',\n    pct: 'Précision',\n    workers: 'M.O.',\n    machines: 'Matériel',\n    materials: 'Matériaux',\n    subtotal: 'Résumé',\n    searching: 'Recherche',\n    of: 'sur',\n    not_found: 'Non trouvé',\n    low_conf: 'À vérifier',\n    price_note: 'Base: Paris 2025',\n    btn_calc: '▶ Calculer',\n    btn_new: '+ Nouveau',\n    btn_lang: '⚙ Langue',\n    btn_edit: '✎ Modifier',\n    btn_delete: '✕ Supprimer',\n    btn_add_work: '+ Ouvrage',\n    btn_done: '✅ Terminé',\n    btn_export_excel: '↓ Excel',\n    btn_export_pdf: '↓ PDF',\n    btn_restart: '↻ Recommencer',\n    btn_help: '? Aide',\n    loading: 'Calcul en cours...',\n    more_in_html: 'Rapport détaillé disponible',\n    resources: 'Détails: ressources et description',\n    enter_work: '*Ajouter ouvrage*\\nFormat: Description, Quantité Unité',\n    work_added: '✅ Ajouté',\n    what_next: '*Options:*',\n    categories: 'Catégories',\n    cat_demolition: 'Démolition',\n    cat_rough: 'Gros œuvre',\n    cat_finishing: 'Finitions',\n    cat_mep: 'CVC',\n    help_title: '*Guide*',\n    help_text: `*Guide d'utilisation*\n\n*1.* Envoyez photos, PDF ou description\nPDF: max 3 pages\n*2.* Vérifiez les ouvrages\n*3.* Calculez les prix\n*4.* Exportez\n\n/start — Nouveau\n/help — Aide`,\n    doc_title: 'DEVIS',\n    col_pos: 'N°', col_code: 'Code', col_desc: 'Désignation', col_unit: 'U', col_qty: 'Qté', col_price: 'P.U.', col_total: 'Total', col_labor: 'Hrs', col_quality: 'Q',\n    grand_total: 'TOTAL', labor_cost: 'M.O.', material_cost: 'Matériaux', labor_days: 'Jours',\n    kpi_total: 'Coût Total', kpi_hours: 'Heures', kpi_days: 'Jours',\n    chart_cost_structure: 'Structure', chart_labor: 'M.O.', chart_material: 'Mat', chart_machines: 'Éq',\n    res_labor: 'MO', res_material: 'Mat', res_machine: 'Éq',\n    collapse_all: 'Réduire', expand_all: 'Développer',\n    quality_high: 'Haute', quality_medium: 'Moyenne', quality_low: 'Faible',\n    export_excel_msg: 'Export Excel', export_pdf_msg: 'Export PDF', btn_refine: 'Affiner', found_pct: 'trouvé', more_resources: 'plus', kpi_items: 'Articles', scope_title: 'Description', show_scope: 'Voir description'\n  },\n\n  'PT': { \n    fallback_start: 'Pressione /start para começar', \n    rooms: 'quartos', works_identified: 'trabalhos', general: 'Geral', no_works: 'Sem trabalhos', items: 'itens', min: 'мин', \n    db: 'PT_SAOPAULO_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR', \n    name: 'Portuguese', flag: '🇧🇷', native: 'Português', cur: 'BRL', sym: 'R$', loc: 'pt-BR', region: 'São Paulo', search_lang: 'Portuguese',\n    ok: '✅ *Português* · São Paulo · BRL',\n    text_prompt: `*Descreva os trabalhos:*\\n\\n_Exemplo:_\\n\\`\\`\\`\\nDrywall 2 camadas 25m2\\nPorcelanato banheiro 15m2\\nPintura paredes 120m2\\n\\`\\`\\``,\n    photo: `*Envie foto, PDF ou descrição*\n\n📄 *Plantas PDF* — planta baixa ou desenho (máx. 3 págs.)\n📷 *Foto* — fotografe o ambiente\n✏️ *Texto* — descreva os trabalhos em lista\n\n_Exemplo para copiar:_\n\\`\\`\\`\nDrywall 2 camadas perfil metálico 25m2\nPorcelanato 60x60 banheiro 15m2\nMassa corrida teto paredes 120m2\nTomadas embutidas 20un\n\\`\\`\\`\n\nOu envie foto/PDF 📷📄`,\n    pdf_received: '📄 *PDF recebido*',\n    pdf_processing: '⏳ Analisando planta...',\n    pdf_pages: 'páginas',\n    pdf_page_limit: '⚠️ Processando apenas as 3 primeiras páginas',\n    pdf_rooms_found: '🏠 Cômodos encontrados',\n    pdf_elements_found: '🧱 Elementos encontrados',\n    pdf_works_generated: '📝 Trabalhos gerados',\n    pdf_analyzing_page: '🔍 Analisando página',\n    pdf_of: 'de',\n    pdf_complete: '✅ Análise concluída',\n    pdf_error: '❌ Erro ao processar PDF',\n    photo_added: '✅ Foto adicionada',\n    photos_count: 'fotos',\n    add_more: '+ Mais fotos',\n    analyze_now: '▶ Analisar',\n    analyzing: 'Analisando...',\n    found: '*Serviços identificados:*',\n    edit_hint: 'Toque para editar',\n    calc: 'Busca de preços',\n    ready: '*ORÇAMENTO*',\n    total: 'TOTAL',\n    days: 'dias',\n    pct: 'Precisão',\n    workers: 'M.O.',\n    machines: 'Equipamentos',\n    materials: 'Materiais',\n    subtotal: 'Resumo',\n    searching: 'Buscando',\n    of: 'de',\n    not_found: 'Não encontrado',\n    low_conf: 'Verificar',\n    price_note: 'Base: São Paulo 2025',\n    btn_calc: '▶ Calcular',\n    btn_new: '+ Novo',\n    btn_lang: '⚙ Idioma',\n    btn_edit: '✎ Editar',\n    btn_delete: '✕ Excluir',\n    btn_add_work: '+ Serviço',\n    btn_done: '✅ Pronto',\n    btn_export_excel: '↓ Excel',\n    btn_export_pdf: '↓ PDF',\n    btn_restart: '↻ Recomeçar',\n    btn_help: '? Ajuda',\n    loading: 'Calculando...',\n    more_in_html: 'Relatório detalhado disponível',\n    resources: 'Detalhes: recursos e escopo',\n    enter_work: '*Adicionar serviço*\\nFormato: Descrição, Quantidade Unidade',\n    work_added: '✅ Adicionado',\n    what_next: '*Opções:*',\n    categories: 'Categorias',\n    cat_demolition: 'Demolição',\n    cat_rough: 'Estrutura',\n    cat_finishing: 'Acabamento',\n    cat_mep: 'Instalações',\n    help_title: '*Guia*',\n    help_text: `*Guia de uso*\n\n*1.* Envie fotos, PDF ou descrição\nPDF: máx. 3 páginas\n*2.* Revise os serviços\n*3.* Calcule preços\n*4.* Exporte\n\n/start — Novo\n/help — Ajuda`,\n    doc_title: 'ORÇAMENTO',\n    col_pos: 'Nº', col_code: 'Código', col_desc: 'Descrição', col_unit: 'Un', col_qty: 'Qtd', col_price: 'P.U.', col_total: 'Total', col_labor: 'Hrs', col_quality: 'Q',\n    grand_total: 'TOTAL', labor_cost: 'M.O.', material_cost: 'Materiais', labor_days: 'Dias',\n    kpi_total: 'Custo Total', kpi_hours: 'Horas', kpi_days: 'Dias',\n    chart_cost_structure: 'Estrutura', chart_labor: 'M.O.', chart_material: 'Mat', chart_machines: 'Eq',\n    res_labor: 'MO', res_material: 'Mat', res_machine: 'Eq',\n    collapse_all: 'Recolher', expand_all: 'Expandir',\n    quality_high: 'Alta', quality_medium: 'Média', quality_low: 'Baixa',\n    export_excel_msg: 'Exportar Excel', export_pdf_msg: 'Exportar PDF', btn_refine: 'Refinar', found_pct: 'encontrado', more_resources: 'mais', kpi_items: 'Itens', scope_title: 'Escopo', show_scope: 'Ver escopo'\n  },\n\n  'ZH': { \n    db: 'ZH_SHANGHAI_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR', \n    name: 'Chinese', flag: '🇨🇳', native: '中文', cur: 'CNY', sym: '¥', loc: 'zh-CN', region: '上海', search_lang: 'Chinese',\n    ok: '✅ *中文* · 上海 · CNY',\n    text_prompt: `*描述工作:*\\n\\n_示例:_\\n\\`\\`\\`\\n石膏板双层 25m2\\n瓷砖卫生间 15m2\\n墙面涂料 120m2\\n\\`\\`\\``,\n    photo: `*发送照片、PDF或描述*\n\n📄 *PDF图纸* — 平面图或图纸（最多3页）\n📷 *照片* — 拍摄房间或物体\n✏️ *文字* — 以列表形式描述\n\n_复制示例：_\n\\`\\`\\`\n石膏板双层轻钢龙骨 25m2\n瓷砖600x600卫生间 15m2\n腻子找平顶墙 120m2\n暗装插座 20个\n\\`\\`\\`\n\n或发送照片/PDF 📷📄`,\n    pdf_received: '📄 *已收到PDF*',\n    pdf_processing: '⏳ 正在分析图纸...',\n    pdf_pages: '页',\n    pdf_page_limit: '⚠️ 仅处理前3页',\n    pdf_rooms_found: '🏠 发现房间',\n    pdf_elements_found: '🧱 发现元素',\n    pdf_works_generated: '📝 已生成工作项',\n    pdf_analyzing_page: '🔍 正在分析第',\n    pdf_of: '页，共',\n    pdf_complete: '✅ 分析完成',\n    pdf_error: '❌ PDF处理错误',\n    photo_added: '✅ 照片已添加',\n    photos_count: '张',\n    add_more: '+ 更多照片',\n    analyze_now: '▶ 开始分析',\n    analyzing: '分析中...',\n    found: '*识别的工作项:*',\n    edit_hint: '点击编辑',\n    calc: '价格查询',\n    ready: '*工程预算*',\n    total: '合计',\n    days: '天',\n    pct: '匹配率',\n    workers: '人工',\n    machines: '机械',\n    materials: '材料',\n    subtotal: '摘要',\n    searching: '搜索',\n    of: '/',\n    not_found: '未找到',\n    low_conf: '需核查',\n    price_note: '价格基准：上海 2025',\n    btn_calc: '▶ 计算',\n    btn_new: '+ 新项目',\n    btn_lang: '⚙ 语言',\n    btn_edit: '✎ 编辑',\n    btn_delete: '✕ 删除',\n    btn_add_work: '+ 添加',\n    btn_done: '✅ 完成',\n    btn_export_excel: '↓ Excel',\n    btn_export_pdf: '↓ PDF',\n    btn_restart: '↻ 重新开始',\n    btn_help: '? 帮助',\n    loading: '计算中...',\n    more_in_html: '详细报告可用',\n    resources: '详情：资源和工作范围',\n    enter_work: '*添加项目*\\n格式：描述，数量 单位',\n    work_added: '✅ 已添加',\n    what_next: '*选项:*',\n    categories: '类别',\n    cat_demolition: '拆除',\n    cat_rough: '结构',\n    cat_finishing: '装饰',\n    cat_mep: '机电',\n    help_title: '*指南*',\n    help_text: `*使用指南*\n\n*1.* 发送照片、PDF或描述\nPDF：最多3页\n*2.* 检查工作项\n*3.* 计算价格\n*4.* 导出\n\n/start — 新项目\n/help — 帮助`,\n    doc_title: '预算',\n    col_pos: '序号', col_code: '编码', col_desc: '名称', col_unit: '单位', col_qty: '数量', col_price: '单价', col_total: '合计', col_labor: '工时', col_quality: '质',\n    grand_total: '总计', labor_cost: '人工费', material_cost: '材料费', labor_days: '工期',\n    kpi_total: '总成本', kpi_hours: '工时', kpi_days: '工期',\n    chart_cost_structure: '成本结构', chart_labor: '人工', chart_material: '材料', chart_machines: '机械',\n    res_labor: '人工', res_material: '材料', res_machine: '机械',\n    collapse_all: '折叠', expand_all: '展开',\n    quality_high: '高', quality_medium: '中', quality_low: '低',\n    export_excel_msg: '导出Excel', export_pdf_msg: '导出PDF', btn_refine: '精确分析', found_pct: '找到', more_resources: '更多', kpi_items: '项目', scope_title: '工作范围', show_scope: '查看范围'\n  },\n\n  'AR': { \n    db: 'AR_DUBAI_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR', \n    name: 'Arabic', flag: '🇦🇪', native: 'العربية', cur: 'AED', sym: 'د.إ', loc: 'ar-AE', region: 'دبي', search_lang: 'Arabic',\n    ok: '✅ *العربية* · دبي · AED',\n    text_prompt: `*صف الأعمال:*\\n\\n_مثال:_\\n\\`\\`\\`\\nجبس بورد طبقتين 25م2\\nبلاط حمام 15م2\\nدهان جدران 120م2\\n\\`\\`\\``,\n    photo: `*أرسل صورة أو PDF أو وصف*\n\n📄 *مخططات PDF* — مخطط الطابق أو الرسم (حتى 3 صفحات)\n📷 *صورة* — التقط صورة للمكان\n✏️ *نص* — صف الأعمال كقائمة\n\n_مثال للنسخ:_\n\\`\\`\\`\nجبس بورد طبقتين هيكل معدني 25م2\nبلاط سيراميك 60x60 حمام 15م2\nمعجون أسقف جدران 120م2\nمقابس مخفية 20قطعة\n\\`\\`\\`\n\nأو أرسل صورة/PDF 📷📄`,\n    pdf_received: '📄 *تم استلام PDF*',\n    pdf_processing: '⏳ جاري تحليل المخطط...',\n    pdf_pages: 'صفحات',\n    pdf_page_limit: '⚠️ معالجة أول 3 صفحات فقط',\n    pdf_rooms_found: '🏠 الغرف الموجودة',\n    pdf_elements_found: '🧱 العناصر الموجودة',\n    pdf_works_generated: '📝 الأعمال المُنشأة',\n    pdf_analyzing_page: '🔍 تحليل الصفحة',\n    pdf_of: 'من',\n    pdf_complete: '✅ اكتمل التحليل',\n    pdf_error: '❌ خطأ في معالجة PDF',\n    photo_added: '✅ تمت الإضافة',\n    photos_count: 'صور',\n    add_more: '+ المزيد',\n    analyze_now: '▶ تحليل',\n    analyzing: 'جاري التحليل...',\n    found: '*الأعمال المحددة:*',\n    edit_hint: 'اضغط للتعديل',\n    calc: 'البحث عن الأسعار',\n    ready: '*التقدير*',\n    total: 'المجموع',\n    days: 'يوم',\n    pct: 'الدقة',\n    workers: 'عمالة',\n    machines: 'معدات',\n    materials: 'مواد',\n    subtotal: 'ملخص',\n    searching: 'بحث',\n    of: 'من',\n    not_found: 'غير موجود',\n    low_conf: 'للمراجعة',\n    price_note: 'الأساس: دبي 2025',\n    btn_calc: '▶ حساب',\n    btn_new: '+ جديد',\n    btn_lang: '⚙ لغة',\n    btn_edit: '✎ تعديل',\n    btn_delete: '✕ حذف',\n    btn_add_work: '+ إضافة',\n    btn_done: '✅ تم',\n    btn_export_excel: '↓ Excel',\n    btn_export_pdf: '↓ PDF',\n    btn_restart: '↻ البدء من جديد',\n    btn_help: '? مساعدة',\n    loading: 'جاري الحساب...',\n    more_in_html: 'تقرير مفصل متاح',\n    resources: 'التفاصيل: الموارد ونطاق العمل',\n    enter_work: '*إضافة بند*\\nالصيغة: الوصف، الكمية الوحدة',\n    work_added: '✅ تمت الإضافة',\n    what_next: '*الخيارات:*',\n    categories: 'الفئات',\n    cat_demolition: 'هدم',\n    cat_rough: 'هيكل',\n    cat_finishing: 'تشطيب',\n    cat_mep: 'ميكانيك',\n    help_title: '*دليل*',\n    help_text: `*دليل الاستخدام*\n\n*1.* أرسل صور أو PDF أو وصف\nPDF: حتى 3 صفحات\n*2.* راجع الأعمال\n*3.* احسب الأسعار\n*4.* صدّر\n\n/start — جديد\n/help — مساعدة`,\n    doc_title: 'التقدير',\n    col_pos: 'رقم', col_code: 'الرمز', col_desc: 'الوصف', col_unit: 'وحدة', col_qty: 'كمية', col_price: 'سعر', col_total: 'المجموع', col_labor: 'ساعات', col_quality: 'ج',\n    grand_total: 'الإجمالي', labor_cost: 'عمالة', material_cost: 'مواد', labor_days: 'أيام',\n    kpi_total: 'التكلفة', kpi_hours: 'ساعات', kpi_days: 'أيام',\n    chart_cost_structure: 'الهيكل', chart_labor: 'عمالة', chart_material: 'مواد', chart_machines: 'معدات',\n    res_labor: 'عمالة', res_material: 'مواد', res_machine: 'معدات',\n    collapse_all: 'طي', expand_all: 'توسيع',\n    quality_high: 'عالية', quality_medium: 'متوسطة', quality_low: 'منخفضة',\n    export_excel_msg: 'تصدير Excel', export_pdf_msg: 'تصدير PDF', btn_refine: 'تحليل أدق', found_pct: 'وجد', more_resources: 'المزيد', kpi_items: 'بنود', scope_title: 'نطاق العمل', show_scope: 'عرض النطاق'\n  },\n\n  'HI': { \n    db: 'HI_MUMBAI_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR', \n    name: 'Hindi', flag: '🇮🇳', native: 'हिन्दी', cur: 'INR', sym: '₹', loc: 'hi-IN', region: 'मुंबई', search_lang: 'Hindi',\n    ok: '✅ *हिन्दी* · मुंबई · INR',\n    text_prompt: `*कार्यों का वर्णन करें:*\\n\\n_उदाहरण:_\\n\\`\\`\\`\\nड्राईवॉल 2 लेयर 25m2\\nटाइल्स बाथरूम 15m2\\nपेंटिंग दीवारें 120m2\\n\\`\\`\\``,\n    photo: `*फ़ोटो, PDF या विवरण भेजें*\n\n📄 *PDF ड्राइंग* — फ्लोर प्लान या ब्लूप्रिंट (अधिकतम 3 पेज)\n📷 *फ़ोटो* — कमरे या वस्तु की फ़ोटो\n✏️ *टेक्स्ट* — कार्यों का विवरण सूची में\n\n_कॉपी उदाहरण:_\n\\`\\`\\`\nड्राईवॉल 2 लेयर मेटल फ्रेम 25m2\nटाइल्स 60x60 बाथरूम 15m2\nपुट्टी छत दीवारें 120m2\nसॉकेट 20 पीस\n\\`\\`\\`\n\nया फ़ोटो/PDF भेजें 📷📄`,\n    pdf_received: '📄 *PDF प्राप्त*',\n    pdf_processing: '⏳ ड्राइंग का विश्लेषण...',\n    pdf_pages: 'पेज',\n    pdf_page_limit: '⚠️ केवल पहले 3 पेज प्रोसेस होंगे',\n    pdf_rooms_found: '🏠 कमरे मिले',\n    pdf_elements_found: '🧱 एलिमेंट मिले',\n    pdf_works_generated: '📝 कार्य बनाए गए',\n    pdf_analyzing_page: '🔍 पेज का विश्लेषण',\n    pdf_of: 'में से',\n    pdf_complete: '✅ विश्लेषण पूर्ण',\n    pdf_error: '❌ PDF प्रोसेसिंग त्रुटि',\n    photo_added: '✅ फ़ोटो जोड़ी गई',\n    photos_count: 'फ़ोटो',\n    add_more: '+ और फ़ोटो',\n    analyze_now: '▶ विश्लेषण',\n    analyzing: 'विश्लेषण...',\n    found: '*पहचाने गए कार्य:*',\n    edit_hint: 'संपादन के लिए टैप करें',\n    calc: 'मूल्य खोज',\n    ready: '*अनुमान*',\n    total: 'कुल',\n    days: 'दिन',\n    pct: 'सटीकता',\n    workers: 'श्रम',\n    machines: 'उपकरण',\n    materials: 'सामग्री',\n    subtotal: 'सारांश',\n    searching: 'खोज',\n    of: 'में से',\n    not_found: 'नहीं मिला',\n    low_conf: 'जाँचें',\n    price_note: 'आधार: मुंबई 2025',\n    btn_calc: '▶ गणना',\n    btn_new: '+ नया',\n    btn_lang: '⚙ भाषा',\n    btn_edit: '✎ संपादित',\n    btn_delete: '✕ हटाएं',\n    btn_add_work: '+ जोड़ें',\n    btn_done: '✅ हो गया',\n    btn_export_excel: '↓ Excel',\n    btn_export_pdf: '↓ PDF',\n    btn_restart: '↻ फिर से',\n    btn_help: '? मदद',\n    loading: 'गणना...',\n    more_in_html: 'विस्तृत रिपोर्ट उपलब्ध',\n    resources: 'विवरण: संसाधन और कार्य क्षेत्र',\n    enter_work: '*आइटम जोड़ें*\\nप्रारूप: विवरण, मात्रा इकाई',\n    work_added: '✅ जोड़ा गया',\n    what_next: '*विकल्प:*',\n    categories: 'श्रेणियां',\n    cat_demolition: 'तोड़फोड़',\n    cat_rough: 'ढांचा',\n    cat_finishing: 'फ़िनिशिंग',\n    cat_mep: 'MEP',\n    help_title: '*गाइड*',\n    help_text: `*उपयोग गाइड*\n\n*1.* फ़ोटो, PDF या विवरण भेजें\nPDF: अधिकतम 3 पेज\n*2.* कार्य जाँचें\n*3.* मूल्य गणना\n*4.* निर्यात\n\n/start — नया\n/help — मदद`,\n    doc_title: 'अनुमान',\n    col_pos: 'क्रम', col_code: 'कोड', col_desc: 'विवरण', col_unit: 'इकाई', col_qty: 'मात्रा', col_price: 'दर', col_total: 'कुल', col_labor: 'घंटे', col_quality: 'गु',\n    grand_total: 'कुल योग', labor_cost: 'श्रम', material_cost: 'सामग्री', labor_days: 'दिन',\n    kpi_total: 'कुल लागत', kpi_hours: 'घंटे', kpi_days: 'दिन',\n    chart_cost_structure: 'संरचना', chart_labor: 'श्रम', chart_material: 'सामग्री', chart_machines: 'उपकरण',\n    res_labor: 'श्रम', res_material: 'सामग्री', res_machine: 'उपकरण',\n    collapse_all: 'संक्षिप्त', expand_all: 'विस्तृत',\n    quality_high: 'उच्च', quality_medium: 'मध्यम', quality_low: 'निम्न',\n    export_excel_msg: 'Excel निर्यात', export_pdf_msg: 'PDF निर्यात', btn_refine: 'सुधारें', found_pct: 'मिला', more_resources: 'और', kpi_items: 'आइटम', scope_title: 'कार्य क्षेत्र', show_scope: 'देखें'\n  }\n};\n\nconst L = LANGS[lang] || LANGS['EN'];\n\n// Update session with language data\nif (sd.sess && sd.sess[chatId]) { \n  sd.sess[chatId].db = L.db; \n  sd.sess[chatId].L = L; \n}\n\n// Get voice/PDF file IDs from session if available\nconst voiceFileId = sd.sess?.[chatId]?.voiceFileId || input.voiceFileId || null;\nconst pdfFileId = sd.sess?.[chatId]?.pdfFileId || input.pdfFileId || null;\nconst pdfFileName = sd.sess?.[chatId]?.pdfFileName || input.pdfFileName || null;\n\nreturn { \n  json: { \n    ...input, \n    L: L, \n    db: L.db, \n    voiceFileId,\n    pdfFileId,\n    pdfFileName,\n    // API Keys passthrough\n  } \n};"},"typeVersion":2},{"id":"e787b927-681d-4414-b6bd-35a77570c8b9","name":"Main","type":"n8n-nodes-base.code","position":[-5088,-1568],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// MAIN ROUTER - Text-Only Version\n// DDC CWICR - Data Driven Construction Cost Estimator\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst update = $('Telegram Trigger1').first().json;\nconst botToken = $input.first().json.bot_token;\nconst isCallback = !!update.callback_query;\n\nlet chatId, callbackData, callbackQueryId, text;\n\nif (isCallback) {\n  const cb = update.callback_query;\n  chatId = cb.message?.chat?.id;\n  callbackData = cb.data || '';\n  callbackQueryId = cb.id;\n  text = '';\n} else {\n  const msg = update.message || {};\n  chatId = msg.chat?.id;\n  callbackData = '';\n  callbackQueryId = '';\n  text = msg.text || '';\n}\n\nconst sd = $getWorkflowStaticData('global');\nif (!sd.sess) sd.sess = {};\nconst cid = String(chatId);\nif (!sd.sess[cid]) sd.sess[cid] = { \n  lang: null, works: [], state: 'new', db: null, L: null, description: ''\n};\nconst S = sd.sess[cid];\n\nlet action = 'none';\n\n// === COMMANDS ===\nif (text.toLowerCase() === '/start') {\n  S.lang = null; S.works = []; S.state = 'wait_lang'; S.db = null; S.L = null;\n  S.description = '';\n  action = 'show_lang';\n}\nelse if (text.toLowerCase() === '/help') {\n  action = 'show_help';\n}\n\n// === LANGUAGE SELECTION ===\nelse if (/^lang_(DE|EN|RU|ES|FR|PT|ZH|AR|HI)$/i.test(callbackData)) {\n  S.lang = callbackData.replace('lang_', '').toUpperCase();\n  S.state = 'wait_text';\n  action = 'lang_selected';\n}\n\n// === EDIT WORK ITEMS ===\nelse if (/^edit_work_(\\d+)$/.test(callbackData)) {\n  const idx = parseInt(callbackData.match(/edit_work_(\\d+)/)[1]);\n  S.editingWorkIndex = idx;\n  S.state = 'editing_work';\n  action = 'show_edit_menu';\n}\nelse if (/^delete_work_(\\d+)$/.test(callbackData)) {\n  const idx = parseInt(callbackData.match(/delete_work_(\\d+)/)[1]);\n  if (S.works[idx]) {\n    S.works.splice(idx, 1);\n    S.works.forEach((w, i) => { w.seq = i + 1; w.id = `W${String(i+1).padStart(3,'0')}`; });\n  }\n  action = 'works_updated';\n}\nelse if (/^qty_work_(\\d+)_(.+)$/.test(callbackData)) {\n  const match = callbackData.match(/qty_work_(\\d+)_(.+)/);\n  const idx = parseInt(match[1]);\n  const change = match[2];\n  if (S.works[idx]) {\n    let q = S.works[idx].qty;\n    if (change === 'plus10') q += 10;\n    else if (change === 'minus10') q = Math.max(1, q - 10);\n    else if (change === 'plus1') q += 1;\n    else if (change === 'minus1') q = Math.max(1, q - 1);\n    else if (change === 'double') q *= 2;\n    else if (change === 'half') q = Math.max(1, Math.round(q / 2));\n    S.works[idx].qty = q;\n  }\n  action = 'works_updated';\n}\nelse if (callbackData === 'add_work') {\n  S.state = 'adding_work';\n  action = 'ask_new_work';\n}\nelse if (callbackData === 'done_editing') {\n  S.state = 'works_shown';\n  action = 'works_updated';\n}\n\n// === CALCULATE ===\nelse if (callbackData === 'calculate') {\n  if (S.works && S.works.length > 0) {\n    S.state = 'calc';\n    action = 'start_calc';\n  } else {\n    action = 'lang_selected'; // show text prompt again\n  }\n}\n\n// === EXPORT ===\nelse if (callbackData === 'view_details') { action = 'view_details'; }\nelse if (callbackData === 'export_excel') { action = 'export_excel'; }\nelse if (callbackData === 'export_pdf') { action = 'export_pdf'; }\n\n// === RESTART ===\nelse if (callbackData === 'restart' || callbackData === 'new_estimate') {\n  S.works = []; S.description = ''; S.state = 'wait_text';\n  action = 'lang_selected';\n}\nelse if (callbackData === 'show_help' || callbackData === 'help') { action = 'show_help'; }\nelse if (callbackData === 'change_language' || callbackData === 'back_to_lang') {\n  S.lang = null; S.state = 'wait_lang';\n  action = 'show_lang';\n}\n\n// === TEXT INPUT ===\nelse if (text && S.state === 'adding_work') {\n  const parts = text.split(',');\n  const name = parts[0]?.trim() || text;\n  let qty = 1, unit = 'm²';\n  if (parts[1]) {\n    const qtyMatch = parts[1].match(/([\\d.,]+)\\s*(.*)/);\n    if (qtyMatch) {\n      qty = parseFloat(qtyMatch[1].replace(',', '.')) || 1;\n      unit = qtyMatch[2]?.trim() || 'm²';\n    }\n  }\n  S.works.push({\n    id: `W${String(S.works.length + 1).padStart(3,'0')}`,\n    name, query: name, qty, unit, conf: 'medium', seq: S.works.length + 1\n  });\n  S.state = 'works_shown';\n  action = 'works_updated';\n}\nelse if (text && S.lang && (S.state === 'wait_text' || S.state === 'new') && text.length > 3) {\n  S.description = text;\n  S.textInput = text;\n  action = 'analyze_text';\n}\n\n// === FALLBACKS ===\nelse if (!S.lang) { action = 'show_lang'; }\nelse if (!text && S.state === 'wait_text') { action = 'lang_selected'; }\n\nreturn { json: { \n  bot_token: botToken, chatId, action, lang: S.lang, works: S.works, \n  callbackData, callbackQueryId, isCallback, text,\n  description: S.description, editingWorkIndex: S.editingWorkIndex\n}};"},"typeVersion":2},{"id":"87655043-2e58-4c4f-8698-8e8029c3d5e4","name":"Block 6 - Calculation","type":"n8n-nodes-base.stickyNote","position":[-2944,-560],"parameters":{"color":3,"width":1936,"height":772,"content":"## 🔄 Block 6: Calculation Loop\n\n**Per Work Item:**\n```\nPrep → LLM Transform → Embed → \nQdrant Search → Rerank → Calculate\n```\n\n**Vector Search:**\n- OpenAI text-embedding-3-small\n- Qdrant similarity search\n- LLM reranking (best match)\n\n**Output per item:**\n- Matched rate code\n- Unit cost\n- Total cost\n- Resources breakdown\n- Scope of work"},"typeVersion":1},{"id":"214f72a6-a2c6-4508-9a6a-ffaa5deafc7b","name":"Block 7 - Reports","type":"n8n-nodes-base.stickyNote","position":[-2944,-1072],"parameters":{"color":6,"width":1928,"height":484,"content":"## 📊 Block 7: Reports\n\n**Final Processing:**\n```\nAggregate → Generate HTML → Send\n```\n**Report Contents:**\n- Work items with costs\n- Resources (labor/material/machine)\n- Scope of work\n- Quality indicators\n- Cost breakdown charts"},"typeVersion":1},{"id":"e7488701-92a6-4a11-9651-0c37965c4900","name":"Block 8 - Export","type":"n8n-nodes-base.stickyNote","position":[-4304,-1072],"parameters":{"color":6,"width":1320,"height":692,"content":"## 📥 Block 8: Export\n\n**Available Exports:**\n- 📊 **Excel (CSV)** — Spreadsheet\n- 📄 **PDF** — Document\n- 🌐 **HTML** — Interactive\n\n**View Details button:**\n- Full resource breakdown\n- Scope of work\n- Quality scores\n\n**Data includes:**\n- Rate codes\n- Unit prices\n- Labor hours\n- Material costs"},"typeVersion":1},{"id":"0accda2e-edfc-4609-9d5c-37ab7b6672a1","name":"Qdrant Info","type":"n8n-nodes-base.stickyNote","position":[-3360,-336],"parameters":{"width":380,"height":528,"content":"## 🔍 Qdrant Vector DB\n\n**Collections (9 languages):**\n- `DDC_CWICR_DE` — German\n- `DDC_CWICR_EN` — English\n- `DDC_CWICR_RU` — Russian\n- `DDC_CWICR_ES` — Spanish\n- `DDC_CWICR_FR` — French\n- `DDC_CWICR_PT` — Portuguese\n- `DDC_CWICR_ZH` — Chinese\n- `DDC_CWICR_AR` — Arabic\n- `DDC_CWICR_HI` — Hindi\n\n**Setup:**\n1. Install Qdrant\n2. Load collections from DDC repo\n3. Configure QDRANT_URL\n\n**Each collection:** ~55,000 vectors"},"typeVersion":1},{"id":"18bb796a-6bf0-44e9-be2b-72ebae258a29","name":"Prep Text LLM","type":"n8n-nodes-base.code","position":[-4272,-1696],"parameters":{"jsCode":"// ═══════════════════════════════════════════════════════════════════════════\n// DDC CWICR - Prep Text LLM Request (for AI Node)\n// Prepare prompt for text-to-works analysis\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst cfg = $('Config').first().json;\nconst tokenConfig = $('🔑 TOKEN').first().json;\nconst cid = String(cfg.chatId);\nconst sd = $getWorkflowStaticData('global');\nconst session = sd.sess?.[cid] || {};\nconst L = cfg.L || {};\n\nconst textInput = cfg.text || session.textInput || cfg.textInput || cfg.description || '';\nconst searchLang = L.search_lang || 'English';\n\nconsole.log('=== PREP TEXT LLM ===');\nconsole.log('Input:', textInput);\nconsole.log('Language:', searchLang);\n\nif (!textInput || textInput.length < 3) {\n  return { json: { ...cfg, chatId: cid, _skip_llm: true, works: [], description: 'No input' }};\n}\n\n// Language-specific examples\nconst LANG_EXAMPLES = {\n  'German': {\n    input: 'Renovierung Badezimmer 8qm',\n    output: '[{\"name\": \"Fliesendemontage Wand Boden\", \"qty\": 24, \"unit\": \"m²\"}, {\"name\": \"Wandfliesen Feinsteinzeug 30x60\", \"qty\": 16, \"unit\": \"m²\"}, {\"name\": \"Bodenfliesen rutschfest\", \"qty\": 8, \"unit\": \"m²\"}, {\"name\": \"WC wandhängend montieren\", \"qty\": 1, \"unit\": \"pcs\"}, {\"name\": \"Waschtisch montieren\", \"qty\": 1, \"unit\": \"pcs\"}]'\n  },\n  'English': {\n    input: 'Bathroom renovation 8sqm',\n    output: '[{\"name\": \"Tile removal walls floor\", \"qty\": 24, \"unit\": \"m²\"}, {\"name\": \"Wall tiles porcelain 30x60\", \"qty\": 16, \"unit\": \"m²\"}, {\"name\": \"Floor tiles anti-slip\", \"qty\": 8, \"unit\": \"m²\"}, {\"name\": \"Wall-hung toilet install\", \"qty\": 1, \"unit\": \"pcs\"}, {\"name\": \"Vanity basin install\", \"qty\": 1, \"unit\": \"pcs\"}]'\n  },\n  'Russian': {\n    input: 'ремонт кухни 20м2 стены и ламинат',\n    output: '[{\"name\": \"Демонтаж старых обоев\", \"qty\": 50, \"unit\": \"m²\"}, {\"name\": \"Штукатурка стен гипсовая\", \"qty\": 50, \"unit\": \"m²\"}, {\"name\": \"Шпаклёвка стен финишная\", \"qty\": 50, \"unit\": \"m²\"}, {\"name\": \"Окраска стен водоэмульсионная\", \"qty\": 50, \"unit\": \"m²\"}, {\"name\": \"Демонтаж напольного покрытия\", \"qty\": 20, \"unit\": \"m²\"}, {\"name\": \"Укладка ламината с подложкой\", \"qty\": 20, \"unit\": \"m²\"}, {\"name\": \"Монтаж плинтуса\", \"qty\": 18, \"unit\": \"m\"}]'\n  },\n  'Spanish': {\n    input: 'reforma cocina 20m2 paredes y suelo',\n    output: '[{\"name\": \"Retirada papel pintado\", \"qty\": 50, \"unit\": \"m²\"}, {\"name\": \"Enlucido paredes\", \"qty\": 50, \"unit\": \"m²\"}, {\"name\": \"Pintura paredes\", \"qty\": 50, \"unit\": \"m²\"}, {\"name\": \"Demolición suelo\", \"qty\": 20, \"unit\": \"m²\"}, {\"name\": \"Tarima flotante\", \"qty\": 20, \"unit\": \"m²\"}]'\n  },\n  'French': {\n    input: 'rénovation cuisine 20m2 murs et sol',\n    output: '[{\"name\": \"Dépose papier peint\", \"qty\": 50, \"unit\": \"m²\"}, {\"name\": \"Enduit murs\", \"qty\": 50, \"unit\": \"m²\"}, {\"name\": \"Peinture murs\", \"qty\": 50, \"unit\": \"m²\"}, {\"name\": \"Dépose revêtement sol\", \"qty\": 20, \"unit\": \"m²\"}, {\"name\": \"Pose parquet flottant\", \"qty\": 20, \"unit\": \"m²\"}]'\n  },\n  'Portuguese': {\n    input: 'reforma cozinha 20m2 paredes e piso',\n    output: '[{\"name\": \"Remoção papel parede\", \"qty\": 50, \"unit\": \"m²\"}, {\"name\": \"Reboco paredes\", \"qty\": 50, \"unit\": \"m²\"}, {\"name\": \"Pintura paredes\", \"qty\": 50, \"unit\": \"m²\"}, {\"name\": \"Remoção piso\", \"qty\": 20, \"unit\": \"m²\"}, {\"name\": \"Piso laminado\", \"qty\": 20, \"unit\": \"m²\"}]'\n  }\n};\n\nconst langExample = LANG_EXAMPLES[searchLang] || LANG_EXAMPLES['English'];\n\nconst parsePrompt = `You are a construction cost estimator. Extract ALL construction works from user's text.\n\nRULES:\n1. Output ONLY valid JSON array - NO explanations, NO markdown code blocks\n2. Generate works in ${searchLang} language  \n3. Be COMPREHENSIVE - include ALL works needed for described scope\n4. Use REALISTIC quantities based on area/room size mentioned\n5. Work names must be SPECIFIC for database search (not generic like \"wall work\")\n\nUNITS: m² (area), m (linear), pcs (items), kg, l\n\nEXAMPLE:\nInput: \"${langExample.input}\"\nOutput: ${langExample.output}\n\nUSER INPUT: \"${textInput}\"\n\nJSON ARRAY OUTPUT:`;\n\nconsole.log('Prompt prepared, length:', parsePrompt.length);\n\nreturn { json: { \n  ...cfg, \n  chatId: cid, \n  textInput,\n  description: textInput.substring(0, 50) + (textInput.length > 50 ? '...' : ''),\n  _parse_prompt: parsePrompt,\n  L\n}};"},"typeVersion":2},{"id":"5428212a-6693-4480-933a-4d4ec9edd0ea","name":"📤 Edit Menu","type":"n8n-nodes-base.httpRequest","position":[-4128,-1520],"parameters":{"url":"=https://api.telegram.org/bot{{ $('🔑 TOKEN').first().json.bot_token }}/sendMessage","method":"POST","options":{},"jsonBody":"={\n  \"chat_id\": {{ $json.chatId }},\n  \"text\": {{ JSON.stringify($json.msg) }},\n  \"parse_mode\": \"Markdown\",\n  \"reply_markup\": { \"inline_keyboard\": {{ JSON.stringify($json.keyboard) }} }\n}","sendBody":true,"specifyBody":"json"},"typeVersion":4.2},{"id":"96e1a4d4-e8f8-42b6-bf14-f21159e676dc","name":"Telegram Trigger1","type":"n8n-nodes-base.telegramTrigger","position":[-5536,-1568],"webhookId":"da06f3e4-5c38-4698-90c5-ed8bcc977443","parameters":{"updates":["callback_query","message"],"additionalFields":{}},"credentials":{"telegramApi":{"id":"","name":""}},"typeVersion":1.2},{"id":"18d9abc4-db2b-4089-864a-d456405ffe09","name":"Prep Lang OK","type":"n8n-nodes-base.code","position":[-4112,-2288],"parameters":{"jsCode":"// Prepare Lang OK message body\nconst cfg = $('Config').item.json;\nconst L = cfg.L || {};\n\n// Two options message\nconst examples = {\n  DE: `✅ *Deutsch* · Berlin · EUR\n\n*Beschreiben Sie Ihr Projekt:*\n\n*Option 1 — Kurzbeschreibung:*\n\\`Renovierung Küche 15m², mittlere Qualität\\`\n\\`Badezimmer komplett neu 8m²\\`\n\n*Option 2 — Detaillierte Liste:*\n\\`\\`\\`\nGipskarton 2-lagig 25m2\nFliesen 60x60 Bad 15m2\nSpachteln Decken Wände 120m2\nSteckdosen 20 Stück\n\\`\\`\\``,\n\n  EN: `✅ *English* · Toronto · CAD\n\n*Describe your project:*\n\n*Option 1 — Brief description:*\n\\`Kitchen renovation 15m², medium quality\\`\n\\`Complete bathroom remodel 8m²\\`\n\n*Option 2 — Detailed list:*\n\\`\\`\\`\nDrywall 2-layer 25m2\nTiles 60x60 bathroom 15m2\nPlastering ceiling walls 120m2\nElectrical outlets 20 pcs\n\\`\\`\\``,\n\n  RU: `✅ *Русский* · СПб · RUB\n\n*Опишите ваш проект:*\n\n*Вариант 1 — Краткое описание:*\n\\`Ремонт кухни 15м², средний класс\\`\n\\`Ванная комната под ключ 8м²\\`\n\n*Вариант 2 — Детальный список:*\n\\`\\`\\`\nГипсокартон 2 слоя 25м2\nПлитка 60x60 ванная 15м2\nШпаклевка потолки стены 120м2\nРозетки 20шт\n\\`\\`\\``,\n\n  ES: `✅ *Español* · Barcelona · EUR\n\n*Describa su proyecto:*\n\n*Opción 1 — Descripción breve:*\n\\`Reforma cocina 15m², calidad media\\`\n\\`Baño completo 8m²\\`\n\n*Opción 2 — Lista detallada:*\n\\`\\`\\`\nPladur 2 capas 25m2\nAzulejos 60x60 baño 15m2\nAlisado techos paredes 120m2\nEnchufes 20uds\n\\`\\`\\``,\n\n  FR: `✅ *Français* · Paris · EUR\n\n*Décrivez votre projet:*\n\n*Option 1 — Description courte:*\n\\`Rénovation cuisine 15m², qualité moyenne\\`\n\\`Salle de bain complète 8m²\\`\n\n*Option 2 — Liste détaillée:*\n\\`\\`\\`\nPlaco 2 couches 25m2\nCarrelage 60x60 sdb 15m2\nEnduit plafonds murs 120m2\nPrises 20pcs\n\\`\\`\\``,\n\n  PT: `✅ *Português* · São Paulo · BRL\n\n*Descreva seu projeto:*\n\n*Opção 1 — Descrição breve:*\n\\`Reforma cozinha 15m², qualidade média\\`\n\\`Banheiro completo 8m²\\`\n\n*Opção 2 — Lista detalhada:*\n\\`\\`\\`\nDrywall 2 camadas 25m2\nPorcelanato 60x60 banheiro 15m2\nMassa corrida teto paredes 120m2\nTomadas 20un\n\\`\\`\\``,\n\n  ZH: `✅ *中文* · 上海 · CNY\n\n*描述您的项目:*\n\n*选项1 — 简要描述:*\n\\`厨房翻新 15m²，中等品质\\`\n\\`卫生间全套 8m²\\`\n\n*选项2 — 详细清单:*\n\\`\\`\\`\n石膏板双层 25m2\n瓷砖60x60卫生间 15m2\n腻子顶墙 120m2\n插座 20个\n\\`\\`\\``,\n\n  AR: `✅ *العربية* · دبي · AED\n\n*صف مشروعك:*\n\n*الخيار 1 — وصف مختصر:*\n\\`تجديد مطبخ 15م²، جودة متوسطة\\`\n\\`حمام كامل 8م²\\`\n\n*الخيار 2 — قائمة مفصلة:*\n\\`\\`\\`\nجبس بورد طبقتين 25م2\nبلاط 60x60 حمام 15م2\nمعجون أسقف جدران 120م2\nمقابس 20 قطعة\n\\`\\`\\``,\n\n  HI: `✅ *हिन्दी* · मुंबई · INR\n\n*अपना प्रोजेक्ट बताएं:*\n\n*विकल्प 1 — संक्षिप्त विवरण:*\n\\`किचन रेनोवेशन 15m², मध्यम क्वालिटी\\`\n\\`बाथरूम पूर्ण 8m²\\`\n\n*विकल्प 2 — विस्तृत सूची:*\n\\`\\`\\`\nड्राईवॉल 2 लेयर 25m2\nटाइल्स 60x60 बाथरूम 15m2\nपुट्टी छत दीवारें 120m2\nसॉकेट 20 पीस\n\\`\\`\\``\n};\n\nconst lang = cfg.lang || 'EN';\nconst text = examples[lang] || examples['EN'];\n\nreturn {\n  json: {\n    chatId: cfg.chatId,\n    _body: {\n      chat_id: cfg.chatId,\n      text: text,\n      parse_mode: \"Markdown\",\n      reply_markup: {\n        inline_keyboard: [\n          [\n            {text: \"❓ Help\", callback_data: \"show_help\"}, \n            {text: \"🌐 Language\", callback_data: \"change_language\"}\n          ]\n        ]\n      }\n    }\n  }\n};"},"typeVersion":2},{"id":"cf603c92-9a4b-4f48-91f9-07b4f3cbdb29","name":"🔧 Config Parse","type":"n8n-nodes-base.set","position":[-4128,-1696],"parameters":{"options":{},"assignments":{"assignments":[{"id":"519bfbb9-8f49-49f9-ba53-8a234a1e6e35","name":"chatInput","type":"string","value":"={{ $json._parse_prompt }}"}]}},"typeVersion":3.4},{"id":"e3771ed1-2c9d-4068-ba17-a158f810191c","name":"🤖 AI Parse Text","type":"@n8n/n8n-nodes-langchain.chainLlm","position":[-3968,-1696],"parameters":{"messages":{"messageValues":[{"message":"={{ $('🔧 Config Parse').item.json.chatInput }}"}]}},"typeVersion":1.4},{"id":"3814f181-8e64-457c-a00f-e85a6c50de9a","name":"OpenAI Model 1","type":"@n8n/n8n-nodes-langchain.lmChatOpenAi","position":[-3920,-240],"parameters":{"model":{"__rl":true,"mode":"list","value":"chatgpt-4o-latest","cachedResultName":"chatgpt-4o-latest"},"options":{"temperature":0.15}},"credentials":{"openAiApi":{"id":"","name":""}},"typeVersion":1.2},{"id":"8f46f5e0-89f6-4e08-9dea-77fb73e35aef","name":"🔧 Config Transform","type":"n8n-nodes-base.set","position":[-1744,-400],"parameters":{"options":{},"assignments":{"assignments":[{"id":"83df83a1-7099-4f1d-a41c-11bdacf6587c","name":"chatInput","type":"string","value":"={{ $json._transform_prompt }}"}]}},"typeVersion":3.4},{"id":"c5024917-6a3e-4ddf-bce0-76b0bae61685","name":"🤖 AI Transform","type":"@n8n/n8n-nodes-langchain.chainLlm","position":[-1568,-400],"parameters":{"messages":{"messageValues":[{"message":"={{ $('🔧 Config Transform').item.json.chatInput }}"}]}},"typeVersion":1.4},{"id":"5b0a861f-bc95-4892-9c01-50e089893efb","name":"OpenAI Model 2","type":"@n8n/n8n-nodes-langchain.lmChatOpenAi","position":[-3600,-32],"parameters":{"model":{"__rl":true,"mode":"list","value":"gpt-4o-mini","cachedResultName":"gpt-4o-mini"},"options":{"temperature":0.3}},"credentials":{"openAiApi":{"id":"","name":""}},"typeVersion":1.2},{"id":"e059086c-d718-45c2-abb1-365997bf0cbf","name":"🔧 Config Rerank","type":"n8n-nodes-base.set","position":[-1744,-208],"parameters":{"options":{},"assignments":{"assignments":[{"id":"39cc26f5-b4d3-4c66-94b2-b9f2f12267e4","name":"system_prompt","type":"string","value":"You are a construction cost database expert. Respond ONLY with valid JSON, no markdown."},{"id":"09774508-e3cf-41ea-844a-a65c00e12cba","name":"chatInput","type":"string","value":"={{ $json._rerank_prompt }}"}]}},"typeVersion":3.4},{"id":"3a82f4fc-6f8c-4c82-acf4-f763d6e53e76","name":"🤖 AI Rerank","type":"@n8n/n8n-nodes-langchain.chainLlm","position":[-1568,-208],"parameters":{"messages":{"messageValues":[{"message":"={{ $('🔧 Config Rerank').item.json.chatInput }}"}]}},"typeVersion":1.4},{"id":"06f2702e-b068-4a92-afa7-67d935b881f1","name":"📝 Parse Text Response","type":"n8n-nodes-base.code","position":[-3664,-1696],"parameters":{"jsCode":"// Parse AI response for text works\nconst aiResponse = $input.first().json;\nconst prepData = $('Prep Text LLM').first().json;\nconst cid = String(prepData.chatId);\nconst sd = $getWorkflowStaticData('global');\nconst L = prepData.L || {};\n\nconsole.log('=== PARSE TEXT RESPONSE ===');\n\nlet text = '';\nif (aiResponse.text) {\n  text = aiResponse.text;\n} else if (aiResponse.response?.text) {\n  text = aiResponse.response.text;\n} else if (aiResponse.output) {\n  text = aiResponse.output;\n}\n\nconsole.log('Raw response:', text?.substring(0, 200));\n\n// Clean response - remove markdown code blocks\ntext = text.replace(/```json\\n?/gi, '').replace(/```\\n?/g, '').trim();\n\n// Extract JSON array\nlet works = [];\ntry {\n  const match = text.match(/\\[.*\\]/s);\n  if (match) {\n    works = JSON.parse(match[0]);\n  } else {\n    works = JSON.parse(text);\n  }\n} catch (e) {\n  console.log('Parse error:', e.message);\n  // Try line by line\n  const lines = text.split('\\n').filter(l => l.trim().startsWith('{'));\n  if (lines.length > 0) {\n    try {\n      works = JSON.parse('[' + lines.join(',') + ']');\n    } catch (e2) {\n      console.log('Fallback parse failed');\n    }\n  }\n}\n\nconsole.log('Parsed works:', works.length);\n\n// Validate and clean works\nworks = works.map((w, i) => ({\n  id: i + 1,\n  name: String(w.name || w.work || '').trim(),\n  qty: parseFloat(w.qty || w.quantity || 1) || 1,\n  unit: String(w.unit || 'm²').trim(),\n  room: w.room || ''\n})).filter(w => w.name.length > 0);\n\n// Save to session\nif (!sd.sess) sd.sess = {};\nif (!sd.sess[cid]) sd.sess[cid] = {};\nsd.sess[cid].works = works;\nsd.sess[cid].description = prepData.description || prepData.textInput?.substring(0, 50) || '';\nsd.sess[cid].state = 'parsed';\n\nconsole.log('Saved', works.length, 'works to session');\n\nreturn { json: { \n  ...prepData, \n  works, \n  _parsed: true,\n  _works_count: works.length\n}};"},"typeVersion":2},{"id":"b13e5f92-4464-463a-bac7-c6c49dc284d2","name":"Sticky AI Parse","type":"n8n-nodes-base.stickyNote","position":[-4304,-1952],"parameters":{"color":6,"width":1112,"height":392,"content":"## 🤖 AI Parse Text\n\n**Purpose:** Extract works from user text\n\n**Models (enable ONE):**\n- ✓ OpenAI Model 1 (gpt-4o-mini)\n- ○ Claude Model 1 (disabled)\n- ○ Gemini Model 1 (disabled)\n\n**To switch:** Disable current, enable other"},"typeVersion":1},{"id":"484e78d7-8254-4b00-81be-3c5c6571dd0a","name":"Sticky AI Transform Rerank","type":"n8n-nodes-base.stickyNote","position":[-1776,-544],"parameters":{"width":456,"height":736,"content":"## 🤖 AI Transform & Rerank\n\n**Transform:** Optimize search query\n**Rerank:** Score candidates 0-100\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n**Models per stage:**\n- OpenAI Model 2/3 (active)\n- Claude Model 2/3 (disabled)\n- Gemini Model 2/3 (disabled)\n\n**To switch models:**\n1. Disable current model node\n2. Enable alternative model\n3. Connects automatically"},"typeVersion":1},{"id":"7b2f5619-46d8-41d5-82f8-c2a769572e4b","name":"Token Setup","type":"n8n-nodes-base.stickyNote","position":[-5536,-1376],"parameters":{"width":280,"height":264,"content":"## ⚠️ Setup 🔑 TOKEN\n\n**Edit TOKEN node values:**\n\n- `bot_token` → Telegram token\n- `QDRANT_URL` → Qdrant server\n\n**Error \"resource not found\"?**\n→ bot_token is invalid\n\n**Get token:** @BotFather → /newbot"},"typeVersion":1},{"id":"a12cfbcc-d1c7-41b9-ae56-a36a35ff3e3b","name":"🔧 Config Embed","type":"n8n-nodes-base.set","position":[-2640,-208],"parameters":{"options":{},"assignments":{"assignments":[{"id":"4f212355-64b3-4ce6-b0ba-d5c9684d12b3","name":"text","type":"string","value":"={{ $json._query }}"},{"id":"6335a7c9-976c-4404-a80b-1969dbb9d6f5","name":"model","type":"string","value":"text-embedding-3-large"},{"id":"5676b77d-c38f-42c5-bb8b-7869c0344efe","name":"dimensions","type":"number","value":"=3072"}]}},"typeVersion":3.4},{"id":"472ff9d9-ddd5-455c-8b35-d1477feb8464","name":"3️⃣ Embeddings API","type":"n8n-nodes-base.httpRequest","position":[-2464,-208],"parameters":{"url":"https://api.openai.com/v1/embeddings","method":"POST","options":{"timeout":30000,"response":{"response":{"neverError":true,"responseFormat":"json"}}},"jsonBody":"={\n  \"model\": \"{{ $('🔧 Config Embed').item.json.model || 'text-embedding-3-large' }}\",\n  \"input\": {{ JSON.stringify($('🔧 Config Embed').item.json.text) }},\n  \"dimensions\": {{ $('🔧 Config Embed').item.json.dimensions || 3072 }}\n}","sendBody":true,"specifyBody":"json","authentication":"predefinedCredentialType","nodeCredentialType":"openAiApi"},"credentials":{"openAiApi":{"id":"","name":""}},"typeVersion":4.2},{"id":"434d32ce-df75-425b-b21f-37051908521b","name":"Sticky Embeddings","type":"n8n-nodes-base.stickyNote","position":[-2928,-80],"parameters":{"width":384,"height":264,"content":"## 🧮 Embeddings\n\n**Settings in Config Embed:**\n- `model`: text-embedding-3-large\n- `dimensions`: 3072\n\n**Change model:**\nEdit 🔧 Config Embed node\n\n**Credentials:**\nUses n8n OpenAI credential in 3️⃣ Embeddings API"},"typeVersion":1},{"id":"c67658c9-4b67-48e5-b782-e671ff1e4763","name":"Google Gemini Chat Model","type":"@n8n/n8n-nodes-langchain.lmChatGoogleGemini","disabled":true,"position":[-3776,-32],"parameters":{"options":{},"modelName":"models/gemini-2.5-pro"},"typeVersion":1},{"id":"e3d0ce38-abd2-4cfd-afdc-671a661909db","name":"Anthropic Chat Model2","type":"@n8n/n8n-nodes-langchain.lmChatAnthropic","disabled":true,"position":[-3760,-240],"parameters":{"model":{"__rl":true,"mode":"list","value":"claude-opus-4-20250514","cachedResultName":"Claude Opus 4"},"options":{}},"typeVersion":1.3},{"id":"fac9a3b6-332b-4659-a975-59809bd0186f","name":"OpenRouter Chat Model1","type":"@n8n/n8n-nodes-langchain.lmChatOpenRouter","disabled":true,"position":[-3600,-240],"parameters":{"options":{}},"typeVersion":1},{"id":"e930de34-857f-4143-8719-370ea7ec0a82","name":"LLM Models","type":"n8n-nodes-base.stickyNote","position":[-4304,-336],"parameters":{"width":904,"height":528,"content":"## 🧠 Available AI Models\n\nConnect any model to LLM Chain nodes:\n\n- **OpenAI** — GPT-4o (default)\n- **Anthropic** — Claude 3.5\n- **Google Gemini** — Gemini Pro\n- **OpenRouter** — Multiple models\n- **xAI Grok** — Grok models\n\nModels are used for:\n- Header analysis\n- Category classification\n- Project type detection\n- Phase generation\n- Work decomposition\n- Validation"},"typeVersion":1},{"id":"0a955ad6-e47e-40f5-94b2-ef964385af75","name":"DeepSeek Chat Model","type":"@n8n/n8n-nodes-langchain.lmChatDeepSeek","disabled":true,"position":[-3936,-32],"parameters":{"options":{}},"typeVersion":1},{"id":"087db646-d3cb-4ba9-9072-f2e73bd81fc5","name":"Sticky Note","type":"n8n-nodes-base.stickyNote","position":[-4304,-1536],"parameters":{"color":6,"width":320,"height":448,"content":""},"typeVersion":1}],"active":false,"pinData":{},"settings":{"executionOrder":"v1"},"versionId":"21ec0f7e-063c-40c0-bbb0-19541e77ad6d","connections":{"Acc":{"main":[[{"node":"Loop","type":"main","index":0}]]},"Agg":{"main":[[{"node":"Generate HTML","type":"main","index":0}]]},"Loop":{"main":[[{"node":"🧹 Prep Cleanup","type":"main","index":0}],[{"node":"📝 Prep Work Msg","type":"main","index":0}]]},"Main":{"main":[[{"node":"Config","type":"main","index":0}]]},"Final":{"main":[[{"node":"Prep HTML File","type":"main","index":0},{"node":"📤 Final","type":"main","index":0}]]},"Route":{"main":[[{"node":"📤 Lang Menu","type":"main","index":0}],[{"node":"Answer Lang CB","type":"main","index":0}],[{"node":"Works Updated","type":"main","index":0}],[{"node":"Edit Menu","type":"main","index":0}],[{"node":"📤 Ask New Work","type":"main","index":0}],[{"node":"Answer Calc CB","type":"main","index":0}],[{"node":"Generate Excel","type":"main","index":0}],[{"node":"Generate PDF","type":"main","index":0}],[{"node":"📤 Help","type":"main","index":0}],[{"node":"View Details","type":"main","index":0}],[{"node":"Prep Text LLM","type":"main","index":0}],[{"node":"📤 Fallback","type":"main","index":0}]]},"Config":{"main":[[{"node":"Route","type":"main","index":0}]]},"IF PDF":{"main":[[{"node":"📤 Send PDF","type":"main","index":0}]]},"Edit Menu":{"main":[[{"node":"📤 Edit Menu","type":"main","index":0}]]},"Prep Works":{"main":[[{"node":"Loop","type":"main","index":0}]]},"🔑 TOKEN":{"main":[[{"node":"Main","type":"main","index":0}]]},"Generate PDF":{"main":[[{"node":"IF PDF","type":"main","index":0}]]},"Prep Lang OK":{"main":[[{"node":"📤 Lang OK","type":"main","index":0}]]},"View Details":{"main":[[{"node":"📤 Details","type":"main","index":0}]]},"Generate HTML":{"main":[[{"node":"Final","type":"main","index":0}]]},"Prep Text LLM":{"main":[[{"node":"🔧 Config Parse","type":"main","index":0}]]},"Works Updated":{"main":[[{"node":"📤 Works Updated","type":"main","index":0}]]},"Answer Calc CB":{"main":[[{"node":"📝 Prep Progress","type":"main","index":0}]]},"Answer Lang CB":{"main":[[{"node":"Prep Lang OK","type":"main","index":0}]]},"Generate Excel":{"main":[[{"node":"📤 Send Excel","type":"main","index":0}]]},"OpenAI Model 1":{"ai_languageModel":[[{"node":"🤖 AI Parse Text","type":"ai_languageModel","index":0}]]},"OpenAI Model 2":{"ai_languageModel":[[{"node":"🤖 AI Transform","type":"ai_languageModel","index":0},{"node":"🤖 AI Rerank","type":"ai_languageModel","index":0}]]},"Prep HTML File":{"main":[[{"node":"📤 Send HTML","type":"main","index":0}]]},"📤 Send Work":{"main":[[{"node":"💾 Save Work Msg","type":"main","index":0}]]},"🤖 AI Rerank":{"main":[[{"node":"8️⃣ Apply Rerank","type":"main","index":0}]]},"📊 Show Works":{"main":[[{"node":"📤 Send Works","type":"main","index":0}]]},"Save Progress ID":{"main":[[{"node":"Prep Works","type":"main","index":0}]]},"📤 Edit Result":{"main":[[{"node":"Acc","type":"main","index":0}]]},"9️⃣ Calculate":{"main":[[{"node":"📊 Update Result","type":"main","index":0}]]},"Telegram Trigger1":{"main":[[{"node":"🔑 TOKEN","type":"main","index":0}]]},"🔧 Config Embed":{"main":[[{"node":"3️⃣ Embeddings API","type":"main","index":0}]]},"🔧 Config Parse":{"main":[[{"node":"🤖 AI Parse Text","type":"main","index":0}]]},"🤖 AI Transform":{"main":[[{"node":"2️⃣ Extract Transform","type":"main","index":0}]]},"🧹 Prep Cleanup":{"main":[[{"node":"🗑️ Delete Work Msg","type":"main","index":0}]]},"1️⃣ Prep Query":{"main":[[{"node":"🔧 Config Transform","type":"main","index":0}]]},"💾 Save Work Msg":{"main":[[{"node":"1️⃣ Prep Query","type":"main","index":0}]]},"📊 Update Result":{"main":[[{"node":"📤 Edit Result","type":"main","index":0}]]},"📝 Prep Progress":{"main":[[{"node":"📤 Send Progress","type":"main","index":0}]]},"📝 Prep Work Msg":{"main":[[{"node":"🗑️ Delete Prev","type":"main","index":0}]]},"📤 Send Progress":{"main":[[{"node":"Save Progress ID","type":"main","index":0}]]},"🔧 Config Rerank":{"main":[[{"node":"🤖 AI Rerank","type":"main","index":0}]]},"🤖 AI Parse Text":{"main":[[{"node":"📝 Parse Text Response","type":"main","index":0}]]},"6️⃣ Prep Rerank":{"main":[[{"node":"🔧 Config Rerank","type":"main","index":0}]]},"🗑️ Delete Prev":{"main":[[{"node":"📤 Send Work","type":"main","index":0}]]},"8️⃣ Apply Rerank":{"main":[[{"node":"9️⃣ Calculate","type":"main","index":0}]]},"5️⃣ Qdrant Search":{"main":[[{"node":"6️⃣ Prep Rerank","type":"main","index":0}]]},"🔧 Config Transform":{"main":[[{"node":"🤖 AI Transform","type":"main","index":0}]]},"3️⃣ Embeddings API":{"main":[[{"node":"4️⃣ Extract Embedding","type":"main","index":0}]]},"🗑️ Delete Work Msg":{"main":[[{"node":"🗑️ Delete Progress Msg","type":"main","index":0}]]},"📝 Parse Text Response":{"main":[[{"node":"📊 Show Works","type":"main","index":0}]]},"2️⃣ Extract Transform":{"main":[[{"node":"🔧 Config Embed","type":"main","index":0}]]},"4️⃣ Extract Embedding":{"main":[[{"node":"5️⃣ Qdrant Search","type":"main","index":0}]]},"🗑️ Delete Progress Msg":{"main":[[{"node":"Agg","type":"main","index":0}]]}}},"lastUpdatedBy":1,"workflowInfo":{"nodeCount":88,"nodeTypes":{"n8n-nodes-base.if":{"count":1},"n8n-nodes-base.set":{"count":5},"n8n-nodes-base.code":{"count":29},"n8n-nodes-base.switch":{"count":1},"n8n-nodes-base.telegram":{"count":3},"n8n-nodes-base.stickyNote":{"count":18},"n8n-nodes-base.httpRequest":{"count":20},"n8n-nodes-base.splitInBatches":{"count":1},"n8n-nodes-base.telegramTrigger":{"count":1},"@n8n/n8n-nodes-langchain.chainLlm":{"count":3},"@n8n/n8n-nodes-langchain.lmChatOpenAi":{"count":2},"@n8n/n8n-nodes-langchain.lmChatDeepSeek":{"count":1},"@n8n/n8n-nodes-langchain.lmChatAnthropic":{"count":1},"@n8n/n8n-nodes-langchain.lmChatOpenRouter":{"count":1},"@n8n/n8n-nodes-langchain.lmChatGoogleGemini":{"count":1}}},"status":"published","readyToDemo":null,"user":{"name":"Artem Boiko","username":"datadrivenconstruction","bio":"Founder DataDrivenConstruction.io | AEC Tech Consultant & Automation Expert | Bridging Software and Construction","verified":true,"links":["https://datadrivenconstruction.io/"],"avatar":"https://gravatar.com/avatar/96a88b84c9f49338945054d2393a04a29e434a2b60a8937de78e6ef9a6305b5f?r=pg&d=retro&size=200"},"nodes":[{"id":19,"icon":"file:httprequest.svg","name":"n8n-nodes-base.httpRequest","codex":{"data":{"alias":["API","Request","URL","Build","cURL"],"resources":{"generic":[{"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/automatically-pulling-and-visualizing-data-with-n8n/","icon":"📈","label":"Automatically pulling and visualizing data with n8n"},{"url":"https://n8n.io/blog/learn-how-to-automatically-cross-post-your-content-with-n8n/","icon":"✍️","label":"Learn how to automatically cross-post your content 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/running-n8n-on-ships-an-interview-with-maranics/","icon":"🛳","label":"Running n8n on ships: An interview with Maranics"},{"url":"https://n8n.io/blog/what-are-apis-how-to-use-them-with-no-code/","icon":" 🪢","label":"What are APIs and how to use them with no code"},{"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/world-poetry-day-workflow/","icon":"📜","label":"Celebrating World Poetry Day with a daily poem in Telegram"},{"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/automate-designs-with-bannerbear-and-n8n/","icon":"🎨","label":"Automate Designs with Bannerbear and n8n"},{"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/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/how-to-use-the-http-request-node-the-swiss-army-knife-for-workflow-automation/","icon":"🧰","label":"How to use the HTTP Request Node - The Swiss Army Knife for Workflow Automation"},{"url":"https://n8n.io/blog/learn-how-to-use-webhooks-with-mattermost-slash-commands/","icon":"🦄","label":"Learn how to use webhooks with Mattermost slash commands"},{"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/automations-for-activists/","icon":"✨","label":"How Common Knowledge use workflow automation for activism"},{"url":"https://n8n.io/blog/creating-scheduled-text-affirmations-with-n8n/","icon":"🤟","label":"Creating scheduled text affirmations with n8n"},{"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.httprequest/"}]},"categories":["Development","Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Helpers"]}}},"group":"[\"output\"]","defaults":{"name":"HTTP Request","color":"#0004F5"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik00MCAyMEM0MCA4Ljk1MzE0IDMxLjA0NjkgMCAyMCAwQzguOTUzMTQgMCAwIDguOTUzMTQgMCAyMEMwIDMxLjA0NjkgOC45NTMxNCA0MCAyMCA0MEMzMS4wNDY5IDQwIDQwIDMxLjA0NjkgNDAgMjBaTTIwIDM2Ljk0NThDMTguODg1MiAzNi45NDU4IDE3LjEzNzggMzUuOTY3IDE1LjQ5OTggMzIuNjk4NUMxNC43OTY0IDMxLjI5MTggMTQuMTk2MSAyOS41NDMxIDEzLjc1MjYgMjcuNjg0N0gyNi4xODk4QzI1LjgwNDUgMjkuNTQwMyAyNS4yMDQ0IDMxLjI5MDEgMjQuNTAwMiAzMi42OTg1QzIyLjg2MjIgMzUuOTY3IDIxLjExNDggMzYuOTQ1OCAyMCAzNi45NDU4Wk0xMi45MDY0IDIwQzEyLjkwNjQgMjEuNjA5NyAxMy4wMDg3IDIzLjE2NCAxMy4yMDAzIDI0LjYzMDVIMjYuNzk5N0MyNi45OTEzIDIzLjE2NCAyNy4wOTM2IDIxLjYwOTcgMjcuMDkzNiAyMEMyNy4wOTM2IDE4LjM5MDMgMjYuOTkxMyAxNi44MzYgMjYuNzk5NyAxNS4zNjk1SDEzLjIwMDNDMTMuMDA4NyAxNi44MzYgMTIuOTA2NCAxOC4zOTAzIDEyLjkwNjQgMjBaTTIwIDMuMDU0MTlDMjEuMTE0OSAzLjA1NDE5IDIyLjg2MjIgNC4wMzA3OCAyNC41MDAxIDcuMzAwMzlDMjUuMjA2NiA4LjcxNDA4IDI1LjgwNzIgMTAuNDA2NyAyNi4xOTIgMTIuMzE1M0gxMy43NTAxQzE0LjE5MzMgMTAuNDA0NyAxNC43OTQyIDguNzEyNTQgMTUuNDk5OCA3LjMwMDY0QzE3LjEzNzcgNC4wMzA4MyAxOC44ODUxIDMuMDU0MTkgMjAgMy4wNTQxOVpNMzAuMTQ3OCAyMEMzMC4xNDc4IDE4LjQwOTkgMzAuMDU0MyAxNi44NjE3IDI5LjgyMjcgMTUuMzY5NUgzNi4zMDQyQzM2LjcyNTIgMTYuODQyIDM2Ljk0NTggMTguMzk2NCAzNi45NDU4IDIwQzM2Ljk0NTggMjEuNjAzNiAzNi43MjUyIDIzLjE1OCAzNi4zMDQyIDI0LjYzMDVIMjkuODIyN0MzMC4wNTQzIDIzLjEzODMgMzAuMTQ3OCAyMS41OTAxIDMwLjE0NzggMjBaTTI2LjI3NjcgNC4yNTUxMkMyNy42MzY1IDYuMzYwMTkgMjguNzExIDkuMTMyIDI5LjM3NzQgMTIuMzE1M0gzNS4xMDQ2QzMzLjI1MTEgOC42NjggMzAuMTA3IDUuNzgzNDYgMjYuMjc2NyA0LjI1NTEyWk0xMC42MjI2IDEyLjMxNTNINC44OTI5M0M2Ljc1MTQ3IDguNjY3ODQgOS44OTM1MSA1Ljc4MzQxIDEzLjcyMzIgNC4yNTUxM0MxMi4zNjM1IDYuMzYwMjEgMTEuMjg5IDkuMTMyMDEgMTAuNjIyNiAxMi4zMTUzWk0zLjA1NDE5IDIwQzMuMDU0MTkgMjEuNjAzIDMuMjc3NDMgMjMuMTU3NSAzLjY5NDg0IDI0LjYzMDVIMTAuMTIxN0M5Ljk0NjE5IDIzLjE0MiA5Ljg1MjIyIDIxLjU5NDMgOS44NTIyMiAyMEM5Ljg1MjIyIDE4LjQwNTcgOS45NDYxOSAxNi44NTggMTAuMTIxNyAxNS4zNjk1SDMuNjk0ODRDMy4yNzc0MyAxNi44NDI1IDMuMDU0MTkgMTguMzk3IDMuMDU0MTkgMjBaTTI2LjI3NjYgMzUuNzQyN0MyNy42MzY1IDMzLjYzOTMgMjguNzExIDMwLjg2OCAyOS4zNzc0IDI3LjY4NDdIMzUuMTA0NkMzMy4yNTEgMzEuMzMyMiAzMC4xMDY4IDM0LjIxNzkgMjYuMjc2NiAzNS43NDI3Wk0xMy43MjM0IDM1Ljc0MjdDOS44OTM2OSAzNC4yMTc5IDYuNzUxNTUgMzEuMzMyNCA0Ljg5MjkzIDI3LjY4NDdIMTAuNjIyNkMxMS4yODkgMzAuODY4IDEyLjM2MzUgMzMuNjM5MyAxMy43MjM0IDM1Ljc0MjdaIiBmaWxsPSIjM0E0MkU5Ii8+Cjwvc3ZnPgo="},"displayName":"HTTP Request","typeVersion":4,"nodeCategories":[{"id":5,"name":"Development"},{"id":9,"name":"Core Nodes"}]},{"id":20,"icon":"fa:map-signs","name":"n8n-nodes-base.if","codex":{"data":{"alias":["Router","Filter","Condition","Logic","Boolean","Branch"],"details":"The IF node can be used to implement binary conditional logic in your workflow. You can set up one-to-many conditions to evaluate each item of data being inputted into the node. That data will either evaluate to TRUE or FALSE and route out of the node accordingly.\n\nThis node has multiple types of conditions: Bool, String, Number, and Date & Time.","resources":{"generic":[{"url":"https://n8n.io/blog/learn-to-automate-your-factorys-incident-reporting-a-step-by-step-guide/","icon":"🏭","label":"Learn to Automate Your Factory's Incident Reporting: A Step by Step Guide"},{"url":"https://n8n.io/blog/2021-the-year-to-automate-the-new-you-with-n8n/","icon":"☀️","label":"2021: The Year to Automate the New You with n8n"},{"url":"https://n8n.io/blog/why-business-process-automation-with-n8n-can-change-your-daily-life/","icon":"🧬","label":"Why business process automation with n8n can change your daily life"},{"url":"https://n8n.io/blog/create-a-toxic-language-detector-for-telegram/","icon":"🤬","label":"Create a toxic language detector for Telegram in 4 step"},{"url":"https://n8n.io/blog/no-code-ecommerce-workflow-automations/","icon":"store","label":"6 e-commerce workflows to power up your Shopify s"},{"url":"https://n8n.io/blog/how-to-build-a-low-code-self-hosted-url-shortener/","icon":"🔗","label":"How to build a low-code, self-hosted URL shortener in 3 steps"},{"url":"https://n8n.io/blog/automate-your-data-processing-pipeline-in-9-steps-with-n8n/","icon":"⚙️","label":"Automate your data processing pipeline in 9 steps"},{"url":"https://n8n.io/blog/how-to-get-started-with-crm-automation-and-no-code-workflow-ideas/","icon":"👥","label":"How to get started with CRM automation (with 3 no-code workflow ideas"},{"url":"https://n8n.io/blog/5-tasks-you-can-automate-with-notion-api/","icon":"⚡️","label":"5 tasks you can automate with the new Notion API "},{"url":"https://n8n.io/blog/automate-google-apps-for-productivity/","icon":"💡","label":"15 Google apps you can combine and automate to increase productivity"},{"url":"https://n8n.io/blog/automation-for-maintainers-of-open-source-projects/","icon":"🏷️","label":"How to automatically manage contributions to open-source projects"},{"url":"https://n8n.io/blog/how-uproc-scraped-a-multi-page-website-with-a-low-code-workflow/","icon":" 🕸️","label":"How uProc scraped a multi-page website with a low-code workflow"},{"url":"https://n8n.io/blog/5-workflow-automations-for-mattermost-that-we-love-at-n8n/","icon":"🤖","label":"5 workflow automations for Mattermost that we love at n8n"},{"url":"https://n8n.io/blog/why-this-product-manager-loves-workflow-automation-with-n8n/","icon":"🧠","label":"Why this Product Manager loves workflow automation with n8n"},{"url":"https://n8n.io/blog/sending-automated-congratulations-with-google-sheets-twilio-and-n8n/","icon":"🙌","label":"Sending Automated Congratulations with Google Sheets, Twilio, and n8n "},{"url":"https://n8n.io/blog/how-to-set-up-a-ci-cd-pipeline-with-no-code/","icon":"🎡","label":"How to set up a no-code CI/CD pipeline with GitHub and TravisCI"},{"url":"https://n8n.io/blog/benefits-of-automation-and-n8n-an-interview-with-hubspots-hugh-durkin/","icon":"🎖","label":"Benefits of automation and n8n: An interview with HubSpot's Hugh Durkin"},{"url":"https://n8n.io/blog/aws-workflow-automation/","label":"7 no-code workflow automations for Amazon Web Services"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.if/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Flow"]}}},"group":"[\"transform\"]","defaults":{"name":"If","color":"#408000"},"iconData":{"icon":"map-signs","type":"icon"},"displayName":"If","typeVersion":2,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":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":49,"icon":"file:telegram.svg","name":"n8n-nodes-base.telegram","codex":{"data":{"alias":["human","form","wait","hitl","approval"],"resources":{"generic":[{"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/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/world-poetry-day-workflow/","icon":"📜","label":"Celebrating World Poetry Day with a daily poem in Telegram"},{"url":"https://n8n.io/blog/using-automation-to-boost-productivity-in-the-workplace/","icon":"💪","label":"Using Automation to Boost Productivity in the Workplace"},{"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/creating-scheduled-text-affirmations-with-n8n/","icon":"🤟","label":"Creating scheduled text affirmations with n8n"},{"url":"https://n8n.io/blog/creating-telegram-bots-with-n8n-a-no-code-platform/","icon":"💬","label":"Creating Telegram Bots with n8n, a No-Code Platform"},{"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/app-nodes/n8n-nodes-base.telegram/"}],"credentialDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/credentials/telegram/"}]},"categories":["Communication","HITL"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"HITL":["Human in the Loop"]}}},"group":"[\"output\"]","defaults":{"name":"Telegram"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBmaWxsPSIjZmZmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiB2aWV3Qm94PSIwIDAgNjYgNjYiPjx1c2UgeGxpbms6aHJlZj0iI2EiIHg9Ii41IiB5PSIuNSIvPjxzeW1ib2wgaWQ9ImEiIG92ZXJmbG93PSJ2aXNpYmxlIj48ZyBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZT0ibm9uZSI+PHBhdGggZmlsbD0iIzM3YWVlMiIgZD0iTTAgMzJjMCAxNy42NzMgMTQuMzI3IDMyIDMyIDMyczMyLTE0LjMyNyAzMi0zMlM0OS42NzMgMCAzMiAwIDAgMTQuMzI3IDAgMzIiLz48cGF0aCBmaWxsPSIjYzhkYWVhIiBkPSJtMjEuNjYxIDM0LjMzOCAzLjc5NyAxMC41MDhzLjQ3NS45ODMuOTgzLjk4MyA4LjA2OC03Ljg2NCA4LjA2OC03Ljg2NGw4LjQwNy0xNi4yMzctMjEuMTE5IDkuODk4eiIvPjxwYXRoIGZpbGw9IiNhOWM2ZDgiIGQ9Im0yNi42OTUgMzcuMDM0LS43MjkgNy43NDZzLS4zMDUgMi4zNzMgMi4wNjggMGw0LjY0NC00LjIwMyIvPjxwYXRoIGQ9Im0yMS43MyAzNC43MTItNy44MDktMi41NDVzLS45MzItLjM3OC0uNjMzLTEuMjM3Yy4wNjItLjE3Ny4xODYtLjMyOC41NTktLjU4OCAxLjczMS0xLjIwNiAzMi4wMjgtMTIuMDk2IDMyLjAyOC0xMi4wOTZzLjg1Ni0uMjg4IDEuMzYxLS4wOTdjLjIzMS4wODguMzc4LjE4Ny41MDMuNTQ4LjA0NS4xMzIuMDcxLjQxMS4wNjguNjg5LS4wMDMuMjAxLS4wMjcuMzg2LS4wNDUuNjc4LS4xODQgMi45NzgtNS43MDYgMjUuMTk4LTUuNzA2IDI1LjE5OHMtLjMzIDEuMy0xLjUxNCAxLjM0NWMtLjQzMi4wMTYtLjk1Ni0uMDcxLTEuNTgyLS42MS0yLjMyMy0xLjk5OC0xMC4zNTItNy4zOTQtMTIuMTI2LTguNThhLjM0LjM0IDAgMCAxLS4xNDYtLjIzOWMtLjAyNS0uMTI1LjEwOC0uMjguMTA4LS4yOHMxMy45OC0xMi40MjcgMTQuMzUyLTEzLjczMWMuMDI5LS4xMDEtLjA3OS0uMTUxLS4yMjYtLjEwNy0uOTI5LjM0Mi0xNy4wMjUgMTAuNTA2LTE4LjgwMSAxMS42MjktLjEwNC4wNjYtLjM5NS4wMjMtLjM5NS4wMjMiLz48L2c+PC9zeW1ib2w+PC9zdmc+"},"displayName":"Telegram","typeVersion":1,"nodeCategories":[{"id":6,"name":"Communication"},{"id":28,"name":"HITL"}]},{"id":50,"icon":"file:telegram.svg","name":"n8n-nodes-base.telegramTrigger","codex":{"data":{"resources":{"generic":[{"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/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/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/creating-telegram-bots-with-n8n-a-no-code-platform/","icon":"💬","label":"Creating Telegram Bots with n8n, a No-Code Platform"},{"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/trigger-nodes/n8n-nodes-base.telegramtrigger/"}],"credentialDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/credentials/telegram/"}]},"categories":["Communication"],"nodeVersion":"1.0","codexVersion":"1.0"}},"group":"[\"trigger\"]","defaults":{"name":"Telegram Trigger"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBmaWxsPSIjZmZmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiB2aWV3Qm94PSIwIDAgNjYgNjYiPjx1c2UgeGxpbms6aHJlZj0iI2EiIHg9Ii41IiB5PSIuNSIvPjxzeW1ib2wgaWQ9ImEiIG92ZXJmbG93PSJ2aXNpYmxlIj48ZyBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZT0ibm9uZSI+PHBhdGggZmlsbD0iIzM3YWVlMiIgZD0iTTAgMzJjMCAxNy42NzMgMTQuMzI3IDMyIDMyIDMyczMyLTE0LjMyNyAzMi0zMlM0OS42NzMgMCAzMiAwIDAgMTQuMzI3IDAgMzIiLz48cGF0aCBmaWxsPSIjYzhkYWVhIiBkPSJtMjEuNjYxIDM0LjMzOCAzLjc5NyAxMC41MDhzLjQ3NS45ODMuOTgzLjk4MyA4LjA2OC03Ljg2NCA4LjA2OC03Ljg2NGw4LjQwNy0xNi4yMzctMjEuMTE5IDkuODk4eiIvPjxwYXRoIGZpbGw9IiNhOWM2ZDgiIGQ9Im0yNi42OTUgMzcuMDM0LS43MjkgNy43NDZzLS4zMDUgMi4zNzMgMi4wNjggMGw0LjY0NC00LjIwMyIvPjxwYXRoIGQ9Im0yMS43MyAzNC43MTItNy44MDktMi41NDVzLS45MzItLjM3OC0uNjMzLTEuMjM3Yy4wNjItLjE3Ny4xODYtLjMyOC41NTktLjU4OCAxLjczMS0xLjIwNiAzMi4wMjgtMTIuMDk2IDMyLjAyOC0xMi4wOTZzLjg1Ni0uMjg4IDEuMzYxLS4wOTdjLjIzMS4wODguMzc4LjE4Ny41MDMuNTQ4LjA0NS4xMzIuMDcxLjQxMS4wNjguNjg5LS4wMDMuMjAxLS4wMjcuMzg2LS4wNDUuNjc4LS4xODQgMi45NzgtNS43MDYgMjUuMTk4LTUuNzA2IDI1LjE5OHMtLjMzIDEuMy0xLjUxNCAxLjM0NWMtLjQzMi4wMTYtLjk1Ni0uMDcxLTEuNTgyLS42MS0yLjMyMy0xLjk5OC0xMC4zNTItNy4zOTQtMTIuMTI2LTguNThhLjM0LjM0IDAgMCAxLS4xNDYtLjIzOWMtLjAyNS0uMTI1LjEwOC0uMjguMTA4LS4yOHMxMy45OC0xMi40MjcgMTQuMzUyLTEzLjczMWMuMDI5LS4xMDEtLjA3OS0uMTUxLS4yMjYtLjEwNy0uOTI5LjM0Mi0xNy4wMjUgMTAuNTA2LTE4LjgwMSAxMS42MjktLjEwNC4wNjYtLjM5NS4wMjMtLjM5NS4wMjMiLz48L2c+PC9zeW1ib2w+PC9zdmc+"},"displayName":"Telegram Trigger","typeVersion":1,"nodeCategories":[{"id":6,"name":"Communication"}]},{"id":112,"icon":"fa:map-signs","name":"n8n-nodes-base.switch","codex":{"data":{"alias":["Router","If","Path","Filter","Condition","Logic","Branch","Case"],"resources":{"generic":[{"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/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/build-your-own-virtual-assistant-with-n8n-a-step-by-step-guide/","icon":"👦","label":"Build your own virtual assistant with n8n: A step by step guide"},{"url":"https://n8n.io/blog/automation-for-maintainers-of-open-source-projects/","icon":"🏷️","label":"How to automatically manage contributions to open-source projects"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.switch/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Flow"]}}},"group":"[\"transform\"]","defaults":{"name":"Switch","color":"#506000"},"iconData":{"icon":"map-signs","type":"icon"},"displayName":"Switch","typeVersion":3,"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":1145,"icon":"file:anthropic.svg","name":"@n8n/n8n-nodes-langchain.lmChatAnthropic","codex":{"data":{"alias":["claude","sonnet","opus"],"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatanthropic/"}]},"categories":["AI","Langchain"],"subcategories":{"AI":["Language Models","Root Nodes"],"Language Models":["Chat Models (Recommended)"]}}},"group":"[\"transform\"]","defaults":{"name":"Anthropic Chat Model"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NiIgaGVpZ2h0PSIzMiIgZmlsbD0ibm9uZSI+PHBhdGggZmlsbD0iIzdEN0Q4NyIgZD0iTTMyLjczIDBoLTYuOTQ1TDM4LjQ1IDMyaDYuOTQ1ek0xMi42NjUgMCAwIDMyaDcuMDgybDIuNTktNi43MmgxMy4yNWwyLjU5IDYuNzJoNy4wODJMMTkuOTI5IDB6bS0uNzAyIDE5LjMzNyA0LjMzNC0xMS4yNDYgNC4zMzQgMTEuMjQ2eiIvPjwvc3ZnPg=="},"displayName":"Anthropic Chat Model","typeVersion":1,"nodeCategories":[{"id":25,"name":"AI"},{"id":26,"name":"Langchain"}]},{"id":1153,"icon":"file:openAiLight.svg","name":"@n8n/n8n-nodes-langchain.lmChatOpenAi","codex":{"data":{"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatopenai/"}]},"categories":["AI","Langchain"],"subcategories":{"AI":["Language Models","Root Nodes"],"Language Models":["Chat Models (Recommended)"]}}},"group":"[\"transform\"]","defaults":{"name":"OpenAI Chat Model"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTM2Ljg2NzEgMTYuMzcxOEMzNy43NzQ2IDEzLjY0OCAzNy40NjIxIDEwLjY2NDIgMzYuMDEwOCA4LjE4NjYxQzMzLjgyODIgNC4zODY1MyAyOS40NDA3IDIuNDMxNDkgMjUuMTU1NiAzLjM1MTUxQzIzLjI0OTMgMS4yMDM5NiAyMC41MTA1IC0wLjAxNzMxNDggMTcuNjM5MiAwLjAwMDE4NTUzM0MxMy4yNTkxIC0wLjAwOTgxNDY4IDkuMzcyNzMgMi44MTAyNSA4LjAyNTIgNi45Nzc4M0M1LjIxMTM5IDcuNTU0MSAyLjc4MjU4IDkuMzE1MzggMS4zNjEzIDExLjgxMTdDLTAuODM3NDkzIDE1LjYwMTggLTAuMzM2MjMyIDIwLjM3OTQgMi42MDEzMyAyMy42Mjk0QzEuNjkzODEgMjYuMzUzMiAyLjAwNjMyIDI5LjMzNzEgMy40NTc2IDMxLjgxNDZDNS42NDAxNSAzNS42MTQ3IDEwLjAyNzcgMzcuNTY5NyAxNC4zMTI4IDM2LjY0OTdDMTYuMjE3OSAzOC43OTczIDE4Ljk1NzkgNDAuMDE4NSAyMS44MjkyIDM5Ljk5OThDMjYuMjExOCA0MC4wMTEgMzAuMDk5NCAzNy4xODg1IDMxLjQ0NjkgMzMuMDE3MUMzNC4yNjA4IDMyLjQ0MDkgMzYuNjg5NiAzMC42Nzk2IDM4LjExMDggMjguMTgzM0M0MC4zMDcxIDI0LjM5MzIgMzkuODA0NiAxOS42MTk0IDM2Ljg2ODMgMTYuMzY5M0wzNi44NjcxIDE2LjM3MThaTTIxLjgzMTcgMzcuMzg2QzIwLjA3OCAzNy4zODg1IDE4LjM3OTIgMzYuNzc0NyAxNy4wMzI5IDM1LjY1MDlDMTcuMDk0MSAzNS42MTg0IDE3LjIwMDQgMzUuNTU5NyAxNy4yNjkxIDM1LjUxNzJMMjUuMjM0MyAzMC45MTcxQzI1LjY0MTggMzAuNjg1OCAyNS44OTE4IDMwLjI1MjEgMjUuODg5MyAyOS43ODMzVjE4LjU1NDNMMjkuMjU1NyAyMC40OTgxQzI5LjI5MTkgMjAuNTE1NiAyOS4zMTU3IDIwLjU1MDYgMjkuMzIwNyAyMC41OTA2VjI5Ljg4OTZDMjkuMzE1NyAzNC4wMjQ3IDI1Ljk2NjggMzcuMzc3MiAyMS44MzE3IDM3LjM4NlpNNS43MjY0IDMwLjUwNzFDNC44NDc2MyAyOC45ODk2IDQuNTMxMzcgMjcuMjEwOCA0LjgzMjYzIDI1LjQ4NDVDNC44OTEzOCAyNS41MTk1IDQuOTk1MTMgMjUuNTgzMiA1LjA2ODg4IDI1LjYyNTdMMTMuMDM0MSAzMC4yMjU4QzEzLjQzNzggMzAuNDYyMSAxMy45Mzc4IDMwLjQ2MjEgMTQuMzQyOCAzMC4yMjU4TDI0LjA2NjggMjQuNjEwN1YyOC40OTgzQzI0LjA2OTMgMjguNTM4MyAyNC4wNTA1IDI4LjU3NyAyNC4wMTkzIDI4LjYwMkwxNS45Njc5IDMzLjI1MDlDMTIuMzgxNSAzNS4zMTU5IDcuODAxNDQgMzQuMDg4NCA1LjcyNzY1IDMwLjUwNzFINS43MjY0Wk0zLjYzMDEgMTMuMTIwNUM0LjUwNTEyIDExLjYwMDQgNS44ODY0IDEwLjQzNzkgNy41MzE0NCA5LjgzNDE1QzcuNTMxNDQgOS45MDI5IDcuNTI3NjkgMTAuMDI0MiA3LjUyNzY5IDEwLjEwOTJWMTkuMzEwNkM3LjUyNTE5IDE5Ljc3ODEgNy43NzUxOSAyMC4yMTE5IDguMTgxNDUgMjAuNDQzMUwxNy45MDU0IDI2LjA1N0wxNC41MzkxIDI4LjAwMDhDMTQuNTA1MyAyOC4wMjMzIDE0LjQ2MjggMjguMDI3IDE0LjQyNTMgMjguMDEwOEw2LjM3MjY2IDIzLjM1ODJDMi43OTM4MyAyMS4yODU2IDEuNTY2MzEgMTYuNzA2OCAzLjYyODg1IDEzLjEyMTdMMy42MzAxIDEzLjEyMDVaTTMxLjI4ODIgMTkuNTU2OUwyMS41NjQyIDEzLjk0MTdMMjQuOTMwNiAxMS45OTkyQzI0Ljk2NDMgMTEuOTc2NyAyNS4wMDY4IDExLjk3MjkgMjUuMDQ0MyAxMS45ODkyTDMzLjA5NyAxNi42MzhDMzYuNjgyMSAxOC43MDkzIDM3LjkxMDggMjMuMjk1NyAzNS44Mzk1IDI2Ljg4MDhDMzQuOTYzMyAyOC4zOTgzIDMzLjU4MzIgMjkuNTYwOCAzMS45Mzk1IDMwLjE2NThWMjAuNjg5NEMzMS45NDMyIDIwLjIyMTkgMzEuNjk0NSAxOS43ODk0IDMxLjI4OTQgMTkuNTU2OUgzMS4yODgyWk0zNC42MzgzIDE0LjUxNDJDMzQuNTc5NSAxNC40NzggMzQuNDc1OCAxNC40MTU1IDM0LjQwMiAxNC4zNzNMMjYuNDM2OCA5Ljc3Mjg5QzI2LjAzMzEgOS41MzY2NCAyNS41MzMxIDkuNTM2NjQgMjUuMTI4MSA5Ljc3Mjg5TDE1LjQwNDEgMTUuMzg4VjExLjUwMDRDMTUuNDAxNiAxMS40NjA0IDE1LjQyMDQgMTEuNDIxNyAxNS40NTE2IDExLjM5NjdMMjMuNTAzIDYuNzUxNThDMjcuMDg5NCA0LjY4Mjc5IDMxLjY3NDUgNS45MTQwNiAzMy43NDIgOS41MDE2NEMzNC42MTU4IDExLjAxNjcgMzQuOTMyIDEyLjc5MDUgMzQuNjM1OCAxNC41MTQySDM0LjYzODNaTTEzLjU3NDEgMjEuNDQzMUwxMC4yMDY1IDE5LjQ5OTRDMTAuMTcwMiAxOS40ODE5IDEwLjE0NjUgMTkuNDQ2OCAxMC4xNDE1IDE5LjQwNjhWMTAuMTA3OUMxMC4xNDQgNS45Njc4MSAxMy41MDI4IDIuNjEyNzQgMTcuNjQyOSAyLjYxNTI0QzE5LjM5NDIgMi42MTUyNCAyMS4wODkyIDMuMjMwMjUgMjIuNDM1NSA0LjM1MDI4QzIyLjM3NDMgNC4zODI3OCAyMi4yNjkzIDQuNDQxNTMgMjIuMTk5MiA0LjQ4NDAzTDE0LjIzNDEgOS4wODQxM0MxMy44MjY2IDkuMzE1MzggMTMuNTc2NiA5Ljc0Nzg5IDEzLjU3OTEgMTAuMjE2N0wxMy41NzQxIDIxLjQ0MDZWMjEuNDQzMVpNMTUuNDAyOSAxNy41MDA2TDE5LjczNDIgMTQuOTk5M0wyNC4wNjU1IDE3LjQ5OTNWMjIuNTAwN0wxOS43MzQyIDI1LjAwMDdMMTUuNDAyOSAyMi41MDA3VjE3LjUwMDZaIiBmaWxsPSIjN0Q3RDg3Ii8+Cjwvc3ZnPgo="},"displayName":"OpenAI Chat Model","typeVersion":1,"nodeCategories":[{"id":25,"name":"AI"},{"id":26,"name":"Langchain"}]},{"id":1262,"icon":"file:google.svg","name":"@n8n/n8n-nodes-langchain.lmChatGoogleGemini","codex":{"data":{"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatgooglegemini/"}]},"categories":["AI","Langchain"],"subcategories":{"AI":["Language Models","Root Nodes"],"Language Models":["Chat Models (Recommended)"]}}},"group":"[\"transform\"]","defaults":{"name":"Google Gemini Chat Model"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNDggNDgiPjxkZWZzPjxwYXRoIGlkPSJhIiBkPSJNNDQuNSAyMEgyNHY4LjVoMTEuOEMzNC43IDMzLjkgMzAuMSAzNyAyNCAzN2MtNy4yIDAtMTMtNS44LTEzLTEzczUuOC0xMyAxMy0xM2MzLjEgMCA1LjkgMS4xIDguMSAyLjlsNi40LTYuNEMzNC42IDQuMSAyOS42IDIgMjQgMiAxMS44IDIgMiAxMS44IDIgMjRzOS44IDIyIDIyIDIyYzExIDAgMjEtOCAyMS0yMiAwLTEuMy0uMi0yLjctLjUtNCIvPjwvZGVmcz48Y2xpcFBhdGggaWQ9ImIiPjx1c2UgeGxpbms6aHJlZj0iI2EiIG92ZXJmbG93PSJ2aXNpYmxlIi8+PC9jbGlwUGF0aD48cGF0aCBmaWxsPSIjRkJCQzA1IiBkPSJNMCAzN1YxMWwxNyAxM3oiIGNsaXAtcGF0aD0idXJsKCNiKSIvPjxwYXRoIGZpbGw9IiNFQTQzMzUiIGQ9Im0wIDExIDE3IDEzIDctNi4xTDQ4IDE0VjBIMHoiIGNsaXAtcGF0aD0idXJsKCNiKSIvPjxwYXRoIGZpbGw9IiMzNEE4NTMiIGQ9Im0wIDM3IDMwLTIzIDcuOSAxTDQ4IDB2NDhIMHoiIGNsaXAtcGF0aD0idXJsKCNiKSIvPjxwYXRoIGZpbGw9IiM0Mjg1RjQiIGQ9Ik00OCA0OCAxNyAyNGwtNC0zIDM1LTEweiIgY2xpcC1wYXRoPSJ1cmwoI2IpIi8+PC9zdmc+"},"displayName":"Google Gemini Chat Model","typeVersion":1,"nodeCategories":[{"id":25,"name":"AI"},{"id":26,"name":"Langchain"}]},{"id":1280,"icon":"file:deepseek.svg","name":"@n8n/n8n-nodes-langchain.lmChatDeepSeek","codex":{"data":{"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatdeepseek/"}]},"categories":["AI","Langchain"],"subcategories":{"AI":["Language Models","Root Nodes"],"Language Models":["Chat Models (Recommended)"]}}},"group":"[\"transform\"]","defaults":{"name":"DeepSeek Chat Model"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+RGVlcFNlZWs8L3RpdGxlPjxwYXRoIGQ9Ik0yMy43NDggNC40ODJjLS4yNTQtLjEyNC0uMzY0LjExMy0uNTEyLjIzNC0uMDUxLjAzOS0uMDk0LjA5LS4xMzcuMTM2LS4zNzIuMzk3LS44MDYuNjU3LTEuMzczLjYyNi0uODI5LS4wNDYtMS41MzcuMjE0LTIuMTYzLjg0OC0uMTMzLS43ODItLjU3NS0xLjI0OC0xLjI0Ny0xLjU0OC0uMzUyLS4xNTYtLjcwOC0uMzExLS45NTUtLjY1LS4xNzItLjI0MS0uMjE5LS41MS0uMzA1LS43NzQtLjA1NS0uMTYtLjExLS4zMjMtLjI5My0uMzUtLjItLjAzMS0uMjc4LjEzNi0uMzU2LjI3Ni0uMzEzLjU3Mi0uNDM0IDEuMjAyLS40MjIgMS44NC4wMjcgMS40MzYuNjMzIDIuNTggMS44MzggMy4zOTMuMTM3LjA5My4xNzIuMTg3LjEyOS4zMjMtLjA4Mi4yOC0uMTguNTUyLS4yNjYuODMzLS4wNTUuMTc5LS4xMzcuMjE3LS4zMjkuMTRhNS41MjYgNS41MjYgMCAwMS0xLjczNi0xLjE4Yy0uODU3LS44MjgtMS42MzEtMS43NDItMi41OTctMi40NThhMTEuMzY1IDExLjM2NSAwIDAwLS42ODktLjQ3MWMtLjk4NS0uOTU3LjEzLTEuNzQzLjM4OC0xLjgzNi4yNy0uMDk4LjA5My0uNDMyLS43NzktLjQyOC0uODcyLjAwNC0xLjY3LjI5NS0yLjY4Ny42ODRhMy4wNTUgMy4wNTUgMCAwMS0uNDY1LjEzNyA5LjU5NyA5LjU5NyAwIDAwLTIuODgzLS4xMDJjLTEuODg1LjIxLTMuMzkgMS4xMDItNC40OTcgMi42MjNDLjA4MiA4LjYwNi0uMjMxIDEwLjY4NC4xNTIgMTIuODVjLjQwMyAyLjI4NCAxLjU2OSA0LjE3NSAzLjM2IDUuNjUzIDEuODU4IDEuNTMzIDMuOTk3IDIuMjg0IDYuNDM4IDIuMTQgMS40ODItLjA4NSAzLjEzMy0uMjg0IDQuOTk0LTEuODYuNDcuMjM0Ljk2Mi4zMjcgMS43OC4zOTcuNjMuMDU5IDEuMjM2LS4wMyAxLjcwNS0uMTI4LjczNS0uMTU2LjY4NC0uODM3LjQxOS0uOTYxLTIuMTU1LTEuMDA0LTEuNjgyLS41OTUtMi4xMTMtLjkyNiAxLjA5Ni0xLjI5NiAyLjc0Ni0yLjY0MiAzLjM5Mi03LjAwMy4wNS0uMzQ3LjAwNy0uNTY1IDAtLjg0NS0uMDA0LS4xNy4wMzUtLjIzNy4yMy0uMjU2YTQuMTczIDQuMTczIDAgMDAxLjU0NS0uNDc1YzEuMzk2LS43NjMgMS45Ni0yLjAxNSAyLjA5My0zLjUxNy4wMi0uMjMtLjAwNC0uNDY3LS4yNDctLjU4OHpNMTEuNTgxIDE4Yy0yLjA4OS0xLjY0Mi0zLjEwMi0yLjE4My0zLjUyLTIuMTYtLjM5Mi4wMjQtLjMyMS40NzEtLjIzNS43NjMuMDkuMjg4LjIwNy40ODYuMzcxLjczOS4xMTQuMTY3LjE5Mi40MTYtLjExMy42MDMtLjY3My40MTYtMS44NDItLjE0LTEuODk3LS4xNjctMS4zNjEtLjgwMi0yLjUtMS44Ni0zLjMwMS0zLjMwNy0uNzc0LTEuMzkzLTEuMjI0LTIuODg3LTEuMjk4LTQuNDgyLS4wMi0uMzg2LjA5My0uNTIyLjQ3Ny0uNTkyYTQuNjk2IDQuNjk2IDAgMDExLjUyOS0uMDM5YzIuMTMyLjMxMiAzLjk0NiAxLjI2NSA1LjQ2OCAyLjc3NC44NjguODYgMS41MjUgMS44ODcgMi4yMDIgMi44OTEuNzIgMS4wNjYgMS40OTQgMi4wODIgMi40OCAyLjkxNC4zNDguMjkyLjYyNS41MTQuODkxLjY3Ny0uODAyLjA5LTIuMTQuMTEtMy4wNTQtLjYxNHptMS02LjQ0YS4zMDYuMzA2IDAgMDEuNDE1LS4yODcuMzAyLjMwMiAwIDAxLjIuMjg4LjMwNi4zMDYgMCAwMS0uMzEuMzA3LjMwMy4zMDMgMCAwMS0uMzA0LS4zMDh6bTMuMTEgMS41OTZjLS4yLjA4MS0uMzk5LjE1MS0uNTkuMTZhMS4yNDUgMS4yNDUgMCAwMS0uNzk4LS4yNTRjLS4yNzQtLjIzLS40Ny0uMzU4LS41NTItLjc1OGExLjczIDEuNzMgMCAwMS4wMTYtLjU4OGMuMDctLjMyNy0uMDA4LS41MzctLjIzOS0uNzI3LS4xODctLjE1Ni0uNDI2LS4xOTktLjY4OC0uMTk5YS41NTkuNTU5IDAgMDEtLjI1NC0uMDc4Yy0uMTEtLjA1NC0uMi0uMTktLjExNC0uMzU4LjAyOC0uMDU0LjE2LS4xODYuMTkyLS4yMS4zNTYtLjIwMi43NjctLjEzNiAxLjE0Ni4wMTYuMzUyLjE0NC42MTguNDA4IDEuMDAxLjc4Mi4zOTEuNDUxLjQ2Mi41NzYuNjg1LjkxNC4xNzYuMjY1LjMzNi41MzcuNDQ1Ljg0OC4wNjcuMTk1LS4wMTkuMzU0LS4yNS40NTJ6IiBmaWxsPSIjNEQ2QkZFIj48L3BhdGg+PC9zdmc+Cg=="},"displayName":"DeepSeek Chat Model","typeVersion":1,"nodeCategories":[{"id":25,"name":"AI"},{"id":26,"name":"Langchain"}]},{"id":1281,"icon":"file:openrouter.svg","name":"@n8n/n8n-nodes-langchain.lmChatOpenRouter","codex":{"data":{"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatopenrouter/"}]},"categories":["AI","Langchain"],"subcategories":{"AI":["Language Models","Root Nodes"],"Language Models":["Chat Models (Recommended)"]}}},"group":"[\"transform\"]","defaults":{"name":"OpenRouter Chat Model"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjOTRBM0I4IiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMCAwIDI0IDI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjx0aXRsZT5PcGVuUm91dGVyPC90aXRsZT48cGF0aCBkPSJNMTYuODA0IDEuOTU3bDcuMjIgNC4xMDV2LjA4N0wxNi43MyAxMC4yMWwuMDE3LTIuMTE3LS44MjEtLjAzYy0xLjA1OS0uMDI4LTEuNjExLjAwMi0yLjI2OC4xMS0xLjA2NC4xNzUtMi4wMzguNTc3LTMuMTQ3IDEuMzUyTDguMzQ1IDExLjAzYy0uMjg0LjE5NS0uNDk1LjMzNi0uNjguNDU1bC0uNTE1LjMyMi0uMzk3LjIzNC4zODUuMjMuNTMuMzM4Yy40NzYuMzE0IDEuMTcuNzk2IDIuNzAxIDEuODY2IDEuMTEuNzc1IDIuMDgzIDEuMTc3IDMuMTQ3IDEuMzUybC4zLjA0NWMuNjk0LjA5MSAxLjM3NS4wOTQgMi44MjUuMDMzbC4wMjItMi4xNTkgNy4yMiA0LjEwNXYuMDg3TDE2LjU4OSAyMmwuMDE0LTEuODYyLS42MzUuMDIyYy0xLjM4Ni4wNDItMi4xMzcuMDAyLTMuMTM4LS4xNjItMS42OTQtLjI4LTMuMjYtLjkyNi00Ljg4MS0yLjA1OWwtMi4xNTgtMS41YTIxLjk5NyAyMS45OTcgMCAwMC0uNzU1LS40OThsLS40NjctLjI4YTU1LjkyNyA1NS45MjcgMCAwMC0uNzYtLjQzQzIuOTA4IDE0LjczLjU2MyAxNC4xMTYgMCAxNC4xMTZWOS44ODhsLjE0LjAwNGMuNTY0LS4wMDcgMi45MS0uNjIyIDMuODA5LTEuMTI0bDEuMDE2LS41OC40MzgtLjI3NGMuNDI4LS4yOCAxLjA3Mi0uNzI2IDIuNjg2LTEuODUzIDEuNjIxLTEuMTMzIDMuMTg2LTEuNzggNC44ODEtMi4wNTkgMS4xNTItLjE5IDEuOTc0LS4yMTMgMy44MTQtLjEzOGwuMDItMS45MDd6Ij48L3BhdGg+PC9zdmc+Cg=="},"displayName":"OpenRouter Chat Model","typeVersion":1,"nodeCategories":[{"id":25,"name":"AI"},{"id":26,"name":"Langchain"}]}],"categories":[{"id":35,"name":"Document Extraction"},{"id":48,"name":"AI RAG"}],"image":[]}}