{
  "id": "Y4vZ0z2b1xAcFQxy",
  "meta": {
    "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": "\u2b50 **Star us on GitHub!**\n\n[github.com/datadrivenconstruction/DDC-CWICR](https://github.com/datadrivenconstruction/OpenConstructionEstimate-DDC-CWICR)\n\n**DDC CWICR** \u2014 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": "\ud83d\udd10 Credentials Setup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -5600,
        -2032
      ],
      "parameters": {
        "color": 5,
        "width": 412,
        "height": 936,
        "content": "## \ud83d\udd10 Credentials Setup\n\n### \ud83d\udd11 TOKEN node:\n- `bot_token` - Telegram Bot API\n- `QDRANT_URL` - Qdrant server\n- `QDRANT_API_KEY` - Qdrant auth\n\n### n8n Credentials (Settings \u2192 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": "## \u2705 SETUP CHECKLIST\n\n**1. Telegram Bot**\n- [ ] Create via @BotFather\n- [ ] Token in \ud83d\udd11 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 \u2192 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": "## \ud83d\ude80 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- \ud83e\udd16 Parse Text (OpenAI/Claude/Gemini)\n- \ud83e\udd16 Transform Query\n- \ud83e\udd16 Rerank Results\n- \ud83d\udcca Embeddings (OpenAI)\n\n**Features:**\n- \ud83d\udcac Text input\n- \ud83c\udf0d 9 languages\n- \ud83d\udd0d Vector search\n- \ud83d\udcca HTML/Excel/PDF\n\n**No API keys in code!**\nUse n8n Credentials only."
      },
      "typeVersion": 1
    },
    {
      "id": "349fcf16-f55a-4300-aa2a-2bb9ed806716",
      "name": "\ud83d\udd11 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": "## \ud83c\udf0d 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": "## \ud83d\udd00 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": "## \ud83c\udf10 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 (\u20ac/$\u20bd\u00a5)\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": "## \ud83e\udde0 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": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// AGG - Final aggregation of calculation results + DDC CWICR info\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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 || '\u20ac',\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": "\ud83d\uddd1\ufe0f Delete Progress Msg",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -2096,
        -784
      ],
      "parameters": {
        "url": "=https://api.telegram.org/bot{{ $('\ud83d\udd11 TOKEN').first().json.bot_token }}/deleteMessage",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "jsonBody": "={\n  \"chat_id\": {{ $('\ud83e\uddf9 Prep Cleanup').first().json.chatId }},\n  \"message_id\": {{ $('\ud83e\uddf9 Prep Cleanup').first().json._delete_progress_msg || 0 }}\n}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "602ec477-8f11-47ec-8cfc-aaf8c9183a09",
      "name": "\ud83d\uddd1\ufe0f Delete Work Msg",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -2288,
        -784
      ],
      "parameters": {
        "url": "=https://api.telegram.org/bot{{ $('\ud83d\udd11 TOKEN').first().json.bot_token }}/deleteMessage",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "jsonBody": "={\n  \"chat_id\": {{ $('\ud83e\uddf9 Prep Cleanup').first().json.chatId }},\n  \"message_id\": {{ $('\ud83e\uddf9 Prep Cleanup').first().json._delete_work_msg || 0 }}\n}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "7a275264-fb92-4cf2-8d35-dbbbbdf5d2a3",
      "name": "\ud83e\uddf9 Prep Cleanup",
      "type": "n8n-nodes-base.code",
      "position": [
        -2464,
        -784
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// PREP CLEANUP - Prepare message IDs for deletion after loop completes\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// ACC - Accumulate calculation results for final aggregation\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst sd = $getWorkflowStaticData('global');\nconst w = $('\ud83d\udcca 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": "\ud83d\udce4 Edit Result",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -2096,
        0
      ],
      "parameters": {
        "url": "=https://api.telegram.org/bot{{ $('\ud83d\udd11 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": "\ud83d\udcca Update Result",
      "type": "n8n-nodes-base.code",
      "position": [
        -2256,
        0
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// UPDATE RESULT - Format result message (\u2713 Found / Not found)\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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 || '\u20ac';\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 ? '\u2713' : '\u2717', tc.toFixed(0), currency);\n\nlet text = '';\nif (found) {\n  text = current + '/' + total + ' ' + shortName + ' \u2713\\n';\n  text += tc.toFixed(0) + ' ' + currency;\n  if (rateCode && hasValidRate) text += ' \u00b7 ' + rateCode.substring(0, 20);\n} else {\n  text = current + '/' + total + ' ' + shortName + '\\n';\n  text += (L.not_found || '\u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e');\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\ufe0f\u20e3 Prep Query",
      "type": "n8n-nodes-base.code",
      "position": [
        -1920,
        -400
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// PREP QUERY - Prepare search query with Qdrant credentials from TOKEN\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst loopItem = $input.first().json;\nconst tokenData = $('\ud83d\udd11 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\u00b2';\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": "\ud83d\udcbe Save Work Msg",
      "type": "n8n-nodes-base.code",
      "position": [
        -2096,
        -400
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// SAVE WORK MSG - Store message ID for later editing/deletion\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst loopItem = $('\ud83d\udcdd 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": "\ud83d\udce4 Send Work",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -2272,
        -400
      ],
      "parameters": {
        "url": "=https://api.telegram.org/bot{{ $('\ud83d\udd11 TOKEN').first().json.bot_token }}/sendMessage",
        "method": "POST",
        "options": {},
        "jsonBody": "={\"chat_id\": {{ $(\"\ud83d\udcdd Prep Work Msg\").item.json.chatId }}, \"text\": {{ JSON.stringify($(\"\ud83d\udcdd Prep Work Msg\").item.json._work_text) }}, \"parse_mode\": \"Markdown\"}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "ac19ffbe-1ef7-4730-bc8b-5e6fa86b36d2",
      "name": "\ud83d\uddd1\ufe0f Delete Prev",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -2464,
        -400
      ],
      "parameters": {
        "url": "=https://api.telegram.org/bot{{ $('\ud83d\udd11 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": "\ud83d\udcdd Prep Work Msg",
      "type": "n8n-nodes-base.code",
      "position": [
        -2640,
        -400
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// PREP WORK MSG - Create localized \"Searching...\" message for current work\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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\u00b2';\n\nlet shortName = name.length > 30 ? name.substring(0, 27) + '...' : name;\n\n// Localized search messages\nconst SEARCH_WORD = {\n  'German': '\ud83d\udd0d Suche',\n  'English': '\ud83d\udd0d Searching',\n  'Russian': '\ud83d\udd0d \u041f\u043e\u0438\u0441\u043a',\n  'Spanish': '\ud83d\udd0d Buscando',\n  'French': '\ud83d\udd0d Recherche',\n  'Portuguese': '\ud83d\udd0d Buscando',\n  'Chinese': '\ud83d\udd0d \u641c\u7d22\u4e2d',\n  'Arabic': '\ud83d\udd0d \u0628\u062d\u062b',\n  'Hindi': '\ud83d\udd0d \u0916\u094b\u091c'\n};\n\nconst searchWord = SEARCH_WORD[lang] || '\ud83d\udd0d Searching';\n\n// Format: \"\ud83d\udd0d \u041f\u043e\u0438\u0441\u043a 3/26\\n\u0413\u0438\u043f\u0441\u043e\u043a\u0430\u0440\u0442\u043e\u043d 25m\u00b2\"\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": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// PREP WORKS - Prepare work items for calculation loop (: 5 items)\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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 || '\u20ac',\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": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// SAVE PROGRESS ID - Store initial progress message ID for cleanup\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst cfg = $('Config').first().json;\nconst prepData = $('\ud83d\udcdd 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": "\ud83d\udce4 Send Progress",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -3488,
        -1024
      ],
      "parameters": {
        "url": "=https://api.telegram.org/bot{{ $('\ud83d\udd11 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": "\ud83d\udcdd Prep Progress",
      "type": "n8n-nodes-base.code",
      "position": [
        -3728,
        -1024
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// PREP PROGRESS - Show calculation progress message (localized)\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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: `\ud83d\udd0d *Preissuche l\u00e4uft...*\\n\\n${totalWorks} Positionen\\n\u23f1 ~${estimatedMinutes} Min`,\n  EN: `\ud83d\udd0d *Searching prices...*\\n\\n${totalWorks} items\\n\u23f1 ~${estimatedMinutes} min`,\n  RU: `\ud83d\udd0d *\u041f\u043e\u0438\u0441\u043a \u0440\u0430\u0441\u0446\u0435\u043d\u043e\u043a...*\\n\\n${totalWorks} \u043f\u043e\u0437\u0438\u0446\u0438\u0439\\n\u23f1 ~${estimatedMinutes} \u043c\u0438\u043d`,\n  ES: `\ud83d\udd0d *Buscando precios...*\\n\\n${totalWorks} elementos\\n\u23f1 ~${estimatedMinutes} min`,\n  FR: `\ud83d\udd0d *Recherche des prix...*\\n\\n${totalWorks} \u00e9l\u00e9ments\\n\u23f1 ~${estimatedMinutes} min`,\n  PT: `\ud83d\udd0d *Buscando pre\u00e7os...*\\n\\n${totalWorks} itens\\n\u23f1 ~${estimatedMinutes} min`,\n  ZH: `\ud83d\udd0d *\u6b63\u5728\u641c\u7d22\u4ef7\u683c...*\\n\\n${totalWorks} \u9879\u76ee\\n\u23f1 ~${estimatedMinutes} \u5206\u949f`,\n  AR: `\ud83d\udd0d *\u062c\u0627\u0631\u064a \u0627\u0644\u0628\u062d\u062b \u0639\u0646 \u0627\u0644\u0623\u0633\u0639\u0627\u0631...*\\n\\n${totalWorks} \u0639\u0646\u0627\u0635\u0631\\n\u23f1 ~${estimatedMinutes} \u062f\u0642\u064a\u0642\u0629`,\n  HI: `\ud83d\udd0d *\u0915\u0940\u092e\u0924\u0947\u0902 \u0916\u094b\u091c \u0930\u0939\u0947 \u0939\u0948\u0902...*\\n\\n${totalWorks} \u0906\u0907\u091f\u092e\\n\u23f1 ~${estimatedMinutes} \u092e\u093f\u0928\u091f`\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": "\ud83d\udce4 Send Works",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -3328,
        -1696
      ],
      "parameters": {
        "url": "=https://api.telegram.org/bot{{ $('\ud83d\udd11 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": "\ud83d\udcca Show Works",
      "type": "n8n-nodes-base.code",
      "position": [
        -3472,
        -1696
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// SHOW WORKS - Display extracted work items with edit buttons\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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) + '\u2026' : 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 || '\u043a\u043e\u043c\u043d\u0430\u0442';\nconst workWord = L.works_identified || '\u043f\u043e\u0437\u0438\u0446\u0438\u0439';\nconst generalWord = L.general || '\u041e\u0431\u0449\u0435\u0435';\n\nlet msg = '*' + rooms.length + ' ' + roomWord;\nif (totalArea > 0) msg += ' \u00b7 ' + (Math.round(totalArea * 10) / 10) + ' m\u00b2';\nmsg += '*\\n';\nmsg += '_' + works.length + ' ' + workWord + '_\\n\\n';\n\nif (works.length === 0) {\n  msg += '_' + (L.no_works || '\u0420\u0430\u0431\u043e\u0442\u044b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b') + '_\\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 ? ' \u00b7 ' + room.area_m2 + ' m\u00b2' : '';\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 + ' \u2014 ' + 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 + ' \u2014 ' + 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: '\u270f' + (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 || '+ \u041f\u043e\u0437\u0438\u0446\u0438\u044f', callback_data: 'add_work' },\n  { text: L.btn_calc || '\u25b6 \u0420\u0430\u0441\u0447\u0451\u0442', callback_data: 'calculate' }\n]);\nkeyboard.push([\n  { text: L.btn_new || '\ud83d\udd04 \u0417\u0430\u043d\u043e\u0432\u043e', 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\ufe0f\u20e3 Calculate",
      "type": "n8n-nodes-base.code",
      "position": [
        -2464,
        0
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// STEP 8: Calculate - FIXED VERSION v3\n// \u0423\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0438\u0441\u043a \u0434\u0430\u043d\u043d\u044b\u0445 \u0432 \u0440\u0430\u0437\u043d\u044b\u0445 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430\u0445\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst inputData = $input.first().json;\n\nconsole.log('\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550');\nconsole.log('STEP 8: CALCULATE v3');\nconsole.log('\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550');\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// \u0423\u041c\u041d\u042b\u0419 \u041f\u041e\u0418\u0421\u041a PAYLOAD - \u0438\u0449\u0435\u043c \u0432\u043e \u0432\u0441\u0435\u0445 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u044b\u0445 \u043c\u0435\u0441\u0442\u0430\u0445\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nlet payload = null;\nlet payloadSource = 'not_found';\n\n// \u0412\u0430\u0440\u0438\u0430\u043d\u0442 1: _best_result.payload\nif (inputData._best_result?.payload?.rate_code) {\n  payload = inputData._best_result.payload;\n  payloadSource = '_best_result.payload';\n}\n// \u0412\u0430\u0440\u0438\u0430\u043d\u0442 2: _best_payload \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e\nelse if (inputData._best_payload?.rate_code) {\n  payload = inputData._best_payload;\n  payloadSource = '_best_payload';\n}\n// \u0412\u0430\u0440\u0438\u0430\u043d\u0442 3: \u0434\u0430\u043d\u043d\u044b\u0435 \u043b\u0435\u0436\u0430\u0442 \u043f\u0440\u044f\u043c\u043e \u0432 _best_result (\u0431\u0435\u0437 \u0432\u043b\u043e\u0436\u0435\u043d\u043d\u043e\u0433\u043e payload)\nelse if (inputData._best_result?.rate_code) {\n  payload = inputData._best_result;\n  payloadSource = '_best_result (direct)';\n}\n// \u0412\u0430\u0440\u0438\u0430\u043d\u0442 4: payload \u0432\u043d\u0443\u0442\u0440\u0438 payload (\u0434\u0432\u043e\u0439\u043d\u0430\u044f \u0432\u043b\u043e\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c)\nelse if (inputData._best_result?.payload?.payload?.rate_code) {\n  payload = inputData._best_result.payload.payload;\n  payloadSource = '_best_result.payload.payload';\n}\n// \u0412\u0430\u0440\u0438\u0430\u043d\u0442 5: \u0438\u0449\u0435\u043c rate_code \u0433\u0434\u0435 \u0443\u0433\u043e\u0434\u043d\u043e \u0432 _best_result\nelse if (inputData._best_result) {\n  const br = inputData._best_result;\n  // \u0420\u0435\u043a\u0443\u0440\u0441\u0438\u0432\u043d\u044b\u0439 \u043f\u043e\u0438\u0441\u043a\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// \u0415\u0441\u043b\u0438 payload \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d - \u0432\u044b\u0432\u043e\u0434\u0438\u043c \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443 \u0434\u043b\u044f \u0434\u0438\u0430\u0433\u043d\u043e\u0441\u0442\u0438\u043a\u0438\nif (!payload) {\n  console.log('');\n  console.log('\u274c 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  // \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c \u043e\u0448\u0438\u0431\u043a\u0443 \u0441 \u0434\u0438\u0430\u0433\u043d\u043e\u0441\u0442\u0438\u043a\u043e\u0439\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// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// \u0422\u0435\u043f\u0435\u0440\u044c payload \u043d\u0430\u0439\u0434\u0435\u043d - \u0438\u0437\u0432\u043b\u0435\u043a\u0430\u0435\u043c \u0434\u0430\u043d\u043d\u044b\u0435\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n// \u0411\u0430\u0437\u043e\u0432\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0440\u0430\u0431\u043e\u0442\u044b\nconst workName = inputData.name || inputData.sq || '';\nconst workQty = inputData.qty || 1;\nconst workUnit = inputData.unit || 'm\u00b2';\n\nconsole.log('');\nconsole.log('Work:', workName);\nconsole.log('Qty:', workQty, workUnit);\n\n// \u0414\u0430\u043d\u043d\u044b\u0435 \u0440\u0430\u0441\u0446\u0435\u043d\u043a\u0438\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// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nconst costSummary = payload.cost_summary || {};\nlet totalCost = parseFloat(costSummary.total_cost_position || costSummary.total_cost || 0);\n\n// \u0415\u0441\u043b\u0438 cost_summary \u043f\u0443\u0441\u0442\u043e\u0439, \u0432\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u043c \u0438\u0437 \u0440\u0435\u0441\u0443\u0440\u0441\u043e\u0432\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// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// \u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c \u0434\u0435\u043b\u0438\u0442\u0435\u043b\u044c \u0435\u0434\u0438\u043d\u0438\u0446\u044b \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nlet unitDivisor = 1;\nconst rateUnitLower = (rateUnit || '').toLowerCase();\n\nif (rateUnitLower.includes('100 ') || rateUnitLower === '100 \u043c' || rateUnitLower === '100 \u043c\u00b2' || rateUnitLower === '100 \u043c2') {\n  unitDivisor = 100;\n} else if (rateUnitLower.match(/^10\\s/) || rateUnitLower.includes('10 \u043c')) {\n  unitDivisor = 10;\n}\n\nconsole.log('Unit divisor:', unitDivisor);\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Price calculation\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0440\u0435\u0441\u0443\u0440\u0441\u043e\u0432\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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  // \u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c \u0442\u0438\u043f \u0440\u0435\u0441\u0443\u0440\u0441\u0430\n  let resourceType = 'material';\n  const rowTypeLower = (rowType || '').toLowerCase();\n  const codeUpper = (code || '').toUpperCase();\n  \n  if (rowTypeLower === '\u043c\u0430\u0448\u0438\u043d\u0438\u0441\u0442' || rowTypeLower.includes('\u043c\u0430\u0448\u0438\u043d\u0438\u0441\u0442')) {\n    resourceType = 'labor';\n  } else if (rowTypeLower === '\u044d\u043b\u0435\u043a\u0442\u0440\u0438\u0447\u0435\u0441\u0442\u0432\u043e' || rowTypeLower.includes('\u044d\u043b\u0435\u043a\u0442\u0440\u0438\u0447\u0435\u0441\u0442\u0432')) {\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  // \u041c\u0430\u0441\u0448\u0442\u0430\u0431\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\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  // \u0421\u0443\u043c\u043c\u0438\u0440\u0443\u0435\u043c \u043f\u043e \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f\u043c\n  if (resourceType === 'labor') {\n    workersTotal += scaledCost;\n    if (unit === '\u0447' || unit === '\u0447\u0435\u043b.-\u0447' || unit === '\u0447\u0435\u043b-\u0447' || unit === '\u043c\u0430\u0448.-\u0447') {\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// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Scope of work\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nconst workSteps = payload.work_steps || [];\nconst scopeOfWork = workSteps.map(s => s.text || '').filter(t => t.length > 0);\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Quality\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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('\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550');\nconsole.log('\u2705 CALCULATION COMPLETE');\nconsole.log('Rate:', rateCode, '-', rateName?.substring(0, 40));\nconsole.log('Total:', tc.toFixed(2), 'EUR');\nconsole.log('Resources:', resources.length);\nconsole.log('\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550');\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// \u0424\u0418\u041d\u0410\u041b\u042c\u041d\u042b\u0419 \u0420\u0415\u0417\u0423\u041b\u042c\u0422\u0410\u0422\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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\ufe0f\u20e3 Apply Rerank",
      "type": "n8n-nodes-base.code",
      "position": [
        -1264,
        -208
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// 8\ufe0f\u20e3 APPLY RERANK v5 - Fixed empty results handling\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst prepData = $('6\ufe0f\u20e3 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 \u043e\u0442 markdown \u0438 \u043b\u0438\u0448\u043d\u0438\u0445 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432\n  text = text.replace(/```json\\n?/g, '').replace(/```/g, '').trim();\n  \n  console.log('Raw LLM response:', text.substring(0, 200));\n  \n  // \u041f\u044b\u0442\u0430\u0435\u043c\u0441\u044f \u043d\u0430\u0439\u0442\u0438 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 - \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c 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// \u041a\u043e\u043c\u0431\u0438\u043d\u0438\u0440\u0443\u0435\u043c LLM \u0438 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 \u0432\u0430\u0436\u043d\u0435\u0435 \u0435\u0441\u043b\u0438 \u0435\u0441\u0442\u044c, \u0438\u043d\u0430\u0447\u0435 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 \u043f\u043e \u043a\u043e\u043c\u0431\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c\u0443 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 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430\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('\u2705 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\ufe0f\u20e3 Prep Rerank",
      "type": "n8n-nodes-base.code",
      "position": [
        -1920,
        -208
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// 6\ufe0f\u20e3 PREP RERANK v4 - Fixed for LangChain/n8n vector store format\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst prepData = $('4\ufe0f\u20e3 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\u00b2';\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 \u043a\u043e\u043c\u043f\u0430\u043a\u0442\u043d\u043e\u0435 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u043a\u0430\u043d\u0434\u0438\u0434\u0430\u0442\u043e\u0432\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\u2022 \u0413\u041a\u041b (\u0433\u0438\u043f\u0441\u043e\u043a\u0430\u0440\u0442\u043e\u043d) \u2260 \u0413\u0412\u041b (\u0433\u0438\u043f\u0441\u043e\u0432\u043e\u043b\u043e\u043a\u043d\u043e) - DIFFERENT materials, max 60\n\u2022 \u0421\u0442\u0435\u043d\u044b \u2260 \u041f\u043e\u0442\u043e\u043b\u043e\u043a - check work location matches\n\u2022 \u041c\u043e\u043d\u0442\u0430\u0436 \u2260 \u0414\u0435\u043c\u043e\u043d\u0442\u0430\u0436 - opposite operations\n\u2022 1 \u0441\u043b\u043e\u0439 \u2260 2 \u0441\u043b\u043e\u044f - check layer count\n\u2022 Profile types: \u041f\u041f60 = ceiling, \u041f\u0421 = wall stud, \u041f\u041d = guide\n\nUNIT MATCHING:\n\u2022 Query unit: ${workUnit}\n\u2022 Rate unit should be compatible or convertible\n\u2022 m\u00b2 work \u2192 m\u00b2 rate (ideal)\n\u2022 m\u00b2 work \u2192 100m\u00b2 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\ufe0f\u20e3 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\ufe0f\u20e3 Extract Embedding",
      "type": "n8n-nodes-base.code",
      "position": [
        -2256,
        -208
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// EXTRACT EMBEDDING - Get vector from OpenAI response\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst prepData = $('2\ufe0f\u20e3 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\ufe0f\u20e3 Extract Transform",
      "type": "n8n-nodes-base.code",
      "position": [
        -1264,
        -400
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// EXTRACT TRANSFORM - Clean AI response and combine queries\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst prepData = $('1\ufe0f\u20e3 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": "\ud83d\udce4 Details",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -3728,
        -720
      ],
      "parameters": {
        "url": "=https://api.telegram.org/bot{{ $('\ud83d\udd11 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 || '\u2193 Excel' }}\", \"callback_data\": \"export_excel\"}, {\"text\": \"{{ $json.L.btn_export_pdf || '\u2193 PDF' }}\", \"callback_data\": \"export_pdf\"}],\n      [{\"text\": \"{{ $json.L.btn_restart || '\u21bb 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": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// DDC CWICR - Data Driven Construction Cost Estimator\n// https://DataDrivenConstruction.io\n// Open source construction cost database with 55,000+ work items\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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 || '\u20ac';\n\nfunction fmtCur(v) { \n  if (!v || v === 0) return sym + ' 0';\n  return sym + ' ' + v.toFixed(2); \n}\n\nlet msg = `*${L.ready} \u2014 ${L.resources}*\\n\\n`;\n\nworks.forEach((w, i) => {\n  const qi = { 'high': '\u25cf', 'medium': '\u25cb', 'low': '\u25cc', 'not_found': '\u2715' }[w.ql] || '\u25cb';\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} \u00d7 ${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(' \u00b7 ')}_\\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 ? '\u2514' : '\u251c';\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 += `\u2514 _...+${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\ud83d\udccb *${L.scope_title || 'Scope of Work'}:*\\n`;\n    scope.forEach((s, si) => {\n      const prefix = si === scope.length - 1 ? '\u2514' : '\u251c';\n      const sText = (s || '').substring(0, 45);\n      msg += `${prefix} ${sText}\\n`;\n    });\n  }\n  msg += `\\n`;\n});\n\n// Summary\nmsg += `\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\\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": "\ud83d\udce4 Fallback",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -4272,
        -1232
      ],
      "parameters": {
        "url": "=https://api.telegram.org/bot{{ $('\ud83d\udd11 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": "\ud83d\udce4 Help",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -3920,
        -720
      ],
      "parameters": {
        "url": "=https://api.telegram.org/bot{{ $('\ud83d\udd11 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\ud83d\udcf8 Photo analysis with AI\\n\ud83d\udcdd Text input with work lists\\n\ud83c\udf0d 9 languages supported\\n\ud83d\udcca 55,000+ work items\\n\ud83d\udcb0 Regional pricing (EUR, USD, RUB, etc.)\\n\ud83d\udcc4 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\": \"\u25c0\ufe0f Back\", \"callback_data\": \"back_to_lang\"}]]\n  }\n}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "db60dd77-e822-4331-bd7c-3aa78e4da29c",
      "name": "\ud83d\udce4 Send PDF",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -3488,
        -880
      ],
      "parameters": {
        "chatId": "={{ $json.chatId }}",
        "operation": "sendDocument",
        "binaryData": true,
        "additionalFields": {
          "caption": "={{ $json.L?.export_pdf_msg || '\ud83d\udcc4 PDF Export (HTML)' }}",
          "fileName": "={{ $json.filename }}"
        },
        "binaryPropertyName": "pdf"
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "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": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// DDC CWICR - Data Driven Construction Cost Estimator\n// https://DataDrivenConstruction.io\n// Open source construction cost database with 55,000+ work items\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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: '\u274c 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": "\ud83d\udce4 Send Excel",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -3728,
        -560
      ],
      "parameters": {
        "chatId": "={{ $json.chatId }}",
        "operation": "sendDocument",
        "binaryData": true,
        "additionalFields": {
          "caption": "={{ $json.L?.export_excel_msg || '\ud83d\udcca Excel Export (CSV)' }}",
          "fileName": "={{ $json.filename }}"
        },
        "binaryPropertyName": "excel"
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "f2db6463-97c3-48e2-84e3-0a8f5fb1708e",
      "name": "Generate Excel",
      "type": "n8n-nodes-base.code",
      "position": [
        -3920,
        -560
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// DDC CWICR - Data Driven Construction Cost Estimator\n// https://DataDrivenConstruction.io\n// Open source construction cost database with 55,000+ work items\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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": "\ud83d\udce4 Send HTML",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -1152,
        -784
      ],
      "parameters": {
        "chatId": "={{ $json.chatId }}",
        "operation": "sendDocument",
        "binaryData": true,
        "additionalFields": {
          "caption": "\ud83d\udcca Professional HTML Report",
          "fileName": "={{ $json.filename }}"
        },
        "binaryPropertyName": "html"
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "547ccce6-8b5c-415d-9c20-6e2ccd30bf91",
      "name": "Prep HTML File",
      "type": "n8n-nodes-base.code",
      "position": [
        -1376,
        -784
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// DDC CWICR - Data Driven Construction Cost Estimator\n// https://DataDrivenConstruction.io\n// Open source construction cost database with 55,000+ work items\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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": "\ud83d\udce4 Final",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1376,
        -928
      ],
      "parameters": {
        "url": "=https://api.telegram.org/bot{{ $('\ud83d\udd11 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": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// DDC CWICR - Final message (ultra-compact for Telegram 4096 limit)\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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) + '\u2026';\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 + ' \u00b7 ' + 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(' \u00b7 '));\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 \u00b7 ' + 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": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// DDC CWICR - Generate HTML Report with expandable resources\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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} \u00b7 ${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 : '\u2014';\n  \n  const hasScope = (w.scope_of_work || []).length > 0;\n  const scopeBtn = hasScope ? '<span class=\"scope-toggle\" onclick=\"event.stopPropagation();toggleScope(' + i + ')\">\ud83d\udccb</span>' : '';\n  \n  html += `<tr class=\"work-row\" data-work=\"${i}\" onclick=\"toggleWork(${i})\">\n    <td class=\"toggle\"><span id=\"icon-${i}\">${isFirst ? '\u25bc' : '\u25b6'}</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\">\ud83d\udccb ${esc(L.scope_title || 'Scope of Work')}:</strong><br>\n        ${scopeItems.map((s, si) => '<span style=\"color:#555\">\u2022 ' + 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\">\u00d7' + 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> \u00b7 \n        <span style=\"color:var(--material-text)\">${L.materials || 'Mat'}: ${fmt(matSum)}</span> \u00b7 \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> \u00b7 \n  Open Source Construction Cost Database \u00b7 ${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 ? '\u25bc' : '\u25b6';\n}\nfunction expandAll() {\n  document.querySelectorAll('tr[data-work]').forEach(r => r.classList.remove('hidden'));\n  document.querySelectorAll('[id^=\"icon-\"]').forEach(i => i.textContent = '\u25bc');\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 = '\u25b6');\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{{ $('\ud83d\udd11 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": "\ud83d\udce4 Works Updated",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -4128,
        -1376
      ],
      "parameters": {
        "url": "=https://api.telegram.org/bot{{ $('\ud83d\udd11 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": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// WORKS UPDATED - Show updated works list after quantity change\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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 = '\u2705 ' + (L.work_added || '\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u043e') + '\\n\\n';\nmsg += '*' + works.length + ' ' + (L.items || '\u043f\u043e\u0437\u0438\u0446\u0438\u0439') + '*\\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 + ' \u2014 ' + w.qty + ' ' + (w.unit || 'm\u00b2') + '\\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: '\u270f\ufe0f' + (i + j + 1), callback_data: 'edit_work_' + (i + j) });\n  }\n  keyboard.push(row);\n}\n\nkeyboard.push([\n  { text: L.btn_add_work || '+ \u041f\u043e\u0437\u0438\u0446\u0438\u044f', callback_data: 'add_work' },\n  { text: L.btn_calc || '\u25b6 \u0420\u0430\u0441\u0447\u0451\u0442', callback_data: 'calculate' }\n]);\nkeyboard.push([{ text: L.btn_new || '\ud83d\udd04 \u0417\u0430\u043d\u043e\u0432\u043e', callback_data: 'restart' }]);\n\nreturn { json: { ...cfg, msg, keyboard } };"
      },
      "typeVersion": 2
    },
    {
      "id": "b4972bc3-4ad6-4fc9-ba80-c2b54fa94ae8",
      "name": "\ud83d\udce4 Ask New Work",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -4272,
        -2128
      ],
      "parameters": {
        "url": "=https://api.telegram.org/bot{{ $('\ud83d\udd11 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": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// EDIT MENU - Show edit buttons for selected work item\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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 = '\u270f\ufe0f *' + (L.btn_edit || '\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435') + '*\\n\\n';\nmsg += '*' + (idx + 1) + '. ' + name + '*\\n';\nmsg += '\ud83d\udccf ' + work.qty + ' ' + (work.unit || 'm\u00b2') + '\\n\\n';\nmsg += (L.edit_hint || '\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e:');\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: '\u00f72', callback_data: 'qty_work_' + idx + '_half' },\n    { text: '\u00d72', callback_data: 'qty_work_' + idx + '_double' }\n  ],\n  [\n    { text: '\ud83d\uddd1 ' + (L.btn_delete || '\u0423\u0434\u0430\u043b\u0438\u0442\u044c'), callback_data: 'delete_work_' + idx }\n  ],\n  [\n    { text: '\u25c0\ufe0f ' + (L.btn_done || '\u041d\u0430\u0437\u0430\u0434'), callback_data: 'done_editing' }\n  ]\n];\n\nreturn { json: { ...cfg, msg, keyboard, chatId: cid } };"
      },
      "typeVersion": 2
    },
    {
      "id": "e1cb2be1-216e-4826-9438-004b65c3fa57",
      "name": "\ud83d\udce4 Lang OK",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -3968,
        -2288
      ],
      "parameters": {
        "url": "=https://api.telegram.org/bot{{ $('\ud83d\udd11 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{{ $('\ud83d\udd11 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": "\ud83d\udce4 Lang Menu",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -4272,
        -2432
      ],
      "parameters": {
        "url": "=https://api.telegram.org/bot{{ $('\ud83d\udd11 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\u25b8 Photo analysis (up to 4)\\n\u25b8 Text description\\n\u25b8 9 languages \u00b7 55,000+ work items\\n\u25b8 Excel & PDF export\\n\\nSelect language:\",\n  \"parse_mode\": \"Markdown\",\n  \"reply_markup\": {\n    \"inline_keyboard\": [\n      [{\"text\": \"\ud83c\udde9\ud83c\uddea Deutsch\", \"callback_data\": \"lang_DE\"}, {\"text\": \"\ud83c\uddec\ud83c\udde7 English\", \"callback_data\": \"lang_EN\"}, {\"text\": \"\ud83c\uddf7\ud83c\uddfa \u0420\u0443\u0441\u0441\u043a\u0438\u0439\", \"callback_data\": \"lang_RU\"}],\n      [{\"text\": \"\ud83c\uddea\ud83c\uddf8 Espa\u00f1ol\", \"callback_data\": \"lang_ES\"}, {\"text\": \"\ud83c\uddeb\ud83c\uddf7 Fran\u00e7ais\", \"callback_data\": \"lang_FR\"}, {\"text\": \"\ud83c\udde7\ud83c\uddf7 Portugu\u00eas\", \"callback_data\": \"lang_PT\"}],\n      [{\"text\": \"\ud83c\udde8\ud83c\uddf3 \u4e2d\u6587\", \"callback_data\": \"lang_ZH\"}, {\"text\": \"\ud83c\udde6\ud83c\uddea \u0627\u0644\u0639\u0631\u0628\u064a\u0629\", \"callback_data\": \"lang_AR\"}, {\"text\": \"\ud83c\uddee\ud83c\uddf3 \u0939\u093f\u0928\u094d\u0926\u0940\", \"callback_data\": \"lang_HI\"}],\n      [{\"text\": \"\u2753 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": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// DDC CWICR - Data Driven Construction Cost Estimator\n// https://DataDrivenConstruction.io\n// Open source construction cost database with 55,000+ work items\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n// CONFIG v8.5 PRO - Professional style localization + PDF SUPPORT\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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\u00fccken Sie /start um zu beginnen', \n    rooms: 'R\u00e4ume', 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: '\ud83c\udde9\ud83c\uddea', native: 'Deutsch', cur: 'EUR', sym: '\u20ac', loc: 'de-DE', region: 'Berlin', search_lang: 'German',\n    // Professional welcome with PDF support\n    ok: '\u2705 *Deutsch* \u00b7 Berlin \u00b7 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\ud83d\udcc4 *PDF-Zeichnungen* \u2014 Grundriss oder Bauplan (max. 3 Seiten)\n\ud83d\udcf7 *Foto* \u2014 Raum oder Objekt fotografieren\n\u270f\ufe0f *Text* \u2014 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 \ud83d\udcf7\ud83d\udcc4`,\n    // PDF specific\n    pdf_received: '\ud83d\udcc4 *PDF erhalten*',\n    pdf_processing: '\u23f3 Analysiere Zeichnung...',\n    pdf_pages: 'Seiten',\n    pdf_page_limit: '\u26a0\ufe0f Nur erste 3 Seiten werden verarbeitet',\n    pdf_rooms_found: '\ud83c\udfe0 R\u00e4ume gefunden',\n    pdf_elements_found: '\ud83e\uddf1 Elemente gefunden',\n    pdf_works_generated: '\ud83d\udcdd Arbeiten generiert',\n    pdf_analyzing_page: '\ud83d\udd0d Analysiere Seite',\n    pdf_of: 'von',\n    pdf_complete: '\u2705 Analyse abgeschlossen',\n    pdf_error: '\u274c PDF-Verarbeitungsfehler',\n    // Rest of translations\n    photo_added: '\u2705 Foto hinzugef\u00fcgt',\n    photos_count: 'Fotos',\n    add_more: '+ Weitere Fotos',\n    analyze_now: '\u25b6 Analyse starten',\n    analyzing: 'Bildanalyse l\u00e4uft...',\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\u00e4te',\n    materials: 'Material',\n    subtotal: 'Zusammenfassung',\n    searching: 'Suche',\n    of: 'von',\n    not_found: 'Keine \u00dcbereinstimmung',\n    low_conf: 'Pr\u00fcfung empfohlen',\n    price_note: 'Preisbasis: Berlin 2025',\n    btn_calc: '\u25b6 Berechnen',\n    btn_new: '+ Neues Projekt',\n    btn_lang: '\u2699 Sprache',\n    btn_edit: '\u270e \u00c4ndern',\n    btn_delete: '\u2715 Entfernen',\n    btn_add_work: '+ Position',\n    btn_done: '\u2705 Fertig',\n    btn_export_excel: '\u2193 Excel',\n    btn_export_pdf: '\u2193 PDF',\n    btn_restart: '\u21bb Neu starten',\n    btn_help: '? Hilfe',\n    loading: 'Preisermittlung l\u00e4uft...',\n    more_in_html: 'Detaillierter Bericht verf\u00fcgbar',\n    resources: 'Details: Ressourcen & Leistungsumfang',\n    enter_work: '*Neue Position hinzuf\u00fcgen*\\nFormat: Bezeichnung, Menge Einheit\\nBeispiel: Gipskartonwand, 15 m\u00b2',\n    work_added: '\u2705 Position hinzugef\u00fcgt',\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\u00e4ne oder Beschreibung.\nPDF: max. 3 Seiten werden analysiert.\n\n*2. Pr\u00fcfung*\n\u00dcberpr\u00fcfen 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 \u2014 Neues Projekt\n/help \u2014 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\u00e4te',\n    res_labor: 'Lohn', res_material: 'Mat', res_machine: 'Ger',\n    collapse_all: 'Alle einklappen', expand_all: 'Alle ausklappen',\n    quality_high: 'Hohe \u00dcbereinstimmung', quality_medium: 'Mittlere \u00dcbereinstimmung', quality_low: 'Geringe \u00dcbereinstimmung',\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: '\u043a\u043e\u043c\u043d\u0430\u0442', works_identified: 'works', general: '\u041e\u0431\u0449\u0435\u0435', no_works: '\u0420\u0430\u0431\u043e\u0442\u044b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b', items: '\u043f\u043e\u0437\u0438\u0446\u0438\u0439', min: '\u043c\u0438\u043d', \n    db: 'ENG_TORONTO_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR', \n    name: 'English', flag: '\ud83c\uddec\ud83c\udde7', native: 'English', cur: 'CAD', sym: '$', loc: 'en-CA', region: 'Toronto', search_lang: 'English',\n    ok: '\u2705 *English* \u00b7 Toronto \u00b7 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\ud83d\udcc4 *PDF drawings* \u2014 floor plan or blueprint (max 3 pages)\n\ud83d\udcf7 *Photo* \u2014 photograph room or object\n\u270f\ufe0f *Text* \u2014 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 \ud83d\udcf7\ud83d\udcc4`,\n    // PDF specific\n    pdf_received: '\ud83d\udcc4 *PDF received*',\n    pdf_processing: '\u23f3 Analyzing drawing...',\n    pdf_pages: 'pages',\n    pdf_page_limit: '\u26a0\ufe0f Processing first 3 pages only',\n    pdf_rooms_found: '\ud83c\udfe0 Rooms found',\n    pdf_elements_found: '\ud83e\uddf1 Elements found',\n    pdf_works_generated: '\ud83d\udcdd Works generated',\n    pdf_analyzing_page: '\ud83d\udd0d Analyzing page',\n    pdf_of: 'of',\n    pdf_complete: '\u2705 Analysis complete',\n    pdf_error: '\u274c PDF processing error',\n    // Rest\n    photo_added: '\u2705 Photo added',\n    photos_count: 'photos',\n    add_more: '+ Add more',\n    analyze_now: '\u25b6 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: '\u25b6 Calculate',\n    btn_new: '+ New Project',\n    btn_lang: '\u2699 Language',\n    btn_edit: '\u270e Edit',\n    btn_delete: '\u2715 Remove',\n    btn_add_work: '+ Add Item',\n    btn_done: '\u2705 Done',\n    btn_export_excel: '\u2193 Excel',\n    btn_export_pdf: '\u2193 PDF',\n    btn_restart: '\u21bb 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\u00b2',\n    work_added: '\u2705 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. \u0420\u0430\u0441\u0447\u0451\u0442*\nPricing from DDC CWICR database.\n\n*4. Export*\nExport as Excel or PDF.\n\n*Commands:*\n/start \u2014 New project\n/help \u2014 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: '\u041d\u0430\u0436\u043c\u0438\u0442\u0435 /start \u0434\u043b\u044f \u043d\u0430\u0447\u0430\u043b\u0430', \n    rooms: '\u043a\u043e\u043c\u043d\u0430\u0442', works_identified: '\u043f\u043e\u0437\u0438\u0446\u0438\u0439', general: '\u041e\u0431\u0449\u0435\u0435', no_works: '\u0420\u0430\u0431\u043e\u0442\u044b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b', items: '\u043f\u043e\u0437\u0438\u0446\u0438\u0439', min: '\u043c\u0438\u043d', \n    db: 'RU_STPETERSBURG_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR', \n    name: 'Russian', flag: '\ud83c\uddf7\ud83c\uddfa', native: '\u0420\u0443\u0441\u0441\u043a\u0438\u0439', cur: 'RUB', sym: '\u20bd', loc: 'ru-RU', region: '\u0421\u0430\u043d\u043a\u0442-\u041f\u0435\u0442\u0435\u0440\u0431\u0443\u0440\u0433', search_lang: 'Russian',\n    ok: '\u2705 *\u0420\u0443\u0441\u0441\u043a\u0438\u0439* \u00b7 \u0421\u041f\u0431 \u00b7 RUB',\n    text_prompt: `*\u041e\u043f\u0438\u0448\u0438\u0442\u0435 \u0440\u0430\u0431\u043e\u0442\u044b:*\\n\\n_\u041f\u0440\u0438\u043c\u0435\u0440:_\\n\\`\\`\\`\\n\u0413\u0438\u043f\u0441\u043e\u043a\u0430\u0440\u0442\u043e\u043d 2 \u0441\u043b\u043e\u044f 25\u043c2\\n\u041f\u043b\u0438\u0442\u043a\u0430 \u0432\u0430\u043d\u043d\u0430\u044f 15\u043c2\\n\u041f\u043e\u043a\u0440\u0430\u0441\u043a\u0430 \u0441\u0442\u0435\u043d 120\u043c2\\n\\`\\`\\``,\n    photo: `*\u041e\u0442\u043f\u0440\u0430\u0432\u044c\u0442\u0435 \u0444\u043e\u0442\u043e, PDF \u0438\u043b\u0438 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435*\n\n\ud83d\udcc4 *PDF \u0447\u0435\u0440\u0442\u0435\u0436\u0438* \u2014 \u043f\u043b\u0430\u043d \u044d\u0442\u0430\u0436\u0430 \u0438\u043b\u0438 \u0447\u0435\u0440\u0442\u0451\u0436 (\u0434\u043e 3 \u0441\u0442\u0440.)\n\ud83d\udcf7 *\u0424\u043e\u0442\u043e* \u2014 \u0441\u0444\u043e\u0442\u043e\u0433\u0440\u0430\u0444\u0438\u0440\u0443\u0439\u0442\u0435 \u043f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u0435\n\u270f\ufe0f *\u0422\u0435\u043a\u0441\u0442* \u2014 \u043e\u043f\u0438\u0448\u0438\u0442\u0435 \u0440\u0430\u0431\u043e\u0442\u044b \u0441\u043f\u0438\u0441\u043a\u043e\u043c\n\n_\u041f\u0440\u0438\u043c\u0435\u0440 \u0434\u043b\u044f \u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f:_\n\\`\\`\\`\n\u0413\u0438\u043f\u0441\u043e\u043a\u0430\u0440\u0442\u043e\u043d 2 \u0441\u043b\u043e\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044c \u041f\u041f60 25\u043c2\n\u041f\u043b\u0438\u0442\u043a\u0430 \u043a\u0435\u0440\u0430\u043c\u043e\u0433\u0440\u0430\u043d\u0438\u0442 60x60 \u0432\u0430\u043d\u043d\u0430\u044f 15\u043c2\n\u0428\u043f\u0430\u043a\u043b\u0435\u0432\u043a\u0430 \u043f\u043e\u0434 \u043f\u043e\u043a\u0440\u0430\u0441\u043a\u0443 \u043f\u043e\u0442\u043e\u043b\u043a\u0438 \u0441\u0442\u0435\u043d\u044b 120\u043c2\n\u0420\u043e\u0437\u0435\u0442\u043a\u0438 \u0441\u043a\u0440\u044b\u0442\u044b\u0439 \u043c\u043e\u043d\u0442\u0430\u0436 20\u0448\u0442\n\\`\\`\\`\n\n\u0418\u043b\u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u044c\u0442\u0435 \u0444\u043e\u0442\u043e/PDF \ud83d\udcf7\ud83d\udcc4`,\n    // PDF specific\n    pdf_received: '\ud83d\udcc4 *PDF \u043f\u043e\u043b\u0443\u0447\u0435\u043d*',\n    pdf_processing: '\u0410\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u0443\u044e \u0447\u0435\u0440\u0442\u0451\u0436...',\n    pdf_pages: '\u0441\u0442\u0440.',\n    pdf_page_limit: '\u26a0\ufe0f \u041e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e \u043f\u0435\u0440\u0432\u044b\u0435 3 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b',\n    pdf_rooms_found: '\ud83c\udfe0 \u041d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u0439',\n    pdf_elements_found: '\ud83e\uddf1 \u041d\u0430\u0439\u0434\u0435\u043d\u043e \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432',\n    pdf_works_generated: '\ud83d\udcdd \u0421\u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u043e \u0440\u0430\u0431\u043e\u0442',\n    pdf_analyzing_page: '\u0410\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u0443\u044e \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443',\n    pdf_of: '\u0438\u0437',\n    pdf_complete: '\u2705 \u0410\u043d\u0430\u043b\u0438\u0437 \u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043d',\n    pdf_error: '\u274c \u041e\u0448\u0438\u0431\u043a\u0430 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 PDF',\n    // Rest\n    photo_added: '\u2705 \u0424\u043e\u0442\u043e \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e',\n    photos_count: '\u0444\u043e\u0442\u043e',\n    add_more: '+ \u0415\u0449\u0451 \u0444\u043e\u0442\u043e',\n    analyze_now: '\u25b6 \u041d\u0430\u0447\u0430\u0442\u044c \u0430\u043d\u0430\u043b\u0438\u0437',\n    analyzing: '\u0410\u043d\u0430\u043b\u0438\u0437 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0439...',\n    found: '*\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0451\u043d\u043d\u044b\u0435 \u0440\u0430\u0431\u043e\u0442\u044b:*',\n    edit_hint: '\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u0434\u043b\u044f \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f',\n    calc: '\u041f\u043e\u0438\u0441\u043a \u0440\u0430\u0441\u0446\u0435\u043d\u043e\u043a',\n    ready: '*\u0421\u041c\u0415\u0422\u0410*',\n    total: '\u0418\u0422\u041e\u0413\u041e',\n    days: '\u0434\u043d.',\n    pct: '\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c',\n    workers: '\u0422\u0440\u0443\u0434',\n    machines: '\u041c\u0435\u0445\u0430\u043d\u0438\u0437\u043c\u044b',\n    materials: '\u041c\u0430\u0442\u0435\u0440\u0438\u0430\u043b\u044b',\n    subtotal: '\u0421\u0432\u043e\u0434\u043a\u0430',\n    searching: '\u041f\u043e\u0438\u0441\u043a',\n    of: '\u0438\u0437',\n    not_found: '\u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e',\n    low_conf: '\u0422\u0440\u0435\u0431\u0443\u0435\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438',\n    price_note: '\u0411\u0430\u0437\u0430 \u0446\u0435\u043d: \u0421\u0430\u043d\u043a\u0442-\u041f\u0435\u0442\u0435\u0440\u0431\u0443\u0440\u0433 2025',\n    btn_calc: '\u25b6 \u0420\u0430\u0441\u0441\u0447\u0438\u0442\u0430\u0442\u044c',\n    btn_new: '+ \u041d\u043e\u0432\u044b\u0439 \u043f\u0440\u043e\u0435\u043a\u0442',\n    btn_lang: '\u2699 \u042f\u0437\u044b\u043a',\n    btn_edit: '\u270e \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c',\n    btn_delete: '\u2715 \u0423\u0434\u0430\u043b\u0438\u0442\u044c',\n    btn_add_work: '+ \u041f\u043e\u0437\u0438\u0446\u0438\u044f',\n    btn_done: '\u2705 \u0413\u043e\u0442\u043e\u0432\u043e',\n    btn_export_excel: '\u2193 Excel',\n    btn_export_pdf: '\u2193 PDF',\n    btn_restart: '\u21bb \u0417\u0430\u043d\u043e\u0432\u043e',\n    btn_help: '? \u0421\u043f\u0440\u0430\u0432\u043a\u0430',\n    loading: '\u0420\u0430\u0441\u0447\u0451\u0442...',\n    more_in_html: '\u041f\u043e\u0434\u0440\u043e\u0431\u043d\u044b\u0439 \u043e\u0442\u0447\u0451\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d',\n    resources: '\u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435: \u0440\u0435\u0441\u0443\u0440\u0441\u044b \u0438 \u0441\u043e\u0441\u0442\u0430\u0432\u044b \u0440\u0430\u0431\u043e\u0442',\n    enter_work: '*\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043f\u043e\u0437\u0438\u0446\u0438\u044e*\\n\u0424\u043e\u0440\u043c\u0430\u0442: \u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435, \u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0415\u0434\u0438\u043d\u0438\u0446\u0430\\n\u041f\u0440\u0438\u043c\u0435\u0440: \u041c\u043e\u043d\u0442\u0430\u0436 \u0413\u041a\u041b, 15 \u043c\u00b2',\n    work_added: '\u2705 \u041f\u043e\u0437\u0438\u0446\u0438\u044f \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430',\n    what_next: '*\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044f:*',\n    categories: '\u041a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0438',\n    cat_demolition: '\u0414\u0435\u043c\u043e\u043d\u0442\u0430\u0436',\n    cat_rough: '\u0427\u0435\u0440\u043d\u043e\u0432\u044b\u0435',\n    cat_finishing: '\u041e\u0442\u0434\u0435\u043b\u043a\u0430',\n    cat_mep: '\u0418\u043d\u0436\u0435\u043d\u0435\u0440\u0438\u044f',\n    help_title: '*\u0420\u0443\u043a\u043e\u0432\u043e\u0434\u0441\u0442\u0432\u043e*',\n    help_text: `*\u0420\u0443\u043a\u043e\u0432\u043e\u0434\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f*\n\n*1. \u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f*\n\u041e\u0442\u043f\u0440\u0430\u0432\u044c\u0442\u0435 \u0444\u043e\u0442\u043e, PDF-\u043f\u043b\u0430\u043d \u0438\u043b\u0438 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435.\nPDF: \u0430\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u0443\u044e\u0442\u0441\u044f \u0434\u043e 3 \u0441\u0442\u0440\u0430\u043d\u0438\u0446.\n\n*2. \u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430*\n\u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0451\u043d\u043d\u044b\u0435 \u0440\u0430\u0431\u043e\u0442\u044b.\n\n*3. \u0420\u0430\u0441\u0447\u0451\u0442*\n\u0426\u0435\u043d\u044b \u0438\u0437 \u0431\u0430\u0437\u044b DDC CWICR.\n\n*4. \u042d\u043a\u0441\u043f\u043e\u0440\u0442*\n\u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0430 \u0432 Excel \u0438\u043b\u0438 PDF.\n\n*\u041a\u043e\u043c\u0430\u043d\u0434\u044b:*\n/start \u2014 \u041d\u043e\u0432\u044b\u0439 \u043f\u0440\u043e\u0435\u043a\u0442\n/help \u2014 \u0421\u043f\u0440\u0430\u0432\u043a\u0430`,\n    doc_title: '\u0421\u041c\u0415\u0422\u0410',\n    col_pos: '\u2116', col_code: '\u0428\u0438\u0444\u0440', col_desc: '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435', col_unit: '\u0415\u0434.', col_qty: '\u041a\u043e\u043b.', col_price: '\u0426\u0435\u043d\u0430', col_total: '\u0421\u0443\u043c\u043c\u0430', col_labor: '\u0427/\u0447', col_quality: '\u041a',\n    grand_total: '\u0412\u0421\u0415\u0413\u041e', labor_cost: '\u0424\u041e\u0422', material_cost: '\u041c\u0430\u0442\u0435\u0440\u0438\u0430\u043b\u044b', labor_days: '\u0414\u043d\u0435\u0439',\n    kpi_total: '\u0421\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c', kpi_hours: '\u0422\u0440\u0443\u0434\u043e\u0437\u0430\u0442\u0440\u0430\u0442\u044b', kpi_days: '\u0421\u0440\u043e\u043a',\n    chart_cost_structure: '\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u0437\u0430\u0442\u0440\u0430\u0442', chart_labor: '\u0422\u0440\u0443\u0434', chart_material: '\u041c\u0430\u0442\u0435\u0440\u0438\u0430\u043b\u044b', chart_machines: '\u041c\u0435\u0445\u0430\u043d\u0438\u0437\u043c\u044b',\n    res_labor: '\u0422\u0440\u0443\u0434', res_material: '\u041c\u0430\u0442', res_machine: '\u041c\u0435\u0445',\n    collapse_all: '\u0421\u0432\u0435\u0440\u043d\u0443\u0442\u044c', expand_all: '\u0420\u0430\u0437\u0432\u0435\u0440\u043d\u0443\u0442\u044c',\n    quality_high: '\u0412\u044b\u0441\u043e\u043a\u0430\u044f \u0442\u043e\u0447\u043d\u043e\u0441\u0442\u044c', quality_medium: '\u0421\u0440\u0435\u0434\u043d\u044f\u044f \u0442\u043e\u0447\u043d\u043e\u0441\u0442\u044c', quality_low: '\u041d\u0438\u0437\u043a\u0430\u044f \u0442\u043e\u0447\u043d\u043e\u0441\u0442\u044c',\n    export_excel_msg: '\u042d\u043a\u0441\u043f\u043e\u0440\u0442 Excel (CSV)', export_pdf_msg: '\u042d\u043a\u0441\u043f\u043e\u0440\u0442 PDF', btn_refine: '\u0423\u0442\u043e\u0447\u043d\u0438\u0442\u044c \u0430\u043d\u0430\u043b\u0438\u0437', found_pct: '\u043d\u0430\u0439\u0434\u0435\u043d\u043e', more_resources: '\u0435\u0449\u0451', kpi_items: '\u041f\u043e\u0437\u0438\u0446\u0438\u0438', scope_title: '\u0421\u043e\u0441\u0442\u0430\u0432 \u0440\u0430\u0431\u043e\u0442', show_scope: '\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u0430\u0432'\n  },\n\n  'ES': { \n    fallback_start: 'Pulse /start para comenzar', \n    rooms: 'habitaciones', works_identified: 'trabajos', general: '\u041e\u0431\u0449\u0435\u0435', no_works: 'Sin trabajos', items: 'elementos', min: '\u043c\u0438\u043d', \n    db: 'ES_BARCELONA_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR', \n    name: 'Spanish', flag: '\ud83c\uddea\ud83c\uddf8', native: 'Espa\u00f1ol', cur: 'EUR', sym: '\u20ac', loc: 'es-ES', region: 'Barcelona', search_lang: 'Spanish',\n    ok: '\u2705 *Espa\u00f1ol* \u00b7 Barcelona \u00b7 EUR',\n    text_prompt: `*Describa los trabajos:*\\n\\n_Ejemplo:_\\n\\`\\`\\`\\nPladur 2 capas 25m2\\nAzulejos ba\u00f1o 15m2\\nPintura paredes 120m2\\n\\`\\`\\``,\n    photo: `*Env\u00ede foto, PDF o descripci\u00f3n*\n\n\ud83d\udcc4 *Planos PDF* \u2014 plano de planta o dibujo (m\u00e1x. 3 p\u00e1gs.)\n\ud83d\udcf7 *Foto* \u2014 fotograf\u00ede el espacio\n\u270f\ufe0f *Texto* \u2014 describa trabajos en lista\n\n_Ejemplo para copiar:_\n\\`\\`\\`\nPladur 2 capas perfil met\u00e1lico 25m2\nAzulejos porcel\u00e1nico 60x60 ba\u00f1o 15m2\nAlisado para pintura techos paredes 120m2\nEnchufes empotrados 20uds\n\\`\\`\\`\n\nO env\u00ede foto/PDF \ud83d\udcf7\ud83d\udcc4`,\n    pdf_received: '\ud83d\udcc4 *PDF recibido*',\n    pdf_processing: '\u23f3 Analizando plano...',\n    pdf_pages: 'p\u00e1ginas',\n    pdf_page_limit: '\u26a0\ufe0f Solo se procesan las primeras 3 p\u00e1ginas',\n    pdf_rooms_found: '\ud83c\udfe0 Habitaciones encontradas',\n    pdf_elements_found: '\ud83e\uddf1 Elementos encontrados',\n    pdf_works_generated: '\ud83d\udcdd Trabajos generados',\n    pdf_analyzing_page: '\ud83d\udd0d Analizando p\u00e1gina',\n    pdf_of: 'de',\n    pdf_complete: '\u2705 An\u00e1lisis completado',\n    pdf_error: '\u274c Error al procesar PDF',\n    photo_added: '\u2705 Foto a\u00f1adida',\n    photos_count: 'fotos',\n    add_more: '+ M\u00e1s fotos',\n    analyze_now: '\u25b6 Analizar',\n    analyzing: 'Analizando...',\n    found: '*Trabajos identificados:*',\n    edit_hint: 'Toque para editar',\n    calc: 'B\u00fasqueda de precios',\n    ready: '*PRESUPUESTO*',\n    total: 'TOTAL',\n    days: 'd\u00edas',\n    pct: 'Precisi\u00f3n',\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: '\u25b6 Calcular',\n    btn_new: '+ Nuevo',\n    btn_lang: '\u2699 Idioma',\n    btn_edit: '\u270e Editar',\n    btn_delete: '\u2715 Eliminar',\n    btn_add_work: '+ Partida',\n    btn_done: '\u2705 Listo',\n    btn_export_excel: '\u2193 Excel',\n    btn_export_pdf: '\u2193 PDF',\n    btn_restart: '\u21bb 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\u00f1adir partida*\\nFormato: Descripci\u00f3n, Cantidad Unidad',\n    work_added: '\u2705 A\u00f1adido',\n    what_next: '*Opciones:*',\n    categories: 'Categor\u00edas',\n    cat_demolition: 'Demolici\u00f3n',\n    cat_rough: 'Estructura',\n    cat_finishing: 'Acabados',\n    cat_mep: 'Instalaciones',\n    help_title: '*Gu\u00eda*',\n    help_text: `*Gu\u00eda de uso*\n\n*1.* Env\u00ede fotos, PDF o descripci\u00f3n\nPDF: m\u00e1x. 3 p\u00e1ginas\n*2.* Revise los trabajos\n*3.* Calcule precios\n*4.* Exporte\n\n/start \u2014 Nuevo\n/help \u2014 Ayuda`,\n    doc_title: 'PRESUPUESTO',\n    col_pos: 'N\u00ba', col_code: 'C\u00f3digo', col_desc: 'Descripci\u00f3n', 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\u00edas',\n    kpi_total: 'Coste Total', kpi_hours: 'Horas', kpi_days: 'D\u00edas',\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\u00e1s', kpi_items: 'Art\u00edculos', scope_title: 'Alcance', show_scope: 'Ver alcance'\n  },\n\n  'FR': { \n    fallback_start: 'Appuyez sur /start pour commencer', \n    rooms: 'pi\u00e8ces', works_identified: 'travaux', general: 'G\u00e9n\u00e9ral', no_works: 'Aucun travail', items: '\u00e9l\u00e9ments', min: '\u043c\u0438\u043d', \n    db: 'FR_PARIS_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR', \n    name: 'French', flag: '\ud83c\uddeb\ud83c\uddf7', native: 'Fran\u00e7ais', cur: 'EUR', sym: '\u20ac', loc: 'fr-FR', region: 'Paris', search_lang: 'French',\n    ok: '\u2705 *Fran\u00e7ais* \u00b7 Paris \u00b7 EUR',\n    text_prompt: `*D\u00e9crivez 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\ud83d\udcc4 *Plans PDF* \u2014 plan d'\u00e9tage ou dessin (max 3 pages)\n\ud83d\udcf7 *Photo* \u2014 photographiez l'espace\n\u270f\ufe0f *Texte* \u2014 d\u00e9crivez les travaux en liste\n\n_Exemple \u00e0 copier:_\n\\`\\`\\`\nPlaco 2 couches ossature m\u00e9tallique 25m2\nCarrelage gr\u00e8s c\u00e9rame 60x60 sdb 15m2\nEnduit plafonds murs 120m2\nPrises encastr\u00e9es 20pcs\n\\`\\`\\`\n\nOu envoyez photo/PDF \ud83d\udcf7\ud83d\udcc4`,\n    pdf_received: '\ud83d\udcc4 *PDF re\u00e7u*',\n    pdf_processing: '\u23f3 Analyse du plan...',\n    pdf_pages: 'pages',\n    pdf_page_limit: '\u26a0\ufe0f Seules les 3 premi\u00e8res pages sont trait\u00e9es',\n    pdf_rooms_found: '\ud83c\udfe0 Pi\u00e8ces trouv\u00e9es',\n    pdf_elements_found: '\ud83e\uddf1 \u00c9l\u00e9ments trouv\u00e9s',\n    pdf_works_generated: '\ud83d\udcdd Travaux g\u00e9n\u00e9r\u00e9s',\n    pdf_analyzing_page: '\ud83d\udd0d Analyse de la page',\n    pdf_of: 'sur',\n    pdf_complete: '\u2705 Analyse termin\u00e9e',\n    pdf_error: '\u274c Erreur de traitement PDF',\n    photo_added: '\u2705 Photo ajout\u00e9e',\n    photos_count: 'photos',\n    add_more: '+ Autres photos',\n    analyze_now: '\u25b6 Analyser',\n    analyzing: 'Analyse en cours...',\n    found: '*Ouvrages identifi\u00e9s:*',\n    edit_hint: 'Appuyez pour modifier',\n    calc: 'Recherche des prix',\n    ready: '*DEVIS*',\n    total: 'TOTAL',\n    days: 'jours',\n    pct: 'Pr\u00e9cision',\n    workers: 'M.O.',\n    machines: 'Mat\u00e9riel',\n    materials: 'Mat\u00e9riaux',\n    subtotal: 'R\u00e9sum\u00e9',\n    searching: 'Recherche',\n    of: 'sur',\n    not_found: 'Non trouv\u00e9',\n    low_conf: '\u00c0 v\u00e9rifier',\n    price_note: 'Base: Paris 2025',\n    btn_calc: '\u25b6 Calculer',\n    btn_new: '+ Nouveau',\n    btn_lang: '\u2699 Langue',\n    btn_edit: '\u270e Modifier',\n    btn_delete: '\u2715 Supprimer',\n    btn_add_work: '+ Ouvrage',\n    btn_done: '\u2705 Termin\u00e9',\n    btn_export_excel: '\u2193 Excel',\n    btn_export_pdf: '\u2193 PDF',\n    btn_restart: '\u21bb Recommencer',\n    btn_help: '? Aide',\n    loading: 'Calcul en cours...',\n    more_in_html: 'Rapport d\u00e9taill\u00e9 disponible',\n    resources: 'D\u00e9tails: ressources et description',\n    enter_work: '*Ajouter ouvrage*\\nFormat: Description, Quantit\u00e9 Unit\u00e9',\n    work_added: '\u2705 Ajout\u00e9',\n    what_next: '*Options:*',\n    categories: 'Cat\u00e9gories',\n    cat_demolition: 'D\u00e9molition',\n    cat_rough: 'Gros \u0153uvre',\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\u00e9rifiez les ouvrages\n*3.* Calculez les prix\n*4.* Exportez\n\n/start \u2014 Nouveau\n/help \u2014 Aide`,\n    doc_title: 'DEVIS',\n    col_pos: 'N\u00b0', col_code: 'Code', col_desc: 'D\u00e9signation', col_unit: 'U', col_qty: 'Qt\u00e9', col_price: 'P.U.', col_total: 'Total', col_labor: 'Hrs', col_quality: 'Q',\n    grand_total: 'TOTAL', labor_cost: 'M.O.', material_cost: 'Mat\u00e9riaux', labor_days: 'Jours',\n    kpi_total: 'Co\u00fbt Total', kpi_hours: 'Heures', kpi_days: 'Jours',\n    chart_cost_structure: 'Structure', chart_labor: 'M.O.', chart_material: 'Mat', chart_machines: '\u00c9q',\n    res_labor: 'MO', res_material: 'Mat', res_machine: '\u00c9q',\n    collapse_all: 'R\u00e9duire', expand_all: 'D\u00e9velopper',\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\u00e9', more_resources: 'plus', kpi_items: 'Articles', scope_title: 'Description', show_scope: 'Voir description'\n  },\n\n  'PT': { \n    fallback_start: 'Pressione /start para come\u00e7ar', \n    rooms: 'quartos', works_identified: 'trabalhos', general: 'Geral', no_works: 'Sem trabalhos', items: 'itens', min: '\u043c\u0438\u043d', \n    db: 'PT_SAOPAULO_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR', \n    name: 'Portuguese', flag: '\ud83c\udde7\ud83c\uddf7', native: 'Portugu\u00eas', cur: 'BRL', sym: 'R$', loc: 'pt-BR', region: 'S\u00e3o Paulo', search_lang: 'Portuguese',\n    ok: '\u2705 *Portugu\u00eas* \u00b7 S\u00e3o Paulo \u00b7 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\u00e7\u00e3o*\n\n\ud83d\udcc4 *Plantas PDF* \u2014 planta baixa ou desenho (m\u00e1x. 3 p\u00e1gs.)\n\ud83d\udcf7 *Foto* \u2014 fotografe o ambiente\n\u270f\ufe0f *Texto* \u2014 descreva os trabalhos em lista\n\n_Exemplo para copiar:_\n\\`\\`\\`\nDrywall 2 camadas perfil met\u00e1lico 25m2\nPorcelanato 60x60 banheiro 15m2\nMassa corrida teto paredes 120m2\nTomadas embutidas 20un\n\\`\\`\\`\n\nOu envie foto/PDF \ud83d\udcf7\ud83d\udcc4`,\n    pdf_received: '\ud83d\udcc4 *PDF recebido*',\n    pdf_processing: '\u23f3 Analisando planta...',\n    pdf_pages: 'p\u00e1ginas',\n    pdf_page_limit: '\u26a0\ufe0f Processando apenas as 3 primeiras p\u00e1ginas',\n    pdf_rooms_found: '\ud83c\udfe0 C\u00f4modos encontrados',\n    pdf_elements_found: '\ud83e\uddf1 Elementos encontrados',\n    pdf_works_generated: '\ud83d\udcdd Trabalhos gerados',\n    pdf_analyzing_page: '\ud83d\udd0d Analisando p\u00e1gina',\n    pdf_of: 'de',\n    pdf_complete: '\u2705 An\u00e1lise conclu\u00edda',\n    pdf_error: '\u274c Erro ao processar PDF',\n    photo_added: '\u2705 Foto adicionada',\n    photos_count: 'fotos',\n    add_more: '+ Mais fotos',\n    analyze_now: '\u25b6 Analisar',\n    analyzing: 'Analisando...',\n    found: '*Servi\u00e7os identificados:*',\n    edit_hint: 'Toque para editar',\n    calc: 'Busca de pre\u00e7os',\n    ready: '*OR\u00c7AMENTO*',\n    total: 'TOTAL',\n    days: 'dias',\n    pct: 'Precis\u00e3o',\n    workers: 'M.O.',\n    machines: 'Equipamentos',\n    materials: 'Materiais',\n    subtotal: 'Resumo',\n    searching: 'Buscando',\n    of: 'de',\n    not_found: 'N\u00e3o encontrado',\n    low_conf: 'Verificar',\n    price_note: 'Base: S\u00e3o Paulo 2025',\n    btn_calc: '\u25b6 Calcular',\n    btn_new: '+ Novo',\n    btn_lang: '\u2699 Idioma',\n    btn_edit: '\u270e Editar',\n    btn_delete: '\u2715 Excluir',\n    btn_add_work: '+ Servi\u00e7o',\n    btn_done: '\u2705 Pronto',\n    btn_export_excel: '\u2193 Excel',\n    btn_export_pdf: '\u2193 PDF',\n    btn_restart: '\u21bb Recome\u00e7ar',\n    btn_help: '? Ajuda',\n    loading: 'Calculando...',\n    more_in_html: 'Relat\u00f3rio detalhado dispon\u00edvel',\n    resources: 'Detalhes: recursos e escopo',\n    enter_work: '*Adicionar servi\u00e7o*\\nFormato: Descri\u00e7\u00e3o, Quantidade Unidade',\n    work_added: '\u2705 Adicionado',\n    what_next: '*Op\u00e7\u00f5es:*',\n    categories: 'Categorias',\n    cat_demolition: 'Demoli\u00e7\u00e3o',\n    cat_rough: 'Estrutura',\n    cat_finishing: 'Acabamento',\n    cat_mep: 'Instala\u00e7\u00f5es',\n    help_title: '*Guia*',\n    help_text: `*Guia de uso*\n\n*1.* Envie fotos, PDF ou descri\u00e7\u00e3o\nPDF: m\u00e1x. 3 p\u00e1ginas\n*2.* Revise os servi\u00e7os\n*3.* Calcule pre\u00e7os\n*4.* Exporte\n\n/start \u2014 Novo\n/help \u2014 Ajuda`,\n    doc_title: 'OR\u00c7AMENTO',\n    col_pos: 'N\u00ba', col_code: 'C\u00f3digo', col_desc: 'Descri\u00e7\u00e3o', 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\u00e9dia', 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: '\ud83c\udde8\ud83c\uddf3', native: '\u4e2d\u6587', cur: 'CNY', sym: '\u00a5', loc: 'zh-CN', region: '\u4e0a\u6d77', search_lang: 'Chinese',\n    ok: '\u2705 *\u4e2d\u6587* \u00b7 \u4e0a\u6d77 \u00b7 CNY',\n    text_prompt: `*\u63cf\u8ff0\u5de5\u4f5c:*\\n\\n_\u793a\u4f8b:_\\n\\`\\`\\`\\n\u77f3\u818f\u677f\u53cc\u5c42 25m2\\n\u74f7\u7816\u536b\u751f\u95f4 15m2\\n\u5899\u9762\u6d82\u6599 120m2\\n\\`\\`\\``,\n    photo: `*\u53d1\u9001\u7167\u7247\u3001PDF\u6216\u63cf\u8ff0*\n\n\ud83d\udcc4 *PDF\u56fe\u7eb8* \u2014 \u5e73\u9762\u56fe\u6216\u56fe\u7eb8\uff08\u6700\u591a3\u9875\uff09\n\ud83d\udcf7 *\u7167\u7247* \u2014 \u62cd\u6444\u623f\u95f4\u6216\u7269\u4f53\n\u270f\ufe0f *\u6587\u5b57* \u2014 \u4ee5\u5217\u8868\u5f62\u5f0f\u63cf\u8ff0\n\n_\u590d\u5236\u793a\u4f8b\uff1a_\n\\`\\`\\`\n\u77f3\u818f\u677f\u53cc\u5c42\u8f7b\u94a2\u9f99\u9aa8 25m2\n\u74f7\u7816600x600\u536b\u751f\u95f4 15m2\n\u817b\u5b50\u627e\u5e73\u9876\u5899 120m2\n\u6697\u88c5\u63d2\u5ea7 20\u4e2a\n\\`\\`\\`\n\n\u6216\u53d1\u9001\u7167\u7247/PDF \ud83d\udcf7\ud83d\udcc4`,\n    pdf_received: '\ud83d\udcc4 *\u5df2\u6536\u5230PDF*',\n    pdf_processing: '\u23f3 \u6b63\u5728\u5206\u6790\u56fe\u7eb8...',\n    pdf_pages: '\u9875',\n    pdf_page_limit: '\u26a0\ufe0f \u4ec5\u5904\u7406\u524d3\u9875',\n    pdf_rooms_found: '\ud83c\udfe0 \u53d1\u73b0\u623f\u95f4',\n    pdf_elements_found: '\ud83e\uddf1 \u53d1\u73b0\u5143\u7d20',\n    pdf_works_generated: '\ud83d\udcdd \u5df2\u751f\u6210\u5de5\u4f5c\u9879',\n    pdf_analyzing_page: '\ud83d\udd0d \u6b63\u5728\u5206\u6790\u7b2c',\n    pdf_of: '\u9875\uff0c\u5171',\n    pdf_complete: '\u2705 \u5206\u6790\u5b8c\u6210',\n    pdf_error: '\u274c PDF\u5904\u7406\u9519\u8bef',\n    photo_added: '\u2705 \u7167\u7247\u5df2\u6dfb\u52a0',\n    photos_count: '\u5f20',\n    add_more: '+ \u66f4\u591a\u7167\u7247',\n    analyze_now: '\u25b6 \u5f00\u59cb\u5206\u6790',\n    analyzing: '\u5206\u6790\u4e2d...',\n    found: '*\u8bc6\u522b\u7684\u5de5\u4f5c\u9879:*',\n    edit_hint: '\u70b9\u51fb\u7f16\u8f91',\n    calc: '\u4ef7\u683c\u67e5\u8be2',\n    ready: '*\u5de5\u7a0b\u9884\u7b97*',\n    total: '\u5408\u8ba1',\n    days: '\u5929',\n    pct: '\u5339\u914d\u7387',\n    workers: '\u4eba\u5de5',\n    machines: '\u673a\u68b0',\n    materials: '\u6750\u6599',\n    subtotal: '\u6458\u8981',\n    searching: '\u641c\u7d22',\n    of: '/',\n    not_found: '\u672a\u627e\u5230',\n    low_conf: '\u9700\u6838\u67e5',\n    price_note: '\u4ef7\u683c\u57fa\u51c6\uff1a\u4e0a\u6d77 2025',\n    btn_calc: '\u25b6 \u8ba1\u7b97',\n    btn_new: '+ \u65b0\u9879\u76ee',\n    btn_lang: '\u2699 \u8bed\u8a00',\n    btn_edit: '\u270e \u7f16\u8f91',\n    btn_delete: '\u2715 \u5220\u9664',\n    btn_add_work: '+ \u6dfb\u52a0',\n    btn_done: '\u2705 \u5b8c\u6210',\n    btn_export_excel: '\u2193 Excel',\n    btn_export_pdf: '\u2193 PDF',\n    btn_restart: '\u21bb \u91cd\u65b0\u5f00\u59cb',\n    btn_help: '? \u5e2e\u52a9',\n    loading: '\u8ba1\u7b97\u4e2d...',\n    more_in_html: '\u8be6\u7ec6\u62a5\u544a\u53ef\u7528',\n    resources: '\u8be6\u60c5\uff1a\u8d44\u6e90\u548c\u5de5\u4f5c\u8303\u56f4',\n    enter_work: '*\u6dfb\u52a0\u9879\u76ee*\\n\u683c\u5f0f\uff1a\u63cf\u8ff0\uff0c\u6570\u91cf \u5355\u4f4d',\n    work_added: '\u2705 \u5df2\u6dfb\u52a0',\n    what_next: '*\u9009\u9879:*',\n    categories: '\u7c7b\u522b',\n    cat_demolition: '\u62c6\u9664',\n    cat_rough: '\u7ed3\u6784',\n    cat_finishing: '\u88c5\u9970',\n    cat_mep: '\u673a\u7535',\n    help_title: '*\u6307\u5357*',\n    help_text: `*\u4f7f\u7528\u6307\u5357*\n\n*1.* \u53d1\u9001\u7167\u7247\u3001PDF\u6216\u63cf\u8ff0\nPDF\uff1a\u6700\u591a3\u9875\n*2.* \u68c0\u67e5\u5de5\u4f5c\u9879\n*3.* \u8ba1\u7b97\u4ef7\u683c\n*4.* \u5bfc\u51fa\n\n/start \u2014 \u65b0\u9879\u76ee\n/help \u2014 \u5e2e\u52a9`,\n    doc_title: '\u9884\u7b97',\n    col_pos: '\u5e8f\u53f7', col_code: '\u7f16\u7801', col_desc: '\u540d\u79f0', col_unit: '\u5355\u4f4d', col_qty: '\u6570\u91cf', col_price: '\u5355\u4ef7', col_total: '\u5408\u8ba1', col_labor: '\u5de5\u65f6', col_quality: '\u8d28',\n    grand_total: '\u603b\u8ba1', labor_cost: '\u4eba\u5de5\u8d39', material_cost: '\u6750\u6599\u8d39', labor_days: '\u5de5\u671f',\n    kpi_total: '\u603b\u6210\u672c', kpi_hours: '\u5de5\u65f6', kpi_days: '\u5de5\u671f',\n    chart_cost_structure: '\u6210\u672c\u7ed3\u6784', chart_labor: '\u4eba\u5de5', chart_material: '\u6750\u6599', chart_machines: '\u673a\u68b0',\n    res_labor: '\u4eba\u5de5', res_material: '\u6750\u6599', res_machine: '\u673a\u68b0',\n    collapse_all: '\u6298\u53e0', expand_all: '\u5c55\u5f00',\n    quality_high: '\u9ad8', quality_medium: '\u4e2d', quality_low: '\u4f4e',\n    export_excel_msg: '\u5bfc\u51faExcel', export_pdf_msg: '\u5bfc\u51faPDF', btn_refine: '\u7cbe\u786e\u5206\u6790', found_pct: '\u627e\u5230', more_resources: '\u66f4\u591a', kpi_items: '\u9879\u76ee', scope_title: '\u5de5\u4f5c\u8303\u56f4', show_scope: '\u67e5\u770b\u8303\u56f4'\n  },\n\n  'AR': { \n    db: 'AR_DUBAI_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR', \n    name: 'Arabic', flag: '\ud83c\udde6\ud83c\uddea', native: '\u0627\u0644\u0639\u0631\u0628\u064a\u0629', cur: 'AED', sym: '\u062f.\u0625', loc: 'ar-AE', region: '\u062f\u0628\u064a', search_lang: 'Arabic',\n    ok: '\u2705 *\u0627\u0644\u0639\u0631\u0628\u064a\u0629* \u00b7 \u062f\u0628\u064a \u00b7 AED',\n    text_prompt: `*\u0635\u0641 \u0627\u0644\u0623\u0639\u0645\u0627\u0644:*\\n\\n_\u0645\u062b\u0627\u0644:_\\n\\`\\`\\`\\n\u062c\u0628\u0633 \u0628\u0648\u0631\u062f \u0637\u0628\u0642\u062a\u064a\u0646 25\u06452\\n\u0628\u0644\u0627\u0637 \u062d\u0645\u0627\u0645 15\u06452\\n\u062f\u0647\u0627\u0646 \u062c\u062f\u0631\u0627\u0646 120\u06452\\n\\`\\`\\``,\n    photo: `*\u0623\u0631\u0633\u0644 \u0635\u0648\u0631\u0629 \u0623\u0648 PDF \u0623\u0648 \u0648\u0635\u0641*\n\n\ud83d\udcc4 *\u0645\u062e\u0637\u0637\u0627\u062a PDF* \u2014 \u0645\u062e\u0637\u0637 \u0627\u0644\u0637\u0627\u0628\u0642 \u0623\u0648 \u0627\u0644\u0631\u0633\u0645 (\u062d\u062a\u0649 3 \u0635\u0641\u062d\u0627\u062a)\n\ud83d\udcf7 *\u0635\u0648\u0631\u0629* \u2014 \u0627\u0644\u062a\u0642\u0637 \u0635\u0648\u0631\u0629 \u0644\u0644\u0645\u0643\u0627\u0646\n\u270f\ufe0f *\u0646\u0635* \u2014 \u0635\u0641 \u0627\u0644\u0623\u0639\u0645\u0627\u0644 \u0643\u0642\u0627\u0626\u0645\u0629\n\n_\u0645\u062b\u0627\u0644 \u0644\u0644\u0646\u0633\u062e:_\n\\`\\`\\`\n\u062c\u0628\u0633 \u0628\u0648\u0631\u062f \u0637\u0628\u0642\u062a\u064a\u0646 \u0647\u064a\u0643\u0644 \u0645\u0639\u062f\u0646\u064a 25\u06452\n\u0628\u0644\u0627\u0637 \u0633\u064a\u0631\u0627\u0645\u064a\u0643 60x60 \u062d\u0645\u0627\u0645 15\u06452\n\u0645\u0639\u062c\u0648\u0646 \u0623\u0633\u0642\u0641 \u062c\u062f\u0631\u0627\u0646 120\u06452\n\u0645\u0642\u0627\u0628\u0633 \u0645\u062e\u0641\u064a\u0629 20\u0642\u0637\u0639\u0629\n\\`\\`\\`\n\n\u0623\u0648 \u0623\u0631\u0633\u0644 \u0635\u0648\u0631\u0629/PDF \ud83d\udcf7\ud83d\udcc4`,\n    pdf_received: '\ud83d\udcc4 *\u062a\u0645 \u0627\u0633\u062a\u0644\u0627\u0645 PDF*',\n    pdf_processing: '\u23f3 \u062c\u0627\u0631\u064a \u062a\u062d\u0644\u064a\u0644 \u0627\u0644\u0645\u062e\u0637\u0637...',\n    pdf_pages: '\u0635\u0641\u062d\u0627\u062a',\n    pdf_page_limit: '\u26a0\ufe0f \u0645\u0639\u0627\u0644\u062c\u0629 \u0623\u0648\u0644 3 \u0635\u0641\u062d\u0627\u062a \u0641\u0642\u0637',\n    pdf_rooms_found: '\ud83c\udfe0 \u0627\u0644\u063a\u0631\u0641 \u0627\u0644\u0645\u0648\u062c\u0648\u062f\u0629',\n    pdf_elements_found: '\ud83e\uddf1 \u0627\u0644\u0639\u0646\u0627\u0635\u0631 \u0627\u0644\u0645\u0648\u062c\u0648\u062f\u0629',\n    pdf_works_generated: '\ud83d\udcdd \u0627\u0644\u0623\u0639\u0645\u0627\u0644 \u0627\u0644\u0645\u064f\u0646\u0634\u0623\u0629',\n    pdf_analyzing_page: '\ud83d\udd0d \u062a\u062d\u0644\u064a\u0644 \u0627\u0644\u0635\u0641\u062d\u0629',\n    pdf_of: '\u0645\u0646',\n    pdf_complete: '\u2705 \u0627\u0643\u062a\u0645\u0644 \u0627\u0644\u062a\u062d\u0644\u064a\u0644',\n    pdf_error: '\u274c \u062e\u0637\u0623 \u0641\u064a \u0645\u0639\u0627\u0644\u062c\u0629 PDF',\n    photo_added: '\u2705 \u062a\u0645\u062a \u0627\u0644\u0625\u0636\u0627\u0641\u0629',\n    photos_count: '\u0635\u0648\u0631',\n    add_more: '+ \u0627\u0644\u0645\u0632\u064a\u062f',\n    analyze_now: '\u25b6 \u062a\u062d\u0644\u064a\u0644',\n    analyzing: '\u062c\u0627\u0631\u064a \u0627\u0644\u062a\u062d\u0644\u064a\u0644...',\n    found: '*\u0627\u0644\u0623\u0639\u0645\u0627\u0644 \u0627\u0644\u0645\u062d\u062f\u062f\u0629:*',\n    edit_hint: '\u0627\u0636\u063a\u0637 \u0644\u0644\u062a\u0639\u062f\u064a\u0644',\n    calc: '\u0627\u0644\u0628\u062d\u062b \u0639\u0646 \u0627\u0644\u0623\u0633\u0639\u0627\u0631',\n    ready: '*\u0627\u0644\u062a\u0642\u062f\u064a\u0631*',\n    total: '\u0627\u0644\u0645\u062c\u0645\u0648\u0639',\n    days: '\u064a\u0648\u0645',\n    pct: '\u0627\u0644\u062f\u0642\u0629',\n    workers: '\u0639\u0645\u0627\u0644\u0629',\n    machines: '\u0645\u0639\u062f\u0627\u062a',\n    materials: '\u0645\u0648\u0627\u062f',\n    subtotal: '\u0645\u0644\u062e\u0635',\n    searching: '\u0628\u062d\u062b',\n    of: '\u0645\u0646',\n    not_found: '\u063a\u064a\u0631 \u0645\u0648\u062c\u0648\u062f',\n    low_conf: '\u0644\u0644\u0645\u0631\u0627\u062c\u0639\u0629',\n    price_note: '\u0627\u0644\u0623\u0633\u0627\u0633: \u062f\u0628\u064a 2025',\n    btn_calc: '\u25b6 \u062d\u0633\u0627\u0628',\n    btn_new: '+ \u062c\u062f\u064a\u062f',\n    btn_lang: '\u2699 \u0644\u063a\u0629',\n    btn_edit: '\u270e \u062a\u0639\u062f\u064a\u0644',\n    btn_delete: '\u2715 \u062d\u0630\u0641',\n    btn_add_work: '+ \u0625\u0636\u0627\u0641\u0629',\n    btn_done: '\u2705 \u062a\u0645',\n    btn_export_excel: '\u2193 Excel',\n    btn_export_pdf: '\u2193 PDF',\n    btn_restart: '\u21bb \u0627\u0644\u0628\u062f\u0621 \u0645\u0646 \u062c\u062f\u064a\u062f',\n    btn_help: '? \u0645\u0633\u0627\u0639\u062f\u0629',\n    loading: '\u062c\u0627\u0631\u064a \u0627\u0644\u062d\u0633\u0627\u0628...',\n    more_in_html: '\u062a\u0642\u0631\u064a\u0631 \u0645\u0641\u0635\u0644 \u0645\u062a\u0627\u062d',\n    resources: '\u0627\u0644\u062a\u0641\u0627\u0635\u064a\u0644: \u0627\u0644\u0645\u0648\u0627\u0631\u062f \u0648\u0646\u0637\u0627\u0642 \u0627\u0644\u0639\u0645\u0644',\n    enter_work: '*\u0625\u0636\u0627\u0641\u0629 \u0628\u0646\u062f*\\n\u0627\u0644\u0635\u064a\u063a\u0629: \u0627\u0644\u0648\u0635\u0641\u060c \u0627\u0644\u0643\u0645\u064a\u0629 \u0627\u0644\u0648\u062d\u062f\u0629',\n    work_added: '\u2705 \u062a\u0645\u062a \u0627\u0644\u0625\u0636\u0627\u0641\u0629',\n    what_next: '*\u0627\u0644\u062e\u064a\u0627\u0631\u0627\u062a:*',\n    categories: '\u0627\u0644\u0641\u0626\u0627\u062a',\n    cat_demolition: '\u0647\u062f\u0645',\n    cat_rough: '\u0647\u064a\u0643\u0644',\n    cat_finishing: '\u062a\u0634\u0637\u064a\u0628',\n    cat_mep: '\u0645\u064a\u0643\u0627\u0646\u064a\u0643',\n    help_title: '*\u062f\u0644\u064a\u0644*',\n    help_text: `*\u062f\u0644\u064a\u0644 \u0627\u0644\u0627\u0633\u062a\u062e\u062f\u0627\u0645*\n\n*1.* \u0623\u0631\u0633\u0644 \u0635\u0648\u0631 \u0623\u0648 PDF \u0623\u0648 \u0648\u0635\u0641\nPDF: \u062d\u062a\u0649 3 \u0635\u0641\u062d\u0627\u062a\n*2.* \u0631\u0627\u062c\u0639 \u0627\u0644\u0623\u0639\u0645\u0627\u0644\n*3.* \u0627\u062d\u0633\u0628 \u0627\u0644\u0623\u0633\u0639\u0627\u0631\n*4.* \u0635\u062f\u0651\u0631\n\n/start \u2014 \u062c\u062f\u064a\u062f\n/help \u2014 \u0645\u0633\u0627\u0639\u062f\u0629`,\n    doc_title: '\u0627\u0644\u062a\u0642\u062f\u064a\u0631',\n    col_pos: '\u0631\u0642\u0645', col_code: '\u0627\u0644\u0631\u0645\u0632', col_desc: '\u0627\u0644\u0648\u0635\u0641', col_unit: '\u0648\u062d\u062f\u0629', col_qty: '\u0643\u0645\u064a\u0629', col_price: '\u0633\u0639\u0631', col_total: '\u0627\u0644\u0645\u062c\u0645\u0648\u0639', col_labor: '\u0633\u0627\u0639\u0627\u062a', col_quality: '\u062c',\n    grand_total: '\u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a', labor_cost: '\u0639\u0645\u0627\u0644\u0629', material_cost: '\u0645\u0648\u0627\u062f', labor_days: '\u0623\u064a\u0627\u0645',\n    kpi_total: '\u0627\u0644\u062a\u0643\u0644\u0641\u0629', kpi_hours: '\u0633\u0627\u0639\u0627\u062a', kpi_days: '\u0623\u064a\u0627\u0645',\n    chart_cost_structure: '\u0627\u0644\u0647\u064a\u0643\u0644', chart_labor: '\u0639\u0645\u0627\u0644\u0629', chart_material: '\u0645\u0648\u0627\u062f', chart_machines: '\u0645\u0639\u062f\u0627\u062a',\n    res_labor: '\u0639\u0645\u0627\u0644\u0629', res_material: '\u0645\u0648\u0627\u062f', res_machine: '\u0645\u0639\u062f\u0627\u062a',\n    collapse_all: '\u0637\u064a', expand_all: '\u062a\u0648\u0633\u064a\u0639',\n    quality_high: '\u0639\u0627\u0644\u064a\u0629', quality_medium: '\u0645\u062a\u0648\u0633\u0637\u0629', quality_low: '\u0645\u0646\u062e\u0641\u0636\u0629',\n    export_excel_msg: '\u062a\u0635\u062f\u064a\u0631 Excel', export_pdf_msg: '\u062a\u0635\u062f\u064a\u0631 PDF', btn_refine: '\u062a\u062d\u0644\u064a\u0644 \u0623\u062f\u0642', found_pct: '\u0648\u062c\u062f', more_resources: '\u0627\u0644\u0645\u0632\u064a\u062f', kpi_items: '\u0628\u0646\u0648\u062f', scope_title: '\u0646\u0637\u0627\u0642 \u0627\u0644\u0639\u0645\u0644', show_scope: '\u0639\u0631\u0636 \u0627\u0644\u0646\u0637\u0627\u0642'\n  },\n\n  'HI': { \n    db: 'HI_MUMBAI_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR', \n    name: 'Hindi', flag: '\ud83c\uddee\ud83c\uddf3', native: '\u0939\u093f\u0928\u094d\u0926\u0940', cur: 'INR', sym: '\u20b9', loc: 'hi-IN', region: '\u092e\u0941\u0902\u092c\u0908', search_lang: 'Hindi',\n    ok: '\u2705 *\u0939\u093f\u0928\u094d\u0926\u0940* \u00b7 \u092e\u0941\u0902\u092c\u0908 \u00b7 INR',\n    text_prompt: `*\u0915\u093e\u0930\u094d\u092f\u094b\u0902 \u0915\u093e \u0935\u0930\u094d\u0923\u0928 \u0915\u0930\u0947\u0902:*\\n\\n_\u0909\u0926\u093e\u0939\u0930\u0923:_\\n\\`\\`\\`\\n\u0921\u094d\u0930\u093e\u0908\u0935\u0949\u0932 2 \u0932\u0947\u092f\u0930 25m2\\n\u091f\u093e\u0907\u0932\u094d\u0938 \u092c\u093e\u0925\u0930\u0942\u092e 15m2\\n\u092a\u0947\u0902\u091f\u093f\u0902\u0917 \u0926\u0940\u0935\u093e\u0930\u0947\u0902 120m2\\n\\`\\`\\``,\n    photo: `*\u092b\u093c\u094b\u091f\u094b, PDF \u092f\u093e \u0935\u093f\u0935\u0930\u0923 \u092d\u0947\u091c\u0947\u0902*\n\n\ud83d\udcc4 *PDF \u0921\u094d\u0930\u093e\u0907\u0902\u0917* \u2014 \u092b\u094d\u0932\u094b\u0930 \u092a\u094d\u0932\u093e\u0928 \u092f\u093e \u092c\u094d\u0932\u0942\u092a\u094d\u0930\u093f\u0902\u091f (\u0905\u0927\u093f\u0915\u0924\u092e 3 \u092a\u0947\u091c)\n\ud83d\udcf7 *\u092b\u093c\u094b\u091f\u094b* \u2014 \u0915\u092e\u0930\u0947 \u092f\u093e \u0935\u0938\u094d\u0924\u0941 \u0915\u0940 \u092b\u093c\u094b\u091f\u094b\n\u270f\ufe0f *\u091f\u0947\u0915\u094d\u0938\u094d\u091f* \u2014 \u0915\u093e\u0930\u094d\u092f\u094b\u0902 \u0915\u093e \u0935\u093f\u0935\u0930\u0923 \u0938\u0942\u091a\u0940 \u092e\u0947\u0902\n\n_\u0915\u0949\u092a\u0940 \u0909\u0926\u093e\u0939\u0930\u0923:_\n\\`\\`\\`\n\u0921\u094d\u0930\u093e\u0908\u0935\u0949\u0932 2 \u0932\u0947\u092f\u0930 \u092e\u0947\u091f\u0932 \u092b\u094d\u0930\u0947\u092e 25m2\n\u091f\u093e\u0907\u0932\u094d\u0938 60x60 \u092c\u093e\u0925\u0930\u0942\u092e 15m2\n\u092a\u0941\u091f\u094d\u091f\u0940 \u091b\u0924 \u0926\u0940\u0935\u093e\u0930\u0947\u0902 120m2\n\u0938\u0949\u0915\u0947\u091f 20 \u092a\u0940\u0938\n\\`\\`\\`\n\n\u092f\u093e \u092b\u093c\u094b\u091f\u094b/PDF \u092d\u0947\u091c\u0947\u0902 \ud83d\udcf7\ud83d\udcc4`,\n    pdf_received: '\ud83d\udcc4 *PDF \u092a\u094d\u0930\u093e\u092a\u094d\u0924*',\n    pdf_processing: '\u23f3 \u0921\u094d\u0930\u093e\u0907\u0902\u0917 \u0915\u093e \u0935\u093f\u0936\u094d\u0932\u0947\u0937\u0923...',\n    pdf_pages: '\u092a\u0947\u091c',\n    pdf_page_limit: '\u26a0\ufe0f \u0915\u0947\u0935\u0932 \u092a\u0939\u0932\u0947 3 \u092a\u0947\u091c \u092a\u094d\u0930\u094b\u0938\u0947\u0938 \u0939\u094b\u0902\u0917\u0947',\n    pdf_rooms_found: '\ud83c\udfe0 \u0915\u092e\u0930\u0947 \u092e\u093f\u0932\u0947',\n    pdf_elements_found: '\ud83e\uddf1 \u090f\u0932\u093f\u092e\u0947\u0902\u091f \u092e\u093f\u0932\u0947',\n    pdf_works_generated: '\ud83d\udcdd \u0915\u093e\u0930\u094d\u092f \u092c\u0928\u093e\u090f \u0917\u090f',\n    pdf_analyzing_page: '\ud83d\udd0d \u092a\u0947\u091c \u0915\u093e \u0935\u093f\u0936\u094d\u0932\u0947\u0937\u0923',\n    pdf_of: '\u092e\u0947\u0902 \u0938\u0947',\n    pdf_complete: '\u2705 \u0935\u093f\u0936\u094d\u0932\u0947\u0937\u0923 \u092a\u0942\u0930\u094d\u0923',\n    pdf_error: '\u274c PDF \u092a\u094d\u0930\u094b\u0938\u0947\u0938\u093f\u0902\u0917 \u0924\u094d\u0930\u0941\u091f\u093f',\n    photo_added: '\u2705 \u092b\u093c\u094b\u091f\u094b \u091c\u094b\u0921\u093c\u0940 \u0917\u0908',\n    photos_count: '\u092b\u093c\u094b\u091f\u094b',\n    add_more: '+ \u0914\u0930 \u092b\u093c\u094b\u091f\u094b',\n    analyze_now: '\u25b6 \u0935\u093f\u0936\u094d\u0932\u0947\u0937\u0923',\n    analyzing: '\u0935\u093f\u0936\u094d\u0932\u0947\u0937\u0923...',\n    found: '*\u092a\u0939\u091a\u093e\u0928\u0947 \u0917\u090f \u0915\u093e\u0930\u094d\u092f:*',\n    edit_hint: '\u0938\u0902\u092a\u093e\u0926\u0928 \u0915\u0947 \u0932\u093f\u090f \u091f\u0948\u092a \u0915\u0930\u0947\u0902',\n    calc: '\u092e\u0942\u0932\u094d\u092f \u0916\u094b\u091c',\n    ready: '*\u0905\u0928\u0941\u092e\u093e\u0928*',\n    total: '\u0915\u0941\u0932',\n    days: '\u0926\u093f\u0928',\n    pct: '\u0938\u091f\u0940\u0915\u0924\u093e',\n    workers: '\u0936\u094d\u0930\u092e',\n    machines: '\u0909\u092a\u0915\u0930\u0923',\n    materials: '\u0938\u093e\u092e\u0917\u094d\u0930\u0940',\n    subtotal: '\u0938\u093e\u0930\u093e\u0902\u0936',\n    searching: '\u0916\u094b\u091c',\n    of: '\u092e\u0947\u0902 \u0938\u0947',\n    not_found: '\u0928\u0939\u0940\u0902 \u092e\u093f\u0932\u093e',\n    low_conf: '\u091c\u093e\u0901\u091a\u0947\u0902',\n    price_note: '\u0906\u0927\u093e\u0930: \u092e\u0941\u0902\u092c\u0908 2025',\n    btn_calc: '\u25b6 \u0917\u0923\u0928\u093e',\n    btn_new: '+ \u0928\u092f\u093e',\n    btn_lang: '\u2699 \u092d\u093e\u0937\u093e',\n    btn_edit: '\u270e \u0938\u0902\u092a\u093e\u0926\u093f\u0924',\n    btn_delete: '\u2715 \u0939\u091f\u093e\u090f\u0902',\n    btn_add_work: '+ \u091c\u094b\u0921\u093c\u0947\u0902',\n    btn_done: '\u2705 \u0939\u094b \u0917\u092f\u093e',\n    btn_export_excel: '\u2193 Excel',\n    btn_export_pdf: '\u2193 PDF',\n    btn_restart: '\u21bb \u092b\u093f\u0930 \u0938\u0947',\n    btn_help: '? \u092e\u0926\u0926',\n    loading: '\u0917\u0923\u0928\u093e...',\n    more_in_html: '\u0935\u093f\u0938\u094d\u0924\u0943\u0924 \u0930\u093f\u092a\u094b\u0930\u094d\u091f \u0909\u092a\u0932\u092c\u094d\u0927',\n    resources: '\u0935\u093f\u0935\u0930\u0923: \u0938\u0902\u0938\u093e\u0927\u0928 \u0914\u0930 \u0915\u093e\u0930\u094d\u092f \u0915\u094d\u0937\u0947\u0924\u094d\u0930',\n    enter_work: '*\u0906\u0907\u091f\u092e \u091c\u094b\u0921\u093c\u0947\u0902*\\n\u092a\u094d\u0930\u093e\u0930\u0942\u092a: \u0935\u093f\u0935\u0930\u0923, \u092e\u093e\u0924\u094d\u0930\u093e \u0907\u0915\u093e\u0908',\n    work_added: '\u2705 \u091c\u094b\u0921\u093c\u093e \u0917\u092f\u093e',\n    what_next: '*\u0935\u093f\u0915\u0932\u094d\u092a:*',\n    categories: '\u0936\u094d\u0930\u0947\u0923\u093f\u092f\u093e\u0902',\n    cat_demolition: '\u0924\u094b\u0921\u093c\u092b\u094b\u0921\u093c',\n    cat_rough: '\u0922\u093e\u0902\u091a\u093e',\n    cat_finishing: '\u092b\u093c\u093f\u0928\u093f\u0936\u093f\u0902\u0917',\n    cat_mep: 'MEP',\n    help_title: '*\u0917\u093e\u0907\u0921*',\n    help_text: `*\u0909\u092a\u092f\u094b\u0917 \u0917\u093e\u0907\u0921*\n\n*1.* \u092b\u093c\u094b\u091f\u094b, PDF \u092f\u093e \u0935\u093f\u0935\u0930\u0923 \u092d\u0947\u091c\u0947\u0902\nPDF: \u0905\u0927\u093f\u0915\u0924\u092e 3 \u092a\u0947\u091c\n*2.* \u0915\u093e\u0930\u094d\u092f \u091c\u093e\u0901\u091a\u0947\u0902\n*3.* \u092e\u0942\u0932\u094d\u092f \u0917\u0923\u0928\u093e\n*4.* \u0928\u093f\u0930\u094d\u092f\u093e\u0924\n\n/start \u2014 \u0928\u092f\u093e\n/help \u2014 \u092e\u0926\u0926`,\n    doc_title: '\u0905\u0928\u0941\u092e\u093e\u0928',\n    col_pos: '\u0915\u094d\u0930\u092e', col_code: '\u0915\u094b\u0921', col_desc: '\u0935\u093f\u0935\u0930\u0923', col_unit: '\u0907\u0915\u093e\u0908', col_qty: '\u092e\u093e\u0924\u094d\u0930\u093e', col_price: '\u0926\u0930', col_total: '\u0915\u0941\u0932', col_labor: '\u0918\u0902\u091f\u0947', col_quality: '\u0917\u0941',\n    grand_total: '\u0915\u0941\u0932 \u092f\u094b\u0917', labor_cost: '\u0936\u094d\u0930\u092e', material_cost: '\u0938\u093e\u092e\u0917\u094d\u0930\u0940', labor_days: '\u0926\u093f\u0928',\n    kpi_total: '\u0915\u0941\u0932 \u0932\u093e\u0917\u0924', kpi_hours: '\u0918\u0902\u091f\u0947', kpi_days: '\u0926\u093f\u0928',\n    chart_cost_structure: '\u0938\u0902\u0930\u091a\u0928\u093e', chart_labor: '\u0936\u094d\u0930\u092e', chart_material: '\u0938\u093e\u092e\u0917\u094d\u0930\u0940', chart_machines: '\u0909\u092a\u0915\u0930\u0923',\n    res_labor: '\u0936\u094d\u0930\u092e', res_material: '\u0938\u093e\u092e\u0917\u094d\u0930\u0940', res_machine: '\u0909\u092a\u0915\u0930\u0923',\n    collapse_all: '\u0938\u0902\u0915\u094d\u0937\u093f\u092a\u094d\u0924', expand_all: '\u0935\u093f\u0938\u094d\u0924\u0943\u0924',\n    quality_high: '\u0909\u091a\u094d\u091a', quality_medium: '\u092e\u0927\u094d\u092f\u092e', quality_low: '\u0928\u093f\u092e\u094d\u0928',\n    export_excel_msg: 'Excel \u0928\u093f\u0930\u094d\u092f\u093e\u0924', export_pdf_msg: 'PDF \u0928\u093f\u0930\u094d\u092f\u093e\u0924', btn_refine: '\u0938\u0941\u0927\u093e\u0930\u0947\u0902', found_pct: '\u092e\u093f\u0932\u093e', more_resources: '\u0914\u0930', kpi_items: '\u0906\u0907\u091f\u092e', scope_title: '\u0915\u093e\u0930\u094d\u092f \u0915\u094d\u0937\u0947\u0924\u094d\u0930', show_scope: '\u0926\u0947\u0916\u0947\u0902'\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": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// MAIN ROUTER - Text-Only Version\n// DDC CWICR - Data Driven Construction Cost Estimator\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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\u00b2';\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\u00b2';\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": "## \ud83d\udd04 Block 6: Calculation Loop\n\n**Per Work Item:**\n```\nPrep \u2192 LLM Transform \u2192 Embed \u2192 \nQdrant Search \u2192 Rerank \u2192 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": "## \ud83d\udcca Block 7: Reports\n\n**Final Processing:**\n```\nAggregate \u2192 Generate HTML \u2192 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": "## \ud83d\udce5 Block 8: Export\n\n**Available Exports:**\n- \ud83d\udcca **Excel (CSV)** \u2014 Spreadsheet\n- \ud83d\udcc4 **PDF** \u2014 Document\n- \ud83c\udf10 **HTML** \u2014 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": "## \ud83d\udd0d Qdrant Vector DB\n\n**Collections (9 languages):**\n- `DDC_CWICR_DE` \u2014 German\n- `DDC_CWICR_EN` \u2014 English\n- `DDC_CWICR_RU` \u2014 Russian\n- `DDC_CWICR_ES` \u2014 Spanish\n- `DDC_CWICR_FR` \u2014 French\n- `DDC_CWICR_PT` \u2014 Portuguese\n- `DDC_CWICR_ZH` \u2014 Chinese\n- `DDC_CWICR_AR` \u2014 Arabic\n- `DDC_CWICR_HI` \u2014 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": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// DDC CWICR - Prep Text LLM Request (for AI Node)\n// Prepare prompt for text-to-works analysis\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst cfg = $('Config').first().json;\nconst tokenConfig = $('\ud83d\udd11 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\u00b2\"}, {\"name\": \"Wandfliesen Feinsteinzeug 30x60\", \"qty\": 16, \"unit\": \"m\u00b2\"}, {\"name\": \"Bodenfliesen rutschfest\", \"qty\": 8, \"unit\": \"m\u00b2\"}, {\"name\": \"WC wandh\u00e4ngend 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\u00b2\"}, {\"name\": \"Wall tiles porcelain 30x60\", \"qty\": 16, \"unit\": \"m\u00b2\"}, {\"name\": \"Floor tiles anti-slip\", \"qty\": 8, \"unit\": \"m\u00b2\"}, {\"name\": \"Wall-hung toilet install\", \"qty\": 1, \"unit\": \"pcs\"}, {\"name\": \"Vanity basin install\", \"qty\": 1, \"unit\": \"pcs\"}]'\n  },\n  'Russian': {\n    input: '\u0440\u0435\u043c\u043e\u043d\u0442 \u043a\u0443\u0445\u043d\u0438 20\u043c2 \u0441\u0442\u0435\u043d\u044b \u0438 \u043b\u0430\u043c\u0438\u043d\u0430\u0442',\n    output: '[{\"name\": \"\u0414\u0435\u043c\u043e\u043d\u0442\u0430\u0436 \u0441\u0442\u0430\u0440\u044b\u0445 \u043e\u0431\u043e\u0435\u0432\", \"qty\": 50, \"unit\": \"m\u00b2\"}, {\"name\": \"\u0428\u0442\u0443\u043a\u0430\u0442\u0443\u0440\u043a\u0430 \u0441\u0442\u0435\u043d \u0433\u0438\u043f\u0441\u043e\u0432\u0430\u044f\", \"qty\": 50, \"unit\": \"m\u00b2\"}, {\"name\": \"\u0428\u043f\u0430\u043a\u043b\u0451\u0432\u043a\u0430 \u0441\u0442\u0435\u043d \u0444\u0438\u043d\u0438\u0448\u043d\u0430\u044f\", \"qty\": 50, \"unit\": \"m\u00b2\"}, {\"name\": \"\u041e\u043a\u0440\u0430\u0441\u043a\u0430 \u0441\u0442\u0435\u043d \u0432\u043e\u0434\u043e\u044d\u043c\u0443\u043b\u044c\u0441\u0438\u043e\u043d\u043d\u0430\u044f\", \"qty\": 50, \"unit\": \"m\u00b2\"}, {\"name\": \"\u0414\u0435\u043c\u043e\u043d\u0442\u0430\u0436 \u043d\u0430\u043f\u043e\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u043e\u043a\u0440\u044b\u0442\u0438\u044f\", \"qty\": 20, \"unit\": \"m\u00b2\"}, {\"name\": \"\u0423\u043a\u043b\u0430\u0434\u043a\u0430 \u043b\u0430\u043c\u0438\u043d\u0430\u0442\u0430 \u0441 \u043f\u043e\u0434\u043b\u043e\u0436\u043a\u043e\u0439\", \"qty\": 20, \"unit\": \"m\u00b2\"}, {\"name\": \"\u041c\u043e\u043d\u0442\u0430\u0436 \u043f\u043b\u0438\u043d\u0442\u0443\u0441\u0430\", \"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\u00b2\"}, {\"name\": \"Enlucido paredes\", \"qty\": 50, \"unit\": \"m\u00b2\"}, {\"name\": \"Pintura paredes\", \"qty\": 50, \"unit\": \"m\u00b2\"}, {\"name\": \"Demolici\u00f3n suelo\", \"qty\": 20, \"unit\": \"m\u00b2\"}, {\"name\": \"Tarima flotante\", \"qty\": 20, \"unit\": \"m\u00b2\"}]'\n  },\n  'French': {\n    input: 'r\u00e9novation cuisine 20m2 murs et sol',\n    output: '[{\"name\": \"D\u00e9pose papier peint\", \"qty\": 50, \"unit\": \"m\u00b2\"}, {\"name\": \"Enduit murs\", \"qty\": 50, \"unit\": \"m\u00b2\"}, {\"name\": \"Peinture murs\", \"qty\": 50, \"unit\": \"m\u00b2\"}, {\"name\": \"D\u00e9pose rev\u00eatement sol\", \"qty\": 20, \"unit\": \"m\u00b2\"}, {\"name\": \"Pose parquet flottant\", \"qty\": 20, \"unit\": \"m\u00b2\"}]'\n  },\n  'Portuguese': {\n    input: 'reforma cozinha 20m2 paredes e piso',\n    output: '[{\"name\": \"Remo\u00e7\u00e3o papel parede\", \"qty\": 50, \"unit\": \"m\u00b2\"}, {\"name\": \"Reboco paredes\", \"qty\": 50, \"unit\": \"m\u00b2\"}, {\"name\": \"Pintura paredes\", \"qty\": 50, \"unit\": \"m\u00b2\"}, {\"name\": \"Remo\u00e7\u00e3o piso\", \"qty\": 20, \"unit\": \"m\u00b2\"}, {\"name\": \"Piso laminado\", \"qty\": 20, \"unit\": \"m\u00b2\"}]'\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\u00b2 (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": "\ud83d\udce4 Edit Menu",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -4128,
        -1520
      ],
      "parameters": {
        "url": "=https://api.telegram.org/bot{{ $('\ud83d\udd11 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
      ],
      "parameters": {
        "updates": [
          "callback_query",
          "message"
        ],
        "additionalFields": {}
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "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: `\u2705 *Deutsch* \u00b7 Berlin \u00b7 EUR\n\n*Beschreiben Sie Ihr Projekt:*\n\n*Option 1 \u2014 Kurzbeschreibung:*\n\\`Renovierung K\u00fcche 15m\u00b2, mittlere Qualit\u00e4t\\`\n\\`Badezimmer komplett neu 8m\u00b2\\`\n\n*Option 2 \u2014 Detaillierte Liste:*\n\\`\\`\\`\nGipskarton 2-lagig 25m2\nFliesen 60x60 Bad 15m2\nSpachteln Decken W\u00e4nde 120m2\nSteckdosen 20 St\u00fcck\n\\`\\`\\``,\n\n  EN: `\u2705 *English* \u00b7 Toronto \u00b7 CAD\n\n*Describe your project:*\n\n*Option 1 \u2014 Brief description:*\n\\`Kitchen renovation 15m\u00b2, medium quality\\`\n\\`Complete bathroom remodel 8m\u00b2\\`\n\n*Option 2 \u2014 Detailed list:*\n\\`\\`\\`\nDrywall 2-layer 25m2\nTiles 60x60 bathroom 15m2\nPlastering ceiling walls 120m2\nElectrical outlets 20 pcs\n\\`\\`\\``,\n\n  RU: `\u2705 *\u0420\u0443\u0441\u0441\u043a\u0438\u0439* \u00b7 \u0421\u041f\u0431 \u00b7 RUB\n\n*\u041e\u043f\u0438\u0448\u0438\u0442\u0435 \u0432\u0430\u0448 \u043f\u0440\u043e\u0435\u043a\u0442:*\n\n*\u0412\u0430\u0440\u0438\u0430\u043d\u0442 1 \u2014 \u041a\u0440\u0430\u0442\u043a\u043e\u0435 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435:*\n\\`\u0420\u0435\u043c\u043e\u043d\u0442 \u043a\u0443\u0445\u043d\u0438 15\u043c\u00b2, \u0441\u0440\u0435\u0434\u043d\u0438\u0439 \u043a\u043b\u0430\u0441\u0441\\`\n\\`\u0412\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043c\u043d\u0430\u0442\u0430 \u043f\u043e\u0434 \u043a\u043b\u044e\u0447 8\u043c\u00b2\\`\n\n*\u0412\u0430\u0440\u0438\u0430\u043d\u0442 2 \u2014 \u0414\u0435\u0442\u0430\u043b\u044c\u043d\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a:*\n\\`\\`\\`\n\u0413\u0438\u043f\u0441\u043e\u043a\u0430\u0440\u0442\u043e\u043d 2 \u0441\u043b\u043e\u044f 25\u043c2\n\u041f\u043b\u0438\u0442\u043a\u0430 60x60 \u0432\u0430\u043d\u043d\u0430\u044f 15\u043c2\n\u0428\u043f\u0430\u043a\u043b\u0435\u0432\u043a\u0430 \u043f\u043e\u0442\u043e\u043b\u043a\u0438 \u0441\u0442\u0435\u043d\u044b 120\u043c2\n\u0420\u043e\u0437\u0435\u0442\u043a\u0438 20\u0448\u0442\n\\`\\`\\``,\n\n  ES: `\u2705 *Espa\u00f1ol* \u00b7 Barcelona \u00b7 EUR\n\n*Describa su proyecto:*\n\n*Opci\u00f3n 1 \u2014 Descripci\u00f3n breve:*\n\\`Reforma cocina 15m\u00b2, calidad media\\`\n\\`Ba\u00f1o completo 8m\u00b2\\`\n\n*Opci\u00f3n 2 \u2014 Lista detallada:*\n\\`\\`\\`\nPladur 2 capas 25m2\nAzulejos 60x60 ba\u00f1o 15m2\nAlisado techos paredes 120m2\nEnchufes 20uds\n\\`\\`\\``,\n\n  FR: `\u2705 *Fran\u00e7ais* \u00b7 Paris \u00b7 EUR\n\n*D\u00e9crivez votre projet:*\n\n*Option 1 \u2014 Description courte:*\n\\`R\u00e9novation cuisine 15m\u00b2, qualit\u00e9 moyenne\\`\n\\`Salle de bain compl\u00e8te 8m\u00b2\\`\n\n*Option 2 \u2014 Liste d\u00e9taill\u00e9e:*\n\\`\\`\\`\nPlaco 2 couches 25m2\nCarrelage 60x60 sdb 15m2\nEnduit plafonds murs 120m2\nPrises 20pcs\n\\`\\`\\``,\n\n  PT: `\u2705 *Portugu\u00eas* \u00b7 S\u00e3o Paulo \u00b7 BRL\n\n*Descreva seu projeto:*\n\n*Op\u00e7\u00e3o 1 \u2014 Descri\u00e7\u00e3o breve:*\n\\`Reforma cozinha 15m\u00b2, qualidade m\u00e9dia\\`\n\\`Banheiro completo 8m\u00b2\\`\n\n*Op\u00e7\u00e3o 2 \u2014 Lista detalhada:*\n\\`\\`\\`\nDrywall 2 camadas 25m2\nPorcelanato 60x60 banheiro 15m2\nMassa corrida teto paredes 120m2\nTomadas 20un\n\\`\\`\\``,\n\n  ZH: `\u2705 *\u4e2d\u6587* \u00b7 \u4e0a\u6d77 \u00b7 CNY\n\n*\u63cf\u8ff0\u60a8\u7684\u9879\u76ee:*\n\n*\u9009\u98791 \u2014 \u7b80\u8981\u63cf\u8ff0:*\n\\`\u53a8\u623f\u7ffb\u65b0 15m\u00b2\uff0c\u4e2d\u7b49\u54c1\u8d28\\`\n\\`\u536b\u751f\u95f4\u5168\u5957 8m\u00b2\\`\n\n*\u9009\u98792 \u2014 \u8be6\u7ec6\u6e05\u5355:*\n\\`\\`\\`\n\u77f3\u818f\u677f\u53cc\u5c42 25m2\n\u74f7\u781660x60\u536b\u751f\u95f4 15m2\n\u817b\u5b50\u9876\u5899 120m2\n\u63d2\u5ea7 20\u4e2a\n\\`\\`\\``,\n\n  AR: `\u2705 *\u0627\u0644\u0639\u0631\u0628\u064a\u0629* \u00b7 \u062f\u0628\u064a \u00b7 AED\n\n*\u0635\u0641 \u0645\u0634\u0631\u0648\u0639\u0643:*\n\n*\u0627\u0644\u062e\u064a\u0627\u0631 1 \u2014 \u0648\u0635\u0641 \u0645\u062e\u062a\u0635\u0631:*\n\\`\u062a\u062c\u062f\u064a\u062f \u0645\u0637\u0628\u062e 15\u0645\u00b2\u060c \u062c\u0648\u062f\u0629 \u0645\u062a\u0648\u0633\u0637\u0629\\`\n\\`\u062d\u0645\u0627\u0645 \u0643\u0627\u0645\u0644 8\u0645\u00b2\\`\n\n*\u0627\u0644\u062e\u064a\u0627\u0631 2 \u2014 \u0642\u0627\u0626\u0645\u0629 \u0645\u0641\u0635\u0644\u0629:*\n\\`\\`\\`\n\u062c\u0628\u0633 \u0628\u0648\u0631\u062f \u0637\u0628\u0642\u062a\u064a\u0646 25\u06452\n\u0628\u0644\u0627\u0637 60x60 \u062d\u0645\u0627\u0645 15\u06452\n\u0645\u0639\u062c\u0648\u0646 \u0623\u0633\u0642\u0641 \u062c\u062f\u0631\u0627\u0646 120\u06452\n\u0645\u0642\u0627\u0628\u0633 20 \u0642\u0637\u0639\u0629\n\\`\\`\\``,\n\n  HI: `\u2705 *\u0939\u093f\u0928\u094d\u0926\u0940* \u00b7 \u092e\u0941\u0902\u092c\u0908 \u00b7 INR\n\n*\u0905\u092a\u0928\u093e \u092a\u094d\u0930\u094b\u091c\u0947\u0915\u094d\u091f \u092c\u0924\u093e\u090f\u0902:*\n\n*\u0935\u093f\u0915\u0932\u094d\u092a 1 \u2014 \u0938\u0902\u0915\u094d\u0937\u093f\u092a\u094d\u0924 \u0935\u093f\u0935\u0930\u0923:*\n\\`\u0915\u093f\u091a\u0928 \u0930\u0947\u0928\u094b\u0935\u0947\u0936\u0928 15m\u00b2, \u092e\u0927\u094d\u092f\u092e \u0915\u094d\u0935\u093e\u0932\u093f\u091f\u0940\\`\n\\`\u092c\u093e\u0925\u0930\u0942\u092e \u092a\u0942\u0930\u094d\u0923 8m\u00b2\\`\n\n*\u0935\u093f\u0915\u0932\u094d\u092a 2 \u2014 \u0935\u093f\u0938\u094d\u0924\u0943\u0924 \u0938\u0942\u091a\u0940:*\n\\`\\`\\`\n\u0921\u094d\u0930\u093e\u0908\u0935\u0949\u0932 2 \u0932\u0947\u092f\u0930 25m2\n\u091f\u093e\u0907\u0932\u094d\u0938 60x60 \u092c\u093e\u0925\u0930\u0942\u092e 15m2\n\u092a\u0941\u091f\u094d\u091f\u0940 \u091b\u0924 \u0926\u0940\u0935\u093e\u0930\u0947\u0902 120m2\n\u0938\u0949\u0915\u0947\u091f 20 \u092a\u0940\u0938\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: \"\u2753 Help\", callback_data: \"show_help\"}, \n            {text: \"\ud83c\udf10 Language\", callback_data: \"change_language\"}\n          ]\n        ]\n      }\n    }\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "cf603c92-9a4b-4f48-91f9-07b4f3cbdb29",
      "name": "\ud83d\udd27 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": "\ud83e\udd16 AI Parse Text",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        -3968,
        -1696
      ],
      "parameters": {
        "messages": {
          "messageValues": [
            {
              "message": "={{ $('\ud83d\udd27 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": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "8f46f5e0-89f6-4e08-9dea-77fb73e35aef",
      "name": "\ud83d\udd27 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": "\ud83e\udd16 AI Transform",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        -1568,
        -400
      ],
      "parameters": {
        "messages": {
          "messageValues": [
            {
              "message": "={{ $('\ud83d\udd27 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": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "e059086c-d718-45c2-abb1-365997bf0cbf",
      "name": "\ud83d\udd27 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": "\ud83e\udd16 AI Rerank",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        -1568,
        -208
      ],
      "parameters": {
        "messages": {
          "messageValues": [
            {
              "message": "={{ $('\ud83d\udd27 Config Rerank').item.json.chatInput }}"
            }
          ]
        }
      },
      "typeVersion": 1.4
    },
    {
      "id": "06f2702e-b068-4a92-afa7-67d935b881f1",
      "name": "\ud83d\udcdd 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\u00b2').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": "## \ud83e\udd16 AI Parse Text\n\n**Purpose:** Extract works from user text\n\n**Models (enable ONE):**\n- \u2713 OpenAI Model 1 (gpt-4o-mini)\n- \u25cb Claude Model 1 (disabled)\n- \u25cb 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": "## \ud83e\udd16 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": "## \u26a0\ufe0f Setup \ud83d\udd11 TOKEN\n\n**Edit TOKEN node values:**\n\n- `bot_token` \u2192 Telegram token\n- `QDRANT_URL` \u2192 Qdrant server\n\n**Error \"resource not found\"?**\n\u2192 bot_token is invalid\n\n**Get token:** @BotFather \u2192 /newbot"
      },
      "typeVersion": 1
    },
    {
      "id": "a12cfbcc-d1c7-41b9-ae56-a36a35ff3e3b",
      "name": "\ud83d\udd27 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\ufe0f\u20e3 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\": \"{{ $('\ud83d\udd27 Config Embed').item.json.model || 'text-embedding-3-large' }}\",\n  \"input\": {{ JSON.stringify($('\ud83d\udd27 Config Embed').item.json.text) }},\n  \"dimensions\": {{ $('\ud83d\udd27 Config Embed').item.json.dimensions || 3072 }}\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "openAiApi"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "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": "## \ud83e\uddee Embeddings\n\n**Settings in Config Embed:**\n- `model`: text-embedding-3-large\n- `dimensions`: 3072\n\n**Change model:**\nEdit \ud83d\udd27 Config Embed node\n\n**Credentials:**\nUses n8n OpenAI credential in 3\ufe0f\u20e3 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": "## \ud83e\udde0 Available AI Models\n\nConnect any model to LLM Chain nodes:\n\n- **OpenAI** \u2014 GPT-4o (default)\n- **Anthropic** \u2014 Claude 3.5\n- **Google Gemini** \u2014 Gemini Pro\n- **OpenRouter** \u2014 Multiple models\n- **xAI Grok** \u2014 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,
  "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": "\ud83e\uddf9 Prep Cleanup",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "\ud83d\udcdd 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": "\ud83d\udce4 Final",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route": {
      "main": [
        [
          {
            "node": "\ud83d\udce4 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": "\ud83d\udce4 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": "\ud83d\udce4 Help",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "View Details",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prep Text LLM",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "\ud83d\udce4 Fallback",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "Route",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF PDF": {
      "main": [
        [
          {
            "node": "\ud83d\udce4 Send PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Menu": {
      "main": [
        [
          {
            "node": "\ud83d\udce4 Edit Menu",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prep Works": {
      "main": [
        [
          {
            "node": "Loop",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udd11 TOKEN": {
      "main": [
        [
          {
            "node": "Main",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate PDF": {
      "main": [
        [
          {
            "node": "IF PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prep Lang OK": {
      "main": [
        [
          {
            "node": "\ud83d\udce4 Lang OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "View Details": {
      "main": [
        [
          {
            "node": "\ud83d\udce4 Details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate HTML": {
      "main": [
        [
          {
            "node": "Final",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prep Text LLM": {
      "main": [
        [
          {
            "node": "\ud83d\udd27 Config Parse",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Works Updated": {
      "main": [
        [
          {
            "node": "\ud83d\udce4 Works Updated",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Answer Calc CB": {
      "main": [
        [
          {
            "node": "\ud83d\udcdd Prep Progress",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Answer Lang CB": {
      "main": [
        [
          {
            "node": "Prep Lang OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Excel": {
      "main": [
        [
          {
            "node": "\ud83d\udce4 Send Excel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Model 1": {
      "ai_languageModel": [
        [
          {
            "node": "\ud83e\udd16 AI Parse Text",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Model 2": {
      "ai_languageModel": [
        [
          {
            "node": "\ud83e\udd16 AI Transform",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "\ud83e\udd16 AI Rerank",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Prep HTML File": {
      "main": [
        [
          {
            "node": "\ud83d\udce4 Send HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udce4 Send Work": {
      "main": [
        [
          {
            "node": "\ud83d\udcbe Save Work Msg",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83e\udd16 AI Rerank": {
      "main": [
        [
          {
            "node": "8\ufe0f\u20e3 Apply Rerank",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcca Show Works": {
      "main": [
        [
          {
            "node": "\ud83d\udce4 Send Works",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Progress ID": {
      "main": [
        [
          {
            "node": "Prep Works",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udce4 Edit Result": {
      "main": [
        [
          {
            "node": "Acc",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "9\ufe0f\u20e3 Calculate": {
      "main": [
        [
          {
            "node": "\ud83d\udcca Update Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Telegram Trigger1": {
      "main": [
        [
          {
            "node": "\ud83d\udd11 TOKEN",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udd27 Config Embed": {
      "main": [
        [
          {
            "node": "3\ufe0f\u20e3 Embeddings API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udd27 Config Parse": {
      "main": [
        [
          {
            "node": "\ud83e\udd16 AI Parse Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83e\udd16 AI Transform": {
      "main": [
        [
          {
            "node": "2\ufe0f\u20e3 Extract Transform",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83e\uddf9 Prep Cleanup": {
      "main": [
        [
          {
            "node": "\ud83d\uddd1\ufe0f Delete Work Msg",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1\ufe0f\u20e3 Prep Query": {
      "main": [
        [
          {
            "node": "\ud83d\udd27 Config Transform",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcbe Save Work Msg": {
      "main": [
        [
          {
            "node": "1\ufe0f\u20e3 Prep Query",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcca Update Result": {
      "main": [
        [
          {
            "node": "\ud83d\udce4 Edit Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcdd Prep Progress": {
      "main": [
        [
          {
            "node": "\ud83d\udce4 Send Progress",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcdd Prep Work Msg": {
      "main": [
        [
          {
            "node": "\ud83d\uddd1\ufe0f Delete Prev",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udce4 Send Progress": {
      "main": [
        [
          {
            "node": "Save Progress ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udd27 Config Rerank": {
      "main": [
        [
          {
            "node": "\ud83e\udd16 AI Rerank",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83e\udd16 AI Parse Text": {
      "main": [
        [
          {
            "node": "\ud83d\udcdd Parse Text Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "6\ufe0f\u20e3 Prep Rerank": {
      "main": [
        [
          {
            "node": "\ud83d\udd27 Config Rerank",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\uddd1\ufe0f Delete Prev": {
      "main": [
        [
          {
            "node": "\ud83d\udce4 Send Work",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "8\ufe0f\u20e3 Apply Rerank": {
      "main": [
        [
          {
            "node": "9\ufe0f\u20e3 Calculate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "5\ufe0f\u20e3 Qdrant Search": {
      "main": [
        [
          {
            "node": "6\ufe0f\u20e3 Prep Rerank",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udd27 Config Transform": {
      "main": [
        [
          {
            "node": "\ud83e\udd16 AI Transform",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3\ufe0f\u20e3 Embeddings API": {
      "main": [
        [
          {
            "node": "4\ufe0f\u20e3 Extract Embedding",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\uddd1\ufe0f Delete Work Msg": {
      "main": [
        [
          {
            "node": "\ud83d\uddd1\ufe0f Delete Progress Msg",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcdd Parse Text Response": {
      "main": [
        [
          {
            "node": "\ud83d\udcca Show Works",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2\ufe0f\u20e3 Extract Transform": {
      "main": [
        [
          {
            "node": "\ud83d\udd27 Config Embed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4\ufe0f\u20e3 Extract Embedding": {
      "main": [
        [
          {
            "node": "5\ufe0f\u20e3 Qdrant Search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\uddd1\ufe0f Delete Progress Msg": {
      "main": [
        [
          {
            "node": "Agg",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}