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