{"id":800,"date":"2026-03-05T16:42:38","date_gmt":"2026-03-05T16:42:38","guid":{"rendered":"https:\/\/i-cte.org\/robot\/?p=800"},"modified":"2026-03-05T16:42:38","modified_gmt":"2026-03-05T16:42:38","slug":"building-research-tools-chatbots-mini-apps-for-data-collection-training","status":"publish","type":"post","link":"https:\/\/i-cte.org\/robot\/building-research-tools-chatbots-mini-apps-for-data-collection-training\/","title":{"rendered":"Building Research Tools: Chatbots &amp; Mini-Apps for Data Collection\/Training"},"content":{"rendered":"\n<!-- \u2705 ICTE Teacher Micro-Lesson \u2014 Building Research Tools: Chatbots & Mini-Apps for Data Collection\/Training\n     WP-safe single block, Multi-speaker Google Voices (2 voices)\n     Includes: Overview + Conversation + Reading + Quiz + Toolkit + Prompts + Listening + Build Lab + Problem-solving + Progress\n     Focus: practice bots, reflective journals, consent-aware survey assistants, voice-based speaking tasks.\n     Notes:\n     - No external libraries.\n     - Consent-first workflow, privacy reminders, de-identification prompts.\n-->\n<div id=\"icte-researchtools\">\n\n  <!-- \u2705 TOP MENU -->\n  <nav class=\"icte-menu\" aria-label=\"Research tools 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\">Toolbox<\/a>\n    <a href=\"#\" data-view=\"prompts\">Prompts<\/a>\n    <a href=\"#\" data-view=\"listening\">Listening<\/a>\n    <a href=\"#\" data-view=\"lab\">Build 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 building research tools 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-05-at-23.28.47.png\"\n            alt=\"Building research tools: chatbots and mini-apps banner\"\n            loading=\"lazy\"\n          \/>\n          <div class=\"hero-title\">\n            <h2>Building Research Tools: Chatbots &#038; Mini-Apps for Data Collection\/Training<\/h2>\n            <p class=\"muted\">\n              Create <b>practice bots<\/b>, <b>reflective journals<\/b>, <b>consent-aware survey assistants<\/b>, and <b>voice-based speaking tasks<\/b>.\n              This lesson emphasizes <b>ethics<\/b>, <b>participant consent<\/b>, and <b>clean data logging<\/b> for research.\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: For best voice options, use 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>Design a <b>practice chatbot<\/b> for training (e.g., speaking prompts, feedback, repetition).<\/li>\n                <li>Build a <b>reflective journal<\/b> mini-app with structured prompts and exportable logs.<\/li>\n                <li>Create a <b>consent-aware survey assistant<\/b> that collects minimal data ethically.<\/li>\n                <li>Implement <b>voice-based tasks<\/b> (record + playback + transcript note fields) for speaking research.<\/li>\n                <li>Generate research-friendly outputs: <b>timestamps<\/b>, <b>anonymized IDs<\/b>, and <b>CSV export<\/b>.<\/li>\n              <\/ul>\n            <\/div>\n\n            <div class=\"qitem\">\n              <div class=\"qtext\">Ethics &#038; data rules<\/div>\n              <ul class=\"ul\">\n                <li><b>Consent first:<\/b> do not collect any data until consent is explicitly given.<\/li>\n                <li><b>Data minimization:<\/b> collect only what you need for the study.<\/li>\n                <li><b>No names:<\/b> use participant IDs (e.g., P001) and de-identify content.<\/li>\n                <li><b>Transparency:<\/b> display what is collected and allow export\/delete.<\/li>\n                <li><b>Local-first:<\/b> store logs in browser (localStorage) unless you have approved secure storage.<\/li>\n              <\/ul>\n            <\/div>\n          <\/div>\n\n          <div class=\"note\">\n            <b>Research control:<\/b> The tool scaffolds learning\/data collection; the researcher controls consent, prompts, and analysis decisions.\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 (Tool Design Coach)<\/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 tool design decisions: goal \u2192 data \u2192 consent \u2192 tasks \u2192 logging \u2192 export \u2192 safety.\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> Good research tools produce <b>clean, structured data<\/b> and a <b>clear consent trail<\/b>.\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 building research tools\">\n            <div class=\"reading-title\">Reading: Turning teaching mini-apps into research tools<\/div>\n\n            <div class=\"reading-p\">\n              <b>1<\/b> Many classroom mini-apps can become research tools if they produce structured logs.\n              For example, a speaking practice bot can record prompts, learner responses, timestamps, and self-ratings.\n              A reflective journal can capture weekly reflections with consistent prompts, improving comparability.\n            <\/div>\n\n            <div class=\"reading-p\">\n              <b>2<\/b> Research tools must be consent-aware. A safe workflow is: show information sheet \u2192 ask for consent \u2192\n              only then enable data logging. Participants should be able to stop, export, or delete their data.\n              Collect only what you need (data minimization) and avoid identifiable information.\n            <\/div>\n\n            <div class=\"reading-p\">\n              <b>3<\/b> For voice-based tasks, a research-friendly design separates learning from collection:\n              the tool can provide prompts and let learners record and replay audio.\n              If transcription is used, it should be disclosed and learners should understand what is stored.\n            <\/div>\n\n            <div class=\"reading-p\">\n              <b>4<\/b> Clean data design improves analysis. Use participant IDs, consistent task IDs, and predefined fields.\n              Save data in simple formats (CSV\/JSON) and keep an audit trail (tool version, prompt version, timestamp).\n            <\/div>\n\n            <div class=\"reading-p\">\n              <b>5<\/b> Researcher control remains essential: the tool scaffolds training, but the researcher defines constructs,\n              validates measures, and interprets results ethically.\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=\"Toolbox\">\n        <div class=\"card\">\n          <div class=\"card-h\">\n            <h3>4) Toolbox (Consent, logging, export, task design)<\/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 toolbox<\/button>\n            <\/div>\n          <\/div>\n\n          <div class=\"grid2\">\n            <div class=\"qitem\">\n              <div class=\"qtext\">Consent flow (minimum viable)<\/div>\n              <pre class=\"pre\" id=\"tkConsent\"><\/pre>\n            <\/div>\n            <div class=\"qitem\">\n              <div class=\"qtext\">Clean data logging schema<\/div>\n              <pre class=\"pre\" id=\"tkSchema\"><\/pre>\n            <\/div>\n          <\/div>\n\n          <div class=\"grid2\" style=\"margin-top:12px;\">\n            <div class=\"qitem\">\n              <div class=\"qtext\">Reflective journal prompts (templates)<\/div>\n              <pre class=\"pre\" id=\"tkJournal\"><\/pre>\n            <\/div>\n            <div class=\"qitem\">\n              <div class=\"qtext\">Voice speaking task templates<\/div>\n              <pre class=\"pre\" id=\"tkVoice\"><\/pre>\n            <\/div>\n          <\/div>\n\n          <div class=\"qitem\" style=\"margin-top:12px;\">\n            <div class=\"qtext\">Export &#038; deletion checklist (participant rights)<\/div>\n            <pre class=\"pre\" id=\"tkRights\"><\/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 help you design tasks and data fields without collecting sensitive information.\n          <\/div>\n\n          <div class=\"grid2\">\n            <div class=\"qitem\">\n              <div class=\"qtext\">Prompt 1 \u2014 Design a practice bot unit + log fields<\/div>\n              <pre class=\"pre\" id=\"p1\"><\/pre>\n            <\/div>\n            <div class=\"qitem\">\n              <div class=\"qtext\">Prompt 2 \u2014 Consent-aware survey assistant script<\/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 Reflective journal scaffolds<\/div>\n              <pre class=\"pre\" id=\"p3\"><\/pre>\n            <\/div>\n            <div class=\"qitem\">\n              <div class=\"qtext\">Prompt 4 \u2014 Voice task rubric + self-rating<\/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 \u201cConsent before collection\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 how to build consent-aware mini-apps for research.<\/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 BUILD LAB -->\n      <!-- ===================== -->\n      <section class=\"view\" data-view=\"lab\" aria-label=\"Build lab\">\n        <div class=\"card\">\n          <div class=\"card-h\">\n            <h3>7) Build Lab \u2014 Generate a Mini-Tool (Consent + Logging + Export)<\/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=\"labExportCSV\">Export CSV<\/button>\n              <button class=\"btn mini ghost\" type=\"button\" id=\"labExportJSON\">Export JSON<\/button>\n              <button class=\"btn mini ghost\" type=\"button\" id=\"labClear\">Clear logs<\/button>\n            <\/div>\n          <\/div>\n\n          <div class=\"note\">\n            This mini-tool includes:\n            <b>(1) Consent gate<\/b>, <b>(2) Participant ID<\/b>, <b>(3) Task runner<\/b> (practice bot + journal + survey assistant),\n            <b>(4) Voice task recorder<\/b>, and <b>(5) Data export<\/b>.\n            <br>\n            Logs are stored <b>locally in your browser<\/b>. For real studies, use approved secure storage and ethics clearance.\n          <\/div>\n\n          <!-- Consent gate -->\n          <div class=\"qitem\">\n            <div class=\"qtext\">A) Consent gate (must complete before logging)<\/div>\n            <div class=\"grid2\">\n              <div>\n                <label class=\"icte-label\">Participant ID (no names)<\/label>\n                <input id=\"pid\" class=\"input\" type=\"text\" placeholder=\"e.g., P001\" \/>\n                <div class=\"icte-small muted\" style=\"margin-top:6px;\">Tip: Use codes like P001, P002 (no real names).<\/div>\n              <\/div>\n              <div>\n                <label class=\"icte-label\">Study info (shown to participant)<\/label>\n                <textarea id=\"infoSheet\" class=\"textarea\" rows=\"5\">This activity collects: (1) your task responses, (2) timestamps, (3) optional self-ratings. No names are collected. You may stop at any time and export or delete your data.<\/textarea>\n              <\/div>\n            <\/div>\n\n            <div style=\"margin-top:10px; display:flex; gap:10px; flex-wrap:wrap; align-items:center;\">\n              <label style=\"display:flex; gap:8px; align-items:center; font-weight:900;\">\n                <input type=\"checkbox\" id=\"consentBox\" \/>\n                I consent to participate and understand what is collected.\n              <\/label>\n              <button class=\"btn\" id=\"enableLogging\" type=\"button\">Enable logging<\/button>\n              <button class=\"btn ghost\" id=\"disableLogging\" type=\"button\">Disable logging<\/button>\n              <span class=\"icte-small muted\" id=\"consentStatus\">Logging: Off (no consent)<\/span>\n            <\/div>\n          <\/div>\n\n          <!-- Tool selector -->\n          <div class=\"grid2\" style=\"margin-top:12px;\">\n            <div class=\"qitem\">\n              <div class=\"qtext\">B) Choose a mini-tool<\/div>\n              <select id=\"toolMode\" class=\"icte-select\">\n                <option value=\"practice\">Practice Bot (training)<\/option>\n                <option value=\"journal\">Reflective Journal<\/option>\n                <option value=\"survey\">Consent-aware Survey Assistant<\/option>\n                <option value=\"voice\">Voice-based Speaking Task<\/option>\n              <\/select>\n              <div class=\"icte-small muted\" style=\"margin-top:6px;\">Tip: You can run multiple modes; logs capture mode + task ID.<\/div>\n            <\/div>\n\n            <div class=\"qitem\">\n              <div class=\"qtext\">C) Task settings<\/div>\n              <label class=\"icte-label\">Task ID<\/label>\n              <input id=\"taskId\" class=\"input\" type=\"text\" placeholder=\"e.g., UNIT1_TASK3\" \/>\n              <label class=\"icte-label\" style=\"margin-top:10px;\">Prompt \/ Question<\/label>\n              <textarea id=\"taskPrompt\" class=\"textarea\" rows=\"4\" placeholder=\"Enter a prompt\/question for the participant...\"><\/textarea>\n            <\/div>\n          <\/div>\n\n          <!-- Interaction -->\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) Interaction<\/h3>\n              <div class=\"card-actions\">\n                <button class=\"btn mini ghost\" id=\"sayPromptA\" type=\"button\">\ud83d\udd0a Read prompt (A)<\/button>\n                <button class=\"btn mini ghost\" id=\"sayPromptB\" type=\"button\">\ud83d\udd0a Read prompt (B)<\/button>\n                <button class=\"btn mini\" id=\"submitResponse\" type=\"button\">Save response<\/button>\n              <\/div>\n            <\/div>\n\n            <div class=\"grid2\">\n              <div>\n                <div class=\"qtext\">Participant response<\/div>\n                <textarea id=\"responseText\" class=\"textarea\" rows=\"7\" placeholder=\"Type or speak response here...\"><\/textarea>\n                <div class=\"icte-small muted\" style=\"margin-top:6px;\">Voice: use Start Voice and speak; it will fill the active field.<\/div>\n              <\/div>\n              <div>\n                <div class=\"qtext\">Optional self-ratings (0\u201310)<\/div>\n                <div class=\"grid2\" style=\"grid-template-columns:1fr 1fr; gap:10px; margin-top:0;\">\n                  <div>\n                    <label class=\"icte-label\">Confidence<\/label>\n                    <input id=\"rateConf\" class=\"input\" type=\"number\" min=\"0\" max=\"10\" value=\"5\" \/>\n                  <\/div>\n                  <div>\n                    <label class=\"icte-label\">Difficulty<\/label>\n                    <input id=\"rateDiff\" class=\"input\" type=\"number\" min=\"0\" max=\"10\" value=\"5\" \/>\n                  <\/div>\n                <\/div>\n                <label class=\"icte-label\" style=\"margin-top:10px;\">Notes (researcher\/participant)<\/label>\n                <textarea id=\"notes\" class=\"textarea\" rows=\"3\" placeholder=\"Optional notes...\"><\/textarea>\n              <\/div>\n            <\/div>\n\n            <div class=\"feedback\" id=\"saveFb\" aria-live=\"polite\"><\/div>\n          <\/div>\n\n          <!-- Voice recorder -->\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) Voice task (record + playback) \u2014 optional<\/h3>\n              <div class=\"card-actions\">\n                <button class=\"btn mini\" id=\"recStart\" type=\"button\">\u23fa Start recording<\/button>\n                <button class=\"btn mini ghost\" id=\"recStop\" type=\"button\">\u23f9 Stop<\/button>\n                <button class=\"btn mini ghost\" id=\"recSaveLog\" type=\"button\">Save audio log<\/button>\n              <\/div>\n            <\/div>\n\n            <div class=\"icte-small muted\">\n              This records audio in-browser. It stores only a local playback URL in the log. For real research storage, use approved secure systems.\n            <\/div>\n\n            <audio id=\"recPlayer\" controls style=\"width:100%; margin-top:10px;\"><\/audio>\n            <div class=\"icte-small muted\" id=\"recStatus\" style=\"margin-top:8px;\">Recorder: idle<\/div>\n          <\/div>\n\n          <!-- Log viewer -->\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;\">F) Local log viewer<\/h3>\n              <div class=\"card-actions\">\n                <button class=\"btn mini ghost\" id=\"refreshLogs\" type=\"button\">Refresh<\/button>\n              <\/div>\n            <\/div>\n            <pre class=\"pre\" id=\"logView\"><\/pre>\n          <\/div>\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> You built a speaking practice bot that logs student audio and reflections.\n            Your ethics committee asks: \u201cHow do you ensure consent, privacy, and participant control?\u201d\n            <br><br>\n            Your task:\n            <ul class=\"ul\">\n              <li>Write a <b>consent workflow<\/b> (4\u20136 steps) that prevents accidental logging.<\/li>\n              <li>List <b>3 data-minimization decisions<\/b> (what you will NOT collect).<\/li>\n              <li>Propose <b>2 participant control features<\/b> (export\/delete\/stop).<\/li>\n              <li>Write a <b>short transparency statement<\/b> for the method section (4\u20136 lines).<\/li>\n            <\/ul>\n          <\/div>\n\n          <div class=\"grid2\">\n            <div class=\"qitem\">\n              <div class=\"qtext\">A) Consent workflow (4\u20136 steps)<\/div>\n              <textarea class=\"textarea\" id=\"psA\" rows=\"10\" placeholder=\"- Show info sheet...&#10;- Ask consent...&#10;- Enable logging only after consent...\"><\/textarea>\n            <\/div>\n            <div class=\"qitem\">\n              <div class=\"qtext\">B) Data minimization (3 bullets)<\/div>\n              <textarea class=\"textarea\" id=\"psB\" rows=\"10\" placeholder=\"- No names...&#10;- No locations...&#10;- No full transcripts...\"><\/textarea>\n            <\/div>\n          <\/div>\n\n          <div class=\"grid2\" style=\"margin-top:12px;\">\n            <div class=\"qitem\">\n              <div class=\"qtext\">C) Participant control features (2 bullets)<\/div>\n              <textarea class=\"textarea\" id=\"psC\" rows=\"7\" placeholder=\"- Export CSV\/JSON...&#10;- Delete all my data...\"><\/textarea>\n            <\/div>\n            <div class=\"qitem\">\n              <div class=\"qtext\">D) Transparency statement (4\u20136 lines)<\/div>\n              <textarea class=\"textarea\" id=\"psD\" rows=\"7\" placeholder=\"In this study, the mini-app was used to... Consent was obtained... Data were de-identified... Participants could...\"><\/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-researchtools *{ box-sizing:border-box; }\n    #icte-researchtools{\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-researchtools .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-researchtools .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-researchtools .icte-menu a:hover{ opacity:1; background:rgba(255,255,255,.14); }\n    #icte-researchtools .icte-menu a.is-current{ background:#fff; color:var(--dark); opacity:1; }\n\n    #icte-researchtools .icte-shell{ max-width:1100px; margin:0 auto; }\n    #icte-researchtools .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-researchtools .icte-hero{ grid-template-columns:1fr; } }\n    #icte-researchtools h2{ margin:0 0 6px 0; font-size:22px; }\n    #icte-researchtools .muted{ color:var(--muted); }\n\n    #icte-researchtools .hero-top{ display:flex; gap:12px; align-items:center; }\n    #icte-researchtools .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-researchtools .hero-top{ flex-direction:column; align-items:flex-start; }\n      #icte-researchtools .hero-img{ width:100%; max-width:520px; }\n    }\n\n    #icte-researchtools .icte-hero__controls{\n      border-left:1px dashed var(--line);\n      padding-left:14px;\n    }\n    @media (max-width: 920px){\n      #icte-researchtools .icte-hero__controls{ border-left:none; padding-left:0; border-top:1px dashed var(--line); padding-top:12px; }\n    }\n\n    #icte-researchtools .icte-row{ display:flex; gap:8px; flex-wrap:wrap; }\n    #icte-researchtools .icte-label{ display:block; font-size:12px; font-weight:900; margin-top:8px; }\n    #icte-researchtools .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-researchtools .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-researchtools .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-researchtools .dot.on{\n      background:#22c55e;\n      box-shadow:0 0 0 4px rgba(34,197,94,.18);\n    }\n\n    #icte-researchtools .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-researchtools .btn:hover{ filter:brightness(.95); transform:translateY(-1px); }\n    #icte-researchtools .btn:active{ transform:translateY(0); }\n    #icte-researchtools .btn.ghost{\n      background:#fff;\n      color:#111827;\n      border:1px solid var(--line);\n    }\n    #icte-researchtools .btn.mini{ padding:8px 10px; border-radius:10px; font-size:13px; }\n    #icte-researchtools .icte-small{ font-size:12px; }\n\n    #icte-researchtools .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-researchtools .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-researchtools .card-h h3{ margin:0; font-size:18px; }\n    #icte-researchtools .card-actions{ display:flex; gap:8px; flex-wrap:wrap; justify-content:flex-end; }\n\n    #icte-researchtools .view{ display:none; }\n    #icte-researchtools .view.is-active{ display:block; }\n\n    #icte-researchtools .grid2{\n      display:grid;\n      grid-template-columns:1fr 1fr;\n      gap:14px;\n      margin-top:10px;\n    }\n    @media (max-width: 920px){ #icte-researchtools .grid2{ grid-template-columns:1fr; } }\n\n    #icte-researchtools .stack{ display:flex; flex-direction:column; gap:10px; }\n    #icte-researchtools .h4{ margin:0 0 6px 0; font-size:15px; }\n    #icte-researchtools .qitem{\n      padding:10px;\n      border:1px solid var(--line);\n      border-radius:14px;\n      background:#fafafa;\n    }\n    #icte-researchtools .qtext{ font-weight:900; margin-bottom:8px; }\n\n    #icte-researchtools .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-researchtools .feedback.ok{ display:block; border-color:rgba(34,197,94,.35); background:#ecfdf5; }\n    #icte-researchtools .feedback.bad{ display:block; border-color:rgba(239,68,68,.35); background:#fef2f2; }\n\n    #icte-researchtools .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-researchtools .reading{\n      border:1px solid var(--line);\n      border-radius:14px;\n      padding:12px;\n      background:#fff;\n      margin-top:10px;\n    }\n    #icte-researchtools .reading-title{ font-weight:900; margin-bottom:8px; }\n    #icte-researchtools .reading-p{ padding:8px 0; border-top:1px dashed var(--line); }\n    #icte-researchtools .reading-p:first-of-type{ border-top:none; }\n\n    #icte-researchtools .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-researchtools .progress-grid{ grid-template-columns:1fr; } }\n    #icte-researchtools .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-researchtools .pnum{ font-size:28px; font-weight:1000; }\n\n    #icte-researchtools .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-researchtools .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-researchtools .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-researchtools .ul{ margin:0; padding-left:18px; }\n    #icte-researchtools .ul li{ margin:6px 0; }\n\n    #icte-researchtools .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-researchtools .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-researchtools .msg{ margin:10px 0; display:flex; gap:10px; align-items:flex-start; }\n    #icte-researchtools .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-researchtools .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-researchtools .msg.user .who{ color:#86efac; }\n    #icte-researchtools .msg.user .bubble{ background:rgba(34,197,94,.10); border-color:rgba(34,197,94,.18); }\n\n    #icte-researchtools .chatbar{\n      margin-top:10px;\n      display:flex;\n      gap:8px;\n      align-items:center;\n    }\n    #icte-researchtools .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-researchtools');\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\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\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\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\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\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\n      function loadVoices(){\n        if(!window.speechSynthesis) return;\n        allVoices = speechSynthesis.getVoices() || [];\n\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        const idxA = Math.max(0, googleVoicesEN.indexOf(voiceA));\n        const idxB = Math.max(0, googleVoicesEN.indexOf(voiceB));\n\n        voiceASelect.value = String(idxA);\n        voiceBSelect.value = String(idxB);\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\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\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\n      function startListening(){\n        if(listening) return;\n        recognition = recognition || initRecognition();\n        if(!recognition){\n          alert(\"Speech Recognition is not available in this browser. Use Chrome\/Edge, or type your answers.\");\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\n      function stopListening(){\n        setMicUI(false);\n        try{ recognition && recognition.stop(); }catch(_){ }\n      }\n\n      btnStartVoice.addEventListener('click', startListening);\n      btnStopVoice.addEventListener('click', stopListening);\n\n      \/* =========================\n         Progress (localStorage)\n      ========================= *\/\n      const LS_KEY = \"icte_researchtools_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:\"goal\", label:\"Tool goal defined\"},\n        {k:\"consent\", label:\"Consent gate implemented\"},\n        {k:\"schema\", label:\"Log schema defined\"},\n        {k:\"tasks\", label:\"Tasks designed\"},\n        {k:\"voice\", label:\"Voice task included\"},\n        {k:\"export\", label:\"Export works\"},\n        {k:\"rights\", label:\"Delete\/stop options\"},\n        {k:\"audit\", label:\"Versioning\/audit trail\"}\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\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. Build research tools like practice bots, reflective journals, consent-aware survey assistants, and voice speaking tasks. Consent first and structured logging are required.\",\n        \"conv-instr\":\"Conversation. Define the tool goal, data fields, consent gate, task design, logging, export, and participant rights.\",\n        \"reading-instr\":\"Reading. Read how to turn mini-apps into research tools and answer the quiz.\",\n        \"toolkit-instr\":\"Toolbox. Use consent scripts, log schemas, journal prompts, voice task templates, and participant rights checklists.\",\n        \"prompts-instr\":\"Prompts. Copy prompts to design bot units and consent-aware assistants and to define logging fields.\",\n        \"list-instr\":\"Listening. Two instructors discuss consent before collection and how to keep data clean.\",\n        \"lab-instr\":\"Build lab. Use the consent gate, run a mini-tool, save structured logs, and export CSV or JSON.\",\n        \"problem-instr\":\"Problem-solving. Write consent, minimization, participant control features, and a transparency statement.\"\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 ethics rules.\",\n          conversation:\"Conversation page. Tool design coach.\",\n          reading:\"Reading page. Read and take the quiz.\",\n          toolkit:\"Toolbox page. Consent, logging, journals, and voice tasks.\",\n          prompts:\"Prompts page. Copy and adapt prompts for tool building.\",\n          listening:\"Listening page. Two-speaker consent dialogue.\",\n          lab:\"Build lab page. Consent gate, logging, export, and voice recording.\",\n          problem:\"Problem-solving page. Ethics committee response planning.\",\n          progress:\"Progress page.\"\n        };\n        await speakAs(\"A\", map[v] || \"Building research tools 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        {\n          bot:\"Step 1: What is your tool\u2019s research goal (one sentence) and what outcome will you measure?\",\n          check:(a)=>a.split(\/\\s+\/).length>=10 && \/(measure|outcome|data|log|collect)\/i.test(a),\n          tips:\"Include goal + outcome\/measure (e.g., speaking confidence, task completion, reflection quality).\"\n        },\n        {\n          bot:\"Step 2: List 5 data fields you will log (minimal and de-identified).\",\n          check:(a)=> (a.match(\/[-\u2022]\/g)||[]).length>=4 || a.split(\/,|\\n\/).length>=5,\n          tips:\"Example fields: participant_id, task_id, response_text, timestamp, rating_confidence.\"\n        },\n        {\n          bot:\"Step 3: Write a consent message (2\u20133 lines) that explains what is collected and rights (export\/delete).\",\n          check:(a)=>\/(consent|collect|export|delete|stop|withdraw)\/i.test(a) && a.length>=120,\n          tips:\"Include: what is collected + can stop + export\/delete.\"\n        },\n        {\n          bot:\"Step 4: Choose ONE mini-tool mode (practice\/journal\/survey\/voice) and describe one task.\",\n          check:(a)=>\/(practice|journal|survey|voice|speaking)\/i.test(a) && a.length>=80,\n          tips:\"Describe: prompt, response type, optional rating.\"\n        },\n        {\n          bot:\"Step 5: Name 2 privacy safeguards and 1 audit trail practice (versioning).\",\n          check:(a)=>\/(de-ident|no names|minimiz|local|secure|encrypt|access)\/i.test(a) && \/(version|audit|log|timestamp)\/i.test(a),\n          tips:\"Include privacy safeguards + audit\/version tracking.\"\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 goal, minimal fields, consent, task design, privacy safeguards, and audit trail.\");\n          markDone(\"conversation\", 100);\n          addCheck(\"goal\"); addCheck(\"schema\"); addCheck(\"consent\"); addCheck(\"tasks\"); addCheck(\"rights\"); addCheck(\"audit\");\n          await speakAs(\"A\",\"Done. You defined tool goal, consent, data schema, tasks, safeguards, and audit trail.\");\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(\/goal|measure|outcome|data|collect|log\/i.test(a)) addCheck(\"goal\");\n        if(\/participant|task_id|timestamp|rating|response\/i.test(a)) addCheck(\"schema\");\n        if(\/consent|export|delete|withdraw|stop\/i.test(a)) addCheck(\"consent\");\n        if(\/practice|journal|survey|voice|speaking\/i.test(a)) addCheck(\"tasks\");\n        if(\/version|audit|timestamp\/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\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 consent-aware research mini-tool.\");\n        coachAsk();\n      });\n\n      addMsg(\"Coach\",\"Ready. Let\u2019s design a consent-aware research mini-tool.\");\n      coachAsk();\n\n      \/* =========================\n         Voice transcript routing (fills focused textarea\/input)\n      ========================= *\/\n      function appendToField(el, text){\n        if(!el) return;\n        const t = norm(text);\n        if(!t) return;\n        if(el.tagName === \"INPUT\"){\n          el.value = (el.value ? (el.value + \" \") : \"\") + t;\n        }else{\n          el.value = (el.value ? (el.value + \" \") : \"\") + t;\n        }\n      }\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\"));\n        return ok && root.contains(el) ? el : null;\n      }\n\n      function handleVoiceTranscript(t){\n        const text = norm(t);\n        if(!text) return;\n\n        const v = activeViewName();\n\n        if(v === \"conversation\"){\n          handleConversationInput(text);\n          return;\n        }\n\n        \/\/ Fill whichever field is focused; else default to responseText in lab\n        const active = getActiveField();\n        if(active){\n          appendToField(active, text);\n          speakAs(\"A\", \"I heard: \" + text);\n          return;\n        }\n\n        if(v === \"lab\"){\n          appendToField(qs('#responseText'), 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) A research tool should collect data only after\u2026\",\n          ans:\"Explicit consent is given\",\n          opts:[\"The student clicks any button\",\"Explicit consent is given\",\"The teacher wants more data\",\"The app loads\"]\n        },\n        { q:\"2) Data minimization means\u2026\",\n          ans:\"Collect only what is necessary for the study\",\n          opts:[\"Collect everything just in case\",\"Collect only what is necessary for the study\",\"Always collect names\",\"Always collect locations\"]\n        },\n        { q:\"3) Clean logging for analysis should include\u2026\",\n          ans:\"Participant ID, task ID, timestamp, and structured fields\",\n          opts:[\"Only free text\",\"Participant ID, task ID, timestamp, and structured fields\",\"Only screenshots\",\"Only audio files\"]\n        },\n        { q:\"4) Participant control features include\u2026\",\n          ans:\"Export and delete options\",\n          opts:[\"No access to their data\",\"Export and delete options\",\"Hidden tracking\",\"Automatic sharing\"]\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(\"consent\"); addCheck(\"schema\"); addCheck(\"rights\"); }\n        markDone(\"reading_quiz\", pct);\n      });\n\n      \/* =========================\n         Toolbox content\n      ========================= *\/\n      qs('#tkConsent').textContent =\n`CONSENT FLOW (minimum)\n1) Show info sheet (what is collected + why).\n2) Ask consent (checkbox + confirm).\n3) Enable logging ONLY after consent.\n4) Allow withdraw: disable logging anytime.\n5) Provide export + delete buttons.`;\n\n      qs('#tkSchema').textContent =\n`LOG SCHEMA (recommended fields)\nparticipant_id: \"P001\"\ntool_mode: \"practice\" | \"journal\" | \"survey\" | \"voice\"\ntask_id: \"UNIT1_TASK3\"\nprompt: \"...\"\nresponse_text: \"...\"\ntimestamp_iso: \"2026-03-05T...\"\nself_ratings: {confidence:0-10, difficulty:0-10}\nnotes: \"...\"\naudio_ref: \"blob:\/\/...\" (optional local-only)\ntool_version: \"v1\"\nprompt_version: \"p1\"`;\n\n      qs('#tkJournal').textContent =\n`REFLECTIVE JOURNAL PROMPTS\n- What did you practice today? (1\u20132 sentences)\n- What was difficult? Why?\n- What strategy helped you?\n- One example sentence you improved\n- Plan for next time (specific)`;\n\n      qs('#tkVoice').textContent =\n`VOICE TASK TEMPLATES\nTask A: Picture description (60\u201390s)\nTask B: Role-play dialogue (two turns)\nTask C: Opinion + reason (3 points)\nLogs: prompt_id, duration, self-rating, notes, transcript(optional)`;\n\n      qs('#tkRights').textContent =\n`PARTICIPANT RIGHTS CHECKLIST\n[ ] Consent required before logging\n[ ] Can stop\/withdraw anytime\n[ ] Can export their data (CSV\/JSON)\n[ ] Can delete local data\n[ ] De-identified IDs only\n[ ] Clear display of what is stored`;\n\n      qs('#toolkitSpeak').addEventListener('click', ()=>{\n        speakAs(\"A\",\"Toolbox. Use a consent gate, a clean log schema, structured journal prompts, voice task templates, and export and deletion options.\");\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 PRACTICE BOT UNIT + LOGGING\nDesign a 10-minute practice bot for [skill].\nInclude:\n- 3 prompts (easy\u2192hard)\n- expected response type (spoken\/written)\n- feedback rules\n- what to log (fields)\nConstraints:\n- de-identified IDs only\n- minimal data collection`;\n\n      p2.textContent =\n`PROMPT 2 \u2014 CONSENT-AWARE SURVEY ASSISTANT\nWrite a short assistant script that:\n1) Shows study info (what is collected).\n2) Asks for consent (yes\/no).\n3) If yes: asks 6 survey items (Likert 1\u20135).\n4) If no: ends politely with no logging.\nRules: no names, no sensitive data, allow withdraw.`;\n\n      p3.textContent =\n`PROMPT 3 \u2014 REFLECTIVE JOURNAL MINI-APP\nCreate weekly journal prompts for 4 weeks.\nEach week includes:\n- reflection prompts\n- a self-rating (0\u201310)\n- one action plan statement\nAlso define the log schema for analysis.`;\n\n      p4.textContent =\n`PROMPT 4 \u2014 VOICE TASK RUBRIC + SELF-RATING\nDesign a voice speaking task:\n- prompt\n- time limit\n- self-rating rubric (fluency, clarity, vocabulary, confidence)\n- what to log (duration, rating, notes, audio reference)\nEthics: de-identify, consent first.`;\n\n      pExample.textContent =\n`EXAMPLE (snippet)\nTool: Speaking practice bot\nLog fields: participant_id, task_id, prompt_id, response_text, timestamp, confidence_rating, difficulty_rating\nConsent: checkbox before enabling \"Save response\"\nRights: export CSV\/JSON; delete all local logs.`;\n\n      qs('#promptsSpeak').addEventListener('click', ()=>{\n        speakAs(\"A\",\"Prompts. Use prompts to design practice bot units, consent-aware survey scripts, reflective journals, and voice tasks with clean logs.\");\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:\"I built a speaking bot that saves every response automatically.\"},\n        {role:\"B\", text:\"That\u2019s risky. You need a consent gate and a clear statement of what is collected before saving anything.\"},\n        {role:\"A\", text:\"What should I log for research?\"},\n        {role:\"B\", text:\"Minimal, structured fields: participant ID, task ID, timestamp, response, and optional self-ratings. Avoid names.\"},\n        {role:\"A\", text:\"And participant rights?\"},\n        {role:\"B\", text:\"They should be able to stop, export, and delete their data at any time.\"},\n        {role:\"A\", text:\"So consent before collection.\"},\n        {role:\"B\", text:\"Exactly. Clean data and ethics go together.\"}\n      ];\n\n      const listenItems = [\n        {q:\"1) A consent gate should happen\u2026\", ans:\"Before any data is logged\"},\n        {q:\"2) Minimal logging includes\u2026\", ans:\"Participant ID, task ID, timestamp, response, and optional ratings\"},\n        {q:\"3) A privacy safeguard is\u2026\", ans:\"Using de-identified participant IDs instead of names\"},\n        {q:\"4) Participant rights include\u2026\", ans:\"Stop, export, and delete\"}\n      ];\n\n      function renderListenQ(){\n        const optsMap = {\n          \"Before any data is logged\":[\n            \"Before any data is logged\",\n            \"After the study ends\",\n            \"Only if the teacher asks\"\n          ],\n          \"Participant ID, task ID, timestamp, response, and optional ratings\":[\n            \"Participant ID, task ID, timestamp, response, and optional ratings\",\n            \"Full names and addresses\",\n            \"Only screenshots\"\n          ],\n          \"Using de-identified participant IDs instead of names\":[\n            \"Using de-identified participant IDs instead of names\",\n            \"Recording locations\",\n            \"Collecting social media handles\"\n          ],\n          \"Stop, export, and delete\":[\n            \"Stop, export, and delete\",\n            \"No access to their data\",\n            \"Automatic sharing with others\"\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 ()=>{\n        await speakDialogue(dialogue);\n      });\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(\"consent\"); addCheck(\"schema\"); addCheck(\"rights\"); }\n        markDone(\"listening\", pct);\n      });\n\n      \/* =========================\n         Build Lab \u2014 consent + logging + export\n      ========================= *\/\n      const consentBox = qs('#consentBox');\n      const enableLogging = qs('#enableLogging');\n      const disableLogging = qs('#disableLogging');\n      const consentStatus = qs('#consentStatus');\n\n      const pid = qs('#pid');\n      const infoSheet = qs('#infoSheet');\n      const toolMode = qs('#toolMode');\n      const taskId = qs('#taskId');\n      const taskPrompt = qs('#taskPrompt');\n      const responseText = qs('#responseText');\n      const notes = qs('#notes');\n      const rateConf = qs('#rateConf');\n      const rateDiff = qs('#rateDiff');\n\n      const saveFb = qs('#saveFb');\n      const logView = qs('#logView');\n\n      const btnSayA = qs('#sayPromptA');\n      const btnSayB = qs('#sayPromptB');\n      const btnSave = qs('#submitResponse');\n\n      const btnExportCSV = qs('#labExportCSV');\n      const btnExportJSON = qs('#labExportJSON');\n      const btnClear = qs('#labClear');\n      const btnRefresh = qs('#refreshLogs');\n\n      const TOOL_VERSION = \"v1\";\n      const PROMPT_VERSION = \"p1\";\n      const LS_LOGS = \"icte_researchtools_logs_v1\";\n      const LS_CONSENT = \"icte_researchtools_consent_v1\";\n\n      function loadLogs(){\n        try{ return JSON.parse(localStorage.getItem(LS_LOGS) || \"[]\") || []; }catch(_){ return []; }\n      }\n      function saveLogs(arr){\n        localStorage.setItem(LS_LOGS, JSON.stringify(arr));\n        renderLogs();\n      }\n\n      function setConsentState(on){\n        const state = {\n          consented: !!on,\n          pid: norm(pid.value) || \"\",\n          info: norm(infoSheet.value) || \"\",\n          ts: new Date().toISOString()\n        };\n        localStorage.setItem(LS_CONSENT, JSON.stringify(state));\n        consentStatus.textContent = on ? \"Logging: ON (consent given)\" : \"Logging: Off (no consent)\";\n        consentStatus.style.fontWeight = \"900\";\n        addCheck(\"consent\");\n        if(on) addCheck(\"rights\");\n      }\n\n      function hasConsent(){\n        try{\n          const st = JSON.parse(localStorage.getItem(LS_CONSENT) || \"{}\");\n          return !!st.consented;\n        }catch(_){ return false; }\n      }\n\n      function ensurePID(){\n        const v = norm(pid.value);\n        if(!v) return null;\n        if(v.length > 24) return v.slice(0,24);\n        return v;\n      }\n\n      enableLogging.addEventListener('click', ()=>{\n        if(!consentBox.checked){\n          alert(\"Consent is required before enabling logging.\");\n          return;\n        }\n        const p = ensurePID();\n        if(!p){\n          alert(\"Please enter a Participant ID (e.g., P001).\");\n          return;\n        }\n        setConsentState(true);\n      });\n      disableLogging.addEventListener('click', ()=> setConsentState(false));\n\n      \/\/ init consent display\n      consentStatus.textContent = hasConsent() ? \"Logging: ON (consent given)\" : \"Logging: Off (no consent)\";\n\n      function renderLogs(){\n        const logs = loadLogs();\n        logView.textContent = logs.length ? JSON.stringify(logs.slice(-20), null, 2) : \"No logs yet.\";\n      }\n\n      btnRefresh.addEventListener('click', renderLogs);\n\n      btnSayA.addEventListener('click', ()=> speakAs(\"A\", norm(taskPrompt.value) || \"No prompt yet.\"));\n      btnSayB.addEventListener('click', ()=> speakAs(\"B\", norm(taskPrompt.value) || \"No prompt yet.\"));\n\n      function showSaveFeedback(ok, msg){\n        saveFb.className = \"feedback \" + (ok ? \"ok\":\"bad\");\n        saveFb.textContent = msg;\n      }\n\n      btnSave.addEventListener('click', ()=>{\n        if(!hasConsent()){\n          showSaveFeedback(false, \"\u26a0\ufe0f Logging is OFF. Consent is required before saving any data.\");\n          return;\n        }\n        const p = ensurePID();\n        if(!p){\n          showSaveFeedback(false, \"\u26a0\ufe0f Missing Participant ID.\");\n          return;\n        }\n        const mode = toolMode.value;\n        const tid = norm(taskId.value) || \"TASK_UNSPECIFIED\";\n        const prompt = norm(taskPrompt.value) || \"(no prompt)\";\n        const resp = norm(responseText.value);\n        if(!resp){\n          showSaveFeedback(false, \"\u26a0\ufe0f Please enter a response before saving.\");\n          return;\n        }\n\n        const entry = {\n          participant_id: p,\n          tool_mode: mode,\n          task_id: tid,\n          prompt: prompt,\n          response_text: resp,\n          timestamp_iso: new Date().toISOString(),\n          self_ratings: {\n            confidence: clamp(parseInt(rateConf.value||\"5\",10),0,10),\n            difficulty: clamp(parseInt(rateDiff.value||\"5\",10),0,10)\n          },\n          notes: norm(notes.value) || \"\",\n          audio_ref: currentAudioRef || \"\",\n          tool_version: TOOL_VERSION,\n          prompt_version: PROMPT_VERSION\n        };\n\n        const logs = loadLogs();\n        logs.push(entry);\n        saveLogs(logs);\n        showSaveFeedback(true, \"\u2705 Saved. (Local log updated)\");\n        addCheck(\"schema\"); addCheck(\"tasks\"); addCheck(\"audit\");\n      });\n\n      \/* =========================\n         Export helpers\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      function toCSV(rows){\n        if(!rows.length) return \"\";\n        const headers = Object.keys(rows[0]);\n        const escCSV = (v)=>{\n          const s = (v===null || v===undefined) ? \"\" : String(v);\n          if(\/[\",\\n]\/.test(s)) return '\"' + s.replace(\/\"\/g,'\"\"') + '\"';\n          return s;\n        };\n        const lines = [];\n        lines.push(headers.map(escCSV).join(\",\"));\n        rows.forEach(r=>{\n          const line = headers.map(h=>{\n            const val = r[h];\n            if(typeof val === \"object\" && val !== null) return escCSV(JSON.stringify(val));\n            return escCSV(val);\n          }).join(\",\");\n          lines.push(line);\n        });\n        return lines.join(\"\\n\");\n      }\n\n      btnExportJSON.addEventListener('click', ()=>{\n        const logs = loadLogs();\n        downloadText(\"icte_researchtool_logs.json\", JSON.stringify(logs, null, 2), \"application\/json\");\n        addCheck(\"export\");\n      });\n\n      btnExportCSV.addEventListener('click', ()=>{\n        const logs = loadLogs();\n        \/\/ flatten a bit for CSV\n        const flat = logs.map(x=>({\n          participant_id: x.participant_id,\n          tool_mode: x.tool_mode,\n          task_id: x.task_id,\n          prompt: x.prompt,\n          response_text: x.response_text,\n          timestamp_iso: x.timestamp_iso,\n          confidence: x.self_ratings ? x.self_ratings.confidence : \"\",\n          difficulty: x.self_ratings ? x.self_ratings.difficulty : \"\",\n          notes: x.notes || \"\",\n          audio_ref: x.audio_ref || \"\",\n          tool_version: x.tool_version || \"\",\n          prompt_version: x.prompt_version || \"\"\n        }));\n        downloadText(\"icte_researchtool_logs.csv\", toCSV(flat), \"text\/csv\");\n        addCheck(\"export\");\n      });\n\n      btnClear.addEventListener('click', ()=>{\n        if(!confirm(\"Delete ALL local logs on this browser?\")) return;\n        localStorage.removeItem(LS_LOGS);\n        renderLogs();\n        addCheck(\"rights\");\n      });\n\n      \/* =========================\n         Voice recorder (MediaRecorder)\n      ========================= *\/\n      const recStart = qs('#recStart');\n      const recStop  = qs('#recStop');\n      const recSave  = qs('#recSaveLog');\n      const recPlayer = qs('#recPlayer');\n      const recStatus = qs('#recStatus');\n\n      let mediaStream = null;\n      let recorder = null;\n      let chunks = [];\n      let currentAudioRef = \"\";\n      let currentAudioBlob = null;\n\n      function setRecStatus(s){ recStatus.textContent = \"Recorder: \" + s; }\n\n      recStart.addEventListener('click', async ()=>{\n        try{\n          if(!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia){\n            alert(\"Audio recording is not supported in this browser.\");\n            return;\n          }\n          mediaStream = await navigator.mediaDevices.getUserMedia({audio:true});\n          chunks = [];\n          recorder = new MediaRecorder(mediaStream);\n          recorder.ondataavailable = (e)=>{ if(e.data && e.data.size>0) chunks.push(e.data); };\n          recorder.onstop = ()=>{\n            currentAudioBlob = new Blob(chunks, {type: recorder.mimeType || \"audio\/webm\"});\n            const url = URL.createObjectURL(currentAudioBlob);\n            currentAudioRef = url; \/\/ local ref only\n            recPlayer.src = url;\n            setRecStatus(\"recorded (ready)\");\n            addCheck(\"voice\");\n          };\n          recorder.start();\n          setRecStatus(\"recording...\");\n        }catch(err){\n          setRecStatus(\"idle\");\n          alert(\"Could not start recording. Please allow microphone access.\");\n        }\n      });\n\n      recStop.addEventListener('click', ()=>{\n        try{\n          if(recorder && recorder.state !== \"inactive\") recorder.stop();\n          if(mediaStream) mediaStream.getTracks().forEach(t=>t.stop());\n        }catch(_){}\n      });\n\n      recSave.addEventListener('click', ()=>{\n        if(!hasConsent()){\n          showSaveFeedback(false, \"\u26a0\ufe0f Consent required before saving any audio log reference.\");\n          return;\n        }\n        if(!currentAudioRef){\n          showSaveFeedback(false, \"\u26a0\ufe0f Record audio first.\");\n          return;\n        }\n        \/\/ Save a minimal log entry referencing the local audio URL (not uploading anywhere)\n        const p = ensurePID();\n        if(!p){\n          showSaveFeedback(false, \"\u26a0\ufe0f Missing Participant ID.\");\n          return;\n        }\n        const mode = \"voice\";\n        const tid = norm(taskId.value) || \"VOICE_TASK\";\n        const prompt = norm(taskPrompt.value) || \"(no prompt)\";\n        const entry = {\n          participant_id: p,\n          tool_mode: mode,\n          task_id: tid,\n          prompt: prompt,\n          response_text: norm(responseText.value) || \"\",\n          timestamp_iso: new Date().toISOString(),\n          self_ratings: {\n            confidence: clamp(parseInt(rateConf.value||\"5\",10),0,10),\n            difficulty: clamp(parseInt(rateDiff.value||\"5\",10),0,10)\n          },\n          notes: norm(notes.value) || \"\",\n          audio_ref: currentAudioRef,\n          tool_version: TOOL_VERSION,\n          prompt_version: PROMPT_VERSION\n        };\n        const logs = loadLogs();\n        logs.push(entry);\n        saveLogs(logs);\n        showSaveFeedback(true, \"\u2705 Audio log saved (local reference).\");\n        addCheck(\"schema\"); addCheck(\"voice\"); addCheck(\"audit\");\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) >= 4 && \/(consent|enable|log|export|delete|withdraw)\/i.test(psA.value);\n        const bOk = countBullets(psB.value) >= 3 && \/(no|not)\\s\/i.test(psB.value.toLowerCase());\n        const cOk = countBullets(psC.value) >= 2 && \/(export|delete|stop|withdraw)\/i.test(psC.value);\n        const dOk = norm(psD.value).length >= 180 && \/(consent|de-ident|participant|export|delete|local|browser)\/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          `- Consent workflow (>=4 steps + consent terms): ${aOk ? \"Yes\" : \"No\"}\\n` +\n          `- Data minimization (>=3 bullets): ${bOk ? \"Yes\" : \"No\"}\\n` +\n          `- Participant control (>=2 bullets): ${cOk ? \"Yes\" : \"No\"}\\n` +\n          `- Transparency statement (4\u20136 lines, includes key ethics terms): ${dOk ? \"Yes\" : \"No\"}`;\n\n        markDone(\"problem_solving\", pct);\n        addCheck(\"consent\"); addCheck(\"rights\"); addCheck(\"audit\");\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         Start state\n      ========================= *\/\n      renderProgress();\n      renderLogs();\n\n    })();\n  <\/script>\n\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Overview Conversation Reading Toolbox Prompts Listening Build Lab Problem-solving Progress Building Research Tools: Chatbots &#038; Mini-Apps for Data Collection\/Training Create<\/p>\n","protected":false},"author":1,"featured_media":799,"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-800","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\/800","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=800"}],"version-history":[{"count":1,"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/posts\/800\/revisions"}],"predecessor-version":[{"id":801,"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/posts\/800\/revisions\/801"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/media\/799"}],"wp:attachment":[{"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/media?parent=800"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/categories?post=800"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/i-cte.org\/robot\/wp-json\/wp\/v2\/tags?post=800"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}