{"id":803,"date":"2026-03-05T23:31:59","date_gmt":"2026-03-05T23:31:59","guid":{"rendered":"https:\/\/i-cte.org\/robot\/?p=803"},"modified":"2026-03-05T23:31:59","modified_gmt":"2026-03-05T23:31:59","slug":"advanced-rag-custom-knowledge-bases-for-research-teams","status":"publish","type":"post","link":"https:\/\/i-cte.org\/robot\/advanced-rag-custom-knowledge-bases-for-research-teams\/","title":{"rendered":"Advanced: RAG + Custom Knowledge Bases for Research Teams"},"content":{"rendered":"\n<!-- \u2705 ICTE Teacher Micro-Lesson \u2014 Advanced: RAG + Custom Knowledge Bases for Research Teams\n     WP-safe single block, Multi-speaker Google Voices (2 voices) + Mic Speech Recognition\n     Includes: Overview + Conversation + Reading + Quiz + Toolkit + Prompts + Listening + RAG Lab + Problem-solving + Progress\n\n     What this DOES (client-side, WP-safe):\n     - Lets you paste PDFs\/notes (as text) \u2192 auto-chunk \u2192 searchable \u201cretrieval\u201d\n     - Produces citation-grounded \u201canswer draft\u201d using chunk IDs (no hallucinated refs)\n     - Provides export\/import of the knowledge base (JSON) + local-only storage\n     - Adds IRB-friendly privacy\/ethics checklist and workflow automation templates\n\n     What this DOES NOT do (needs a backend):\n     - Real PDF parsing\/embedding\/vector DB \/ LLM generation.\n     - You can connect these UI outputs to your RAG backend later (OpenAI, local LLM, etc.).\n-->\n<div id=\"icte-rag\">\n\n  <!-- \u2705 TOP MENU -->\n  <nav class=\"icte-menu\" aria-label=\"RAG lesson navigation\">\n    <a href=\"#\" class=\"is-current\" data-view=\"overview\">Overview<\/a>\n    <a href=\"#\" data-view=\"conversation\">Conversation<\/a>\n    <a href=\"#\" data-view=\"reading\">Reading<\/a>\n    <a href=\"#\" data-view=\"toolkit\">RAG Toolkit<\/a>\n    <a href=\"#\" data-view=\"prompts\">Prompts<\/a>\n    <a href=\"#\" data-view=\"listening\">Listening<\/a>\n    <a href=\"#\" data-view=\"lab\">RAG Lab<\/a>\n    <a href=\"#\" data-view=\"problem\">Problem-solving<\/a>\n    <a href=\"#\" data-view=\"progress\">Progress<\/a>\n  <\/nav>\n\n  <section class=\"icte-shell\" aria-label=\"ICTE RAG lesson\">\n\n    <!-- \u2705 Header -->\n    <header class=\"icte-hero\">\n      <div class=\"icte-hero__text\">\n        <div class=\"hero-top\">\n          <img decoding=\"async\"\n            class=\"hero-img\"\n            src=\"https:\/\/i-cte.org\/robot\/wp-content\/uploads\/2026\/03\/Screenshot-2026-03-06-at-06.29.30.png\"\n            alt=\"Advanced RAG and custom knowledge bases banner\"\n            loading=\"lazy\"\n          \/>\n          <div class=\"hero-title\">\n            <h2>Advanced: RAG + Custom Knowledge Bases for Research Teams<\/h2>\n            <p class=\"muted\">\n              Turn <b>PDFs\/notes<\/b> into a searchable assistant using <b>retrieval-augmented generation<\/b>:\n              retrieval \u2192 evidence \u2192 citation-grounded drafting. Includes workflow automation ideas and\n              <b>privacy\/ethics (IRB-friendly)<\/b> design principles.\n            <\/p>\n          <\/div>\n        <\/div>\n      <\/div>\n\n      <div class=\"icte-hero__controls\">\n        <div class=\"icte-pill\">\n          <span class=\"dot\" id=\"icteMicDot\" aria-hidden=\"true\"><\/span>\n          <span id=\"icteMicStatus\">Mic: Off<\/span>\n        <\/div>\n\n        <div class=\"icte-row\">\n          <button class=\"btn\" id=\"icteStartVoice\" type=\"button\">Start Voice<\/button>\n          <button class=\"btn ghost\" id=\"icteStopVoice\" type=\"button\">Stop<\/button>\n        <\/div>\n\n        <!-- \u2705 Multi-speaker Google Voices -->\n        <div class=\"grid2\" style=\"margin-top:10px;\">\n          <label class=\"icte-label\">\n            Speaker A (Google)\n            <select id=\"voiceA\" class=\"icte-select\" aria-label=\"Speaker A voice\"><\/select>\n          <\/label>\n\n          <label class=\"icte-label\">\n            Speaker B (Google)\n            <select id=\"voiceB\" class=\"icte-select\" aria-label=\"Speaker B voice\"><\/select>\n          <\/label>\n        <\/div>\n\n        <div class=\"icte-row\" style=\"margin-top:10px;\">\n          <button class=\"btn mini ghost\" type=\"button\" id=\"speakActive\">\ud83d\udd0a Read this page<\/button>\n          <button class=\"btn mini ghost\" type=\"button\" id=\"stopSpeak\">\u23f9 Stop audio<\/button>\n        <\/div>\n\n        <div class=\"icte-small muted\">\n          Tip: Best experience in Chrome\/Edge. If voices don\u2019t appear yet, click once on the page and wait 2\u20133 seconds.\n        <\/div>\n      <\/div>\n    <\/header>\n\n    <!-- \u2705 Views -->\n    <main class=\"icte-main\">\n\n      <!-- ===================== -->\n      <!-- \u2705 OVERVIEW -->\n      <!-- ===================== -->\n      <section class=\"view is-active\" data-view=\"overview\" aria-label=\"Lesson overview\">\n        <div class=\"card\">\n          <div class=\"card-h\">\n            <h3>1) Outcomes<\/h3>\n            <div class=\"card-actions\">\n              <button class=\"btn mini\" type=\"button\" data-say=\"overview-instr\">\ud83d\udd0a Read instructions<\/button>\n            <\/div>\n          <\/div>\n\n          <div class=\"grid2\">\n            <div class=\"qitem\">\n              <div class=\"qtext\">By the end, you can\u2026<\/div>\n              <ul class=\"ul\">\n                <li>Explain the RAG pipeline: <b>ingest \u2192 chunk \u2192 index \u2192 retrieve \u2192 cite \u2192 draft<\/b>.<\/li>\n                <li>Build a <b>team knowledge base<\/b> from PDFs\/notes (as text) with consistent chunk IDs.<\/li>\n                <li>Produce <b>citation-grounded answers<\/b> that point to the retrieved evidence.<\/li>\n                <li>Design IRB-friendly safeguards: <b>data minimization<\/b>, access control, retention, and de-identification.<\/li>\n                <li>Plan workflow automation: triage questions, track evidence, export logs, and audit trails.<\/li>\n              <\/ul>\n            <\/div>\n\n            <div class=\"qitem\">\n              <div class=\"qtext\">Safety rules (IRB-friendly)<\/div>\n              <ul class=\"ul\">\n                <li>Do not upload identifiable participant data to external tools without approval.<\/li>\n                <li>Prefer <b>local-only<\/b> storage for prototypes; disclose what is stored and allow deletion.<\/li>\n                <li>Ground every claim in evidence; if missing, label: <b>INSUFFICIENT EVIDENCE<\/b>.<\/li>\n                <li>Keep an audit trail: tool version, prompts, retrieved chunks, and final edits.<\/li>\n              <\/ul>\n            <\/div>\n          <\/div>\n\n          <div class=\"note\">\n            <b>RAG mindset:<\/b> The assistant does not \u201cknow.\u201d It <b>retrieves<\/b> evidence and drafts answers <b>with citations<\/b>.\n          <\/div>\n        <\/div>\n      <\/section>\n\n      <!-- ===================== -->\n      <!-- \u2705 CONVERSATION -->\n      <!-- ===================== -->\n      <section class=\"view\" data-view=\"conversation\" aria-label=\"Conversation coach\">\n        <div class=\"card\">\n          <div class=\"card-h\">\n            <h3>2) Conversation (RAG System Designer)<\/h3>\n            <div class=\"card-actions\">\n              <button class=\"btn mini\" type=\"button\" data-say=\"conv-instr\">\ud83d\udd0a Read instructions<\/button>\n              <button class=\"btn mini ghost\" type=\"button\" id=\"convHear\">\ud83d\udd0a Read last question<\/button>\n              <button class=\"btn mini ghost\" type=\"button\" id=\"convReset\">Reset<\/button>\n            <\/div>\n          <\/div>\n\n          <p class=\"muted\">\n            Practice an implementation workflow: use-case \u2192 sources \u2192 chunking \u2192 retrieval \u2192 citation format \u2192 privacy controls \u2192 evaluation.\n          <\/p>\n\n          <div class=\"chat\" id=\"convChat\" aria-live=\"polite\" aria-label=\"Conversation chat log\"><\/div>\n\n          <div class=\"chatbar\">\n            <input id=\"convText\" class=\"input\" type=\"text\" placeholder=\"Type your answer (or use voice)...\" autocomplete=\"off\" \/>\n            <button class=\"btn\" id=\"convSend\" type=\"button\">Send<\/button>\n          <\/div>\n\n          <div class=\"note\">\n            <b>Tip:<\/b> In a team KB, define \u201csource of truth\u201d (publisher PDF? internal protocol? meeting notes?) and enforce it.\n          <\/div>\n        <\/div>\n      <\/section>\n\n      <!-- ===================== -->\n      <!-- \u2705 READING + QUIZ -->\n      <!-- ===================== -->\n      <section class=\"view\" data-view=\"reading\" aria-label=\"Reading and quiz\">\n        <div class=\"card\">\n          <div class=\"card-h\">\n            <h3>3) Reading + Comprehension Quiz<\/h3>\n            <div class=\"card-actions\">\n              <button class=\"btn mini\" type=\"button\" data-say=\"reading-instr\">\ud83d\udd0a Read instructions<\/button>\n              <button class=\"btn mini ghost\" type=\"button\" id=\"readTextBtn\">\ud83d\udd0a Read the text<\/button>\n              <button class=\"btn mini ghost\" type=\"button\" id=\"quizCheck\">Check answers<\/button>\n            <\/div>\n          <\/div>\n\n          <article class=\"reading\" id=\"readingText\" aria-label=\"Reading text about RAG\">\n            <div class=\"reading-title\">Reading: What RAG is (and why it matters for research teams)<\/div>\n\n            <div class=\"reading-p\">\n              <b>1<\/b> RAG (retrieval-augmented generation) combines <b>retrieval<\/b> with <b>drafting<\/b>. Instead of answering from memory,\n              the system searches a knowledge base (PDFs, notes, protocols), retrieves the most relevant chunks, and writes an answer grounded in that evidence.\n            <\/div>\n\n            <div class=\"reading-p\">\n              <b>2<\/b> The most important rule is <b>citation grounding<\/b>: every major claim should point to the retrieved evidence.\n              If evidence is missing or ambiguous, the assistant must say so. This reduces hallucinations and strengthens academic integrity.\n            <\/div>\n\n            <div class=\"reading-p\">\n              <b>3<\/b> Building a knowledge base usually involves: ingesting documents, splitting into chunks, adding metadata (source, year, page),\n              indexing (keywords or embeddings), and supporting retrieval. Chunk quality and metadata consistency are critical for reliable citations.\n            <\/div>\n\n            <div class=\"reading-p\">\n              <b>4<\/b> Research teams also need workflow automation: question triage, evidence logging, citation checks, and version control.\n              A good system records what chunks were used to answer each question (audit trail).\n            <\/div>\n\n            <div class=\"reading-p\">\n              <b>5<\/b> Privacy\/ethics matter. IRB-friendly design uses data minimization, de-identification, access control, retention policies, and transparency.\n              The system should store only what is necessary, and participants should understand what is collected and why (when applicable).\n            <\/div>\n          <\/article>\n\n          <h4 class=\"h4\" style=\"margin-top:12px;\">Comprehension check (choose the best answer)<\/h4>\n          <div id=\"quiz\" class=\"stack\"><\/div>\n          <div class=\"feedback\" id=\"quizFb\" aria-live=\"polite\"><\/div>\n        <\/div>\n      <\/section>\n\n      <!-- ===================== -->\n      <!-- \u2705 TOOLKIT -->\n      <!-- ===================== -->\n      <section class=\"view\" data-view=\"toolkit\" aria-label=\"RAG toolkit\">\n        <div class=\"card\">\n          <div class=\"card-h\">\n            <h3>4) RAG Toolkit (Pipeline, citations, evaluation, IRB-friendly safeguards)<\/h3>\n            <div class=\"card-actions\">\n              <button class=\"btn mini\" type=\"button\" data-say=\"toolkit-instr\">\ud83d\udd0a Read instructions<\/button>\n              <button class=\"btn mini ghost\" type=\"button\" id=\"toolkitSpeak\">\ud83d\udd0a Read toolkit<\/button>\n            <\/div>\n          <\/div>\n\n          <div class=\"grid2\">\n            <div class=\"qitem\">\n              <div class=\"qtext\">RAG pipeline checklist<\/div>\n              <pre class=\"pre\" id=\"tkPipe\"><\/pre>\n            <\/div>\n            <div class=\"qitem\">\n              <div class=\"qtext\">Citation grounding format<\/div>\n              <pre class=\"pre\" id=\"tkCite\"><\/pre>\n            <\/div>\n          <\/div>\n\n          <div class=\"grid2\" style=\"margin-top:12px;\">\n            <div class=\"qitem\">\n              <div class=\"qtext\">Evaluation plan (quality gates)<\/div>\n              <pre class=\"pre\" id=\"tkEval\"><\/pre>\n            <\/div>\n            <div class=\"qitem\">\n              <div class=\"qtext\">Privacy\/ethics (IRB-friendly) checklist<\/div>\n              <pre class=\"pre\" id=\"tkIRB\"><\/pre>\n            <\/div>\n          <\/div>\n\n          <div class=\"qitem\" style=\"margin-top:12px;\">\n            <div class=\"qtext\">Workflow automation ideas (team)<\/div>\n            <pre class=\"pre\" id=\"tkAuto\"><\/pre>\n          <\/div>\n        <\/div>\n      <\/section>\n\n      <!-- ===================== -->\n      <!-- \u2705 PROMPTS -->\n      <!-- ===================== -->\n      <section class=\"view\" data-view=\"prompts\" aria-label=\"Prompts and examples\">\n        <div class=\"card\">\n          <div class=\"card-h\">\n            <h3>5) Prompts + Examples (Copy &#038; Adapt)<\/h3>\n            <div class=\"card-actions\">\n              <button class=\"btn mini\" type=\"button\" data-say=\"prompts-instr\">\ud83d\udd0a Read instructions<\/button>\n              <button class=\"btn mini ghost\" type=\"button\" id=\"promptsSpeak\">\ud83d\udd0a Read prompts<\/button>\n              <button class=\"btn mini ghost\" type=\"button\" id=\"copyPrompts\">Copy all<\/button>\n            <\/div>\n          <\/div>\n\n          <div class=\"note\">\n            These prompts force RAG behavior: retrieve evidence, cite chunks, and refuse unsupported claims.\n          <\/div>\n\n          <div class=\"grid2\">\n            <div class=\"qitem\">\n              <div class=\"qtext\">Prompt 1 \u2014 Citation-grounded answering<\/div>\n              <pre class=\"pre\" id=\"p1\"><\/pre>\n            <\/div>\n            <div class=\"qitem\">\n              <div class=\"qtext\">Prompt 2 \u2014 Chunking + metadata plan<\/div>\n              <pre class=\"pre\" id=\"p2\"><\/pre>\n            <\/div>\n          <\/div>\n\n          <div class=\"grid2\" style=\"margin-top:12px;\">\n            <div class=\"qitem\">\n              <div class=\"qtext\">Prompt 3 \u2014 IRB\/privacy impact assessment<\/div>\n              <pre class=\"pre\" id=\"p3\"><\/pre>\n            <\/div>\n            <div class=\"qitem\">\n              <div class=\"qtext\">Prompt 4 \u2014 Workflow automation spec<\/div>\n              <pre class=\"pre\" id=\"p4\"><\/pre>\n            <\/div>\n          <\/div>\n\n          <div class=\"qitem\" style=\"margin-top:12px;\">\n            <div class=\"qtext\">Mini example (expected output style)<\/div>\n            <pre class=\"pre\" id=\"pExample\"><\/pre>\n          <\/div>\n        <\/div>\n      <\/section>\n\n      <!-- ===================== -->\n      <!-- \u2705 LISTENING -->\n      <!-- ===================== -->\n      <section class=\"view\" data-view=\"listening\" aria-label=\"Two-speaker listening\">\n        <div class=\"card\">\n          <div class=\"card-h\">\n            <h3>6) Listening (Two Google Voices) \u2014 \u201cRetrieve, then cite\u201d<\/h3>\n            <div class=\"card-actions\">\n              <button class=\"btn mini\" type=\"button\" data-say=\"list-instr\">\ud83d\udd0a Read instructions<\/button>\n              <button class=\"btn mini\" type=\"button\" id=\"listenPlay\">\u25b6\ufe0f Play dialogue<\/button>\n              <button class=\"btn mini ghost\" type=\"button\" id=\"listenStop\">\u23f9 Stop<\/button>\n              <button class=\"btn mini ghost\" type=\"button\" id=\"listenCheck\">Check answers<\/button>\n            <\/div>\n          <\/div>\n\n          <p class=\"muted\">Listen to two instructors discussing RAG, citations, and IRB-friendly safeguards.<\/p>\n\n          <div id=\"listenQ\" class=\"stack\"><\/div>\n          <div class=\"feedback\" id=\"listenFb\" aria-live=\"polite\"><\/div>\n        <\/div>\n      <\/section>\n\n      <!-- ===================== -->\n      <!-- \u2705 RAG LAB -->\n      <!-- ===================== -->\n      <section class=\"view\" data-view=\"lab\" aria-label=\"RAG lab\">\n        <div class=\"card\">\n          <div class=\"card-h\">\n            <h3>7) RAG Lab \u2014 Paste Notes\/PDF Text \u2192 Chunk \u2192 Retrieve \u2192 Draft with Citations<\/h3>\n            <div class=\"card-actions\">\n              <button class=\"btn mini\" type=\"button\" data-say=\"lab-instr\">\ud83d\udd0a Read instructions<\/button>\n              <button class=\"btn mini ghost\" type=\"button\" id=\"kbBuild\">Build KB<\/button>\n              <button class=\"btn mini ghost\" type=\"button\" id=\"kbClear\">Clear KB<\/button>\n              <button class=\"btn mini ghost\" type=\"button\" id=\"kbExport\">Export KB<\/button>\n              <button class=\"btn mini ghost\" type=\"button\" id=\"kbImportBtn\">Import KB<\/button>\n              <input id=\"kbImport\" type=\"file\" accept=\"application\/json\" style=\"display:none;\" \/>\n            <\/div>\n          <\/div>\n\n          <div class=\"note\">\n            Paste text from PDFs\/notes (de-identified). The lab will create chunk IDs like <b>[SRC1:C03]<\/b>.\n            Retrieval uses keyword scoring (client-side). You then draft an answer that cites the retrieved chunks.\n          <\/div>\n\n          <div class=\"grid2\">\n            <div class=\"qitem\">\n              <div class=\"qtext\">A) Add source text (PDF\/notes as plain text)<\/div>\n              <label class=\"icte-label\">Source ID (e.g., SRC1)<\/label>\n              <input id=\"srcId\" class=\"input\" type=\"text\" placeholder=\"SRC1\" \/>\n              <label class=\"icte-label\" style=\"margin-top:10px;\">Source metadata (optional)<\/label>\n              <input id=\"srcMeta\" class=\"input\" type=\"text\" placeholder=\"e.g., Author Year Title; Pages 1\u201310; Internal protocol v2\" \/>\n              <label class=\"icte-label\" style=\"margin-top:10px;\">Paste text<\/label>\n              <textarea id=\"srcText\" class=\"textarea\" rows=\"12\" placeholder=\"Paste de-identified text here...\"><\/textarea>\n              <div class=\"icte-small muted\" style=\"margin-top:6px;\">\n                Tip: If from a PDF, paste the relevant section + include page numbers in metadata.\n              <\/div>\n            <\/div>\n\n            <div class=\"qitem\">\n              <div class=\"qtext\">B) Chunking settings<\/div>\n              <label class=\"icte-label\">Chunk size (approx words)<\/label>\n              <input id=\"chunkSize\" class=\"input\" type=\"number\" min=\"60\" max=\"350\" value=\"160\" \/>\n              <label class=\"icte-label\" style=\"margin-top:10px;\">Chunk overlap (words)<\/label>\n              <input id=\"chunkOverlap\" class=\"input\" type=\"number\" min=\"0\" max=\"120\" value=\"30\" \/>\n              <label class=\"icte-label\" style=\"margin-top:10px;\">Stopwords removal (retrieval)<\/label>\n              <select id=\"stopMode\" class=\"icte-select\">\n                <option value=\"basic\" selected>Basic (recommended)<\/option>\n                <option value=\"none\">None<\/option>\n              <\/select>\n\n              <div class=\"note\" style=\"margin-top:12px;\">\n                <b>Chunking guidance:<\/b> smaller chunks = more precise citations; larger chunks = more context. Overlap helps prevent \u201clost sentences.\u201d\n              <\/div>\n            <\/div>\n          <\/div>\n\n          <div class=\"qitem\" style=\"margin-top:12px;\">\n            <div class=\"qtext\">C) Knowledge base status<\/div>\n            <div class=\"grid2\" style=\"grid-template-columns:1fr 1fr; gap:12px; margin-top:0;\">\n              <div class=\"pbox\">\n                <div class=\"pnum\" id=\"kbSources\">0<\/div>\n                <div class=\"muted\">Sources<\/div>\n              <\/div>\n              <div class=\"pbox\">\n                <div class=\"pnum\" id=\"kbChunks\">0<\/div>\n                <div class=\"muted\">Chunks<\/div>\n              <\/div>\n            <\/div>\n            <pre class=\"pre\" id=\"kbPreview\" style=\"margin-top:10px;\">No knowledge base yet. Click \u201cBuild KB\u201d.<\/pre>\n          <\/div>\n\n          <div class=\"qitem\" style=\"margin-top:12px;\">\n            <div class=\"card-h\" style=\"border-bottom:none; padding-bottom:0; margin-bottom:8px;\">\n              <h3 style=\"margin:0; font-size:16px;\">D) Retrieve evidence<\/h3>\n              <div class=\"card-actions\">\n                <button class=\"btn mini\" id=\"runSearch\" type=\"button\">Search<\/button>\n                <button class=\"btn mini ghost\" id=\"clearSearch\" type=\"button\">Clear<\/button>\n              <\/div>\n            <\/div>\n\n            <label class=\"icte-label\">Research question \/ query<\/label>\n            <input id=\"query\" class=\"input\" type=\"text\" placeholder=\"Type a research question or query...\" \/>\n\n            <div class=\"grid2\" style=\"margin-top:10px;\">\n              <div>\n                <label class=\"icte-label\">Top K results<\/label>\n                <input id=\"topK\" class=\"input\" type=\"number\" min=\"3\" max=\"12\" value=\"6\" \/>\n              <\/div>\n              <div>\n                <label class=\"icte-label\">Require citations in draft<\/label>\n                <select id=\"citeMode\" class=\"icte-select\">\n                  <option value=\"strict\" selected>Strict (every key claim cites a chunk)<\/option>\n                  <option value=\"normal\">Normal<\/option>\n                <\/select>\n              <\/div>\n            <\/div>\n\n            <div class=\"qitem\" style=\"margin-top:12px;\">\n              <div class=\"qtext\">Retrieved chunks (evidence)<\/div>\n              <pre class=\"pre\" id=\"resultsBox\">No search yet.<\/pre>\n            <\/div>\n\n            <div class=\"qitem\" style=\"margin-top:12px;\">\n              <div class=\"card-h\" style=\"border-bottom:none; padding-bottom:0; margin-bottom:8px;\">\n                <h3 style=\"margin:0; font-size:16px;\">E) Citation-grounded answer draft<\/h3>\n                <div class=\"card-actions\">\n                  <button class=\"btn mini ghost\" id=\"draftBtn\" type=\"button\">Generate draft template<\/button>\n                  <button class=\"btn mini ghost\" id=\"copyDraft\" type=\"button\">Copy draft<\/button>\n                <\/div>\n              <\/div>\n\n              <textarea id=\"draftBox\" class=\"textarea\" rows=\"10\" placeholder=\"Your citation-grounded draft will appear here...\"><\/textarea>\n              <div class=\"icte-small muted\" style=\"margin-top:6px;\">\n                Draft is a structured template grounded in retrieved chunk IDs; you fill the final wording.\n              <\/div>\n            <\/div>\n\n            <div class=\"feedback\" id=\"labFb\" aria-live=\"polite\"><\/div>\n          <\/div>\n\n        <\/div>\n      <\/section>\n\n      <!-- ===================== -->\n      <!-- \u2705 PROBLEM-SOLVING -->\n      <!-- ===================== -->\n      <section class=\"view\" data-view=\"problem\" aria-label=\"Problem solving task\">\n        <div class=\"card\">\n          <div class=\"card-h\">\n            <h3>8) Problem-solving<\/h3>\n            <div class=\"card-actions\">\n              <button class=\"btn mini\" type=\"button\" data-say=\"problem-instr\">\ud83d\udd0a Read instructions<\/button>\n              <button class=\"btn mini ghost\" type=\"button\" id=\"psCheck\">Check my solution<\/button>\n              <button class=\"btn mini ghost\" type=\"button\" id=\"psReset\">Reset<\/button>\n            <\/div>\n          <\/div>\n\n          <div class=\"note\">\n            <b>Scenario:<\/b> Your team wants a RAG assistant trained on interview protocols + internal notes.\n            The IRB asks: \u201cHow will you prevent privacy risk and hallucinated citations?\u201d\n            <br><br>\n            Your task:\n            <ul class=\"ul\">\n              <li>Write a <b>pipeline spec<\/b> (6\u201310 bullets): ingest \u2192 chunk \u2192 retrieve \u2192 cite \u2192 draft.<\/li>\n              <li>List <b>4 privacy\/ethics safeguards<\/b> (IRB-friendly).<\/li>\n              <li>Define <b>3 evaluation checks<\/b> (citation accuracy, retrieval quality, refusal behavior).<\/li>\n              <li>Draft a <b>team workflow<\/b> for question triage + audit trail (4\u20136 steps).<\/li>\n            <\/ul>\n          <\/div>\n\n          <div class=\"grid2\">\n            <div class=\"qitem\">\n              <div class=\"qtext\">A) Pipeline spec (6\u201310 bullets)<\/div>\n              <textarea class=\"textarea\" id=\"psA\" rows=\"10\" placeholder=\"- Ingest PDFs as text...&#10;- Chunk with overlap...&#10;- Retrieve top-k...&#10;- Draft answer with citations...\"><\/textarea>\n            <\/div>\n            <div class=\"qitem\">\n              <div class=\"qtext\">B) Privacy\/ethics safeguards (4 bullets)<\/div>\n              <textarea class=\"textarea\" id=\"psB\" rows=\"10\" placeholder=\"- De-identify transcripts...&#10;- Role-based access...&#10;- Data retention policy...&#10;- Local-first prototype...\"><\/textarea>\n            <\/div>\n          <\/div>\n\n          <div class=\"grid2\" style=\"margin-top:12px;\">\n            <div class=\"qitem\">\n              <div class=\"qtext\">C) Evaluation checks (3 bullets)<\/div>\n              <textarea class=\"textarea\" id=\"psC\" rows=\"8\" placeholder=\"- Citation points to correct chunk...&#10;- Retrieval includes relevant evidence...&#10;- Refuses when no evidence...\"><\/textarea>\n            <\/div>\n            <div class=\"qitem\">\n              <div class=\"qtext\">D) Team workflow (4\u20136 steps)<\/div>\n              <textarea class=\"textarea\" id=\"psD\" rows=\"8\" placeholder=\"- Question triage...&#10;- Retrieve evidence...&#10;- Human review...&#10;- Log decisions...\"><\/textarea>\n            <\/div>\n          <\/div>\n\n          <div class=\"feedback\" id=\"psFb\" aria-live=\"polite\"><\/div>\n        <\/div>\n      <\/section>\n\n      <!-- ===================== -->\n      <!-- \u2705 PROGRESS -->\n      <!-- ===================== -->\n      <section class=\"view\" data-view=\"progress\" aria-label=\"Progress tracking\">\n        <div class=\"card\">\n          <div class=\"card-h\">\n            <h3>Progress<\/h3>\n            <div class=\"card-actions\">\n              <button class=\"btn mini ghost\" type=\"button\" id=\"progressReset\">Clear progress<\/button>\n            <\/div>\n          <\/div>\n\n          <div class=\"progress-grid\">\n            <div class=\"pbox\">\n              <div class=\"pnum\" id=\"pDone\">0<\/div>\n              <div class=\"muted\">Activities completed<\/div>\n            <\/div>\n            <div class=\"pbox\">\n              <div class=\"pnum\" id=\"pScore\">0%<\/div>\n              <div class=\"muted\">Average score<\/div>\n            <\/div>\n            <div class=\"pbox\">\n              <div class=\"pnum\" id=\"pChecks\">0<\/div>\n              <div class=\"muted\">Checklist items used<\/div>\n            <\/div>\n          <\/div>\n\n          <div class=\"note\">Saved locally in your browser (local storage).<\/div>\n\n          <h4 class=\"h4\">Checklist bank<\/h4>\n          <div class=\"bank\" id=\"checksBank\"><\/div>\n        <\/div>\n      <\/section>\n\n    <\/main>\n  <\/section>\n\n  <style>\n    \/* ===== WP-SAFE STYLES (scoped) ===== *\/\n    #icte-rag *{ box-sizing:border-box; }\n    #icte-rag{\n      --green:#28a745;\n      --dark:#132018;\n      --card:#ffffff;\n      --muted:#6b7280;\n      --line:#e5e7eb;\n      --shadow:0 8px 24px rgba(17,24,39,.08);\n      --radius:16px;\n      font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;\n      color:#111827;\n    }\n    #icte-rag .icte-menu{\n      width:100%;\n      background:var(--green);\n      padding:10px 12px;\n      text-align:center;\n      overflow-x:auto;\n      white-space:nowrap;\n      position:sticky;\n      top:0;\n      z-index:999;\n      box-shadow:0 2px 10px rgba(0,0,0,.12);\n      border-radius:12px;\n      margin-bottom:12px;\n    }\n    #icte-rag .icte-menu a{\n      display:inline-block;\n      color:#fff;\n      text-decoration:none;\n      font-weight:900;\n      padding:8px 10px;\n      border-radius:999px;\n      margin:0 3px;\n      opacity:.92;\n      transition:.15s;\n    }\n    #icte-rag .icte-menu a:hover{ opacity:1; background:rgba(255,255,255,.14); }\n    #icte-rag .icte-menu a.is-current{ background:#fff; color:var(--dark); opacity:1; }\n\n    #icte-rag .icte-shell{ max-width:1100px; margin:0 auto; }\n    #icte-rag .icte-hero{\n      display:grid;\n      grid-template-columns: 1.2fr .8fr;\n      gap:14px;\n      background:linear-gradient(135deg,#e9fff0, #ffffff);\n      border:1px solid var(--line);\n      border-radius:var(--radius);\n      padding:16px;\n      box-shadow:var(--shadow);\n      margin-bottom:14px;\n    }\n    @media (max-width: 920px){ #icte-rag .icte-hero{ grid-template-columns:1fr; } }\n    #icte-rag h2{ margin:0 0 6px 0; font-size:22px; }\n    #icte-rag .muted{ color:var(--muted); }\n\n    #icte-rag .hero-top{ display:flex; gap:12px; align-items:center; }\n    #icte-rag .hero-img{\n      width:140px;\n      height:auto;\n      border-radius:14px;\n      border:1px solid var(--line);\n      background:#111;\n      box-shadow:var(--shadow);\n    }\n    @media (max-width: 920px){\n      #icte-rag .hero-top{ flex-direction:column; align-items:flex-start; }\n      #icte-rag .hero-img{ width:100%; max-width:520px; }\n    }\n\n    #icte-rag .icte-hero__controls{\n      border-left:1px dashed var(--line);\n      padding-left:14px;\n    }\n    @media (max-width: 920px){\n      #icte-rag .icte-hero__controls{ border-left:none; padding-left:0; border-top:1px dashed var(--line); padding-top:12px; }\n    }\n\n    #icte-rag .icte-row{ display:flex; gap:8px; flex-wrap:wrap; }\n    #icte-rag .icte-label{ display:block; font-size:12px; font-weight:900; margin-top:8px; }\n    #icte-rag .icte-select{\n      width:100%;\n      padding:10px 10px;\n      border:1px solid var(--line);\n      border-radius:12px;\n      background:#fff;\n      outline:none;\n      margin-top:6px;\n    }\n\n    #icte-rag .icte-pill{\n      display:flex; align-items:center; gap:8px;\n      padding:10px 12px;\n      border:1px solid var(--line);\n      border-radius:999px;\n      background:#fff;\n      margin-bottom:10px;\n      font-weight:900;\n    }\n    #icte-rag .dot{\n      width:10px; height:10px; border-radius:50%;\n      background:#9ca3af;\n      box-shadow:0 0 0 4px rgba(156,163,175,.18);\n    }\n    #icte-rag .dot.on{\n      background:#22c55e;\n      box-shadow:0 0 0 4px rgba(34,197,94,.18);\n    }\n\n    #icte-rag .btn{\n      border:none;\n      padding:10px 12px;\n      border-radius:12px;\n      background:var(--green);\n      color:#fff;\n      font-weight:900;\n      cursor:pointer;\n      transition:.15s;\n    }\n    #icte-rag .btn:hover{ filter:brightness(.95); transform:translateY(-1px); }\n    #icte-rag .btn:active{ transform:translateY(0); }\n    #icte-rag .btn.ghost{\n      background:#fff;\n      color:#111827;\n      border:1px solid var(--line);\n    }\n    #icte-rag .btn.mini{ padding:8px 10px; border-radius:10px; font-size:13px; }\n    #icte-rag .icte-small{ font-size:12px; }\n\n    #icte-rag .card{\n      background:var(--card);\n      border:1px solid var(--line);\n      border-radius:var(--radius);\n      padding:14px;\n      box-shadow:var(--shadow);\n      margin-bottom:14px;\n    }\n    #icte-rag .card-h{\n      display:flex; gap:10px; align-items:flex-start; justify-content:space-between;\n      border-bottom:1px solid var(--line);\n      padding-bottom:10px;\n      margin-bottom:10px;\n    }\n    #icte-rag .card-h h3{ margin:0; font-size:18px; }\n    #icte-rag .card-actions{ display:flex; gap:8px; flex-wrap:wrap; justify-content:flex-end; }\n\n    #icte-rag .view{ display:none; }\n    #icte-rag .view.is-active{ display:block; }\n\n    #icte-rag .grid2{\n      display:grid;\n      grid-template-columns:1fr 1fr;\n      gap:14px;\n      margin-top:10px;\n    }\n    @media (max-width: 920px){ #icte-rag .grid2{ grid-template-columns:1fr; } }\n\n    #icte-rag .stack{ display:flex; flex-direction:column; gap:10px; }\n    #icte-rag .h4{ margin:0 0 6px 0; font-size:15px; }\n    #icte-rag .qitem{\n      padding:10px;\n      border:1px solid var(--line);\n      border-radius:14px;\n      background:#fafafa;\n    }\n    #icte-rag .qtext{ font-weight:900; margin-bottom:8px; }\n\n    #icte-rag .feedback{\n      margin-top:10px;\n      padding:10px 12px;\n      border-radius:14px;\n      border:1px solid var(--line);\n      background:#f9fafb;\n      font-weight:800;\n      display:none;\n      white-space:pre-line;\n    }\n    #icte-rag .feedback.ok{ display:block; border-color:rgba(34,197,94,.35); background:#ecfdf5; }\n    #icte-rag .feedback.bad{ display:block; border-color:rgba(239,68,68,.35); background:#fef2f2; }\n\n    #icte-rag .note{\n      background:#eff6ff;\n      border:1px solid rgba(59,130,246,.22);\n      padding:10px 12px;\n      border-radius:14px;\n      margin:10px 0;\n    }\n\n    #icte-rag .reading{\n      border:1px solid var(--line);\n      border-radius:14px;\n      padding:12px;\n      background:#fff;\n      margin-top:10px;\n    }\n    #icte-rag .reading-title{ font-weight:900; margin-bottom:8px; }\n    #icte-rag .reading-p{ padding:8px 0; border-top:1px dashed var(--line); }\n    #icte-rag .reading-p:first-of-type{ border-top:none; }\n\n    #icte-rag .progress-grid{\n      display:grid;\n      grid-template-columns:repeat(3,1fr);\n      gap:12px;\n      margin-top:10px;\n      margin-bottom:10px;\n    }\n    @media (max-width: 920px){ #icte-rag .progress-grid{ grid-template-columns:1fr; } }\n    #icte-rag .pbox{\n      border:1px solid var(--line);\n      border-radius:16px;\n      background:#fff;\n      padding:12px;\n      text-align:center;\n      box-shadow:var(--shadow);\n    }\n    #icte-rag .pnum{ font-size:28px; font-weight:1000; }\n\n    #icte-rag .bank{\n      border:1px solid var(--line);\n      border-radius:14px;\n      padding:12px;\n      background:#fff;\n      display:flex;\n      flex-wrap:wrap;\n      gap:8px;\n    }\n    #icte-rag .tag{\n      border:1px solid var(--line);\n      background:#f9fafb;\n      padding:6px 10px;\n      border-radius:999px;\n      font-weight:900;\n      font-size:13px;\n    }\n\n    #icte-rag .pre{\n      white-space:pre-wrap;\n      background:#0b1220;\n      color:#e5e7eb;\n      border-radius:12px;\n      padding:12px;\n      border:1px solid rgba(255,255,255,.08);\n      overflow:auto;\n      font-size:13px;\n      line-height:1.45;\n    }\n\n    #icte-rag .ul{ margin:0; padding-left:18px; }\n    #icte-rag .ul li{ margin:6px 0; }\n\n    #icte-rag .textarea{\n      width:100%;\n      padding:12px;\n      border:1px solid var(--line);\n      border-radius:12px;\n      outline:none;\n      resize:vertical;\n    }\n\n    \/* Conversation UI *\/\n    #icte-rag .chat{\n      background:#0b1220;\n      color:#e5e7eb;\n      border-radius:14px;\n      padding:12px;\n      min-height:220px;\n      max-height:420px;\n      overflow:auto;\n      border:1px solid rgba(255,255,255,.08);\n    }\n    #icte-rag .msg{ margin:10px 0; display:flex; gap:10px; align-items:flex-start; }\n    #icte-rag .who{\n      min-width:90px;\n      font-weight:900;\n      font-size:12px;\n      color:#93c5fd;\n      text-transform:uppercase;\n      letter-spacing:.06em;\n    }\n    #icte-rag .bubble{\n      flex:1;\n      background:rgba(255,255,255,.06);\n      padding:10px 10px;\n      border-radius:12px;\n      line-height:1.45;\n      border:1px solid rgba(255,255,255,.06);\n      white-space:pre-line;\n    }\n    #icte-rag .msg.user .who{ color:#86efac; }\n    #icte-rag .msg.user .bubble{ background:rgba(34,197,94,.10); border-color:rgba(34,197,94,.18); }\n\n    #icte-rag .chatbar{\n      margin-top:10px;\n      display:flex;\n      gap:8px;\n      align-items:center;\n    }\n    #icte-rag .input{\n      flex:1;\n      padding:12px 12px;\n      border:1px solid var(--line);\n      border-radius:12px;\n      outline:none;\n    }\n  <\/style>\n\n  <script>\n    (function(){\n      const root = document.getElementById('icte-rag');\n      if(!root) return;\n\n      \/* =========================\n         Helpers\n      ========================= *\/\n      const qs  = (sel, el=root) => el.querySelector(sel);\n      const qsa = (sel, el=root) => Array.from(el.querySelectorAll(sel));\n      const clamp = (n,min,max)=>Math.max(min,Math.min(max,n));\n      const norm = (s)=> (s||\"\").toString().trim();\n      const esc  = (s)=> (s||\"\").replace(\/[&<>\"']\/g, m=>({ \"&\":\"&amp;\",\"<\":\"&lt;\",\">\":\"&gt;\",'\"':\"&quot;\",\"'\":\"&#039;\" }[m]));\n      const nowISO = ()=> new Date().toISOString();\n\n      \/* =========================\n         Navigation\n      ========================= *\/\n      const navLinks = qsa('.icte-menu a');\n      const views = qsa('.view');\n      function showView(name){\n        views.forEach(v=> v.classList.toggle('is-active', v.getAttribute('data-view')===name));\n        navLinks.forEach(a=> a.classList.toggle('is-current', a.getAttribute('data-view')===name));\n        window.scrollTo({top: root.offsetTop - 10, behavior:'smooth'});\n      }\n      navLinks.forEach(a=>{\n        a.addEventListener('click', (e)=>{\n          e.preventDefault();\n          const v = a.getAttribute('data-view');\n          if(v) showView(v);\n        });\n      });\n      function activeViewName(){\n        const v = qs('.view.is-active');\n        return v ? v.getAttribute('data-view') : \"overview\";\n      }\n\n      \/* =========================\n         Multi-speaker Google Voices\n      ========================= *\/\n      const voiceASelect = qs('#voiceA');\n      const voiceBSelect = qs('#voiceB');\n      let allVoices = [];\n      let googleVoicesEN = [];\n      let voiceA = null;\n      let voiceB = null;\n\n      function isEnglish(v){ return (v.lang||\"\").toLowerCase().startsWith(\"en\"); }\n      function isGoogle(v){ return (v.name||\"\").toLowerCase().includes(\"google\"); }\n      function looksMaleName(name){\n        const n=(name||\"\").toLowerCase();\n        return n.includes(\"male\") || n.includes(\"david\") || n.includes(\"mark\") || n.includes(\"daniel\") || n.includes(\"james\") || n.includes(\"john\") || n.includes(\"michael\");\n      }\n      function looksFemaleName(name){\n        const n=(name||\"\").toLowerCase();\n        return n.includes(\"female\") || n.includes(\"susan\") || n.includes(\"amy\") || n.includes(\"emma\") || n.includes(\"olivia\") || n.includes(\"sophia\") || n.includes(\"ava\");\n      }\n      function pickDefaultVoices(list){\n        if(!list || !list.length) return {a:null,b:null};\n        const a = list.find(v=>looksFemaleName(v.name)) || list[0] || null;\n        let b = list.find(v=>v!==a && looksMaleName(v.name)) || list.find(v=>v!==a) || a || null;\n        if(b === a && list.length > 1) b = list.find(v=>v!==a) || b;\n        return {a,b};\n      }\n      function buildSelect(sel, list){\n        sel.innerHTML = \"\";\n        list.forEach((v,i)=>{\n          const opt=document.createElement(\"option\");\n          opt.value=String(i);\n          opt.textContent=`${v.name} (${v.lang})`;\n          sel.appendChild(opt);\n        });\n      }\n      function loadVoices(){\n        if(!window.speechSynthesis) return;\n        allVoices = speechSynthesis.getVoices() || [];\n        const google = allVoices.filter(v => isEnglish(v) && isGoogle(v));\n        const english = allVoices.filter(v => isEnglish(v));\n        googleVoicesEN = google.length ? google : english;\n\n        buildSelect(voiceASelect, googleVoicesEN);\n        buildSelect(voiceBSelect, googleVoicesEN);\n\n        const picked = pickDefaultVoices(googleVoicesEN);\n        voiceA = picked.a;\n        voiceB = picked.b;\n\n        voiceASelect.value = String(Math.max(0, googleVoicesEN.indexOf(voiceA)));\n        voiceBSelect.value = String(Math.max(0, googleVoicesEN.indexOf(voiceB)));\n\n        voiceASelect.onchange = ()=>{\n          const idx=parseInt(voiceASelect.value,10);\n          voiceA = googleVoicesEN[idx] || voiceA;\n          if(voiceB === voiceA && googleVoicesEN.length > 1){\n            voiceB = googleVoicesEN.find(v=>v!==voiceA) || voiceB;\n            voiceBSelect.value = String(googleVoicesEN.indexOf(voiceB));\n          }\n        };\n        voiceBSelect.onchange = ()=>{\n          const idx=parseInt(voiceBSelect.value,10);\n          voiceB = googleVoicesEN[idx] || voiceB;\n          if(voiceB === voiceA && googleVoicesEN.length > 1){\n            voiceB = googleVoicesEN.find(v=>v!==voiceA) || voiceB;\n            voiceBSelect.value = String(googleVoicesEN.indexOf(voiceB));\n          }\n        };\n      }\n      if(window.speechSynthesis){\n        loadVoices();\n        speechSynthesis.onvoiceschanged = loadVoices;\n        setTimeout(loadVoices, 700);\n      }\n\n      function stopSpeak(){ if(window.speechSynthesis) speechSynthesis.cancel(); }\n      function speakAs(role, text, opts){\n        const t = norm(text);\n        if(!t || !window.speechSynthesis) return Promise.resolve();\n        const o = opts || {};\n        const u = new SpeechSynthesisUtterance(t);\n        if(role===\"A\" && voiceA) u.voice = voiceA;\n        if(role===\"B\" && voiceB) u.voice = voiceB;\n        u.rate = o.rate ?? 1.02;\n        u.pitch = o.pitch ?? 1.0;\n        u.volume = o.volume ?? 1.0;\n        return new Promise(resolve=>{\n          u.onend=resolve; u.onerror=resolve;\n          speechSynthesis.speak(u);\n        });\n      }\n      async function speakDialogue(lines){\n        stopSpeak();\n        for(const line of lines){\n          await speakAs(line.role, line.text);\n        }\n      }\n      qs('#stopSpeak').addEventListener('click', stopSpeak);\n\n      \/* =========================\n         Mic Speech Recognition (ASR)\n      ========================= *\/\n      const micDot = qs('#icteMicDot');\n      const micStatus = qs('#icteMicStatus');\n      const btnStartVoice = qs('#icteStartVoice');\n      const btnStopVoice  = qs('#icteStopVoice');\n\n      let recognition = null;\n      let listening = false;\n\n      function setMicUI(on){\n        listening = on;\n        micDot.classList.toggle('on', on);\n        micStatus.textContent = on ? \"Mic: Listening\u2026\" : \"Mic: Off\";\n      }\n      function initRecognition(){\n        const SR = window.SpeechRecognition || window.webkitSpeechRecognition;\n        if(!SR) return null;\n        const r = new SR();\n        r.lang = 'en-US';\n        r.continuous = true;\n        r.interimResults = false;\n        r.maxAlternatives = 1;\n        return r;\n      }\n      function startListening(){\n        if(listening) return;\n        recognition = recognition || initRecognition();\n        if(!recognition){\n          alert(\"Speech Recognition not available. Use Chrome\/Edge or type.\");\n          return;\n        }\n        recognition.onresult = (e)=>{\n          const res = e.results[e.results.length - 1];\n          const transcript = res && res[0] ? res[0].transcript : \"\";\n          handleVoiceTranscript(transcript);\n        };\n        recognition.onerror = ()=> setMicUI(false);\n        recognition.onend = ()=>{ if(listening){ try{ recognition.start(); }catch(_){ } } };\n        setMicUI(true);\n        try{ recognition.start(); }catch(_){ }\n      }\n      function stopListening(){\n        setMicUI(false);\n        try{ recognition && recognition.stop(); }catch(_){ }\n      }\n      btnStartVoice.addEventListener('click', startListening);\n      btnStopVoice.addEventListener('click', stopListening);\n\n      \/* =========================\n         Progress (localStorage)\n      ========================= *\/\n      const LS_KEY = \"icte_rag_progress_v1\";\n      let progress = {};\n      try{ progress = JSON.parse(localStorage.getItem(LS_KEY) || \"{}\") || {}; }catch(_){ progress = {}; }\n\n      function saveProgress(){ localStorage.setItem(LS_KEY, JSON.stringify(progress)); renderProgress(); }\n      function markDone(key, scorePct){\n        progress[key] = { done:true, score: clamp(scorePct,0,100), ts: Date.now() };\n        saveProgress();\n      }\n      function resetProgress(){ localStorage.removeItem(LS_KEY); location.reload(); }\n      qs('#progressReset').addEventListener('click', resetProgress);\n\n      const checks = [\n        {k:\"usecase\", label:\"Use-case defined\"},\n        {k:\"sources\", label:\"Sources inventoried\"},\n        {k:\"chunk\", label:\"Chunking set\"},\n        {k:\"retrieve\", label:\"Retrieval run\"},\n        {k:\"cite\", label:\"Citations used\"},\n        {k:\"refuse\", label:\"Refusal rules\"},\n        {k:\"privacy\", label:\"IRB\/privacy safeguards\"},\n        {k:\"audit\", label:\"Audit trail\/logging\"}\n      ];\n      qs('#checksBank').innerHTML = checks.map(x=>`<span class=\"tag\">${esc(x.label)}<\/span>`).join(\"\");\n\n      function addCheck(k){\n        progress._checks = progress._checks || {};\n        progress._checks[k] = true;\n        saveProgress();\n      }\n      function renderProgress(){\n        const items = Object.values(progress).filter(p=>p && p.done);\n        const done = items.length;\n        const avg = done ? Math.round(items.reduce((s,p)=>s+(p.score||0),0)\/done) : 0;\n        qs('#pDone').textContent = String(done);\n        qs('#pScore').textContent = String(avg) + \"%\";\n        qs('#pChecks').textContent = String(Object.keys(progress._checks || {}).length);\n      }\n\n      \/* =========================\n         Instruction TTS\n      ========================= *\/\n      const SAY = {\n        \"overview-instr\":\"Overview. You will learn the RAG pipeline, build a searchable team knowledge base, generate citation-grounded drafts, and design IRB-friendly privacy safeguards.\",\n        \"conv-instr\":\"Conversation. Define your use-case, sources, chunking strategy, retrieval method, citation format, privacy controls, and evaluation plan.\",\n        \"reading-instr\":\"Reading. Read about retrieval-augmented generation and why citation grounding matters, then answer the quiz.\",\n        \"toolkit-instr\":\"Toolkit. Use checklists for pipeline steps, citation grounding, evaluation gates, privacy safeguards, and workflow automation.\",\n        \"prompts-instr\":\"Prompts. Copy prompts that enforce retrieval, citations, and refusal when evidence is missing.\",\n        \"list-instr\":\"Listening. Two instructors discuss retrieve then cite, and IRB-friendly safeguards.\",\n        \"lab-instr\":\"Lab. Paste notes and PDF text, chunk it, retrieve top evidence, and create a citation-grounded answer draft template.\",\n        \"problem-instr\":\"Problem-solving. Write a pipeline spec, privacy safeguards, evaluation checks, and a team workflow with audit trail.\"\n      };\n      qsa('[data-say]').forEach(btn=>{\n        btn.addEventListener('click', async ()=>{\n          const k = btn.getAttribute('data-say');\n          if(SAY[k]) await speakAs(\"A\", SAY[k]);\n        });\n      });\n\n      qs('#speakActive').addEventListener('click', async ()=>{\n        const v = activeViewName();\n        const map = {\n          overview:\"Overview page. Outcomes and safety rules for RAG and research teams.\",\n          conversation:\"Conversation page. RAG system designer coaching.\",\n          reading:\"Reading page. Read and take the quiz.\",\n          toolkit:\"Toolkit page. Pipeline, citations, evaluation, and privacy safeguards.\",\n          prompts:\"Prompts page. Copy and adapt prompts for citation-grounded RAG.\",\n          listening:\"Listening page. Two-speaker dialogue about retrieval and citations.\",\n          lab:\"RAG Lab page. Build knowledge base, search, and draft with citations.\",\n          problem:\"Problem-solving page. Write specs, safeguards, evaluation checks, and workflow.\",\n          progress:\"Progress page.\"\n        };\n        await speakAs(\"A\", map[v] || \"RAG lesson.\");\n      });\n\n      \/* =========================\n         Conversation coach\n      ========================= *\/\n      const convChat = qs('#convChat');\n      const convText = qs('#convText');\n      const convSend = qs('#convSend');\n      const convReset = qs('#convReset');\n      const convHear  = qs('#convHear');\n\n      let convStep = 0;\n      let lastCoachQ = \"\";\n\n      const convSteps = [\n        { bot:\"Step 1: Define your use-case (who uses the assistant, and for what decisions?).\",\n          check:(a)=>a.split(\/\\s+\/).length>=12 && \/(team|research|assistant|decision|workflow|paper|protocol)\/i.test(a),\n          tips:\"Include user (team) + purpose (decisions, papers, protocols, teaching).\"\n        },\n        { bot:\"Step 2: List 4 source types you will ingest (PDFs, notes, protocols, etc.) and name your source-of-truth order.\",\n          check:(a)=> (a.match(\/[-\u2022]\/g)||[]).length>=3 || \/(pdf|note|protocol|dataset|minutes|memo)\/i.test(a),\n          tips:\"Example order: publisher PDF > internal protocol > meeting notes.\"\n        },\n        { bot:\"Step 3: Choose chunking strategy (size + overlap) and metadata you will store (author\/year\/page\/team).\",\n          check:(a)=>\/(chunk|overlap|page|metadata|author|year|source)\/i.test(a),\n          tips:\"State chunk size\/overlap and at least 3 metadata fields.\"\n        },\n        { bot:\"Step 4: Define citation format and refusal rule when evidence is missing.\",\n          check:(a)=>\/(cite|citation|\\[.*:c\\d+\\]|insufficient|refuse|no evidence)\/i.test(a),\n          tips:\"Example: cite as [SRC1:C03] and refuse with 'INSUFFICIENT EVIDENCE'.\"\n        },\n        { bot:\"Step 5: Name 3 IRB-friendly privacy safeguards and 2 evaluation checks.\",\n          check:(a)=>\/(de-ident|access|retention|minimiz|local|encrypt|audit)\/i.test(a) && \/(citation|retrieval|halluc|refus|accuracy)\/i.test(a),\n          tips:\"Include privacy safeguards + evaluation checks (citation correctness, refusal).\"\n        }\n      ];\n\n      function addMsg(who, text){\n        const div = document.createElement('div');\n        div.className = 'msg ' + (who==='You' ? 'user' : 'bot');\n        div.innerHTML = `<div class=\"who\">${esc(who)}<\/div><div class=\"bubble\">${esc(text)}<\/div>`;\n        convChat.appendChild(div);\n        convChat.scrollTop = convChat.scrollHeight;\n      }\n\n      async function coachAsk(){\n        const step = convSteps[convStep];\n        if(!step){\n          addMsg(\"Coach\",\"\u2705 Done. You defined a use-case, sources, chunking\/metadata, citation + refusal rules, privacy safeguards, and evaluation checks.\");\n          markDone(\"conversation\", 100);\n          addCheck(\"usecase\"); addCheck(\"sources\"); addCheck(\"chunk\"); addCheck(\"cite\"); addCheck(\"refuse\"); addCheck(\"privacy\"); addCheck(\"audit\");\n          await speakAs(\"A\",\"Done. You defined use-case, sources, chunking, citations, refusal rules, privacy safeguards, and evaluation checks.\");\n          return;\n        }\n        lastCoachQ = step.bot;\n        addMsg(\"Coach\", step.bot);\n        await speakAs(\"A\", step.bot);\n      }\n\n      async function handleConversationInput(text){\n        const a = norm(text);\n        if(!a) return;\n        addMsg(\"You\", a);\n\n        if(\/team|decision|workflow|assistant|protocol\/i.test(a)) addCheck(\"usecase\");\n        if(\/pdf|note|protocol|dataset|minutes|memo\/i.test(a)) addCheck(\"sources\");\n        if(\/chunk|overlap|metadata|page|author|year\/i.test(a)) addCheck(\"chunk\");\n        if(\/cite|citation|\\[.*:c\\d+\\]\/i.test(a)) addCheck(\"cite\");\n        if(\/insufficient|refuse|no evidence\/i.test(a)) addCheck(\"refuse\");\n        if(\/de-ident|access|retention|minimiz|encrypt|local\/i.test(a)) addCheck(\"privacy\");\n        if(\/audit|log|version\/i.test(a)) addCheck(\"audit\");\n\n        const step = convSteps[convStep];\n        const ok = step.check(a);\n        const msg = ok ? \"\u2705 Good. Next.\" : \"\u26a0\ufe0f Try again. \" + step.tips;\n        addMsg(\"Coach\", msg);\n        await speakAs(\"A\", msg);\n\n        if(ok){ convStep++; setTimeout(coachAsk, 200); }\n      }\n\n      convSend.addEventListener('click', ()=>{ handleConversationInput(convText.value); convText.value=\"\"; });\n      convText.addEventListener('keydown', (e)=>{\n        if(e.key===\"Enter\"){ e.preventDefault(); handleConversationInput(convText.value); convText.value=\"\"; }\n      });\n      convHear.addEventListener('click', ()=> speakAs(\"A\", lastCoachQ || \"No question yet.\"));\n      convReset.addEventListener('click', ()=>{\n        convChat.innerHTML=\"\";\n        convStep=0;\n        addMsg(\"Coach\",\"Ready. Let\u2019s design a RAG assistant with citations and privacy safeguards.\");\n        coachAsk();\n      });\n\n      addMsg(\"Coach\",\"Ready. Let\u2019s design a RAG assistant with citations and privacy safeguards.\");\n      coachAsk();\n\n      \/* =========================\n         Voice transcript routing (focused field)\n      ========================= *\/\n      function appendToField(el, text){\n        if(!el) return;\n        const t = norm(text);\n        if(!t) return;\n        el.value = (el.value ? (el.value + \" \") : \"\") + t;\n      }\n      function getActiveField(){\n        const el = document.activeElement;\n        if(!el) return null;\n        const ok = (el.tagName === \"TEXTAREA\" || (el.tagName === \"INPUT\" && (el.type === \"text\" || el.type === \"number\")));\n        return ok && root.contains(el) ? el : null;\n      }\n      function handleVoiceTranscript(t){\n        const text = norm(t);\n        if(!text) return;\n\n        const v = activeViewName();\n        if(v === \"conversation\"){\n          handleConversationInput(text);\n          return;\n        }\n\n        const active = getActiveField();\n        if(active){\n          appendToField(active, text);\n          speakAs(\"A\",\"I heard: \" + text);\n          return;\n        }\n\n        speakAs(\"A\",\"I heard: \" + text);\n      }\n\n      \/* =========================\n         Reading TTS\n      ========================= *\/\n      qs('#readTextBtn').addEventListener('click', ()=>{\n        const parts = qsa('#readingText .reading-p').map(p=>p.textContent).join(\" \");\n        speakAs(\"A\", \"Reading. \" + parts);\n      });\n\n      \/* =========================\n         Quiz\n      ========================= *\/\n      const quiz = qs('#quiz');\n      const quizFb = qs('#quizFb');\n\n      const quizItems = [\n        { q:\"1) RAG primarily improves reliability by\u2026\",\n          ans:\"Retrieving evidence from a knowledge base and citing it\",\n          opts:[\n            \"Guessing from memory\",\n            \"Retrieving evidence from a knowledge base and citing it\",\n            \"Formatting answers nicely\",\n            \"Using longer prompts only\"\n          ]\n        },\n        { q:\"2) Citation grounding means\u2026\",\n          ans:\"Every major claim points to retrieved evidence (chunk IDs\/pages)\",\n          opts:[\n            \"Citations are optional\",\n            \"Every major claim points to retrieved evidence (chunk IDs\/pages)\",\n            \"Only include one reference at the end\",\n            \"Use any DOI you can find\"\n          ]\n        },\n        { q:\"3) A key chunking goal is\u2026\",\n          ans:\"Stable, searchable units with useful metadata for citation\",\n          opts:[\n            \"One giant chunk per PDF\",\n            \"Stable, searchable units with useful metadata for citation\",\n            \"No metadata needed\",\n            \"Only summarize, never retrieve\"\n          ]\n        },\n        { q:\"4) IRB-friendly design emphasizes\u2026\",\n          ans:\"Data minimization, de-identification, access control, retention policy\",\n          opts:[\n            \"Collect everything for future use\",\n            \"Data minimization, de-identification, access control, retention policy\",\n            \"Sharing raw transcripts widely\",\n            \"Hiding what the system stores\"\n          ]\n        }\n      ];\n\n      function renderQuiz(){\n        quiz.innerHTML = quizItems.map((it)=>{\n          const options = ['<option value=\"\">Choose\u2026<\/option>']\n            .concat(it.opts.map(o=>`<option value=\"${esc(o)}\">${esc(o)}<\/option>`))\n            .join(\"\");\n          return `\n            <div class=\"qitem\">\n              <div class=\"qtext\">${esc(it.q)}<\/div>\n              <select class=\"icte-select\" data-ans=\"${esc(it.ans)}\">${options}<\/select>\n            <\/div>\n          `;\n        }).join(\"\");\n      }\n      renderQuiz();\n\n      qs('#quizCheck').addEventListener('click', ()=>{\n        const sels = qsa('#quiz select');\n        let correct = 0;\n        sels.forEach(s=>{\n          const ok = s.value === s.getAttribute('data-ans');\n          s.style.borderColor = ok ? \"rgba(34,197,94,.6)\" : \"rgba(239,68,68,.6)\";\n          if(ok) correct++;\n        });\n        const pct = Math.round((correct \/ sels.length) * 100);\n        quizFb.className = \"feedback \" + (pct>=70 ? \"ok\":\"bad\");\n        quizFb.textContent = `Score: ${correct}\/${sels.length} (${pct}%).`;\n        if(pct>=70){ addCheck(\"retrieve\"); addCheck(\"cite\"); addCheck(\"privacy\"); }\n        markDone(\"reading_quiz\", pct);\n      });\n\n      \/* =========================\n         Toolkit content\n      ========================= *\/\n      qs('#tkPipe').textContent =\n`RAG PIPELINE (checklist)\n1) Ingest: collect approved docs (PDFs\/notes\/protocols) + scope\n2) De-identify: remove names\/IDs if human subjects data\n3) Chunk: split into stable chunks (100\u2013250 words) + overlap\n4) Metadata: source_id, title, author\/year, pages, version, date\n5) Index: keyword index (basic) or embeddings (advanced)\n6) Retrieve: top-k evidence chunks for a query\n7) Draft: answer using retrieved chunks only\n8) Cite: cite chunk IDs\/pages for each key claim\n9) Refuse: if evidence missing \u2192 \"INSUFFICIENT EVIDENCE\"\n10) Log: store query, retrieved chunks, draft, edits (audit trail)`;\n\n      qs('#tkCite').textContent =\n`CITATION GROUNDING FORMAT (team-friendly)\n- Cite as: [SRC_ID:C##] with optional metadata:\n  [SRC1:C03 | p.12 | Smith 2023]\n- Rules:\n  * Every key claim has at least 1 citation\n  * If conflicting evidence, cite both chunks and report uncertainty\n  * Never invent references\/DOIs`;\n\n      qs('#tkEval').textContent =\n`EVALUATION PLAN (quality gates)\nRetrieval:\n- Does top-k include relevant evidence? (human check)\n- Are key sources missing? (coverage)\nCitations:\n- Does each citation actually support the claim? (spot check)\n- Are citations stable (same chunk IDs after updates)?\nRefusal behavior:\n- When evidence is absent, does it refuse instead of guessing?\nSafety:\n- Any sensitive\/identifying data leakage?\nWorkflow:\n- Is an audit trail recorded (query, chunks, output, edits)?`;\n\n      qs('#tkIRB').textContent =\n`PRIVACY\/ETHICS (IRB-friendly)\n- Data minimization: store only what you need\n- De-identification: remove names, IDs, locations from participant data\n- Access control: role-based access; least privilege\n- Retention: define deletion schedule and triggers\n- Transparency: disclose what is stored; enable export\/delete where appropriate\n- Local-first prototyping; secure storage for production\n- No external sharing of restricted data without approval\n- Document AI involvement in methods (what AI did vs humans)`;\n\n      qs('#tkAuto').textContent =\n`WORKFLOW AUTOMATION IDEAS (research team)\n- Intake form for questions (tag: literature\/methods\/IRB\/stats)\n- Auto-retrieval + evidence bundle saved to a ticket\n- Citation checks: flag claims without citations\n- Update alerts: when a source doc changes, mark impacted answers \"needs review\"\n- Export: weekly evidence logs for meetings\n- Templates: standardized outputs (methods summaries, lit matrices, SOP answers)`;\n\n      qs('#toolkitSpeak').addEventListener('click', ()=>{\n        speakAs(\"A\",\"Toolkit. Use the RAG pipeline checklist, citation grounding format, evaluation gates, IRB-friendly privacy safeguards, and workflow automation ideas for teams.\");\n      });\n\n      \/* =========================\n         Prompts content\n      ========================= *\/\n      const p1 = qs('#p1'), p2 = qs('#p2'), p3 = qs('#p3'), p4 = qs('#p4'), pExample = qs('#pExample');\n\n      p1.textContent =\n`PROMPT 1 \u2014 CITATION-GROUNDED ANSWERING (RAG mode)\nYou MUST use only the provided evidence chunks.\nFor each key claim, cite chunk IDs like [SRC1:C03].\nIf evidence is missing, write: INSUFFICIENT EVIDENCE.\nOutput:\n1) Answer (with citations)\n2) Evidence list (chunk IDs)\n3) Uncertainty \/ conflicts\nEvidence:\n[PASTE CHUNKS HERE]`;\n\n      p2.textContent =\n`PROMPT 2 \u2014 CHUNKING + METADATA PLAN\nGiven our sources (PDFs\/notes):\n- propose chunk size + overlap\n- define metadata fields for citations (author\/year\/page\/version)\n- define naming scheme for source IDs\n- propose update\/version workflow\nConstraints: stable citations and team consistency.`;\n\n      p3.textContent =\n`PROMPT 3 \u2014 IRB\/PRIVACY IMPACT ASSESSMENT\nGiven the planned RAG assistant:\n- what data is ingested and stored?\n- any human subjects data? identify risk\n- propose safeguards: minimization, de-identification, access control, retention\n- write a short IRB-friendly description of protections.`;\n\n      p4.textContent =\n`PROMPT 4 \u2014 WORKFLOW AUTOMATION SPEC\nDesign a team workflow:\n- question intake\n- retrieval + evidence bundling\n- drafting + citation checks\n- human review and approval\n- logging and audit trail\n- updates and re-validation triggers\nOutput a 6\u201310 step SOP.`;\n\n      pExample.textContent =\n`EXAMPLE (snippet)\nClaim: \"The protocol recommends two-stage coding.\"\nEvidence: [PROTO:C04] states the two stages and definitions.\nAnswer: \"The protocol specifies a two-stage coding process (open coding followed by focused coding) [PROTO:C04].\"\nIf no evidence: \"INSUFFICIENT EVIDENCE \u2014 the KB does not contain a protocol section on coding stages.\"`;\n\n      qs('#promptsSpeak').addEventListener('click', ()=>{\n        speakAs(\"A\",\"Prompts. Use prompts that enforce retrieval and citations, define chunking and metadata, conduct privacy impact assessment, and specify a workflow automation SOP.\");\n      });\n\n      qs('#copyPrompts').addEventListener('click', ()=>{\n        const all =\n`PROMPT 1\\n${p1.textContent}\\n\\nPROMPT 2\\n${p2.textContent}\\n\\nPROMPT 3\\n${p3.textContent}\\n\\nPROMPT 4\\n${p4.textContent}\\n\\nEXAMPLE\\n${pExample.textContent}`;\n        navigator.clipboard.writeText(all)\n          .then(()=> speakAs(\"A\",\"Copied prompts.\"))\n          .catch(()=> alert(\"Clipboard blocked. Copy manually.\"));\n      });\n\n      \/* =========================\n         Listening\n      ========================= *\/\n      const listenFb = qs('#listenFb');\n      const listenQ  = qs('#listenQ');\n\n      const dialogue = [\n        {role:\"A\", text:\"Our team wants an assistant that answers questions from PDFs and internal notes.\"},\n        {role:\"B\", text:\"Then use RAG. Retrieve relevant chunks first, and draft answers that cite the exact chunk IDs and pages.\"},\n        {role:\"A\", text:\"What if the system cannot find evidence?\"},\n        {role:\"B\", text:\"It should refuse. Say insufficient evidence rather than guessing. That protects integrity.\"},\n        {role:\"A\", text:\"How do we keep it IRB-friendly?\"},\n        {role:\"B\", text:\"De-identify human subjects data, minimize what you store, use access control, and define retention policies.\"},\n        {role:\"A\", text:\"So the goal is evidence plus citations, with privacy safeguards.\"},\n        {role:\"B\", text:\"Exactly. Retrieval, citations, and audit trails make the system trustworthy for research teams.\"}\n      ];\n\n      const listenItems = [\n        {q:\"1) In RAG, answers should be grounded by\u2026\", ans:\"Retrieved evidence chunks with citations\"},\n        {q:\"2) If evidence is missing, the assistant should\u2026\", ans:\"Refuse and label insufficient evidence\"},\n        {q:\"3) IRB-friendly safeguards include\u2026\", ans:\"De-identification, minimization, access control, retention\"},\n        {q:\"4) A team benefit of RAG is\u2026\", ans:\"Audit trails linking questions to evidence\"}\n      ];\n\n      function renderListenQ(){\n        const optsMap = {\n          \"Retrieved evidence chunks with citations\":[\n            \"Retrieved evidence chunks with citations\",\n            \"Model memory only\",\n            \"Longer outputs\"\n          ],\n          \"Refuse and label insufficient evidence\":[\n            \"Refuse and label insufficient evidence\",\n            \"Invent a citation\",\n            \"Answer confidently anyway\"\n          ],\n          \"De-identification, minimization, access control, retention\":[\n            \"De-identification, minimization, access control, retention\",\n            \"Collect everything indefinitely\",\n            \"Share raw transcripts publicly\"\n          ],\n          \"Audit trails linking questions to evidence\":[\n            \"Audit trails linking questions to evidence\",\n            \"No logging\",\n            \"Removing citations\"\n          ]\n        };\n\n        listenQ.innerHTML = listenItems.map((it)=>{\n          const options = ['<option value=\"\">Choose\u2026<\/option>']\n            .concat((optsMap[it.ans]||[it.ans]).map(o=>`<option value=\"${esc(o)}\">${esc(o)}<\/option>`)).join(\"\");\n          return `\n            <div class=\"qitem\">\n              <div class=\"qtext\">${esc(it.q)}<\/div>\n              <select class=\"icte-select\" data-ans=\"${esc(it.ans)}\">${options}<\/select>\n            <\/div>\n          `;\n        }).join(\"\");\n      }\n      renderListenQ();\n\n      qs('#listenPlay').addEventListener('click', async ()=>{ await speakDialogue(dialogue); });\n      qs('#listenStop').addEventListener('click', stopSpeak);\n\n      qs('#listenCheck').addEventListener('click', ()=>{\n        const sels = qsa('#listenQ select');\n        let correct = 0;\n        sels.forEach(s=>{\n          const ok = s.value === s.getAttribute('data-ans');\n          s.style.borderColor = ok ? \"rgba(34,197,94,.6)\" : \"rgba(239,68,68,.6)\";\n          if(ok) correct++;\n        });\n        const pct = Math.round((correct \/ sels.length) * 100);\n        listenFb.className = \"feedback \" + (pct>=70 ? \"ok\":\"bad\");\n        listenFb.textContent = `Score: ${correct}\/${sels.length} (${pct}%).`;\n        if(pct>=70){ addCheck(\"retrieve\"); addCheck(\"cite\"); addCheck(\"privacy\"); addCheck(\"audit\"); }\n        markDone(\"listening\", pct);\n      });\n\n      \/* =========================\n         RAG Lab \u2014 KB + chunking + retrieval + draft template\n      ========================= *\/\n      const kbBuild = qs('#kbBuild');\n      const kbClear = qs('#kbClear');\n      const kbExport = qs('#kbExport');\n      const kbImportBtn = qs('#kbImportBtn');\n      const kbImport = qs('#kbImport');\n\n      const srcId = qs('#srcId');\n      const srcMeta = qs('#srcMeta');\n      const srcText = qs('#srcText');\n      const chunkSize = qs('#chunkSize');\n      const chunkOverlap = qs('#chunkOverlap');\n      const stopMode = qs('#stopMode');\n\n      const kbSources = qs('#kbSources');\n      const kbChunks = qs('#kbChunks');\n      const kbPreview = qs('#kbPreview');\n\n      const query = qs('#query');\n      const topK = qs('#topK');\n      const citeMode = qs('#citeMode');\n      const resultsBox = qs('#resultsBox');\n      const runSearch = qs('#runSearch');\n      const clearSearch = qs('#clearSearch');\n\n      const draftBtn = qs('#draftBtn');\n      const draftBox = qs('#draftBox');\n      const copyDraft = qs('#copyDraft');\n      const labFb = qs('#labFb');\n\n      const LS_KB = \"icte_rag_kb_v1\";\n\n      function loadKB(){\n        try{ return JSON.parse(localStorage.getItem(LS_KB) || '{\"sources\":[],\"chunks\":[]}'); }\n        catch(_){ return {sources:[], chunks:[]}; }\n      }\n      function saveKB(kb){\n        localStorage.setItem(LS_KB, JSON.stringify(kb));\n        renderKB();\n      }\n      function clearKB(){\n        localStorage.removeItem(LS_KB);\n        renderKB();\n      }\n\n      function tokenize(text, stop){\n        const t = (text||\"\").toLowerCase().replace(\/[^a-z0-9\\s'-]\/g,\" \");\n        let parts = t.split(\/\\s+\/).filter(Boolean);\n        if(stop){\n          const stopSet = new Set((\"a an the and or but if then so because as is are was were be been being to of in on for with at from by about into over after before under between among this that these those i we you they he she it my our your their also can could may might would should\").split(\" \"));\n          parts = parts.filter(w=>w.length>2 && !stopSet.has(w));\n        }else{\n          parts = parts.filter(w=>w.length>2);\n        }\n        return parts;\n      }\n\n      function chunkWords(words, size, overlap){\n        const out = [];\n        const step = Math.max(1, size - overlap);\n        for(let i=0; i<words.length; i += step){\n          const chunk = words.slice(i, i+size);\n          if(chunk.length<15) break;\n          out.push(chunk.join(\" \"));\n          if(i + size >= words.length) break;\n        }\n        return out;\n      }\n\n      function buildKBFromInput(){\n        const id = norm(srcId.value) || (\"SRC\" + String(Date.now()).slice(-4));\n        const meta = norm(srcMeta.value);\n        const text = norm(srcText.value);\n        const cs = clamp(parseInt(chunkSize.value||\"160\",10), 60, 350);\n        const ov = clamp(parseInt(chunkOverlap.value||\"30\",10), 0, 120);\n        if(text.length < 120){\n          labFb.className = \"feedback bad\";\n          labFb.textContent = \"\u26a0\ufe0f Paste more text (at least ~120 characters) before building chunks.\";\n          return;\n        }\n\n        const kb = loadKB();\n\n        \/\/ source registration\n        const src = { source_id:id, meta: meta || \"\", created_at: nowISO(), tool_version:\"v1\" };\n        kb.sources.push(src);\n\n        \/\/ chunking\n        const words = text.replace(\/\\s+\/g,\" \").split(\" \").filter(Boolean);\n        const chunks = chunkWords(words, cs, ov);\n\n        const existingCount = kb.chunks.filter(c=>c.source_id===id).length;\n        let cIndex = existingCount;\n\n        chunks.forEach((ct)=>{\n          cIndex += 1;\n          const chunk_id = `${id}:C${String(cIndex).padStart(2,\"0\")}`;\n          kb.chunks.push({\n            chunk_id,\n            source_id: id,\n            meta: src.meta,\n            text: ct,\n            created_at: nowISO()\n          });\n        });\n\n        saveKB(kb);\n        srcText.value = \"\";\n        labFb.className = \"feedback ok\";\n        labFb.textContent = `\u2705 KB updated. Added source ${id} with ${chunks.length} chunks.`;\n        addCheck(\"sources\"); addCheck(\"chunk\"); addCheck(\"audit\");\n        markDone(\"lab_kb\", 90);\n      }\n\n      function renderKB(){\n        const kb = loadKB();\n        kbSources.textContent = String(kb.sources.length);\n        kbChunks.textContent = String(kb.chunks.length);\n\n        if(!kb.chunks.length){\n          kbPreview.textContent = \"No knowledge base yet. Click \u201cBuild KB\u201d.\";\n          return;\n        }\n\n        const sample = kb.chunks.slice(0,6).map(c=>{\n          const head = `[${c.chunk_id}] ${c.meta ? (\"| \" + c.meta) : \"\"}`;\n          const snip = c.text.slice(0,220) + (c.text.length>220 ? \"\u2026\" : \"\");\n          return `${head}\\n${snip}`;\n        }).join(\"\\n\\n---\\n\\n\");\n        kbPreview.textContent = sample;\n      }\n\n      kbBuild.addEventListener('click', buildKBFromInput);\n      kbClear.addEventListener('click', ()=>{\n        if(!confirm(\"Clear the entire local knowledge base?\")) return;\n        clearKB();\n        resultsBox.textContent = \"No search yet.\";\n        draftBox.value = \"\";\n        labFb.className = \"feedback ok\";\n        labFb.textContent = \"\u2705 Knowledge base cleared (local only).\";\n      });\n\n      function downloadText(filename, text, mime){\n        const blob = new Blob([text], {type: mime || \"text\/plain\"});\n        const url = URL.createObjectURL(blob);\n        const a = document.createElement(\"a\");\n        a.href = url;\n        a.download = filename;\n        document.body.appendChild(a);\n        a.click();\n        a.remove();\n        setTimeout(()=>URL.revokeObjectURL(url), 500);\n      }\n\n      kbExport.addEventListener('click', ()=>{\n        const kb = loadKB();\n        downloadText(\"icte_rag_kb.json\", JSON.stringify(kb, null, 2), \"application\/json\");\n        addCheck(\"audit\");\n      });\n\n      kbImportBtn.addEventListener('click', ()=> kbImport.click());\n      kbImport.addEventListener('change', async ()=>{\n        const file = kbImport.files && kbImport.files[0];\n        if(!file) return;\n        try{\n          const txt = await file.text();\n          const data = JSON.parse(txt);\n          if(!data || !Array.isArray(data.chunks)){\n            alert(\"Invalid KB file.\");\n            return;\n          }\n          saveKB(data);\n          labFb.className = \"feedback ok\";\n          labFb.textContent = \"\u2705 KB imported (local only).\";\n          addCheck(\"audit\");\n        }catch(_){\n          alert(\"Could not import KB (invalid JSON).\");\n        }finally{\n          kbImport.value = \"\";\n        }\n      });\n\n      function scoreChunk(qTokens, cTokens){\n        \/\/ simple term overlap score with slight boost for repeated query terms\n        const cFreq = new Map();\n        cTokens.forEach(t=>cFreq.set(t, (cFreq.get(t)||0)+1));\n        let score = 0;\n        qTokens.forEach(t=>{\n          const f = cFreq.get(t) || 0;\n          if(f>0) score += 1 + Math.min(2, f-1)*0.5;\n        });\n        return score;\n      }\n\n      function runRetrieval(){\n        const kb = loadKB();\n        const q = norm(query.value);\n        if(!kb.chunks.length){\n          labFb.className = \"feedback bad\";\n          labFb.textContent = \"\u26a0\ufe0f Build or import a KB first.\";\n          return;\n        }\n        if(q.length < 3){\n          labFb.className = \"feedback bad\";\n          labFb.textContent = \"\u26a0\ufe0f Enter a query to search.\";\n          return;\n        }\n\n        const stop = (stopMode.value === \"basic\");\n        const qTokens = tokenize(q, stop);\n        if(!qTokens.length){\n          labFb.className = \"feedback bad\";\n          labFb.textContent = \"\u26a0\ufe0f Query too short after stopword removal. Add keywords.\";\n          return;\n        }\n\n        const scored = kb.chunks.map(ch=>{\n          const cTokens = tokenize(ch.text, stop);\n          const s = scoreChunk(qTokens, cTokens);\n          return { ...ch, score: s };\n        }).filter(x=>x.score>0).sort((a,b)=>b.score-a.score);\n\n        const k = clamp(parseInt(topK.value||\"6\",10), 3, 12);\n        const top = scored.slice(0,k);\n\n        if(!top.length){\n          resultsBox.textContent = \"No matching chunks. Try different keywords or add more sources.\";\n          labFb.className = \"feedback bad\";\n          labFb.textContent = \"\u26a0\ufe0f Retrieval found no evidence. In strict mode, you must refuse or expand KB.\";\n          addCheck(\"retrieve\");\n          return;\n        }\n\n        const fmt = top.map((r, idx)=>{\n          const head = `${idx+1}) [${r.chunk_id}] score=${r.score.toFixed(1)} ${r.meta ? (\"| \" + r.meta) : \"\"}`;\n          const body = r.text;\n          return head + \"\\n\" + body;\n        }).join(\"\\n\\n---\\n\\n\");\n\n        resultsBox.textContent = fmt;\n        labFb.className = \"feedback ok\";\n        labFb.textContent = `\u2705 Retrieved ${top.length} evidence chunks. Next: draft with citations.`;\n        addCheck(\"retrieve\");\n        \/\/ store last results for draft generation\n        lastRetrieved = top;\n        markDone(\"lab_retrieval\", 90);\n      }\n\n      let lastRetrieved = [];\n\n      runSearch.addEventListener('click', runRetrieval);\n      clearSearch.addEventListener('click', ()=>{\n        query.value = \"\";\n        resultsBox.textContent = \"No search yet.\";\n        draftBox.value = \"\";\n        lastRetrieved = [];\n      });\n\n      function buildDraftTemplate(){\n        const q = norm(query.value);\n        if(!q){\n          labFb.className = \"feedback bad\";\n          labFb.textContent = \"\u26a0\ufe0f Add a query first (then retrieve evidence).\";\n          return;\n        }\n        if(!lastRetrieved.length){\n          labFb.className = \"feedback bad\";\n          labFb.textContent = \"\u26a0\ufe0f Run retrieval first to get evidence chunks.\";\n          return;\n        }\n        const strict = (citeMode.value === \"strict\");\n\n        const citeList = lastRetrieved.map(c=>`- [${c.chunk_id}] ${c.meta ? (\"| \" + c.meta) : \"\"}`).join(\"\\n\");\n        const evidenceBundle = lastRetrieved.map(c=>`[${c.chunk_id}] ${c.text.slice(0,220)}${c.text.length>220?\"\u2026\":\"\"}`).join(\"\\n\\n\");\n\n        const draft =\n`QUESTION:\n${q}\n\nANSWER (DRAFT \u2014 YOU EDIT):\n- Main point 1: ________________________________ ${strict ? \"[CITE: ____]\" : \"\"}\n- Main point 2: ________________________________ ${strict ? \"[CITE: ____]\" : \"\"}\n- Main point 3: ________________________________ ${strict ? \"[CITE: ____]\" : \"\"}\n\nEVIDENCE (TOP CHUNKS):\n${citeList}\n\nCONFLICTS \/ LIMITATIONS:\n- If evidence conflicts, cite both chunk IDs and state uncertainty.\n- If evidence is missing, write: INSUFFICIENT EVIDENCE.\n\nEVIDENCE SNIPPETS (for drafting):\n${evidenceBundle}\n\nAUDIT TRAIL (copy into your log):\n- query: \"${q}\"\n- retrieved_chunks: ${lastRetrieved.map(x=>x.chunk_id).join(\", \")}\n- timestamp: ${nowISO()}\n- tool_version: v1\n`;\n        draftBox.value = draft;\n        labFb.className = \"feedback ok\";\n        labFb.textContent = \"\u2705 Draft template generated. Fill in claims and replace [CITE: ____] with chunk IDs like [SRC1:C03].\";\n        addCheck(\"cite\"); addCheck(\"audit\");\n        markDone(\"lab_draft\", 95);\n      }\n\n      draftBtn.addEventListener('click', buildDraftTemplate);\n      copyDraft.addEventListener('click', async ()=>{\n        const txt = norm(draftBox.value);\n        if(!txt){\n          labFb.className = \"feedback bad\";\n          labFb.textContent = \"\u26a0\ufe0f Generate a draft first.\";\n          return;\n        }\n        try{\n          await navigator.clipboard.writeText(txt);\n          labFb.className = \"feedback ok\";\n          labFb.textContent = \"\u2705 Draft copied.\";\n        }catch(_){\n          labFb.className = \"feedback bad\";\n          labFb.textContent = \"\u26a0\ufe0f Clipboard blocked. Copy manually.\";\n        }\n      });\n\n      \/* =========================\n         Problem-solving checker\n      ========================= *\/\n      const psA = qs('#psA');\n      const psB = qs('#psB');\n      const psC = qs('#psC');\n      const psD = qs('#psD');\n      const psFb = qs('#psFb');\n\n      function countBullets(txt){ return (txt.match(\/^\\s*[-\u2022]\/gm)||[]).length; }\n\n      qs('#psCheck').addEventListener('click', ()=>{\n        const aOk = countBullets(psA.value) >= 6 && \/(ingest|chunk|retriev|cite|draft|log|audit)\/i.test(psA.value);\n        const bOk = countBullets(psB.value) >= 4 && \/(de-ident|access|retention|minimiz|encrypt|local|irb)\/i.test(psB.value);\n        const cOk = countBullets(psC.value) >= 3 && \/(citation|retrieval|refus|accuracy|halluc)\/i.test(psC.value);\n        const dOk = countBullets(psD.value) >= 4 && \/(triage|review|approve|log|audit|update)\/i.test(psD.value);\n\n        const pct = Math.round(((aOk?1:0)+(bOk?1:0)+(cOk?1:0)+(dOk?1:0))\/4*100);\n\n        psFb.className = \"feedback \" + (pct>=70 ? \"ok\":\"bad\");\n        psFb.textContent =\n          `Check: ${pct}%\\n` +\n          `- Pipeline spec (>=6 bullets + pipeline terms): ${aOk ? \"Yes\" : \"No\"}\\n` +\n          `- Privacy safeguards (>=4 bullets + IRB terms): ${bOk ? \"Yes\" : \"No\"}\\n` +\n          `- Evaluation checks (>=3 bullets + quality terms): ${cOk ? \"Yes\" : \"No\"}\\n` +\n          `- Team workflow (>=4 bullets + workflow terms): ${dOk ? \"Yes\" : \"No\"}`;\n\n        markDone(\"problem_solving\", pct);\n        addCheck(\"audit\"); addCheck(\"privacy\"); addCheck(\"cite\"); addCheck(\"retrieve\"); addCheck(\"refuse\");\n      });\n\n      qs('#psReset').addEventListener('click', ()=>{\n        psA.value = \"\";\n        psB.value = \"\";\n        psC.value = \"\";\n        psD.value = \"\";\n        psFb.className = \"feedback\";\n        psFb.textContent = \"\";\n      });\n\n      \/* =========================\n         Initial render\n      ========================= *\/\n      renderProgress();\n      renderKB();\n\n    })();\n  <\/script>\n\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Overview Conversation Reading RAG Toolkit Prompts Listening RAG Lab Problem-solving Progress Advanced: RAG + Custom Knowledge Bases for Research Teams<\/p>\n","protected":false},"author":1,"featured_media":802,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"colormag_page_layout":"default_layout","footnotes":""},"categories":[52,45],"tags":[],"class_list":["post-803","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai-for-research","category-ai-for-teachers"],"_links":{"self":[{"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/posts\/803","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/comments?post=803"}],"version-history":[{"count":1,"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/posts\/803\/revisions"}],"predecessor-version":[{"id":804,"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/posts\/803\/revisions\/804"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/media\/802"}],"wp:attachment":[{"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/media?parent=803"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/categories?post=803"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/tags?post=803"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}