{
  "name": "Offline \u2013 Build Question Bank from Lesson (Lesson Bundle \u2192 Supabase) 12/2",
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        16,
        320
      ],
      "id": "94765859-be5a-4c59-84cf-701c2f8110a5",
      "name": "When clicking \u2018Execute workflow\u2019"
    },
    {
      "parameters": {
        "url": "https://raw.githubusercontent.com/norahkerendian/dsc180a-q1/refs/heads/main/data_scraping/inferential_lessons.json?token=GHSAT0AAAAAADL5WWDGDRJBCP2OXVJ7YA2Q2JPTETQ",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        240,
        320
      ],
      "id": "e2ac2e11-822b-493a-81ba-6a4ad92a4434",
      "name": "HTTP Request (unused for now)"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "lesson_slug",
              "name": "lesson_slug",
              "value": "={{ $json.slug }}",
              "type": "string"
            },
            {
              "id": "lesson_title",
              "name": "lesson_title",
              "value": "={{ $json.title }}",
              "type": "string"
            },
            {
              "id": "topic",
              "name": "topic",
              "value": "={{ $json.topic || $json.title }}",
              "type": "string"
            },
            {
              "id": "level",
              "name": "level",
              "value": "={{ $json.level }}",
              "type": "number"
            },
            {
              "id": "lesson_text",
              "name": "lesson_text",
              "value": "=={{ $json.text_chunk }}",
              "type": "string"
            },
            {
              "id": "0c3999ed-6dbf-4ed5-b2eb-e865e9587c59",
              "name": "chunk_id",
              "value": "={{ $json.chunk_index }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1072,
        320
      ],
      "id": "04d3df20-23b9-48fd-99b5-f5fa5112d908",
      "name": "Set \u2013 Lesson Metadata"
    },
    {
      "parameters": {
        "functionCode": "// DeepSeek returns JSON inside choices[0].message.content.\n\nconst raw =\n  $json.choices &&\n  Array.isArray($json.choices) &&\n  $json.choices[0] &&\n  $json.choices[0].message &&\n  typeof $json.choices[0].message.content === 'string'\n    ? $json.choices[0].message.content.trim()\n    : '';\n\nif (!raw) {\n  throw new Error('LLM response is empty or missing choices[0].message.content');\n}\n\n// Clean markdown code fences and DeepSeek <think> tags\nconst cleaned = raw\n  // remove ```json and ``` fences\n  .replace(/```json/gi, '')\n  .replace(/```/g, '')\n  // remove DeepSeek r1 thinking blocks if present\n  .replace(/<think>[\\\\s\\\\S]*?<\\\\/think>/gi, '')\n  .trim();\n\nlet bundle;\n\ntry {\n  bundle = JSON.parse(cleaned);\n} catch (err) {\n  throw new Error(\n    'Failed to parse LLM JSON: ' +\n      err.message +\n      '\\nRaw content (first 500 chars):\\n' +\n      cleaned.slice(0, 500)\n  );\n}\n\n// Expecting shape:\n// {\n//   \"lesson\": { \"slug\": \"...\", \"title\": \"...\", \"topic\": \"...\", \"level\": 0 },\n//   \"questions\": [ ... ]\n// }\nconst lesson = bundle.lesson || {};\nconst questions = Array.isArray(bundle.questions) ? bundle.questions : [];\n\nconst metaNode = $node['Set \u2013 Lesson Metadata'].json || {};\n\nreturn questions.map((q, index) => ({\n  json: {\n    // lesson metadata (fallback-safe)\n    lesson_slug: lesson.slug || metaNode.lesson_slug,\n    lesson_title: lesson.title || metaNode.lesson_title,\n    topic: lesson.topic || metaNode.topic,\n    level:\n      typeof lesson.level === 'number'\n        ? lesson.level\n        : metaNode.level,\n\n    // question info\n    question: q.question,\n    answer: q.answer,\n    difficulty: typeof q.difficulty === 'number' ? q.difficulty : 1,\n    category: q.category || 'unspecified',\n    question_type: q.question_type || 'short_answer',\n    source: 'baseline',\n    index,\n  }\n}));\n"
      },
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        2416,
        320
      ],
      "id": "80c16f3d-ee34-44da-bdb2-be2494e5b9b5",
      "name": "Function \u2013 Flatten Lesson Bundle"
    },
    {
      "parameters": {
        "operation": "insert"
      },
      "type": "n8n-nodes-base.supabase",
      "typeVersion": 1,
      "position": [
        2688,
        320
      ],
      "id": "5625e78a-f95f-4349-8782-a88cca0b4bf2",
      "name": "Supabase \u2013 Insert Questions"
    },
    {
      "parameters": {
        "content": "## Notes\n1. This workflow builds a question bank from a single LESSON_TEXT using DeepSeek.\n2. The Set \u2013 Lesson Metadata node is where you paste your synthetic lesson for now.\n3. Supabase \u2013 Insert Questions needs column mappings configured in the n8n UI.\n4. Later, we can swap the Set node to pull lessons from Supabase instead of hardcoding.",
        "height": 240,
        "width": 560
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "id": "f2c6d2b4-9eef-482e-a0e3-1160cb1eed5f",
      "name": "Sticky Note \u2013 Workflow 1 Overview"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "dee42752-1db6-45d5-b514-2c829145a877",
              "name": "parsedData",
              "value": "={{ JSON.parse($json.data) }}",
              "type": "array"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        448,
        320
      ],
      "id": "75158c1e-c889-4683-ae34-dd6f85998aea",
      "name": "Edit Fields"
    },
    {
      "parameters": {
        "jsCode": "// const inputItems = $input.all();\n// const chunkSize = 800;\n\n// let out = [];\n\n// for (const item of inputItems) {\n//   const lesson = item.json;\n//   const text = lesson.content_md || \"\";\n\n//   for (let i = 0; i < text.length; i += chunkSize) {\n//     out.push({\n//       json: {\n//         ...lesson,\n//         text_chunk: text.slice(i, i + chunkSize),\n//         chunk_index: Math.floor(i / chunkSize),\n//       }\n//     });\n//   }\n// }\n\n// return out;\n\nconst chunkSize = 900;\n\nfunction clean(str) {\n  return str\n    .replace(/\\r?\\n+/g, \" \")        // remove line breaks\n    .replace(/\\s+/g, \" \")           // collapse whitespace\n    .replace(/\\[.*?\\]\\(.*?\\)/g, \"\") // remove markdown links\n    .replace(/[#*_`>~=-]/g, \"\")     // remove symbols\n    .trim();\n}\n\nconst inputItems = $input.all();\nlet out = [];\n\nfor (const item of inputItems) {\n  const lesson = item.json;\n  const text = clean(lesson.content_md || \"\");\n\n  for (let i = 0; i < text.length; i += chunkSize) {\n    out.push({\n      json: {\n        ...lesson,\n        text_chunk: text.slice(i, i + chunkSize),\n        chunk_index: Math.floor(i / chunkSize),\n      }\n    });\n  }\n}\n\nreturn out;\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        864,
        320
      ],
      "id": "f4b742dc-638f-4dd9-a773-55a1358a08cb",
      "name": "chunking"
    },
    {
      "parameters": {
        "jsCode": "// parsedData should be an array of lessons from your JSON\nconst lessons = $json.parsedData;\n\n// Optional filter if you want to test only some levels first:\n// const filtered = lessons.filter(l => l.level <= 1);\n// const source = filtered;\n\nconst source = lessons;\n\nreturn source.map(item => ({\n  json: item\n}));"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        656,
        320
      ],
      "id": "6e2f094c-0266-46ba-b5d5-f7f48f9ae762",
      "name": "mapping"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://host.docker.internal:11434/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "{\n  \"model\": \"deepseek-r1:1.5b\",\n  \"messages\": [\n    {\n      \"role\": \"system\",\n      \"content\": \"You generate high-quality beginner-friendly quiz questions using only the information in the provided answers. Your job is to write clear, factual questions whose answers exactly match the provided text. Return JSON only.\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": \"Below is a list of factual answers extracted from a lesson. Write a corresponding question for each answer. Rules: Use only the information in the answers. Do not introduce new facts. Keep questions simple and direct. Return JSON only in this exact format: { \\\"qa_pairs\\\": [ { \\\"question\\\": \\\"...\\\", \\\"answer\\\": \\\"...\\\" }, { \\\"question\\\": \\\"...\\\", \\\"answer\\\": \\\"...\\\" } ] }. Answers: {{ $json.answers }}\"\n    }\n  ],\n  \"temperature\": 0.2\n}\n",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2224,
        320
      ],
      "id": "23aa715e-cebd-417a-a219-ae888d0d21e0",
      "name": "generate questions"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://host.docker.internal:11434/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "{\n  \"model\":\"llama3.2:1b\",\n  \"messages\":[\n    {\"role\":\"system\",\"content\":\"Summarize the lesson in 1-2 short, factual sentences for beginners.\"},\n    {\"role\":\"user\",\"content\":\"Text: {{ $json.safe_text }}\"}\n  ],\n  \"max_tokens\":120,\n  \"temperature\":0.2\n}\n",
        "options": {
          "timeout": 240000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1872,
        320
      ],
      "id": "ee8edea5-d9d8-447f-8df1-d4901af29f55",
      "name": "generate answers"
    },
    {
      "parameters": {
        "jsCode": "const raw = $json.choices?.[0]?.message?.content || \"\";\nconst cleaned = raw\n  .replace(/```json/gi, \"\")\n  .replace(/```/g, \"\")\n  .replace(/<think>[\\s\\S]*?<\\/think>/gi, \"\")\n  .trim();\n\nreturn [\n  {\n    json: {\n      summary: cleaned\n    }\n  }\n];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2048,
        320
      ],
      "id": "3317d2f7-1a81-41bf-8b9f-bc96a1fdf4a6",
      "name": "extract summary"
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        1280,
        320
      ],
      "id": "4be667af-592d-4609-b713-b8a6a78ac4d5",
      "name": "Loop Over Items"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.noOp",
      "name": "Replace Me",
      "typeVersion": 1,
      "position": [
        1696,
        384
      ],
      "id": "0c36188a-8a2e-4b83-a018-2de3645f07dc"
    },
    {
      "parameters": {
        "jsCode": "// Take raw chunk\nlet text = ($json.lesson_text || \"\").toString();\n\n// Clean & shrink aggressively for llama3.2:1b stability\ntext = text\n  .replace(/\\r?\\n+/g, \" \")\n  .replace(/\\s+/g, \" \")\n  .replace(/\\[.*?\\]\\(.*?\\)/g, \"\")\n  .replace(/[#*_`>~=-]/g, \"\")\n  .trim()\n  .slice(0, 600);   // HARD limit\n\nreturn [\n  {\n    json: {\n      ...$json,\n      safe_text: text\n    }\n  }\n];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1488,
        224
      ],
      "id": "9f1ab716-784e-4941-99b0-6ae1e5ea75df",
      "name": "Prepare Lesson Text"
    }
  ],
  "connections": {
    "When clicking \u2018Execute workflow\u2019": {
      "main": [
        [
          {
            "node": "HTTP Request (unused for now)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request (unused for now)": {
      "main": [
        [
          {
            "node": "Edit Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set \u2013 Lesson Metadata": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Function \u2013 Flatten Lesson Bundle": {
      "main": [
        [
          {
            "node": "Supabase \u2013 Insert Questions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields": {
      "main": [
        [
          {
            "node": "mapping",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "chunking": {
      "main": [
        [
          {
            "node": "Set \u2013 Lesson Metadata",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "mapping": {
      "main": [
        [
          {
            "node": "chunking",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "generate questions": {
      "main": [
        [
          {
            "node": "Function \u2013 Flatten Lesson Bundle",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "generate answers": {
      "main": [
        [
          {
            "node": "extract summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "extract summary": {
      "main": [
        [
          {
            "node": "generate questions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [
          {
            "node": "Prepare Lesson Text",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Replace Me",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Replace Me": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Lesson Text": {
      "main": [
        [
          {
            "node": "generate answers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "5ca0d598-c390-4b54-9b72-d118de2eca1b",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "id": "mwe4eSWKC9ZIEhX9",
  "tags": []
}