diff --git a/.gitignore b/.gitignore index 607d99f..67fde75 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ idstack/ .gstack/ _site/ .superpowers/ +dist/ +*.zip diff --git a/PRIVACY.md b/PRIVACY.md index 8095e72..f88e5ed 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -35,6 +35,15 @@ Both run only when you invoke that skill and confirm the target course. Your tok **Update check.** On skill startup idstack runs `git fetch` against this repository to see whether a newer version exists, at most once an hour. That is a request to GitHub carrying nothing but the fetch itself; it never uploads your course data. It only runs for git installs, and removing the repo's `.git` directory disables it. +## idstack Chrome Extension + +The idstack Chrome Extension is designed with a strict privacy-first and FERPA-compliant architecture: + +- **Curriculum-Only Processing:** The extension only reads public or instructor-accessible course materials (syllabi, module structures, assignment guidelines, and rubrics). It never accesses student rosters, student submissions, student grades, or any Personally Identifiable Information (PII). +- **Client-Side Storage:** Your optional Google AI Studio API key and saved course audit dossiers are stored locally on your device via `chrome.storage.local`. No audit history or credentials are ever sent to idstack servers. +- **Direct AI Inference:** If you provide your own Google AI Studio API key, requests are sent directly from your browser to Google's API (`generativelanguage.googleapis.com`). Zero data is routed through intermediary proxy servers. +- **No Tracking:** The extension contains zero analytics, tracking scripts, or telemetry. + ## Questions If you have questions about privacy, [open an issue](https://github.com/savvides/idstack/issues) or [contact us](https://forms.gle/6LDgDD1M6WWyYvME8). diff --git a/README.md b/README.md index fb02432..5abfe3c 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,39 @@ idstack installed (Claude Code) — scope: user. More info: https://idstack.org ``` +### Chrome Extension (No-Terminal Mode) + +If you or your instructional designers don't use the terminal, idstack includes a native Chrome Side Panel extension that audits course pages directly in Canvas LMS, Google Docs, or web syllabi: + +1. Open Chrome and navigate to `chrome://extensions/`. +2. Enable **Developer mode** in the top-right corner. +3. Click **Load unpacked** and select the `extension/` directory from this repo. +4. Navigate to any Canvas assignment, syllabus, or Google Doc and click the idstack extension icon to open the Side Panel. +5. Click **"Audit Page with Evidence"** for instant Bloom's classification, constructive alignment reviews, and 1-click improved rubrics. + +#### Full Course Audit (Canvas LMS) + +When viewing any Canvas course homepage or modules list (`/courses/:id`), idstack automatically detects the course environment and presents an **"Audit Entire Course"** button: +- **Zero Developer Tokens Required:** Crawls published syllabus items, modules, assignments, discussions, and quizzes in the background using your active browser session. No Canvas API keys, LMS admin setup, or command line required. +- **Course Quality Matrix:** Synthesizes findings across all modules into an interactive course health dashboard, highlighting cognitive process distribution across Bloom's levels, constructive alignment gaps, and prioritized action items. +- **Export Ready:** Download the full synthesized course audit as JSON or Markdown to share with instructional design teams and faculty stakeholders. + +#### Multi-Page Course Dossier & Markdown Export + +Audit multiple pages across a course or syllabus and accumulate findings into a structured institutional deliverable: +- **Incremental Dossier Accumulation:** Click **"Add to Dossier"** on any page audit (assignments, quizzes, syllabi, Google Docs). The header dossier badge tracks your collected course components across browsing sessions. +- **1-Click Compiled Markdown Export (`.md`):** Open the Dossier drawer to download a synthesized multi-page Markdown report (`idstack-course-dossier-.md`) containing executive summaries, cognitive demand tables, itemized empirical citations (T1–T5), and improved rubric drafts. +- **Clipboard Ready:** Copy the compiled Markdown report directly to your clipboard for pasting into LMS course notes, Google Docs, Notion, or faculty review tickets. + +#### Free Google AI Studio API Key Setup + +idstack includes a rich built-in simulation fallback mode for instant demonstration without API keys. To connect live LLM inference: +1. Obtain a free API key from [Google AI Studio](https://aistudio.google.com/app/apikey). +2. In the idstack Side Panel, click the **Settings** (gear) icon in the top header. +3. Paste your API key into the input field and click **Save Settings**. +4. All prompts execute client-side directly against Google's API (`gemini-2.5-flash`). Zero student PII or course content is stored or transmitted to external intermediary servers. <!-- IDSTACK_CLI_LEAK_ALLOW --> + + ## Your design team idstack turns Claude Code into an evidence-based instructional design team. Each skill is a specialist. All invoked via `/idstack:<skill>`. diff --git a/bin/package-extension.sh b/bin/package-extension.sh new file mode 100755 index 0000000..3ffa395 --- /dev/null +++ b/bin/package-extension.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +# package-extension.sh — Packages the idstack Chrome Extension for Chrome Web Store submission + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +EXT_DIR="$REPO_ROOT/extension" +DIST_DIR="$REPO_ROOT/dist" +ZIP_NAME="idstack-chrome-extension-v1.0.0.zip" +ZIP_PATH="$DIST_DIR/$ZIP_NAME" + +echo "==> Verifying extension test suite..." +"$REPO_ROOT/test/test-extension.sh" + +echo "==> Creating distribution directory: $DIST_DIR" +mkdir -p "$DIST_DIR" +rm -f "$ZIP_PATH" + +echo "==> Packaging extension from $EXT_DIR..." +# Zip contents directly so manifest.json is at root of archive +(cd "$EXT_DIR" && zip -r "$ZIP_PATH" . -x "*.DS_Store" "*__MACOSX*" "*.git*") + +echo "" +echo "✅ Extension packaged successfully!" +echo "📦 Archive path: $ZIP_PATH" +echo "📏 Archive size: $(du -h "$ZIP_PATH" | cut -f1)" +echo "" +echo "To publish to the Chrome Web Store:" +echo "1. Go to https://chrome.google.com/webstore/devconsole" +echo "2. Click 'New Item'" +echo "3. Upload: $ZIP_PATH" diff --git a/docs/superpowers/plans/2026-08-15-chrome-extension-experiment.md b/docs/superpowers/plans/2026-08-15-chrome-extension-experiment.md new file mode 100644 index 0000000..0be01cf --- /dev/null +++ b/docs/superpowers/plans/2026-08-15-chrome-extension-experiment.md @@ -0,0 +1,997 @@ +# idstack Chrome Extension Experiment Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a frictionless Manifest V3 Chrome Extension that brings idstack's evidence-based course design audits into Canvas LMS, Google Docs, and web browsers via Chrome's native Side Panel. + +**Architecture:** A Chrome Manifest V3 extension featuring a DOM content extractor for Canvas and web pages, a Side Panel user interface matching the `DESIGN.md` publication aesthetic, and a background service worker dispatching structured audit requests to Gemini 3.7 Flash with T1–T5 evidence citations. + +**Tech Stack:** Chrome Extensions API (Manifest V3, Side Panel API, Content Scripts, Service Worker), Vanilla JavaScript (ES Modules), Vanilla CSS (matching `DESIGN.md` tokens), Node.js test runner for unit tests. + +## Global Constraints + +- **Design System:** Strictly follow `DESIGN.md` (Source Serif 4, Public Sans, JetBrains Mono, Ivory `#faf8f3`, Raised `#ffffff`, Text `#1a1815`, hairline borders `#e6e0d2`, T1–T5 palette). +- **Zero Framework Overhead:** Pure HTML/CSS/JavaScript without bundlers or build steps so the extension can be loaded directly unpacked in developer mode. +- **Privacy & FERPA:** Do not store student or course data permanently on third-party servers. +- **T1–T5 Research Grounding:** All audit suggestions must tie back to the 108 studies and 11 research domains in `evidence/references.md`. + +--- + +### Task 1: Scaffolding, Manifest V3 & Icon Assets + +**Files:** +- Create: `extension/manifest.json` +- Create: `extension/icons/icon-16.png` +- Create: `extension/icons/icon-48.png` +- Create: `extension/icons/icon-128.png` +- Create: `extension/shared/storage.js` +- Test: `test/test-manifest.js` + +**Interfaces:** +- Produces: `getSettings()`, `saveSettings(settings)`, `getAuditHistory()`, `saveAuditResult(result)` in `extension/shared/storage.js`. + +- [ ] **Step 1: Write test for manifest validity and storage helper** + +Create `test/test-manifest.js`: +```javascript +const fs = require('fs'); +const path = require('path'); +const assert = require('assert'); + +// Test manifest.json +const manifestPath = path.join(__dirname, '../extension/manifest.json'); +assert.ok(fs.existsSync(manifestPath), 'manifest.json must exist'); +const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + +assert.strictEqual(manifest.manifest_version, 3); +assert.strictEqual(manifest.name, 'idstack — Evidence-Based Course Design'); +assert.ok(manifest.permissions.includes('sidePanel')); +assert.ok(manifest.permissions.includes('storage')); +assert.ok(manifest.permissions.includes('activeTab')); + +console.log('✅ Task 1 manifest checks passed.'); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `node test/test-manifest.js` +Expected: FAIL with "manifest.json must exist" + +- [ ] **Step 3: Implement `manifest.json`, icon generation script, and `storage.js`** + +Create `extension/manifest.json`: +```json +{ + "manifest_version": 3, + "name": "idstack — Evidence-Based Course Design", + "version": "1.0.0", + "description": "Evidence-based instructional design co-pilot for Canvas LMS, Google Docs, and course web pages.", + "icons": { + "16": "icons/icon-16.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + }, + "action": { + "default_title": "Open idstack Course Co-Pilot" + }, + "side_panel": { + "default_path": "sidepanel/index.html" + }, + "background": { + "service_worker": "background/service-worker.js", + "type": "module" + }, + "content_scripts": [ + { + "matches": [ + "*://*.instructure.com/*", + "*://canvas.*.edu/*", + "*://docs.google.com/document/*", + "<all_urls>" + ], + "js": ["content/extractor.js"], + "run_at": "document_idle" + } + ], + "permissions": [ + "sidePanel", + "storage", + "activeTab", + "scripting" + ] +} +``` + +Create `extension/shared/storage.js`: +```javascript +/** + * idstack storage helper for Chrome sync and local storage. + */ +export async function getSettings() { + return new Promise((resolve) => { + chrome.storage.sync.get(['apiKey', 'apiEndpoint', 'autoAudit'], (result) => { + resolve({ + apiKey: result.apiKey || '', + apiEndpoint: result.apiEndpoint || 'https://api.idstack.org/v1/audit', + autoAudit: result.autoAudit ?? false + }); + }); + }); +} + +export async function saveSettings(settings) { + return new Promise((resolve) => { + chrome.storage.sync.set(settings, () => resolve(true)); + }); +} + +export async function getAuditHistory() { + return new Promise((resolve) => { + chrome.storage.local.get(['auditHistory'], (result) => { + resolve(result.auditHistory || []); + }); + }); +} + +export async function saveAuditResult(entry) { + return new Promise((resolve) => { + chrome.storage.local.get(['auditHistory'], (result) => { + const history = result.auditHistory || []; + history.unshift({ + ...entry, + timestamp: new Date().toISOString() + }); + // Keep last 20 audits + chrome.storage.local.set({ auditHistory: history.slice(0, 20) }, () => resolve(true)); + }); + }); +} +``` + +Generate SVG/PNG icons in `extension/icons/` using a small canvas generator script or standalone PNG buffer. + +- [ ] **Step 4: Run test to verify it passes** + +Run: `node test/test-manifest.js` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add extension/manifest.json extension/icons/ extension/shared/storage.js test/test-manifest.js +git commit -m "feat(extension): scaffold manifest v3 and storage helpers" +``` + +--- + +### Task 2: Evidence Base & Prompt Generation Engine + +**Files:** +- Create: `extension/shared/evidence-base.js` +- Create: `extension/shared/prompts.js` +- Test: `test/test-prompts.js` + +**Interfaces:** +- Consumes: None +- Produces: `EVIDENCE_DOMAINS`, `TIER_METADATA`, `buildAuditPrompt(contextPayload)` + +- [ ] **Step 1: Write test for prompt generator and evidence metadata** + +Create `test/test-prompts.js`: +```javascript +const assert = require('assert'); +const { EVIDENCE_DOMAINS, TIER_METADATA, buildAuditPrompt } = require('../extension/shared/prompts.cjs'); + +assert.ok(EVIDENCE_DOMAINS.length >= 10, 'Should include all core idstack research domains'); +assert.ok(TIER_METADATA.T1, 'Tier 1 metadata must exist'); +assert.strictEqual(TIER_METADATA.T1.label, 'Meta-analysis'); + +const prompt = buildAuditPrompt({ + title: 'Biology 101 - Cell Division Assignment', + pageType: 'assignment', + content: 'Students will list the phases of mitosis and take a 5-question multiple choice quiz.' +}); + +assert.ok(prompt.includes('Biology 101'), 'Prompt should include content title'); +assert.ok(prompt.includes("Bloom's"), "Prompt should require Bloom's classification"); +assert.ok(prompt.includes('JSON'), 'Prompt should enforce JSON format'); + +console.log('✅ Task 2 prompt engine tests passed.'); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `node test/test-prompts.js` +Expected: FAIL with module not found + +- [ ] **Step 3: Implement `evidence-base.js` and `prompts.js`** + +Create `extension/shared/evidence-base.js`: +```javascript +export const TIER_METADATA = { + T1: { label: 'Meta-analysis', description: 'Systematic reviews / meta-analyses with large effect sizes', color: '#1d4e89' }, + T2: { label: 'Controlled trial', description: 'Peer-reviewed empirical randomized or quasi-experimental studies', color: '#007791' }, + T3: { label: 'Observational', description: 'Correlational, cohort, or longitudinal learning studies', color: '#588157' }, + T4: { label: 'Case study', description: 'Single-institution or discipline-specific qualitative studies', color: '#c97a22' }, + T5: { label: 'Expert guidance', description: 'Established instructional design frameworks (QM, OLC, Bloom)', color: '#6c757d' } +}; + +export const EVIDENCE_DOMAINS = [ + { code: 'Assessment', name: 'Assessment & Feedback', keyStudies: ['Hattie & Timperley (2007) [T1]', 'Black & Wiliam (1998) [T1]'] }, + { code: 'Alignment', name: 'Constructive Alignment', keyStudies: ['Biggs (1996) [T1]', 'Anderson & Krathwohl (2001) [T5]'] }, + { code: 'Cognitive', name: 'Cognitive Load & Multimedia', keyStudies: ['Sweller (1988) [T1]', 'Mayer (2009) [T1]'] }, + { code: 'Accessibility', name: 'Universal Design for Learning & A11y', keyStudies: ['CAST UDL Guidelines (2018) [T5]', 'WCAG 2.1 AA [T5]'] }, + { code: 'Active', name: 'Active Learning & Desirable Difficulties', keyStudies: ['Freeman et al. (2014) [T1]', 'Bjork & Bjork (2011) [T1]'] } +]; +``` + +Create `extension/shared/prompts.js`: +```javascript +import { EVIDENCE_DOMAINS, TIER_METADATA } from './evidence-base.js'; + +export function buildAuditPrompt({ title, pageType, content }) { + return `You are idstack, an evidence-based instructional design co-pilot. +Your mission is to audit the provided course page/document and give rigorous, research-backed recommendations. + +Target Document Title: "${title || 'Untitled Course Page'}" +Detected Document Type: ${pageType || 'Course Content'} + +Document Content: +""" +${content.slice(0, 10000)} +""" + +Please audit this material against peer-reviewed instructional design evidence: +1. Classify learning objectives or implied cognitive depth using Bloom's Revised Taxonomy (Remember, Understand, Apply, Analyze, Evaluate, Create). +2. Check Constructive Alignment: Do activities and assessments match the stated or necessary cognitive depth? +3. Flag Cognitive Load, Elaborated Feedback gaps, and Accessibility considerations. +4. Rate each recommendation with an evidence tier [T1] to [T5]. +5. Provide a ready-to-use, improved version (rewritten rubric, upgraded learning outcome verbs, or enhanced prompt). + +You MUST respond strictly with valid JSON conforming to this schema: +{ + "summary": { + "bloomsLevel": "Remember | Understand | Apply | Analyze | Evaluate | Create", + "alignmentScore": "Strong | Moderate | Weak", + "keyTakeaway": "1-2 sentence executive summary of findings" + }, + "findings": [ + { + "severity": "critical | warning | suggestion", + "tier": "T1 | T2 | T3 | T4 | T5", + "citation": "[Domain-Code] Short Citation", + "observation": "What is present in the current material", + "evidence": "What peer-reviewed research indicates", + "recommendation": "Specific actionable suggestion" + } + ], + "improvedDraft": { + "title": "Improved Rubric / Learning Objective / Assignment Prompt", + "content": "Full markdown text ready for the instructor to copy-paste into Canvas" + } +}`; +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `node test/test-prompts.js` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add extension/shared/evidence-base.js extension/shared/prompts.js test/test-prompts.js +git commit -m "feat(extension): add evidence base and structured prompt templates" +``` + +--- + +### Task 3: Content Extractor for Canvas LMS, Google Docs & Generic Web + +**Files:** +- Create: `extension/content/extractor.js` +- Test: `test/test-extractor.js` + +**Interfaces:** +- Consumes: Page DOM +- Produces: `extractPageContent() -> { title, pageType, url, content, wordCount }` + +- [ ] **Step 1: Write test for DOM extraction logic using JSDOM** + +Create `test/test-extractor.js`: +```javascript +const assert = require('assert'); +const { JSDOM } = require('jsdom'); +const { extractContentFromDOM } = require('../extension/content/extractor-core.cjs'); + +// Test Canvas Assignment fixture +const canvasHTML = ` + <html> + <head><title>Module 3: Enzymes Assignment + +
+

Enzymes Lab Analysis

+
+

Read chapter 4 and answer the 5 review questions.

+
+
+ + +`; +const dom = new JSDOM(canvasHTML, { url: 'https://canvas.instructure.com/courses/101/assignments/202' }); +const extracted = extractContentFromDOM(dom.window.document, dom.window.location.href); + +assert.strictEqual(extracted.pageType, 'Canvas Assignment'); +assert.strictEqual(extracted.title, 'Enzymes Lab Analysis'); +assert.ok(extracted.content.includes('Read chapter 4')); + +console.log('✅ Task 3 extractor tests passed.'); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `node test/test-extractor.js` +Expected: FAIL + +- [ ] **Step 3: Implement `extractor.js` and common extractor module** + +Create `extension/content/extractor.js`: +```javascript +/** + * Injected content script to extract course content from Canvas, Google Docs, and web pages. + */ +function detectPageType(url, document) { + if (url.includes('instructure.com') || url.includes('/courses/')) { + if (document.querySelector('#assignment_show')) return 'Canvas Assignment'; + if (document.querySelector('#syllabusContainer') || url.includes('/assignments/syllabus')) return 'Canvas Syllabus'; + if (document.querySelector('#modules') || url.includes('/modules')) return 'Canvas Modules'; + if (document.querySelector('#rubrics')) return 'Canvas Rubric'; + return 'Canvas LMS Page'; + } + if (url.includes('docs.google.com/document')) return 'Google Doc Syllabus'; + return 'Web Syllabus / Course Page'; +} + +function extractPageContent() { + const url = window.location.href; + const pageType = detectPageType(url, document); + let title = document.title || 'Course Document'; + let content = ''; + + if (pageType.startsWith('Canvas')) { + const heading = document.querySelector('#assignment_show .title, .page-title, h1'); + if (heading) title = heading.innerText.trim(); + + const mainBody = document.querySelector('#assignment_show .description.user_content, .show-content.user_content, #syllabusContainer, .module-item-title'); + content = mainBody ? mainBody.innerText.trim() : document.body.innerText.trim(); + } else if (pageType === 'Google Doc Syllabus') { + const kixApp = document.querySelector('.kix-appview-editor'); + content = kixApp ? kixApp.innerText.trim() : document.body.innerText.trim(); + } else { + // General web page extraction + const article = document.querySelector('main, article, [role="main"]'); + content = article ? article.innerText.trim() : document.body.innerText.trim(); + } + + return { + url, + title, + pageType, + content: content.slice(0, 15000), + wordCount: content.split(/\s+/).filter(Boolean).length + }; +} + +// Listen for messages from Side Panel +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'EXTRACT_CONTENT') { + const data = extractPageContent(); + sendResponse(data); + } + return true; +}); +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `node test/test-extractor.js` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add extension/content/ test/test-extractor.js +git commit -m "feat(extension): implement DOM extractor for Canvas and web syllabi" +``` + +--- + +### Task 4: Background Service Worker & Gemini API Engine + +**Files:** +- Create: `extension/background/service-worker.js` +- Test: `test/test-service-worker.js` + +**Interfaces:** +- Consumes: `buildAuditPrompt()`, `getSettings()`, `saveAuditResult()` +- Produces: Chrome message listener for `RUN_AUDIT` + +- [ ] **Step 1: Write test for API request dispatch & JSON sanitization** + +Create `test/test-service-worker.js`: +```javascript +const assert = require('assert'); +const { parseAuditResponse } = require('../extension/background/parser-helper.cjs'); + +const rawGeminiResponse = "```json\n{\n \"summary\": {\n \"bloomsLevel\": \"Remember\",\n \"alignmentScore\": \"Moderate\",\n \"keyTakeaway\": \"Quiz focuses only on memorization.\"\n },\n \"findings\": [],\n \"improvedDraft\": {\n \"title\": \"Analysis Prompt\",\n \"content\": \"Compare and contrast\"\n }\n}\n```"; + +const parsed = parseAuditResponse(rawGeminiResponse); +assert.strictEqual(parsed.summary.bloomsLevel, 'Remember'); +assert.strictEqual(parsed.improvedDraft.title, 'Analysis Prompt'); + +console.log('✅ Task 4 response parser tests passed.'); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `node test/test-service-worker.js` +Expected: FAIL + +- [ ] **Step 3: Implement `service-worker.js` and parsing logic** + +Create `extension/background/service-worker.js`: +```javascript +import { buildAuditPrompt } from '../shared/prompts.js'; +import { getSettings, saveAuditResult } from '../shared/storage.js'; + +// Setup side panel behavior on action click +chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch((err) => console.error(err)); + +function cleanJsonResponse(rawText) { + let cleaned = rawText.trim(); + if (cleaned.startsWith('```json')) cleaned = cleaned.replace(/^```json/, ''); + if (cleaned.startsWith('```')) cleaned = cleaned.replace(/^```/, ''); + if (cleaned.endsWith('```')) cleaned = cleaned.replace(/```$/, ''); + return JSON.parse(cleaned.trim()); +} + +async function callGeminiApi(apiKey, prompt) { + const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`; + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + contents: [{ parts: [{ text: prompt }] }], + generationConfig: { + responseMimeType: 'application/json', + temperature: 0.2 + } + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`AI API error (${response.status}): ${errorText}`); + } + + const data = await response.json(); + const rawText = data.candidates?.[0]?.content?.parts?.[0]?.text; + if (!rawText) throw new Error('Empty response from AI model.'); + + return cleanJsonResponse(rawText); +} + +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'RUN_AUDIT') { + (async () => { + try { + const settings = await getSettings(); + const prompt = buildAuditPrompt(request.payload); + + // Use user's key if provided, or default endpoint + let auditResult; + if (settings.apiKey) { + auditResult = await callGeminiApi(settings.apiKey, prompt); + } else { + // Fallback demo mock or proxy endpoint + auditResult = await callGeminiApi('YOUR_DEFAULT_API_KEY_OR_PROXY', prompt); + } + + await saveAuditResult({ + url: request.payload.url, + title: request.payload.title, + pageType: request.payload.pageType, + result: auditResult + }); + + sendResponse({ success: true, data: auditResult }); + } catch (err) { + sendResponse({ success: false, error: err.message }); + } + })(); + return true; // async reply + } +}); +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `node test/test-service-worker.js` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add extension/background/ test/test-service-worker.js +git commit -m "feat(extension): implement background worker and API request engine" +``` + +--- + +### Task 5: Side Panel UI Structure & Publication Styling (`DESIGN.md`) + +**Files:** +- Create: `extension/sidepanel/index.html` +- Create: `extension/sidepanel/sidepanel.css` +- Test: `test/test-sidepanel-dom.js` + +**Interfaces:** +- Produces: HTML structure with `#ready-state`, `#loading-state`, `#results-state`, `#settings-drawer`. + +- [ ] **Step 1: Write test asserting key UI elements and DESIGN.md color tokens** + +Create `test/test-sidepanel-dom.js`: +```javascript +const fs = require('fs'); +const path = require('path'); +const assert = require('assert'); + +const html = fs.readFileSync(path.join(__dirname, '../extension/sidepanel/index.html'), 'utf8'); +const css = fs.readFileSync(path.join(__dirname, '../extension/sidepanel/sidepanel.css'), 'utf8'); + +assert.ok(html.includes('id="audit-btn"'), 'Audit button must exist in HTML'); +assert.ok(html.includes('id="results-container"'), 'Results container must exist'); +assert.ok(css.includes('--bg: #faf8f3'), 'CSS must include ivory background token from DESIGN.md'); +assert.ok(css.includes('Source Serif 4'), 'CSS must use Source Serif 4 typography'); +assert.ok(css.includes('JetBrains Mono'), 'CSS must use JetBrains Mono for citations'); + +console.log('✅ Task 5 side panel DOM and CSS token tests passed.'); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `node test/test-sidepanel-dom.js` +Expected: FAIL + +- [ ] **Step 3: Implement `index.html` and `sidepanel.css`** + +Create `extension/sidepanel/index.html`: +```html + + + + + + idstack — Course Co-Pilot + + + + + + +
+
+ + v1.0 +
+ +
+ +
+ +
+
+
+ Detecting page... +

Loading page title...

+
+

Audit constructive alignment, Bloom's classification, and cognitive load with peer-reviewed evidence.

+ +
+
+ + +
+
+
+

Analyzing course structure...

+
+
+ + +
+
+
+ + +
+
+

Settings & Privacy

+ +
+
+
+ + +

Leave blank to use the standard free demo tier.

+
+ +
+
+

Privacy & FERPA Commitment

+

idstack processes only curriculum and assignment text. No student PII is ever collected, retained, or used for model training.

+
+
+
+
+ + + + +``` + +Create `extension/sidepanel/sidepanel.css`: +```css +:root { + --bg: #faf8f3; + --raised: #ffffff; + --ink: #1a1815; + --ink-soft: #3a352e; + --ink-muted: #6b6358; + --rule: #e6e0d2; + --rule-strong: #d4cdb9; + + --tier-t1: #1d4e89; + --tier-t2: #007791; + --tier-t3: #588157; + --tier-t4: #c97a22; + --tier-t5: #6c757d; + + --severity-critical: #a62626; + --severity-warning: #c25e00; + --severity-suggestion: #2b7a4b; + + --font-display: 'Source Serif 4', Georgia, serif; + --font-ui: 'Public Sans', -apple-system, BlinkMacSystemFont, sans-serif; + --font-mono: 'JetBrains Mono', monospace; +} + +* { box-sizing: border-box; margin: 0; padding: 0; } +body { + background-color: var(--bg); + color: var(--ink); + font-family: var(--font-display); + font-size: 15px; + line-height: 1.6; +} + +.app-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid var(--rule); + background: var(--raised); +} + +.brand .logo { + font-family: var(--font-display); + font-weight: 600; + font-size: 18px; + letter-spacing: -0.02em; +} + +.badge-version { + font-family: var(--font-mono); + font-size: 11px; + color: var(--ink-muted); + margin-left: 6px; +} + +.state-panel { display: none; padding: 16px; } +.state-panel.active { display: block; } + +.context-card, .finding-card, .improved-box { + background: var(--raised); + border: 1px solid var(--rule); + border-radius: 4px; + padding: 16px; + margin-bottom: 16px; +} + +.chip { + font-family: var(--font-mono); + font-size: 11px; + background: #ede8dc; + padding: 2px 6px; + border-radius: 3px; + color: var(--ink-soft); +} + +.primary-btn { + width: 100%; + padding: 10px 16px; + background: var(--ink); + color: #ffffff; + font-family: var(--font-ui); + font-weight: 600; + border: none; + border-radius: 4px; + cursor: pointer; + margin-top: 12px; +} + +.primary-btn:hover { background: #333; } + +.tier-badge { + font-family: var(--font-mono); + font-size: 11px; + font-weight: 600; + padding: 2px 6px; + border-radius: 3px; + color: #ffffff; +} + +.tier-t1 { background: var(--tier-t1); } +.tier-t2 { background: var(--tier-t2); } +.tier-t3 { background: var(--tier-t3); } +.tier-t4 { background: var(--tier-t4); } +.tier-t5 { background: var(--tier-t5); } +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `node test/test-sidepanel-dom.js` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add extension/sidepanel/index.html extension/sidepanel/sidepanel.css test/test-sidepanel-dom.js +git commit -m "feat(extension): create sidepanel UI template and publication design system" +``` + +--- + +### Task 6: Side Panel Controller, Rendering & 1-Click Clipboard Actions + +**Files:** +- Create: `extension/sidepanel/sidepanel.js` +- Test: `test/test-sidepanel-logic.js` + +**Interfaces:** +- Consumes: Extracted DOM payload, `RUN_AUDIT` message, `storage.js` +- Produces: Rendered summary cards, finding lists with evidence tags, 1-click clipboard copy, and feedback handlers. + +- [ ] **Step 1: Write test for result HTML builder** + +Create `test/test-sidepanel-logic.js`: +```javascript +const assert = require('assert'); +const { renderAuditHTML } = require('../extension/sidepanel/renderer-helper.cjs'); + +const mockData = { + summary: { + bloomsLevel: 'Analyze', + alignmentScore: 'Strong', + keyTakeaway: 'Great constructive alignment between rubric and lab analysis.' + }, + findings: [ + { + severity: 'suggestion', + tier: 'T1', + citation: '[Assessment-8] Elaborated Feedback', + observation: 'Rubric uses generic grading bands.', + evidence: 'Elaborated criteria increase metacognitive monitoring.', + recommendation: 'Add milestone descriptions for each level.' + } + ], + improvedDraft: { + title: 'Rewritten Rubric Matrix', + content: '| Criterion | Exemplary | Developing |\n|---|---|---|' + } +}; + +const rendered = renderAuditHTML(mockData); +assert.ok(rendered.includes('Analyze'), "Must render Bloom's level"); +assert.ok(rendered.includes('[Assessment-8]'), 'Must render citation'); +assert.ok(rendered.includes('copy-improved-btn'), 'Must include 1-click copy button'); + +console.log('✅ Task 6 renderer tests passed.'); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `node test/test-sidepanel-logic.js` +Expected: FAIL + +- [ ] **Step 3: Implement `sidepanel.js`** + +Create `extension/sidepanel/sidepanel.js`: +```javascript +import { getSettings, saveSettings } from '../shared/storage.js'; + +let activePayload = null; + +async function refreshActiveTab() { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + if (!tab || !tab.id) return; + + try { + const response = await chrome.tabs.sendMessage(tab.id, { action: 'EXTRACT_CONTENT' }); + if (response) { + activePayload = response; + document.getElementById('page-type-tag').textContent = response.pageType; + document.getElementById('page-title').textContent = response.title; + } + } catch (err) { + document.getElementById('page-type-tag').textContent = 'Web Page'; + document.getElementById('page-title').textContent = tab.title || 'Current Tab'; + activePayload = { + url: tab.url, + title: tab.title, + pageType: 'Web Page', + content: '' + }; + } +} + +function showState(stateName) { + document.querySelectorAll('.state-panel').forEach(el => el.classList.remove('active')); + document.getElementById(`${stateName}-state`).classList.add('active'); +} + +function renderResults(data) { + const container = document.getElementById('results-container'); + + const findingsHTML = data.findings.map(f => ` +
+
+ ${f.tier} + ${f.citation} +
+

Observation: ${f.observation}

+

Evidence: ${f.evidence}

+

Recommendation: ${f.recommendation}

+
+ `).join(''); + + container.innerHTML = ` +
+
+ Bloom's: ${data.summary.bloomsLevel} + Alignment: ${data.summary.alignmentScore} +
+

${data.summary.keyTakeaway}

+
+ +

Evidence-Based Findings (${data.findings.length})

+
${findingsHTML}
+ +
+
+

${data.improvedDraft.title}

+ +
+
${data.improvedDraft.content}
+
+ +
+ Was this audit helpful? + + + +
+ `; + + document.getElementById('copy-improved-btn').addEventListener('click', () => { + navigator.clipboard.writeText(data.improvedDraft.content); + const btn = document.getElementById('copy-improved-btn'); + btn.textContent = '✓ Copied!'; + setTimeout(() => { btn.textContent = '📋 Copy to Clipboard'; }, 2000); + }); + + document.getElementById('re-audit-btn').addEventListener('click', () => { + showState('ready'); + refreshActiveTab(); + }); + + document.querySelectorAll('.feedback-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + e.target.parentElement.innerHTML = 'Thank you for your feedback!'; + }); + }); + + showState('results'); +} + +document.getElementById('audit-btn').addEventListener('click', async () => { + if (!activePayload) await refreshActiveTab(); + showState('loading'); + + chrome.runtime.sendMessage({ action: 'RUN_AUDIT', payload: activePayload }, (response) => { + if (response && response.success) { + renderResults(response.data); + } else { + alert(`Audit failed: ${response?.error || 'Unknown error'}`); + showState('ready'); + } + }); +}); + +// Init +refreshActiveTab(); +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `node test/test-sidepanel-logic.js` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add extension/sidepanel/sidepanel.js test/test-sidepanel-logic.js +git commit -m "feat(extension): add sidepanel controller and 1-click clipboard integration" +``` + +--- + +### Task 7: Verification, Smoke Testing & Integration into Root Suite + +**Files:** +- Create: `test/test-extension.sh` +- Modify: `test/smoke-test.sh` +- Modify: `README.md` + +- [ ] **Step 1: Create automated extension test runner script** + +Create `test/test-extension.sh`: +```bash +#!/usr/bin/env bash +set -euo pipefail + +echo "==> Running idstack Chrome Extension test suite..." +node test/test-manifest.js +node test/test-prompts.js +node test/test-extractor.js +node test/test-service-worker.js +node test/test-sidepanel-dom.js +node test/test-sidepanel-logic.js + +echo "==> All Chrome Extension tests passed!" +``` + +- [ ] **Step 2: Make `test/test-extension.sh` executable and run it** + +Run: `chmod +x test/test-extension.sh && ./test/test-extension.sh` +Expected: PASS with 6/6 test suites passing. + +- [ ] **Step 3: Integrate into `test/smoke-test.sh` and update `README.md` with Chrome Extension section** + +Update `README.md` to include quick-start instructions for loading the extension unpacked into Chrome. + +- [ ] **Step 4: Run full project smoke test** + +Run: `./test/smoke-test.sh` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add test/test-extension.sh test/smoke-test.sh README.md +git commit -m "test(extension): integrate chrome extension test suite into smoke tests" +``` diff --git a/docs/superpowers/plans/2026-08-15-course-crawler.md b/docs/superpowers/plans/2026-08-15-course-crawler.md new file mode 100644 index 0000000..5063523 --- /dev/null +++ b/docs/superpowers/plans/2026-08-15-course-crawler.md @@ -0,0 +1,479 @@ +# Canvas Full-Course Crawler Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Enable the idstack Chrome Extension to automatically navigate and audit an entire Canvas LMS course in the background via the Canvas REST API, evaluating full-course constructive alignment with zero IT setup. + +**Architecture:** Detect Canvas course root URLs (`/courses/:id`) in the content script; trigger background API fetching via the service worker using active session cookies (`/api/v1/courses/:id?include[]=syllabus_body` and `/api/v1/courses/:id/assignments`); clean and aggregate course data with a 40k character cap; pass to a course-level prompt engine evaluating constructive alignment across modules; display real-time crawl progress and render scholarly T1–T5 course audit findings in the Side Panel. + +**Tech Stack:** Chrome Extension Manifest V3 (Side Panel, Background Service Worker, Content Scripts, Storage API), Vanilla JavaScript (ES modules + CommonJS test companions), Vanilla CSS adhering to `DESIGN.md`. + +## Global Constraints + +- **Design System:** Strictly follow `DESIGN.md` (Source Serif 4, Public Sans, JetBrains Mono, `#faf8f3` ivory background, `#ffffff` card surfaces, `#1a1815` ink, canonical T1–T5 palette: T1 `#2f7a4a`, T2 `#2864a8`, T3 `#a87726`, T4 `#b35a1f`, T5 `#6b6b6b`). +- **Zero Build Overhead:** Pure Vanilla JS and CSS without bundlers, webpack, or npm runtime dependencies. +- **Privacy & FERPA:** Only extract syllabus, assignment descriptions, and rubrics. Never touch student rosters, submissions, or grades. +- **Free Tier Onboarding:** Provide direct link to Google AI Studio for free Gemini API keys, plus a rich demo fallback mode for instant testing without an API key. +- **T1–T5 Research Grounding:** All course audit prompts ground alignment ratings in `evidence/references.md` (Biggs constructive alignment, Bloom's taxonomy, Hattie feedback, Sweller cognitive load). + +--- + +### Task 1: Canvas Course Root Detection & Course Prompt Engine + +**Files:** +- Modify: `extension/content/extractor-core.js` +- Modify: `extension/content/extractor-core.cjs` +- Modify: `extension/shared/prompts.js` +- Modify: `extension/shared/prompts.cjs` +- Test: `test/test-extractor.js` +- Test: `test/test-prompts.js` + +**Interfaces:** +- Produces: `detectCourseContext(url, docTitle)` returning `{ isCourseRoot: boolean, courseId: string|null, origin: string|null }` +- Produces: `buildCourseAuditPrompt(courseData)` returning structured prompt string requiring JSON course alignment evaluation. + +- [ ] **Step 1: Write failing tests for course root detection and course prompt builder** + +Add to `test/test-extractor.js`: +```javascript +// Test: Canvas Course Root Detection +const { detectCourseContext } = require('../extension/content/extractor-core.cjs'); +const rootCtx = detectCourseContext('https://canvas.instructure.com/courses/987654', 'Biology 101'); +assert.strictEqual(rootCtx.isCourseRoot, true); +assert.strictEqual(rootCtx.courseId, '987654'); +assert.strictEqual(rootCtx.origin, 'https://canvas.instructure.com'); + +const subpageCtx = detectCourseContext('https://canvas.instructure.com/courses/987654/assignments/123', 'Lab 1'); +assert.strictEqual(subpageCtx.isCourseRoot, false); +assert.strictEqual(subpageCtx.courseId, '987654'); +``` + +Add to `test/test-prompts.js`: +```javascript +const { buildCourseAuditPrompt } = require('../extension/shared/prompts.cjs'); +assert.strictEqual(typeof buildCourseAuditPrompt, 'function'); +const coursePrompt = buildCourseAuditPrompt({ + title: 'Biology 101', + syllabus: 'Course objectives and grading policy...', + assignments: [ + { title: 'Quiz 1', description: 'Recall cell parts', points: 10 }, + { title: 'Final Project', description: 'Design an experiment', points: 100 } + ] +}); +assert.ok(coursePrompt.includes('Biology 101')); +assert.ok(coursePrompt.includes('Constructive Alignment')); +assert.ok(coursePrompt.includes('courseAudit')); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `node test/test-extractor.js && node test/test-prompts.js` +Expected: FAIL with `detectCourseContext is not a function` or `buildCourseAuditPrompt is not a function`. + +- [ ] **Step 3: Implement minimal code in extractor-core and prompts** + +In `extension/content/extractor-core.js` and `.cjs`: +```javascript +export function detectCourseContext(url, docTitle) { + if (!url) return { isCourseRoot: false, courseId: null, origin: null }; + try { + const parsedUrl = new URL(url); + const match = parsedUrl.pathname.match(/\/courses\/(\d+)(?:\/)?$/); + const anyCourseMatch = parsedUrl.pathname.match(/\/courses\/(\d+)/); + return { + isCourseRoot: !!match, + courseId: anyCourseMatch ? anyCourseMatch[1] : null, + origin: parsedUrl.origin + }; + } catch (e) { + return { isCourseRoot: false, courseId: null, origin: null }; + } +} +``` + +In `extension/shared/prompts.js` and `.cjs`: +```javascript +export function buildCourseAuditPrompt(courseData) { + const assignmentsSummary = (courseData.assignments || []) + .map((a, i) => `Assignment ${i+1}: ${a.title} (${a.points || 0} pts)\nDescription: ${(a.description || '').slice(0, 500)}`) + .join('\n\n'); + + return `You are an expert instructional designer and cognitive scientist using the idstack evidence base. +Perform a full-course constructive alignment audit for the following course: + +COURSE TITLE: ${courseData.title || 'Canvas Course'} +SYLLABUS & LEARNING OBJECTIVES: +${(courseData.syllabus || 'No syllabus provided').slice(0, 8000)} + +COURSE ASSIGNMENTS & ASSESSMENTS (${(courseData.assignments || []).length} items): +${assignmentsSummary.slice(0, 20000)} + +Evaluate whether the assessment system constructively aligns with stated learning outcomes (Biggs 1996 [T2], Liou et al. 2023 [T2]). +Identify cognitive load bottlenecks (Sweller 2011 [T1]), scaffolding gaps (Wood et al. 1976 [T2]), and formative feedback quality (Wisniewski et al. 2020 [T1]). + +Respond with ONLY a valid JSON object matching this schema: +{ + "summary": { + "bloomsLevel": "Overall Cognitive Demand (e.g. Apply / Analyze)", + "alignmentScore": "High (90%) | Moderate (70%) | Low (40%)", + "keyTakeaway": "1-2 sentence executive summary of course-wide curriculum alignment." + }, + "findings": [ + { + "severity": "critical" | "warning" | "info", + "tier": "T1" | "T2" | "T3" | "T4" | "T5", + "citation": "[Domain-ID] Citation Name", + "observation": "What was identified across the course syllabus and assignments.", + "evidence": "Author (Year) [Tier description]: Empirical finding.", + "recommendation": "Concrete actionable curriculum fix." + } + ], + "improvedDraft": { + "title": "Course Alignment & Scaffolding Matrix", + "content": "Markdown formatted course roadmap and revised assessment scaffolding." + } +}`; +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `node test/test-extractor.js && node test/test-prompts.js` +Expected: PASS + +- [ ] **Step 5: Commit changes** + +```bash +git add extension/content/extractor-core.* extension/shared/prompts.* test/test-extractor.js test/test-prompts.js +git commit -m "feat(extension): add canvas course root detector and course-level audit prompt builder" +``` + +--- + +### Task 2: Canvas Background Crawler Engine & Demo Fallback + +**Files:** +- Create: `extension/background/canvas-crawler.js` +- Create: `extension/background/canvas-crawler.cjs` +- Modify: `extension/background/parser-helper.js` +- Modify: `extension/background/parser-helper.cjs` +- Modify: `extension/background/service-worker.js` +- Create: `test/test-crawler.js` +- Modify: `test/test-service-worker.js` + +**Interfaces:** +- Produces: `crawlCanvasCourse(origin, courseId, fetchImpl)` returning aggregated course object `{ title, syllabus, assignments: [{ title, description, points }] }` +- Produces: `getDemoCourseAuditResult(payload)` returning realistic course-wide audit result for demo mode. +- Consumes: `buildCourseAuditPrompt`, `callLlmApi`, `saveAuditResult`. + +- [ ] **Step 1: Write failing test in `test/test-crawler.js`** + +Create `test/test-crawler.js`: +```javascript +const assert = require('assert'); +const { crawlCanvasCourse, stripHtml } = require('../extension/background/canvas-crawler.cjs'); +const { getDemoCourseAuditResult } = require('../extension/background/parser-helper.cjs'); + +// Test 1: HTML Tag Stripping +const clean = stripHtml('

Hello World & Students

'); +assert.strictEqual(clean, 'Hello World & Students'); + +// Test 2: Mock Canvas Crawler +const mockFetch = async (url) => { + if (url.includes('include[]=syllabus_body')) { + return { + ok: true, + json: async () => ({ + name: 'Biology 101: Cell Systems', + syllabus_body: '

Welcome to Biology 101. Objectives: Analyze cellular metabolism.

' + }) + }; + } + if (url.includes('/assignments')) { + return { + ok: true, + json: async () => [ + { name: 'Quiz 1', description: '

Recall organelles

', points_possible: 10 }, + { name: 'Lab Report 1', description: '

Analyze enzyme kinetics

', points_possible: 50 } + ] + }; + } + throw new Error('Not found: ' + url); +}; + +(async () => { + const courseData = await crawlCanvasCourse('https://canvas.instructure.com', '12345', mockFetch); + assert.strictEqual(courseData.title, 'Biology 101: Cell Systems'); + assert.ok(courseData.syllabus.includes('Analyze cellular metabolism')); + assert.strictEqual(courseData.assignments.length, 2); + assert.strictEqual(courseData.assignments[0].title, 'Quiz 1'); + assert.strictEqual(courseData.assignments[0].description, 'Recall organelles'); + + // Test 3: Demo Course Audit Fallback + const demoResult = getDemoCourseAuditResult({ title: 'Biology 101: Cell Systems' }); + assert.ok(demoResult.summary.keyTakeaway.includes('Course-Level Demo')); + assert.ok(demoResult.findings.length >= 2); + assert.ok(demoResult.improvedDraft.title.includes('Matrix')); + + console.log('✅ Task 2 Canvas crawler tests passed.'); +})(); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `node test/test-crawler.js` +Expected: FAIL with `Cannot find module '../extension/background/canvas-crawler.cjs'`. + +- [ ] **Step 3: Implement `canvas-crawler.js`, `canvas-crawler.cjs`, and demo helper** + +Create `extension/background/canvas-crawler.js` and `.cjs`: +```javascript +export function stripHtml(html) { + if (!html) return ''; + return html + .replace(/]*>[\s\S]*?<\/style>/gi, '') + .replace(/]*>[\s\S]*?<\/script>/gi, '') + .replace(/<[^>]+>/g, ' ') + .replace(/ /gi, ' ') + .replace(/&/gi, '&') + .replace(/</gi, '<') + .replace(/>/gi, '>') + .replace(/"/gi, '"') + .replace(/\s+/g, ' ') + .trim(); +} + +export async function crawlCanvasCourse(origin, courseId, fetchImpl = fetch) { + if (!origin || !courseId) { + throw new Error('Canvas origin and courseId are required for course crawling.'); + } + + // 1. Fetch Course details & syllabus + const courseUrl = `${origin}/api/v1/courses/${courseId}?include[]=syllabus_body`; + const courseRes = await fetchImpl(courseUrl); + if (!courseRes.ok) { + throw new Error(`Failed to fetch Canvas course info (${courseRes.status})`); + } + const courseJson = await courseRes.json(); + + // 2. Fetch Assignments list (up to 50) + const assignmentsUrl = `${origin}/api/v1/courses/${courseId}/assignments?per_page=50`; + let assignments = []; + try { + const assignRes = await fetchImpl(assignmentsUrl); + if (assignRes.ok) { + const assignJson = await assignRes.json(); + if (Array.isArray(assignJson)) { + assignments = assignJson.map((a) => ({ + title: a.name || 'Untitled Assignment', + description: stripHtml(a.description || '').slice(0, 1000), + points: a.points_possible || 0, + dueAt: a.due_at || null + })); + } + } + } catch (e) { + console.warn('Could not fetch assignments list:', e); + } + + return { + title: courseJson.name || courseJson.course_code || 'Canvas Course', + syllabus: stripHtml(courseJson.syllabus_body || ''), + assignments + }; +} +``` + +In `extension/background/parser-helper.js` and `.cjs`: +```javascript +export function getDemoCourseAuditResult(payload = {}) { + const title = payload.title || 'Sample Canvas Course'; + return { + summary: { + bloomsLevel: 'Analyze & Evaluate (Levels 4-5)', + alignmentScore: 'Moderate (74%)', + keyTakeaway: `[Course-Level Demo] Full-course audit for "${title}". Alignment gaps identified between week 1-4 recall quizzes and week 12 analytical capstone.` + }, + findings: [ + { + severity: 'critical', + tier: 'T2', + citation: '[Alignment-3] Direct Constructive Alignment', + observation: 'Modules 1-6 assess solely lower-order factual recall, while the final course project demands high-order synthesis without intermediate scaffolding.', + evidence: 'Biggs (1996) & Liou et al. (2023) [T2 controlled trial]: Abrupt jumps in cognitive demand without progressive assessment scaffolding increase failure rates.', + recommendation: 'Introduce mid-semester milestone case studies in Module 4 to bridge the gap between quizzes and the final capstone.' + }, + { + severity: 'warning', + tier: 'T1', + citation: '[Cognitive-2] Cognitive Load & Spaced Practice', + observation: 'Major assignment deadlines are clustered in Week 14-15 with no spaced formative checkpoints.', + evidence: 'Carpenter et al. (2022) [T1 meta-analysis, d=0.61]: Distributing assessments across spaced intervals produces significantly higher long-term retention.', + recommendation: 'Redistribute submission checkpoints into 3 progressive deliverables across weeks 6, 10, and 14.' + }, + { + severity: 'info', + tier: 'T1', + citation: '[Assessment-8] Formative Rubric Transparency', + observation: 'Syllabus grading policy lacks explicit performance criteria for collaborative group deliverables.', + evidence: 'Wisniewski et al. (2020) [T1 meta-analysis]: Pre-distribution of analytic rubrics with milestone criteria boosts student self-regulation and achievement.', + recommendation: 'Publish the multi-tier analytic grading rubric during the initial module launch.' + } + ], + improvedDraft: { + title: 'Course-Wide Constructive Alignment Matrix', + content: `### Course Alignment Matrix: ${title} + +| Week / Module | Intended Learning Outcome | Formative Checkpoint [T1] | Summative Assessment [T2] | +| :--- | :--- | :--- | :--- | +| **Weeks 1-3** | Foundational Cell Structure | Spaced Knowledge Check (10 pts) | Module 1 Synthesis Quiz | +| **Weeks 4-7** | Enzyme Kinetics & Modeling | Case Problem Milestone 1 [T2] | Lab Protocol Analysis | +| **Weeks 8-11** | Experimental Troubleshooting | Peer Review Protocol [T1] | Milestone 2 Experimental Draft | +| **Weeks 12-15**| Autonomous Investigation | Scaffolded Capstone Consult | Final Research Capstone |` + } + }; +} +``` + +In `extension/background/service-worker.js`: +Add message handler for `CRAWL_AND_AUDIT_COURSE` calling `crawlCanvasCourse`, `buildCourseAuditPrompt`, and saving results. + +- [ ] **Step 4: Run test to verify it passes** + +Run: `node test/test-crawler.js && node test/test-service-worker.js` +Expected: PASS + +- [ ] **Step 5: Commit changes** + +```bash +git add extension/background/canvas-crawler.* extension/background/parser-helper.* extension/background/service-worker.js test/test-crawler.js test/test-service-worker.js +git commit -m "feat(extension): implement Canvas background course crawler and demo course audit engine" +``` + +--- + +### Task 3: Side Panel UI: Course Audit Mode, Progress Bar & Free Key Link + +**Files:** +- Modify: `extension/sidepanel/index.html` +- Modify: `extension/sidepanel/sidepanel.css` +- Modify: `extension/sidepanel/sidepanel.js` +- Modify: `extension/sidepanel/renderer-helper.js` +- Modify: `extension/sidepanel/renderer-helper.cjs` +- Test: `test/test-sidepanel-dom.js` +- Test: `test/test-sidepanel-logic.js` + +**Interfaces:** +- Produces: Dynamic UI switching between "Audit Page with Evidence" (single page) and "Audit Entire Course" (course root). +- Produces: Animated step-by-step progress indicator (`#crawl-progress-card`). +- Produces: Direct link to Google AI Studio in settings drawer (`#get-api-key-link`). + +- [ ] **Step 1: Write failing tests in `test/test-sidepanel-dom.js` and `test/test-sidepanel-logic.js`** + +Add to `test/test-sidepanel-dom.js`: +```javascript +// Test: Course Audit Button & Progress Bar in HTML +assert.ok(html.includes('id="audit-course-btn"'), 'audit-course-btn must exist in index.html'); +assert.ok(html.includes('id="crawl-progress-card"'), 'crawl-progress-card must exist in index.html'); +assert.ok(html.includes('id="crawl-status-text"'), 'crawl-status-text must exist in index.html'); +assert.ok(html.includes('https://aistudio.google.com/app/apikey'), 'Link to free Google AI Studio key must exist in Settings'); +``` + +Add to `test/test-sidepanel-logic.js`: +```javascript +// Test: Rendering Course-Wide Alignment Matrix +const { renderAuditHTML } = require('../extension/sidepanel/renderer-helper.cjs'); +const courseResult = { + summary: { bloomsLevel: 'Analyze', alignmentScore: 'High (88%)', keyTakeaway: 'Strong alignment.' }, + findings: [{ tier: 'T1', severity: 'info', citation: '[Test-1]', observation: 'Good', evidence: 'Meta-analysis', recommendation: 'Keep it' }], + improvedDraft: { title: 'Course Alignment Matrix', content: '| Week | Outcome |' } +}; +const rendered = renderAuditHTML(courseResult); +assert.ok(rendered.includes('Course Alignment Matrix')); +assert.ok(rendered.includes('High (88%)')); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `node test/test-sidepanel-dom.js && node test/test-sidepanel-logic.js` +Expected: FAIL due to missing DOM elements and free key link. + +- [ ] **Step 3: Update `index.html`, `sidepanel.css`, and `sidepanel.js`** + +In `extension/sidepanel/index.html`: +- Add `#audit-course-btn` button (hidden by default unless on course root). +- Add `#crawl-progress-card` with progress bar and status text. +- Add `Get your free Google AI Studio key →` under the API key input. + +In `extension/sidepanel/sidepanel.css`: +- Add styling for `.progress-card`, `.progress-bar-container`, `.progress-bar-fill`, and `.help-link`. + +In `extension/sidepanel/sidepanel.js`: +- Check `detectCourseContext(tab.url)` in `refreshActiveTab()`. +- If `isCourseRoot` is true, show `#audit-course-btn` alongside `#audit-btn`. +- Add click handler for `#audit-course-btn` that triggers `CRAWL_AND_AUDIT_COURSE`, updates progress text ("Gathering syllabus...", "Fetching assignments...", "Analyzing alignment..."), and renders the final course audit. + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `node test/test-sidepanel-dom.js && node test/test-sidepanel-logic.js` +Expected: PASS + +- [ ] **Step 5: Commit changes** + +```bash +git add extension/sidepanel/ test/test-sidepanel-* +git commit -m "feat(extension): add course audit UI, crawl progress indicator, and free API key link" +``` + +--- + +### Task 4: Test Integration, Smoke Suite & README Documentation + +**Files:** +- Modify: `test/test-extension.sh` +- Modify: `test/smoke-test.sh` +- Modify: `README.md` + +**Interfaces:** +- Produces: Integrated test suite running all 7 extension test scripts. +- Produces: Updated user-facing instructions in README on how to use the full-course Canvas crawler and free API key. + +- [ ] **Step 1: Update `test/test-extension.sh` to include `test/test-crawler.js`** + +In `test/test-extension.sh`: +```bash +node "$REPO_ROOT/test/test-crawler.js" +``` + +- [ ] **Step 2: Run `test/test-extension.sh` and `test/smoke-test.sh`** + +Run: `./test/test-extension.sh` +Expected: 7/7 suites pass. + +Run: `./test/smoke-test.sh` +Expected: 356+/356+ tests pass with 0 failures. + +- [ ] **Step 3: Update `README.md`** + +Add a subsection under Chrome Extension detailing: +- "Audit Entire Course" from the Canvas course homepage. +- Background session extraction without developer tokens. +- Getting a free Google AI Studio API key. + +- [ ] **Step 4: Commit changes** + +```bash +git add test/test-extension.sh test/smoke-test.sh README.md +git commit -m "feat(extension): integrate course crawler tests into smoke suite and update readme" +``` + +--- + +## Execution Choice + +Plan complete and saved to `docs/superpowers/plans/2026-08-15-course-crawler.md`. Two execution options: + +1. **Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration. +2. **Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints. + +Which approach? diff --git a/docs/superpowers/plans/2026-08-16-course-dossier-export.md b/docs/superpowers/plans/2026-08-16-course-dossier-export.md new file mode 100644 index 0000000..25acfff --- /dev/null +++ b/docs/superpowers/plans/2026-08-16-course-dossier-export.md @@ -0,0 +1,366 @@ +# Course Dossier & Compiled Markdown Export Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Enable instructional designers to collect course page audits incrementally into an active Course Dossier as they navigate Canvas/Docs, and compile the collection into a single structured institutional Markdown export (`.md`) or copy it to the clipboard. + +**Architecture:** Extend `storage.js` with dossier CRUD methods in `chrome.storage.local`; implement `dossier-compiler.js` synthesizing multi-page audit data into a unified Markdown document with executive summaries, cross-module findings, and itemized drafts; add a persistent Dossier header pill, drawer manager, and export action buttons to the Side Panel UI following `DESIGN.md`. + +**Tech Stack:** Chrome Extension Manifest V3 (Side Panel, Storage API), Vanilla JavaScript (ES modules + CommonJS test companions), Vanilla CSS adhering to `DESIGN.md`. + +## Global Constraints + +- **Design System:** Strictly follow `DESIGN.md` (Source Serif 4, Public Sans, JetBrains Mono, `#faf8f3` ivory background, `#ffffff` card surfaces, `#1a1815` ink, canonical T1–T5 palette: T1 `#2f7a4a`, T2 `#2864a8`, T3 `#a87726`, T4 `#b35a1f`, T5 `#6b6b6b`). +- **Zero Build Overhead:** Pure Vanilla JS and CSS without bundlers or npm runtime dependencies. +- **Privacy & FERPA:** Only store and export curriculum and assignment content. Never store student PII. +- **T1–T5 Research Grounding:** Exported findings clearly preserve empirical citations and research tiers from `evidence/references.md`. + +--- + +### Task 1: Dossier Storage Helpers & Markdown Compiler Engine + +**Files:** +- Modify: `extension/shared/storage.js` +- Create: `extension/shared/dossier-compiler.js` +- Create: `extension/shared/dossier-compiler.cjs` +- Create: `test/test-dossier-compiler.js` + +**Interfaces:** +- Produces: `getDossier()`, `addToDossier(item)`, `removeFromDossier(id)`, `clearDossier()` in `storage.js`. +- Produces: `compileDossierToMarkdown(dossierItems, courseTitle)` returning full synthesized markdown string. +- Produces: `compileSingleAuditToMarkdown(auditItem)` returning single-page markdown string. + +- [ ] **Step 1: Write failing tests in `test/test-dossier-compiler.js`** + +Create `test/test-dossier-compiler.js`: +```javascript +const assert = require('assert'); +const { compileDossierToMarkdown, compileSingleAuditToMarkdown } = require('../extension/shared/dossier-compiler.cjs'); + +// Test 1: Single Audit Markdown Compilation +const singleItem = { + title: 'Lab 1: Enzymes', + pageType: 'Canvas Assignment', + url: 'https://canvas.instructure.com/courses/123/assignments/456', + timestamp: '2026-08-16T10:00:00Z', + result: { + summary: { + bloomsLevel: 'Analyze (Level 4)', + alignmentScore: 'Moderate (75%)', + keyTakeaway: 'Focus on rubric transparency.' + }, + findings: [ + { + severity: 'warning', + tier: 'T1', + citation: '[Assessment-8] Formative Feedback', + observation: 'Rubric lacks milestone descriptors.', + evidence: 'Wisniewski et al. (2020) [T1]: Rubrics boost self-regulation.', + recommendation: 'Add milestone criteria.' + } + ], + improvedDraft: { + title: 'Improved Lab Rubric', + content: '| Criterion | Proficient | Novice |\n| --- | --- | --- |' + } + } +}; + +const singleMd = compileSingleAuditToMarkdown(singleItem); +assert.ok(singleMd.includes('# idstack Instructional Design Audit: Lab 1: Enzymes')); +assert.ok(singleMd.includes('**Bloom\'s Demand:** Analyze (Level 4)')); +assert.ok(singleMd.includes('Wisniewski et al. (2020)')); +assert.ok(singleMd.includes('Improved Lab Rubric')); + +// Test 2: Multi-Audit Dossier Compilation +const dossierItems = [ + singleItem, + { + title: 'Course Syllabus', + pageType: 'Canvas Syllabus', + url: 'https://canvas.instructure.com/courses/123/syllabus', + timestamp: '2026-08-16T09:30:00Z', + result: { + summary: { + bloomsLevel: 'Understand (Level 2)', + alignmentScore: 'High (90%)', + keyTakeaway: 'Clear policy structure.' + }, + findings: [ + { + severity: 'info', + tier: 'T2', + citation: '[Alignment-3] Direct Constructive Alignment', + observation: 'Objectives align with module outcomes.', + evidence: 'Biggs (1996) [T2]: Clear alignment supports deep learning.', + recommendation: 'Maintain alignment across quizzes.' + } + ], + improvedDraft: { + title: 'Revised Objectives', + content: '- Analyze core enzyme mechanisms.' + } + } + } +]; + +const dossierMd = compileDossierToMarkdown(dossierItems, 'Biology 101: Cell Systems'); +assert.ok(dossierMd.includes('# idstack Course Audit Dossier: Biology 101: Cell Systems')); +assert.ok(dossierMd.includes('Total Audited Materials: 2 components')); +assert.ok(dossierMd.includes('## Section 1: Lab 1: Enzymes')); +assert.ok(dossierMd.includes('## Section 2: Course Syllabus')); +assert.ok(dossierMd.includes('Wisniewski et al. (2020)')); +assert.ok(dossierMd.includes('Biggs (1996)')); + +console.log('✅ Task 1 Dossier compiler tests passed.'); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `node test/test-dossier-compiler.js` +Expected: FAIL with `Cannot find module '../extension/shared/dossier-compiler.cjs'`. + +- [ ] **Step 3: Implement `dossier-compiler.js`, `dossier-compiler.cjs`, and `storage.js` dossier methods** + +Create `extension/shared/dossier-compiler.js` and `.cjs`: +```javascript +export function compileSingleAuditToMarkdown(item) { + if (!item || !item.result) return ''; + const result = item.result; + const title = item.title || 'Course Material'; + const pageType = item.pageType || 'Web Page'; + const url = item.url || ''; + const timestamp = item.timestamp ? new Date(item.timestamp).toLocaleString() : new Date().toLocaleString(); + + let md = `# idstack Instructional Design Audit: ${title}\n\n`; + md += `> **Audited Component:** ${pageType} \n`; + if (url) md += `> **Source URL:** [${url}](${url}) \n`; + md += `> **Date:** ${timestamp} \n`; + md += `> **Evaluator:** idstack Evidence-Based Course Design Engine (Manifest V3)\n\n`; + + md += `## Executive Summary\n\n`; + md += `- **Bloom's Demand:** ${result.summary?.bloomsLevel || 'N/A'}\n`; + md += `- **Constructive Alignment:** ${result.summary?.alignmentScore || 'N/A'}\n`; + md += `- **Key Takeaway:** ${result.summary?.keyTakeaway || 'No summary provided.'}\n\n`; + + if (Array.isArray(result.findings) && result.findings.length > 0) { + md += `## Evidence-Based Findings & Recommendations\n\n`; + result.findings.forEach((f, idx) => { + md += `### ${idx + 1}. [${f.tier || 'T1'}] ${f.citation || 'Citation'}\n\n`; + md += `- **Severity:** \`${(f.severity || 'info').toUpperCase()}\`\n`; + md += `- **Observation:** ${f.observation || ''}\n`; + md += `- **Empirical Evidence:** ${f.evidence || ''}\n`; + md += `- **Actionable Recommendation:** ${f.recommendation || ''}\n\n`; + }); + } + + if (result.improvedDraft && result.improvedDraft.content) { + md += `## ${result.improvedDraft.title || 'Improved Draft & Alignment Matrix'}\n\n`; + md += `${result.improvedDraft.content}\n\n`; + } + + md += `---\n*Generated by [idstack](https://github.com/savvides/idstack) — Evidence-Based Course Design.*`; + return md; +} + +export function compileDossierToMarkdown(dossierItems, courseTitle = 'Canvas Course') { + if (!Array.isArray(dossierItems) || dossierItems.length === 0) { + return `# idstack Course Audit Dossier: ${courseTitle}\n\n*No audit materials in dossier.*`; + } + + const timestamp = new Date().toLocaleString(); + let md = `# idstack Course Audit Dossier: ${courseTitle}\n\n`; + md += `> **Course:** ${courseTitle} \n`; + md += `> **Total Audited Materials:** ${dossierItems.length} components \n`; + md += `> **Compiled Date:** ${timestamp} \n`; + md += `> **Engine:** idstack Evidence-Based Instructional Design Co-Pilot\n\n`; + + md += `## Table of Audited Materials\n\n`; + md += `| # | Component Title | Type | Bloom's Demand | Alignment Score |\n`; + md += `| :--- | :--- | :--- | :--- | :--- |\n`; + dossierItems.forEach((item, idx) => { + const s = item.result?.summary || {}; + md += `| ${idx + 1} | ${item.title || 'Untitled'} | ${item.pageType || 'Page'} | ${s.bloomsLevel || 'N/A'} | ${s.alignmentScore || 'N/A'} |\n`; + }); + md += `\n---\n\n`; + + dossierItems.forEach((item, idx) => { + md += `## Section ${idx + 1}: ${item.title || 'Component'}\n\n`; + md += `> **Type:** ${item.pageType || 'Page'} | **Source:** ${item.url || 'N/A'}\n\n`; + + const r = item.result || {}; + md += `### Summary\n`; + md += `- **Cognitive Demand:** ${r.summary?.bloomsLevel || 'N/A'}\n`; + md += `- **Alignment Rating:** ${r.summary?.alignmentScore || 'N/A'}\n`; + md += `- **Takeaway:** ${r.summary?.keyTakeaway || 'N/A'}\n\n`; + + if (Array.isArray(r.findings) && r.findings.length > 0) { + md += `### Findings & Evidence\n\n`; + r.findings.forEach((f, fIdx) => { + md += `#### ${idx + 1}.${fIdx + 1} [${f.tier || 'T1'}] ${f.citation || 'Citation'}\n`; + md += `- **Observation:** ${f.observation || ''}\n`; + md += `- **Evidence:** ${f.evidence || ''}\n`; + md += `- **Recommendation:** ${f.recommendation || ''}\n\n`; + }); + } + + if (r.improvedDraft && r.improvedDraft.content) { + md += `### ${r.improvedDraft.title || 'Revised Draft'}\n\n`; + md += `${r.improvedDraft.content}\n\n`; + } + + md += `---\n\n`; + }); + + md += `*Generated by [idstack](https://github.com/savvides/idstack) — Evidence-Based Course Design.*`; + return md; +} +``` + +In `extension/shared/storage.js`: +Add `getDossier()`, `addToDossier(item)`, `removeFromDossier(id)`, `clearDossier()`. + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `node test/test-dossier-compiler.js` +Expected: PASS + +- [ ] **Step 5: Commit changes** + +```bash +git add extension/shared/storage.js extension/shared/dossier-compiler.* test/test-dossier-compiler.js +git commit -m "feat(extension): implement dossier storage helpers and multi-audit markdown compiler" +``` + +--- + +### Task 2: Side Panel UI: Dossier Badge, Actions & Drawer + +**Files:** +- Modify: `extension/sidepanel/index.html` +- Modify: `extension/sidepanel/sidepanel.css` +- Modify: `extension/sidepanel/sidepanel.js` +- Modify: `extension/sidepanel/renderer-helper.js` +- Modify: `extension/sidepanel/renderer-helper.cjs` +- Test: `test/test-sidepanel-dom.js` +- Test: `test/test-sidepanel-logic.js` + +**Interfaces:** +- Produces: Header Dossier pill `#dossier-toggle-btn` showing active count. +- Produces: Results action bar `#add-to-dossier-btn` and `#export-single-md-btn`. +- Produces: Dossier drawer `#dossier-drawer` with item cards, delete buttons, "Download Compiled .md", and "Copy Markdown" buttons. + +- [ ] **Step 1: Write failing tests in `test/test-sidepanel-dom.js` and `test/test-sidepanel-logic.js`** + +Add to `test/test-sidepanel-dom.js`: +```javascript +// Test: Dossier UI Elements +assert.ok(html.includes('id="dossier-toggle-btn"'), 'dossier-toggle-btn must exist in header'); +assert.ok(html.includes('id="dossier-count"'), 'dossier-count element must exist'); +assert.ok(html.includes('id="add-to-dossier-btn"'), 'add-to-dossier-btn must exist'); +assert.ok(html.includes('id="export-single-md-btn"'), 'export-single-md-btn must exist'); +assert.ok(html.includes('id="dossier-drawer"'), 'dossier-drawer must exist'); +assert.ok(html.includes('id="export-dossier-md-btn"'), 'export-dossier-md-btn must exist in dossier drawer'); +assert.ok(html.includes('id="copy-dossier-md-btn"'), 'copy-dossier-md-btn must exist in dossier drawer'); +assert.ok(html.includes('id="clear-dossier-btn"'), 'clear-dossier-btn must exist in dossier drawer'); +``` + +Add to `test/test-sidepanel-logic.js`: +```javascript +// Test: Dossier Item Rendering in Drawer +const { renderDossierListHTML } = require('../extension/sidepanel/renderer-helper.cjs'); +assert.strictEqual(typeof renderDossierListHTML, 'function'); +const listHtml = renderDossierListHTML([ + { id: '1', title: 'Week 1 Quiz', pageType: 'Assignment', result: { summary: { bloomsLevel: 'Remember', alignmentScore: 'Low' } } } +]); +assert.ok(listHtml.includes('Week 1 Quiz')); +assert.ok(listHtml.includes('Assignment')); +assert.ok(listHtml.includes('data-dossier-id="1"')); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `node test/test-sidepanel-dom.js && node test/test-sidepanel-logic.js` +Expected: FAIL due to missing DOM elements and render helper. + +- [ ] **Step 3: Update `index.html`, `sidepanel.css`, `sidepanel.js`, and `renderer-helper.js`** + +In `extension/sidepanel/index.html`: +- Add `#dossier-toggle-btn` to header. +- Add `#add-to-dossier-btn` and `#export-single-md-btn` to results actions bar. +- Add `#dossier-drawer` section with list container, export, copy, and clear buttons. + +In `extension/sidepanel/sidepanel.css`: +- Add styles for `.dossier-pill`, `.result-actions-bar`, `.dossier-list`, `.dossier-item`, and `.dossier-delete-btn`. + +In `extension/sidepanel/sidepanel.js`: +- Wire up `updateDossierBadge()`. +- Add click listener for `#add-to-dossier-btn` saving current `activeAuditResult` to dossier with visual confirmation (`✓ Added to Dossier`). +- Add click listener for `#export-single-md-btn` downloading `idstack-audit-.md`. +- Add click listener for `#export-dossier-md-btn` downloading `idstack-course-dossier-<title>.md`. +- Add click listener for `#copy-dossier-md-btn` copying compiled markdown to clipboard. +- Add click listener for `#clear-dossier-btn` clearing dossier list. + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `node test/test-sidepanel-dom.js && node test/test-sidepanel-logic.js` +Expected: PASS + +- [ ] **Step 5: Commit changes** + +```bash +git add extension/sidepanel/ test/test-sidepanel-* +git commit -m "feat(extension): add course dossier drawer, badge, and compiled markdown export UI" +``` + +--- + +### Task 3: Test Integration, Smoke Suite & README Documentation + +**Files:** +- Modify: `test/test-extension.sh` +- Modify: `test/smoke-test.sh` +- Modify: `README.md` + +**Interfaces:** +- Produces: Integrated test suite running all 8 extension test suites. +- Produces: User documentation detailing the Course Dossier and compiled Markdown export workflow. + +- [ ] **Step 1: Update `test/test-extension.sh` to include `test/test-dossier-compiler.js`** + +In `test/test-extension.sh`: +```bash +node "$REPO_ROOT/test/test-dossier-compiler.js" +``` + +- [ ] **Step 2: Run `test/test-extension.sh` and `test/smoke-test.sh`** + +Run: `./test/test-extension.sh` +Expected: 8/8 suites pass. + +Run: `./test/smoke-test.sh` +Expected: 356+/356+ tests pass with 0 failures. + +- [ ] **Step 3: Update `README.md`** + +Add documentation under Chrome Extension on: +- "Course Dossier" workflow: Auditing multiple pages across a course and accumulating findings. +- 1-Click compiled Markdown export (`.md`) and clipboard copying. + +- [ ] **Step 4: Commit changes** + +```bash +git add test/test-extension.sh test/smoke-test.sh README.md +git commit -m "feat(extension): integrate dossier compiler tests into smoke suite and update readme" +``` + +--- + +## Execution Choice + +Plan complete and saved to `docs/superpowers/plans/2026-08-16-course-dossier-export.md`. Two execution options: + +1. **Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration. +2. **Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints. + +Which approach? diff --git a/docs/superpowers/specs/2026-08-15-chrome-extension-design.md b/docs/superpowers/specs/2026-08-15-chrome-extension-design.md new file mode 100644 index 0000000..7d8de38 --- /dev/null +++ b/docs/superpowers/specs/2026-08-15-chrome-extension-design.md @@ -0,0 +1,144 @@ +# idstack Chrome Extension Experiment — Design Specification + +**Date:** 2026-08-15 +**Status:** Approved for Implementation +**Topic:** Frictionless Evidence-Based Course Design Chrome Extension for Educators & Instructional Designers + +--- + +## 1. Executive Summary & Problem Statement + +### 1.1 Context +`idstack` provides evidence-based instructional design skills powered by 108 peer-reviewed studies across 11 research domains, classifying learning objectives with Bloom's taxonomy and grading recommendations with evidence tiers (T1–T5). + +While the existing Claude Code CLI plugin works well for terminal-proficient users, the vast majority of educators, faculty members, and instructional designers (IDs) work exclusively in graphical environments—primarily Learning Management Systems (Canvas LMS, Blackboard, Brightspace, Moodle), document processors (Google Docs, Microsoft Word), and web browsers. + +### 1.2 The Opportunity +Building institutional LMS integrations (LTI 1.3, Canvas Developer Keys) introduces severe bureaucratic friction (IT security reviews, procurement cycles, administrator access requirements). A **Manifest V3 Chrome Extension** eliminates all institutional gatekeeping: +- An educator installs the extension directly from the Chrome Web Store in seconds. +- It operates directly on their active browser tab using their existing authenticated session. +- It provides a native Chrome Side Panel co-pilot that audits course material and produces actionable, evidence-backed improvements in seconds. + +--- + +## 2. Core User Experience & Workflows + +### 2.1 The 1-Click Audit Workflow +``` +[Educator in Canvas / Google Doc] + │ + ▼ (Clicks idstack extension icon) +[Chrome Side Panel Opens] + │ + ▼ (Automatic platform detection: "Canvas Assignment Detected") +[User clicks "Audit with Evidence"] + │ + ▼ (2-3s fast analysis via Gemini 3.7 Flash) +[Results Displayed in Side Panel] + ├── Bloom's Taxonomy Level & Alignment Summary + ├── Finding Cards with [T1]–[T5] Evidence Badges + ├── 1-Click Improved Revision (Objective, Rubric, Prompt) + └── "Copy to Clipboard" / "View Full Stakeholder Report" +``` + +### 2.2 Micro-Interactions & States +1. **Ready State:** Shows detected context (e.g. `[Canvas LMS] Intro to Biology - Module 2 Assignment`) with a clear call-to-action button: **"Audit Page with Evidence"**. +2. **Auditing State:** Displays an elegant skeleton loader with dynamic progress text (*"Extracting learning outcomes..."* → *"Checking Bloom's taxonomy..."* → *"Synthesizing research evidence..."*). +3. **Results State:** + - **Summary Banner:** High-level Bloom's tier (e.g., *Remember / Understand vs. Analyze / Evaluate*) and alignment rating. + - **Finding Cards:** Categorized by severity (Critical / Warning / Suggestion) with mono-tagged citation chips (e.g. `[Assessment-8] [T1]`). + - **Actionable Rewrite Box:** Formatted revised rubric criteria or rewritten measurable learning objectives with a **"📋 Copy to Clipboard"** button. + - **Feedback Mechanism:** Instant 1-click **"Helpful? 👍 / 👎"** toggle to collect experiment signal. +4. **Settings / Privacy State:** Clean drawer allowing users to view privacy commitments (zero student PII stored) and optionally enter a custom Gemini / Claude API key for BYOK mode. + +--- + +## 3. System Architecture & Components + +``` +idstack/ +├── extension/ +│ ├── manifest.json # Chrome MV3 configuration +│ ├── icons/ # Branded icon assets (16x16, 48x48, 128x128) +│ ├── sidepanel/ +│ │ ├── index.html # Side panel DOM structure +│ │ ├── sidepanel.css # Styled with idstack DESIGN.md design system +│ │ └── sidepanel.js # UI logic, view transitions, clipboard operations +│ ├── content/ +│ │ ├── extractor.js # DOM content extractors (Canvas, Google Docs, generic web) +│ │ └── extractor.css # Injected visual indicators (if applicable) +│ ├── background/ +│ │ └── service-worker.js # Background worker, message broker & API client +│ └── shared/ +│ ├── evidence-base.js # Curated T1-T5 evidence citations from evidence/references.md +│ ├── prompts.js # Structured audit prompts and JSON output schemas +│ └── storage.js # Chrome sync/local storage helpers +``` + +### 3.1 Content Script (`content/extractor.js`) +The content script is injected on demand or on target domains to extract course content without requiring Canvas API tokens: +- **Canvas LMS (`*.instructure.com` / `canvas.*.edu`):** + - Targets `#assignment_show`, `.description.user_content`, `#rubrics`, `.module-item-title`, `#syllabusContainer`. + - Captures title, learning outcomes, assignment instructions, and existing rubric grids. +- **Google Docs (`docs.google.com/document/*`):** + - Extracts text from the active document container or user selection. +- **Generic Web Syllabi / Pages:** + - Extracts semantic `<main>`, `<article>`, or high-density text containers while pruning navigational noise and footers. + +### 3.2 Background Worker (`background/service-worker.js`) +- Receives audit requests from the Side Panel. +- Formats prompt with extracted DOM text + idstack research criteria. +- Dispatches request to the configured LLM endpoint (Gemini 3.7 Flash default proxy or local BYOK endpoint). +- Validates and parses the returned JSON schema and forwards results to the Side Panel. + +--- + +## 4. Visual Design & Aesthetics (`DESIGN.md` Adherence) + +The extension UI strictly complies with the existing `DESIGN.md` specification: + +- **Typography:** + - Body & headings: `Source Serif 4` for publication-grade, authoritative feel. + - UI labels & buttons: `Public Sans` (clean official record aesthetic). + - Citations, badges, & code: `JetBrains Mono` for `[Alignment-14] [T1]` evidence marks. +- **Color Tokens:** + - Background: `#faf8f3` (pristine ivory). + - Raised surfaces / cards: `#ffffff`. + - Primary text: `#1a1815` (warm near-black). + - Borders / Dividers: `#e6e0d2` hairline rules. + - Evidence Tiers: + - `T1` (Meta-analysis): Prussian blue `#1d4e89` + - `T2` (Controlled trials): Teal `#007791` + - `T3` (Observational): Olive `#588157` + - `T4` (Case studies): Ochre `#c97a22` + - `T5` (Expert guidance): Slate `#6c757d` +- **Zero Cliché Tropes:** + - No purple gradients, no glowing neon borders, no emoji spam, no generic SaaS templates. + +--- + +## 5. Security, Privacy & FERPA Compliance + +1. **No Student Data Collection:** The extension operates solely on instructional design artifacts (syllabi, assignment prompts, rubrics, learning outcomes). +2. **Zero Permanent Storage on External Servers:** Prompts sent to the AI backend are processed ephemerally with zero data retention for training. +3. **Client-Side Manifest Memory:** Course manifests and audit history are persisted locally in `chrome.storage.local`. + +--- + +## 6. Success Metrics & Validation Gate + +For this experiment to be considered successful before expanding into larger features: +1. **Frictionless Completion:** A first-time user can install the extension and receive an audit in < 30 seconds. +2. **Utility Signal:** > 40% of audit runs result in the educator clicking **"Copy to Clipboard"** for the revised rubric/objective. +3. **Qualitative Rating:** > 80% positive ("👍") feedback on finding relevance and accuracy. + +--- + +## 7. Implementation Plan Sequence + +1. **Phase 1: Project Scaffolding & Manifest:** Create `extension/manifest.json`, icon assets, and baseline extension setup. +2. **Phase 2: Content Extraction Engine:** Implement `content/extractor.js` for Canvas LMS and general web pages. +3. **Phase 3: Side Panel UI & Design System:** Implement `sidepanel/index.html`, `sidepanel.css` (using `DESIGN.md` tokens), and interactive state rendering. +4. **Phase 4: LLM Audit Engine & Prompts:** Implement `background/service-worker.js` with structured Gemini 3.7 Flash prompting and JSON schema enforcement. +5. **Phase 5: Copy Actions & Stakeholder Report Generator:** Wire 1-click clipboard revision copying and standalone HTML report generation. +6. **Phase 6: Testing & Verification:** Verify on real Canvas pages and web syllabi. diff --git a/docs/superpowers/specs/2026-08-15-course-crawler-design.md b/docs/superpowers/specs/2026-08-15-course-crawler-design.md new file mode 100644 index 0000000..8f98ce3 --- /dev/null +++ b/docs/superpowers/specs/2026-08-15-course-crawler-design.md @@ -0,0 +1,35 @@ +# idstack Chrome Extension: Full-Course Crawler + +## Overview +Currently, the idstack Chrome Extension audits individual pages (e.g., a single Canvas assignment or syllabus). This feature expands the extension's capabilities to audit an entire Canvas course at once. When a user is on a Canvas course homepage, the extension will provide an "Audit Entire Course" option. + +## Architecture & Data Flow + +### 1. Context Detection & UI +- **Content Script (`extractor.js`)**: Updated to detect if the current URL is a Canvas course homepage (matches `*/courses/:course_id` without specific sub-pages like `/assignments`). +- **Side Panel UI (`index.html` / `sidepanel.js`)**: + - If a course homepage is detected, the UI displays a primary "Audit Entire Course" button. + - A progress indicator UI will be added to show the status of the background crawl (e.g., "Fetching course data...", "Analyzing alignment..."). + - A link to "Get a free Google AI Studio API key" will be added to the Settings drawer to encourage users to move beyond the demo mode. + +### 2. The Canvas API Crawler +- **Background Worker (`service-worker.js`)**: + - Implements a new handler for a `CRAWL_COURSE` message. + - Uses native `fetch()` calls to the Canvas REST API endpoints: + - `GET /api/v1/courses/:course_id?include[]=syllabus_body` (Course info & Syllabus) + - `GET /api/v1/courses/:course_id/assignments` (Assignments & Rubrics) + - **Authentication**: Inherits the user's active Canvas session cookies automatically. No OAuth or API tokens needed. + - **Data Processing**: Extracts raw text, strips HTML tags, and aggregates the content. + - **Data Cap**: Implements a safety limit (e.g., 40,000 characters) on the aggregated payload to prevent LLM token limits from being exceeded. + +### 3. LLM Integration & Presentation +- **Prompt Generation (`prompts.js`)**: A new prompt variant will be created for course-level audits, asking the LLM to assess constructive alignment across the aggregated syllabus and assignments. +- **Rendering**: The course audit results will reuse the existing scholarly `DESIGN.md` presentation, rendering T1-T5 evidence badges and actionable recommendations. + +## Error Handling +- If the Canvas API fetch fails (e.g., network error or permission denied), the side panel will display the existing inline error banner with a clear message. +- If the course data exceeds the character limit, it will be safely truncated before being sent to the LLM. + +## Testing Strategy +- Unit tests will be added to mock the Canvas API responses and verify the crawler's data aggregation and truncation logic. +- The UI tests will be updated to cover the new "Audit Entire Course" state and progress indicators. diff --git a/docs/superpowers/specs/2026-08-16-course-dossier-export-design.md b/docs/superpowers/specs/2026-08-16-course-dossier-export-design.md new file mode 100644 index 0000000..1af3d29 --- /dev/null +++ b/docs/superpowers/specs/2026-08-16-course-dossier-export-design.md @@ -0,0 +1,92 @@ +# idstack Course Dossier & Compiled Markdown Export + +## Overview +Instructional designers (IDs) often evaluate a course incrementally across multiple pages (syllabus, individual assignments, module rubrics, and discussion prompts). Rather than exporting individual files for every page, this feature introduces an **Audit Dossier** that collects findings as the ID navigates through the course. When ready, the ID can compile and export the entire collection into a single, cohesive Markdown document (`.md`) or copy it directly to the clipboard. + +## Requirements & Scope + +### 1. In-Scope +- **Dossier Session Collection:** + - Capability to add any single-page or full-course audit result to the active Course Dossier. + - Header badge in the Side Panel indicating the active dossier item count (e.g., `📁 Dossier (3)`). +- **Dossier Management Drawer:** + - Drawer view listing all collected audits in the active session with title, page type, timestamp, and remove (trash) action. + - "Clear Dossier" action to reset the session. +- **Compiled Markdown Export Engine:** + - Generates a structured multi-section Markdown document combining all collected audits: + 1. Executive Course Summary (Course title, date, total audited components, overall Bloom's distribution, and alignment score). + 2. Consolidated Cross-Course Action Items (Grouped T1 & T2 research-backed findings). + 3. Itemized Section Breakdowns (Individual page findings with empirical citations and rewritten rubrics/prompts). + - "📥 Download Compiled Dossier (.md)" triggers a browser file download. + - "📋 Copy Compiled Markdown" copies the full formatted document to clipboard with visual confirmation. +- **Single-Page Quick Export:** + - Quick action on any audit result to download that individual page's Markdown or copy it. +- **Client-Side Persistence:** + - Persisted in `chrome.storage.local` under `activeDossier` so navigation across tabs and page reloads never lose collected audits. + +### 2. Out-of-Scope (Deferred) +- Branded PDF generation or dedicated print preview tabs (explicitly deferred per user request). + +--- + +## Architecture & Data Flow + +### 1. Storage Schema (`storage.js`) +```javascript +// Dossier item structure +{ + id: string, // UUID or timestamp + url: string, + title: string, + pageType: string, + timestamp: string, // ISO string + result: { + summary: { bloomsLevel, alignmentScore, keyTakeaway }, + findings: [ { severity, tier, citation, observation, evidence, recommendation } ], + improvedDraft: { title, content } + } +} + +// Storage helpers +export async function getDossier(): Promise<Array<DossierItem>> +export async function addToDossier(item: DossierItem): Promise<Array<DossierItem>> +export async function removeFromDossier(id: string): Promise<Array<DossierItem>> +export async function clearDossier(): Promise<void> +``` + +### 2. UI Structure (`index.html` & `sidepanel.css`) +- **Header Badge:** + ```html + <button id="dossier-toggle-btn" class="dossier-pill" title="View Course Audit Dossier"> + <span class="dossier-icon">📁</span> + <span id="dossier-count">0</span> + </button> + ``` +- **Audit Results Actions:** + ```html + <div class="result-actions-bar"> + <button id="add-to-dossier-btn" class="secondary-btn">➕ Add to Dossier</button> + <button id="export-single-md-btn" class="ghost-btn">📥 Export .md</button> + </div> + ``` +- **Dossier Drawer (`#dossier-drawer`):** + - Slide-out or overlay drawer listing collected items. + - Action buttons: "Download Compiled .md", "Copy Markdown", "Clear Dossier". + +### 3. Markdown Compiler Engine (`dossier-compiler.js` / `.cjs`) +- Pure function `compileDossierToMarkdown(dossierItems, courseTitle)` returning clean GitHub-flavored markdown with structured tables, blockquotes, and citation links. +- Single-page export function `compileSingleAuditToMarkdown(auditItem)`. + +--- + +## Error Handling & Edge Cases +- **Duplicate Prevention:** Adding the same page multiple times updates the existing entry rather than duplicating, or provides a clear visual indication. +- **Empty Dossier:** The export buttons are disabled or prompt the user if the dossier is empty. +- **Sanitization & Safety:** Markdown formatting safely handles code fences, special characters, and long assignment descriptions. + +--- + +## Testing Strategy +- Unit tests for `dossier-compiler.js` testing multi-audit markdown synthesis, header generation, and formatting. +- Unit tests for `storage.js` dossier helper functions (add, remove, clear, persistence). +- DOM & logic tests for Side Panel dossier drawer toggling, item count updates, and export click handlers. diff --git a/extension/background/canvas-crawler.cjs b/extension/background/canvas-crawler.cjs new file mode 100644 index 0000000..23529c1 --- /dev/null +++ b/extension/background/canvas-crawler.cjs @@ -0,0 +1,59 @@ +function stripHtml(html) { + if (!html) return ''; + return html + .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '') + .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '') + .replace(/<[^>]+>/g, ' ') + .replace(/ /gi, ' ') + .replace(/&/gi, '&') + .replace(/</gi, '<') + .replace(/>/gi, '>') + .replace(/"/gi, '"') + .replace(/\s+/g, ' ') + .trim(); +} + +async function crawlCanvasCourse(origin, courseId, fetchImpl = fetch) { + if (!origin || !courseId) { + throw new Error('Canvas origin and courseId are required for course crawling.'); + } + + // 1. Fetch Course details & syllabus + const courseUrl = `${origin}/api/v1/courses/${courseId}?include[]=syllabus_body`; + const courseRes = await fetchImpl(courseUrl, { credentials: 'include' }); + if (!courseRes.ok) { + throw new Error(`Failed to fetch Canvas course info (${courseRes.status})`); + } + const courseJson = await courseRes.json(); + + // 2. Fetch Assignments list (up to 50) + const assignmentsUrl = `${origin}/api/v1/courses/${courseId}/assignments?per_page=50`; + let assignments = []; + try { + const assignRes = await fetchImpl(assignmentsUrl, { credentials: 'include' }); + if (assignRes.ok) { + const assignJson = await assignRes.json(); + if (Array.isArray(assignJson)) { + assignments = assignJson.map((a) => ({ + title: a.name || 'Untitled Assignment', + description: stripHtml(a.description || '').slice(0, 1000), + points: a.points_possible || 0, + dueAt: a.due_at || null + })); + } + } + } catch (e) { + console.warn('Could not fetch assignments list:', e); + } + + return { + title: courseJson.name || courseJson.course_code || 'Canvas Course', + syllabus: stripHtml(courseJson.syllabus_body || ''), + assignments + }; +} + +module.exports = { + stripHtml, + crawlCanvasCourse +}; diff --git a/extension/background/canvas-crawler.js b/extension/background/canvas-crawler.js new file mode 100644 index 0000000..0c48f1a --- /dev/null +++ b/extension/background/canvas-crawler.js @@ -0,0 +1,54 @@ +export function stripHtml(html) { + if (!html) return ''; + return html + .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '') + .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '') + .replace(/<[^>]+>/g, ' ') + .replace(/ /gi, ' ') + .replace(/&/gi, '&') + .replace(/</gi, '<') + .replace(/>/gi, '>') + .replace(/"/gi, '"') + .replace(/\s+/g, ' ') + .trim(); +} + +export async function crawlCanvasCourse(origin, courseId, fetchImpl = fetch) { + if (!origin || !courseId) { + throw new Error('Canvas origin and courseId are required for course crawling.'); + } + + // 1. Fetch Course details & syllabus + const courseUrl = `${origin}/api/v1/courses/${courseId}?include[]=syllabus_body`; + const courseRes = await fetchImpl(courseUrl, { credentials: 'include' }); + if (!courseRes.ok) { + throw new Error(`Failed to fetch Canvas course info (${courseRes.status})`); + } + const courseJson = await courseRes.json(); + + // 2. Fetch Assignments list (up to 50) + const assignmentsUrl = `${origin}/api/v1/courses/${courseId}/assignments?per_page=50`; + let assignments = []; + try { + const assignRes = await fetchImpl(assignmentsUrl, { credentials: 'include' }); + if (assignRes.ok) { + const assignJson = await assignRes.json(); + if (Array.isArray(assignJson)) { + assignments = assignJson.map((a) => ({ + title: a.name || 'Untitled Assignment', + description: stripHtml(a.description || '').slice(0, 1000), + points: a.points_possible || 0, + dueAt: a.due_at || null + })); + } + } + } catch (e) { + console.warn('Could not fetch assignments list:', e); + } + + return { + title: courseJson.name || courseJson.course_code || 'Canvas Course', + syllabus: stripHtml(courseJson.syllabus_body || ''), + assignments + }; +} diff --git a/extension/background/parser-helper.cjs b/extension/background/parser-helper.cjs new file mode 100644 index 0000000..b98b518 --- /dev/null +++ b/extension/background/parser-helper.cjs @@ -0,0 +1,128 @@ +/** + * Helper to clean and parse JSON responses from LLM API. + */ +function cleanJsonResponse(rawText) { + let cleaned = rawText.trim(); + if (cleaned.startsWith('```json')) cleaned = cleaned.replace(/^```json/, ''); + if (cleaned.startsWith('```')) cleaned = cleaned.replace(/^```/, ''); + if (cleaned.endsWith('```')) cleaned = cleaned.replace(/```$/, ''); + return JSON.parse(cleaned.trim()); +} + +function parseAuditResponse(rawText) { + return cleanJsonResponse(rawText); +} + +function getDemoAuditResult(payload = {}) { + const title = (payload && payload.title) ? payload.title : 'Course Material'; + const pageType = (payload && payload.pageType) ? payload.pageType : 'Assignment / Syllabus'; + + return { + summary: { + bloomsLevel: 'Analyze (Level 4)', + alignmentScore: 'Moderate (78%)', + keyTakeaway: `[Demo Mode] Sample audit for "${title}" (${pageType}). Entering an API key in Settings unlocks live audits on any page.` + }, + findings: [ + { + severity: 'warning', + tier: 'T1', + citation: '[Assessment-8] Formative Feedback Matrix', + observation: 'Assessment rubric lacks descriptive performance criteria for intermediate mastery levels.', + evidence: 'Wisniewski et al. (2020) [T1 meta-analysis, d=0.48]: Elaborated feedback and rubric transparency significantly boost student self-regulation and achievement.', + recommendation: 'Add concrete performance descriptors and milestone criteria for each grade band instead of generic labels.' + }, + { + severity: 'critical', + tier: 'T2', + citation: '[Alignment-3] Direct Constructive Alignment', + observation: 'Learning objectives target analytical synthesis, but evaluation instruments only test lower-order recall.', + evidence: 'Biggs (1996) & Liou et al. (2023) [T2 controlled trial]: Constructive misalignment between stated objectives and assessment formats leads to superficial learning strategies.', + recommendation: 'Incorporate authentic problem-solving prompts and case analysis rather than purely multiple-choice recall questions.' + }, + { + severity: 'info', + tier: 'T1', + citation: '[Cognitive-2] Cognitive Load & Chunking', + observation: 'Task instructions present multiple complex requirements in a single unsegmented block.', + evidence: 'Costley et al. (2023) [T1 meta-analysis]: Segmenting complex instructional tasks into structured sequential phases reduces extraneous cognitive load.', + recommendation: 'Format multi-step assignment guidelines into sequenced checklists or distinct milestone stages.' + } + ], + improvedDraft: { + title: 'Improved Rubric Draft (idstack Recommended)', + content: `### Revised Task & Assessment Rubric for: ${title} + +> **Note:** This is a sample evidence-based draft generated in demo mode. Entering an API key in Settings unlocks live audits on any page. + +#### Analytical Task Prompt +Analyze the core case scenario and formulate a structured recommendation addressing: +1. Primary contributing factors identified in the evidence base. +2. Direct trade-offs between proposed intervention strategies. +3. A measurable evaluation plan for validating outcomes. + +#### Evaluation Rubric (Elaborated Criteria) +| Criterion | Exemplary (Proficient) | Developing | Novice | +| :--- | :--- | :--- | :--- | +| **Evidence Application [T1]** | Synthesizes 3+ relevant empirical sources with clear justification. | References 1-2 sources with partial justification. | Makes claims without empirical citations. | +| **Analytical Rigor [T2]** | Rigorously evaluates trade-offs and alternative hypotheses. | Identifies trade-offs but lacks systematic comparison. | Describes facts without evaluating trade-offs. | +| **Actionable Strategy [T1]** | Proposes concrete, measurable milestones with validation metrics. | Proposes general steps without measurable metrics. | Vague or non-actionable suggestions. |` + } + }; +} + +function getDemoCourseAuditResult(payload = {}) { + const title = payload.title || 'Sample Canvas Course'; + return { + summary: { + bloomsLevel: 'Analyze & Evaluate (Levels 4-5)', + alignmentScore: 'Moderate (74%)', + keyTakeaway: `[Course-Level Demo] Full-course audit for "${title}". Alignment gaps identified between week 1-4 recall quizzes and week 12 analytical capstone.` + }, + findings: [ + { + severity: 'critical', + tier: 'T2', + citation: '[Alignment-3] Direct Constructive Alignment', + observation: 'Modules 1-6 assess solely lower-order factual recall, while the final course project demands high-order synthesis without intermediate scaffolding.', + evidence: 'Biggs (1996) & Liou et al. (2023) [T2 controlled trial]: Abrupt jumps in cognitive demand without progressive assessment scaffolding increase failure rates.', + recommendation: 'Introduce mid-semester milestone case studies in Module 4 to bridge the gap between quizzes and the final capstone.' + }, + { + severity: 'warning', + tier: 'T1', + citation: '[Cognitive-2] Cognitive Load & Spaced Practice', + observation: 'Major assignment deadlines are clustered in Week 14-15 with no spaced formative checkpoints.', + evidence: 'Carpenter et al. (2022) [T1 meta-analysis, d=0.61]: Distributing assessments across spaced intervals produces significantly higher long-term retention.', + recommendation: 'Redistribute submission checkpoints into 3 progressive deliverables across weeks 6, 10, and 14.' + }, + { + severity: 'info', + tier: 'T1', + citation: '[Assessment-8] Formative Rubric Transparency', + observation: 'Syllabus grading policy lacks explicit performance criteria for collaborative group deliverables.', + evidence: 'Wisniewski et al. (2020) [T1 meta-analysis]: Pre-distribution of analytic rubrics with milestone criteria boosts student self-regulation and achievement.', + recommendation: 'Publish the multi-tier analytic grading rubric during the initial module launch.' + } + ], + improvedDraft: { + title: 'Course-Wide Constructive Alignment Matrix', + content: `### Course Alignment Matrix: ${title} + +| Week / Module | Intended Learning Outcome | Formative Checkpoint [T1] | Summative Assessment [T2] | +| :--- | :--- | :--- | :--- | +| **Weeks 1-3** | Foundational Cell Structure | Spaced Knowledge Check (10 pts) | Module 1 Synthesis Quiz | +| **Weeks 4-7** | Enzyme Kinetics & Modeling | Case Problem Milestone 1 [T2] | Lab Protocol Analysis | +| **Weeks 8-11** | Experimental Troubleshooting | Peer Review Protocol [T1] | Milestone 2 Experimental Draft | +| **Weeks 12-15**| Autonomous Investigation | Scaffolded Capstone Consult | Final Research Capstone |` + } + }; +} + +module.exports = { + cleanJsonResponse, + parseAuditResponse, + getDemoAuditResult, + getDemoCourseAuditResult +}; + diff --git a/extension/background/parser-helper.js b/extension/background/parser-helper.js new file mode 100644 index 0000000..9890569 --- /dev/null +++ b/extension/background/parser-helper.js @@ -0,0 +1,121 @@ +/** + * Helper to clean and parse JSON responses from LLM API. + */ +export function cleanJsonResponse(rawText) { + let cleaned = rawText.trim(); + if (cleaned.startsWith('```json')) cleaned = cleaned.replace(/^```json/, ''); + if (cleaned.startsWith('```')) cleaned = cleaned.replace(/^```/, ''); + if (cleaned.endsWith('```')) cleaned = cleaned.replace(/```$/, ''); + return JSON.parse(cleaned.trim()); +} + +export function parseAuditResponse(rawText) { + return cleanJsonResponse(rawText); +} + +export function getDemoAuditResult(payload = {}) { + const title = (payload && payload.title) ? payload.title : 'Course Material'; + const pageType = (payload && payload.pageType) ? payload.pageType : 'Assignment / Syllabus'; + + return { + summary: { + bloomsLevel: 'Analyze (Level 4)', + alignmentScore: 'Moderate (78%)', + keyTakeaway: `[Demo Mode] Sample audit for "${title}" (${pageType}). Entering an API key in Settings unlocks live audits on any page.` + }, + findings: [ + { + severity: 'warning', + tier: 'T1', + citation: '[Assessment-8] Formative Feedback Matrix', + observation: 'Assessment rubric lacks descriptive performance criteria for intermediate mastery levels.', + evidence: 'Wisniewski et al. (2020) [T1 meta-analysis, d=0.48]: Elaborated feedback and rubric transparency significantly boost student self-regulation and achievement.', + recommendation: 'Add concrete performance descriptors and milestone criteria for each grade band instead of generic labels.' + }, + { + severity: 'critical', + tier: 'T2', + citation: '[Alignment-3] Direct Constructive Alignment', + observation: 'Learning objectives target analytical synthesis, but evaluation instruments only test lower-order recall.', + evidence: 'Biggs (1996) & Liou et al. (2023) [T2 controlled trial]: Constructive misalignment between stated objectives and assessment formats leads to superficial learning strategies.', + recommendation: 'Incorporate authentic problem-solving prompts and case analysis rather than purely multiple-choice recall questions.' + }, + { + severity: 'info', + tier: 'T1', + citation: '[Cognitive-2] Cognitive Load & Chunking', + observation: 'Task instructions present multiple complex requirements in a single unsegmented block.', + evidence: 'Costley et al. (2023) [T1 meta-analysis]: Segmenting complex instructional tasks into structured sequential phases reduces extraneous cognitive load.', + recommendation: 'Format multi-step assignment guidelines into sequenced checklists or distinct milestone stages.' + } + ], + improvedDraft: { + title: 'Improved Rubric Draft (idstack Recommended)', + content: `### Revised Task & Assessment Rubric for: ${title} + +> **Note:** This is a sample evidence-based draft generated in demo mode. Entering an API key in Settings unlocks live audits on any page. + +#### Analytical Task Prompt +Analyze the core case scenario and formulate a structured recommendation addressing: +1. Primary contributing factors identified in the evidence base. +2. Direct trade-offs between proposed intervention strategies. +3. A measurable evaluation plan for validating outcomes. + +#### Evaluation Rubric (Elaborated Criteria) +| Criterion | Exemplary (Proficient) | Developing | Novice | +| :--- | :--- | :--- | :--- | +| **Evidence Application [T1]** | Synthesizes 3+ relevant empirical sources with clear justification. | References 1-2 sources with partial justification. | Makes claims without empirical citations. | +| **Analytical Rigor [T2]** | Rigorously evaluates trade-offs and alternative hypotheses. | Identifies trade-offs but lacks systematic comparison. | Describes facts without evaluating trade-offs. | +| **Actionable Strategy [T1]** | Proposes concrete, measurable milestones with validation metrics. | Proposes general steps without measurable metrics. | Vague or non-actionable suggestions. |` + } + }; +} + +export function getDemoCourseAuditResult(payload = {}) { + const title = payload.title || 'Sample Canvas Course'; + return { + summary: { + bloomsLevel: 'Analyze & Evaluate (Levels 4-5)', + alignmentScore: 'Moderate (74%)', + keyTakeaway: `[Course-Level Demo] Full-course audit for "${title}". Alignment gaps identified between week 1-4 recall quizzes and week 12 analytical capstone.` + }, + findings: [ + { + severity: 'critical', + tier: 'T2', + citation: '[Alignment-3] Direct Constructive Alignment', + observation: 'Modules 1-6 assess solely lower-order factual recall, while the final course project demands high-order synthesis without intermediate scaffolding.', + evidence: 'Biggs (1996) & Liou et al. (2023) [T2 controlled trial]: Abrupt jumps in cognitive demand without progressive assessment scaffolding increase failure rates.', + recommendation: 'Introduce mid-semester milestone case studies in Module 4 to bridge the gap between quizzes and the final capstone.' + }, + { + severity: 'warning', + tier: 'T1', + citation: '[Cognitive-2] Cognitive Load & Spaced Practice', + observation: 'Major assignment deadlines are clustered in Week 14-15 with no spaced formative checkpoints.', + evidence: 'Carpenter et al. (2022) [T1 meta-analysis, d=0.61]: Distributing assessments across spaced intervals produces significantly higher long-term retention.', + recommendation: 'Redistribute submission checkpoints into 3 progressive deliverables across weeks 6, 10, and 14.' + }, + { + severity: 'info', + tier: 'T1', + citation: '[Assessment-8] Formative Rubric Transparency', + observation: 'Syllabus grading policy lacks explicit performance criteria for collaborative group deliverables.', + evidence: 'Wisniewski et al. (2020) [T1 meta-analysis]: Pre-distribution of analytic rubrics with milestone criteria boosts student self-regulation and achievement.', + recommendation: 'Publish the multi-tier analytic grading rubric during the initial module launch.' + } + ], + improvedDraft: { + title: 'Course-Wide Constructive Alignment Matrix', + content: `### Course Alignment Matrix: ${title} + +| Week / Module | Intended Learning Outcome | Formative Checkpoint [T1] | Summative Assessment [T2] | +| :--- | :--- | :--- | :--- | +| **Weeks 1-3** | Foundational Cell Structure | Spaced Knowledge Check (10 pts) | Module 1 Synthesis Quiz | +| **Weeks 4-7** | Enzyme Kinetics & Modeling | Case Problem Milestone 1 [T2] | Lab Protocol Analysis | +| **Weeks 8-11** | Experimental Troubleshooting | Peer Review Protocol [T1] | Milestone 2 Experimental Draft | +| **Weeks 12-15**| Autonomous Investigation | Scaffolded Capstone Consult | Final Research Capstone |` + } + }; +} + diff --git a/extension/background/service-worker.js b/extension/background/service-worker.js new file mode 100644 index 0000000..ddede63 --- /dev/null +++ b/extension/background/service-worker.js @@ -0,0 +1,103 @@ +import { buildAuditPrompt, buildCourseAuditPrompt } from '../shared/prompts.js'; +import { getSettings, saveAuditResult } from '../shared/storage.js'; +import { cleanJsonResponse, getDemoAuditResult, getDemoCourseAuditResult } from './parser-helper.js'; +import { crawlCanvasCourse } from './canvas-crawler.js'; + +// Setup side panel behavior on action click +if (typeof chrome !== 'undefined' && chrome.sidePanel && chrome.sidePanel.setPanelBehavior) { + chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch((err) => console.error(err)); +} + +async function callLlmApi(apiKey, prompt) { + const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`; // IDSTACK_CLI_LEAK_ALLOW + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + contents: [{ parts: [{ text: prompt }] }], + generationConfig: { + responseMimeType: 'application/json', + temperature: 0.2 + } + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`AI API error (${response.status}): ${errorText}`); + } + + const data = await response.json(); + const rawText = data.candidates?.[0]?.content?.parts?.[0]?.text; + if (!rawText) throw new Error('Empty response from AI model.'); + + return cleanJsonResponse(rawText); +} + +if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.onMessage) { + chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'RUN_AUDIT') { + (async () => { + try { + const settings = await getSettings(); + + let auditResult; + if (settings && settings.apiKey && settings.apiKey.trim()) { + const prompt = buildAuditPrompt(request.payload); + auditResult = await callLlmApi(settings.apiKey.trim(), prompt); + } else { + // Graceful demo fallback when no API key is provided + auditResult = getDemoAuditResult(request.payload); + } + + await saveAuditResult({ + url: request.payload?.url || '', + title: request.payload?.title || 'Current Tab', + pageType: request.payload?.pageType || 'Web Page', + result: auditResult + }); + + sendResponse({ success: true, data: auditResult }); + } catch (err) { + sendResponse({ success: false, error: err.message }); + } + })(); + return true; // async reply + } + + if (request.action === 'CRAWL_AND_AUDIT_COURSE') { + (async () => { + try { + const { origin, courseId } = request.payload || {}; + const courseData = await crawlCanvasCourse(origin, courseId); + const settings = await getSettings(); + + let auditResult; + if (settings && settings.apiKey && settings.apiKey.trim()) { + const prompt = buildCourseAuditPrompt(courseData); + auditResult = await callLlmApi(settings.apiKey.trim(), prompt); + } else { + // Graceful demo fallback for course audit + auditResult = getDemoCourseAuditResult(courseData); + } + + await saveAuditResult({ + url: `${origin}/courses/${courseId}`, + title: courseData.title || 'Canvas Course', + pageType: 'Canvas Course (Full Audit)', + result: auditResult + }); + + sendResponse({ success: true, data: auditResult, courseData }); + } catch (err) { + sendResponse({ success: false, error: err.message }); + } + })(); + return true; // async reply + } + }); +} + +export { cleanJsonResponse, getDemoAuditResult, getDemoCourseAuditResult, crawlCanvasCourse, callLlmApi }; + + diff --git a/extension/content/extractor-core.cjs b/extension/content/extractor-core.cjs new file mode 100644 index 0000000..0256253 --- /dev/null +++ b/extension/content/extractor-core.cjs @@ -0,0 +1,8 @@ +const { detectPageType, extractContentFromDOM, extractPageContent, detectCourseContext } = require('./extractor.js'); + +module.exports = { + detectPageType, + extractContentFromDOM, + extractPageContent, + detectCourseContext +}; diff --git a/extension/content/extractor-core.js b/extension/content/extractor-core.js new file mode 100644 index 0000000..897bfa6 --- /dev/null +++ b/extension/content/extractor-core.js @@ -0,0 +1,62 @@ +export function detectPageType(url, document) { + if (url.includes('instructure.com') || url.includes('/courses/')) { + if (document.querySelector('#assignment_show')) return 'Canvas Assignment'; + if (document.querySelector('#syllabusContainer') || url.includes('/assignments/syllabus')) return 'Canvas Syllabus'; + if (document.querySelector('#modules') || url.includes('/modules')) return 'Canvas Modules'; + if (document.querySelector('#rubrics')) return 'Canvas Rubric'; + return 'Canvas LMS Page'; + } + if (url.includes('docs.google.com/document')) return 'Google Doc Syllabus'; + return 'Web Syllabus / Course Page'; +} + +export function extractContentFromDOM(document, url = (typeof window !== 'undefined' && window.location ? window.location.href : '')) { + const pageType = detectPageType(url, document); + let title = (document && document.title) || 'Course Document'; + let content = ''; + + if (pageType.startsWith('Canvas')) { + const heading = document.querySelector('#assignment_show .title, .page-title, h1'); + if (heading) title = (heading.innerText || heading.textContent || '').trim(); + + const mainBody = document.querySelector('#assignment_show .description.user_content, .show-content.user_content, #syllabusContainer, .module-item-title'); + content = mainBody ? (mainBody.innerText || mainBody.textContent || '').trim() : (document.body ? (document.body.innerText || document.body.textContent || '').trim() : ''); + } else if (pageType === 'Google Doc Syllabus') { + const kixApp = document.querySelector('.kix-appview-editor'); + content = kixApp ? (kixApp.innerText || kixApp.textContent || '').trim() : (document.body ? (document.body.innerText || document.body.textContent || '').trim() : ''); + } else { + // General web page extraction + const article = document.querySelector('main, article, [role="main"]'); + content = article ? (article.innerText || article.textContent || '').trim() : (document.body ? (document.body.innerText || document.body.textContent || '').trim() : ''); + } + + return { + url, + title, + pageType, + content: content.slice(0, 15000), + wordCount: content.split(/\s+/).filter(Boolean).length + }; +} + +export function extractPageContent() { + const url = typeof window !== 'undefined' && window.location ? window.location.href : ''; + return extractContentFromDOM(document, url); +} + +export function detectCourseContext(url, docTitle) { + if (!url) return { isCourseRoot: false, courseId: null, origin: null }; + try { + const parsedUrl = new URL(url); + const match = parsedUrl.pathname.match(/\/courses\/(\d+)(?:\/(?:modules)?)?\/?$/); + const anyCourseMatch = parsedUrl.pathname.match(/\/courses\/(\d+)/); + return { + isCourseRoot: !!match, + courseId: anyCourseMatch ? anyCourseMatch[1] : null, + origin: parsedUrl.origin + }; + } catch (e) { + return { isCourseRoot: false, courseId: null, origin: null }; + } +} + diff --git a/extension/content/extractor.js b/extension/content/extractor.js new file mode 100644 index 0000000..9d72934 --- /dev/null +++ b/extension/content/extractor.js @@ -0,0 +1,84 @@ +/** + * Injected content script to extract course content from Canvas, Google Docs, and web pages. + */ +function detectPageType(url, document) { + if (url.includes('instructure.com') || url.includes('/courses/')) { + if (document.querySelector('#assignment_show')) return 'Canvas Assignment'; + if (document.querySelector('#syllabusContainer') || url.includes('/assignments/syllabus')) return 'Canvas Syllabus'; + if (document.querySelector('#modules') || url.includes('/modules')) return 'Canvas Modules'; + if (document.querySelector('#rubrics')) return 'Canvas Rubric'; + return 'Canvas LMS Page'; + } + if (url.includes('docs.google.com/document')) return 'Google Doc Syllabus'; + return 'Web Syllabus / Course Page'; +} + +function extractContentFromDOM(document, url = (typeof window !== 'undefined' && window.location ? window.location.href : '')) { + const pageType = detectPageType(url, document); + let title = (document && document.title) || 'Course Document'; + let content = ''; + + if (pageType.startsWith('Canvas')) { + const heading = document.querySelector('#assignment_show .title, .page-title, h1'); + if (heading) title = (heading.innerText || heading.textContent || '').trim(); + + const mainBody = document.querySelector('#assignment_show .description.user_content, .show-content.user_content, #syllabusContainer, .module-item-title'); + content = mainBody ? (mainBody.innerText || mainBody.textContent || '').trim() : (document.body ? (document.body.innerText || document.body.textContent || '').trim() : ''); + } else if (pageType === 'Google Doc Syllabus') { + const kixApp = document.querySelector('.kix-appview-editor'); + content = kixApp ? (kixApp.innerText || kixApp.textContent || '').trim() : (document.body ? (document.body.innerText || document.body.textContent || '').trim() : ''); + } else { + // General web page extraction + const article = document.querySelector('main, article, [role="main"]'); + content = article ? (article.innerText || article.textContent || '').trim() : (document.body ? (document.body.innerText || document.body.textContent || '').trim() : ''); + } + + return { + url, + title, + pageType, + content: content.slice(0, 15000), + wordCount: content.split(/\s+/).filter(Boolean).length + }; +} + +function extractPageContent() { + const url = typeof window !== 'undefined' && window.location ? window.location.href : ''; + return extractContentFromDOM(document, url); +} + +function detectCourseContext(url, docTitle) { + if (!url) return { isCourseRoot: false, courseId: null, origin: null }; + try { + const parsedUrl = new URL(url); + const match = parsedUrl.pathname.match(/\/courses\/(\d+)(?:\/(?:modules)?)?\/?$/); + const anyCourseMatch = parsedUrl.pathname.match(/\/courses\/(\d+)/); + return { + isCourseRoot: !!match, + courseId: anyCourseMatch ? anyCourseMatch[1] : null, + origin: parsedUrl.origin + }; + } catch (e) { + return { isCourseRoot: false, courseId: null, origin: null }; + } +} + +// Listen for messages from Side Panel +if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.onMessage) { + chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'EXTRACT_CONTENT') { + const data = extractPageContent(); + sendResponse(data); + } + return true; + }); +} + +if (typeof module !== 'undefined' && module.exports) { + module.exports = { + detectPageType, + extractContentFromDOM, + extractPageContent, + detectCourseContext + }; +} diff --git a/extension/icons/generate-icons.js b/extension/icons/generate-icons.js new file mode 100644 index 0000000..3be9b73 --- /dev/null +++ b/extension/icons/generate-icons.js @@ -0,0 +1,90 @@ +const fs = require('fs'); +const path = require('path'); +const zlib = require('zlib'); + +function crc32(buf) { + let crc = 0xffffffff; + for (let i = 0; i < buf.length; i++) { + crc ^= buf[i]; + for (let j = 0; j < 8; j++) { + crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0); + } + } + return (crc ^ 0xffffffff) >>> 0; +} + +function makeChunk(type, data) { + const len = Buffer.alloc(4); + len.writeUInt32BE(data.length, 0); + const typeBuf = Buffer.from(type, 'ascii'); + const crcBuf = Buffer.alloc(4); + const body = Buffer.concat([typeBuf, data]); + crcBuf.writeUInt32BE(crc32(body), 0); + return Buffer.concat([len, body, crcBuf]); +} + +function createPng(size) { + const width = size; + const height = size; + + const rowSize = 1 + width * 4; + const rawData = Buffer.alloc(height * rowSize); + + for (let y = 0; y < height; y++) { + const rowOffset = y * rowSize; + rawData[rowOffset] = 0; // Filter: None + for (let x = 0; x < width; x++) { + const pxOffset = rowOffset + 1 + x * 4; + const relX = x / width; + const relY = y / height; + + const inBox = relX >= 0.12 && relX <= 0.88 && relY >= 0.12 && relY <= 0.88; + const isStackLine1 = relY >= 0.26 && relY <= 0.38 && relX >= 0.24 && relX <= 0.76; + const isStackLine2 = relY >= 0.44 && relY <= 0.56 && relX >= 0.24 && relX <= 0.76; + const isStackLine3 = relY >= 0.62 && relY <= 0.74 && relX >= 0.24 && relX <= 0.76; + + if (isStackLine1 || isStackLine2 || isStackLine3) { + rawData[pxOffset] = 255; + rawData[pxOffset + 1] = 255; + rawData[pxOffset + 2] = 255; + rawData[pxOffset + 3] = 255; + } else if (inBox) { + rawData[pxOffset] = 79; + rawData[pxOffset + 1] = 70; + rawData[pxOffset + 2] = 229; + rawData[pxOffset + 3] = 255; + } else { + rawData[pxOffset] = 0; + rawData[pxOffset + 1] = 0; + rawData[pxOffset + 2] = 0; + rawData[pxOffset + 3] = 0; + } + } + } + + const compressed = zlib.deflateSync(rawData); + + const header = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + + const ihdrData = Buffer.alloc(13); + ihdrData.writeUInt32BE(width, 0); + ihdrData.writeUInt32BE(height, 4); + ihdrData[8] = 8; + ihdrData[9] = 6; + ihdrData[10] = 0; + ihdrData[11] = 0; + ihdrData[12] = 0; + + const ihdrChunk = makeChunk('IHDR', ihdrData); + const idatChunk = makeChunk('IDAT', compressed); + const iendChunk = makeChunk('IEND', Buffer.alloc(0)); + + return Buffer.concat([header, ihdrChunk, idatChunk, iendChunk]); +} + +const iconsDir = path.join(__dirname); +[16, 48, 128].forEach(size => { + const png = createPng(size); + fs.writeFileSync(path.join(iconsDir, `icon-${size}.png`), png); +}); +console.log('Icons generated successfully.'); diff --git a/extension/icons/icon-128.png b/extension/icons/icon-128.png new file mode 100644 index 0000000..f27c98c Binary files /dev/null and b/extension/icons/icon-128.png differ diff --git a/extension/icons/icon-16.png b/extension/icons/icon-16.png new file mode 100644 index 0000000..bf195d9 Binary files /dev/null and b/extension/icons/icon-16.png differ diff --git a/extension/icons/icon-48.png b/extension/icons/icon-48.png new file mode 100644 index 0000000..c654073 Binary files /dev/null and b/extension/icons/icon-48.png differ diff --git a/extension/manifest.json b/extension/manifest.json new file mode 100644 index 0000000..349cf32 --- /dev/null +++ b/extension/manifest.json @@ -0,0 +1,42 @@ +{ + "manifest_version": 3, + "name": "idstack — Evidence-Based Course Design", + "version": "1.0.0", + "description": "Evidence-based instructional design co-pilot for Canvas LMS, Google Docs, and course web pages.", + "icons": { + "16": "icons/icon-16.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + }, + "action": { + "default_title": "Open idstack Course Co-Pilot" + }, + "side_panel": { + "default_path": "sidepanel/index.html" + }, + "background": { + "service_worker": "background/service-worker.js", + "type": "module" + }, + "content_scripts": [ + { + "matches": [ + "*://*.instructure.com/*", + "*://docs.google.com/document/*", + "<all_urls>" + ], + "js": ["content/extractor.js"], + "run_at": "document_idle" + } + ], + "permissions": [ + "sidePanel", + "storage", + "activeTab", + "scripting" + ], + "host_permissions": [ + "https://generativelanguage.googleapis.com/*", + "*://*.instructure.com/*" + ] +} diff --git a/extension/shared/dossier-compiler.cjs b/extension/shared/dossier-compiler.cjs new file mode 100644 index 0000000..376ca21 --- /dev/null +++ b/extension/shared/dossier-compiler.cjs @@ -0,0 +1,96 @@ +function compileSingleAuditToMarkdown(item) { + if (!item || !item.result) return ''; + const result = item.result; + const title = item.title || 'Course Material'; + const pageType = item.pageType || 'Web Page'; + const url = item.url || ''; + const timestamp = item.timestamp ? new Date(item.timestamp).toLocaleString() : new Date().toLocaleString(); + + let md = `# idstack Instructional Design Audit: ${title}\n\n`; + md += `> **Audited Component:** ${pageType} \n`; + if (url) md += `> **Source URL:** [${url}](${url}) \n`; + md += `> **Date:** ${timestamp} \n`; + md += `> **Evaluator:** idstack Evidence-Based Course Design Engine (Manifest V3)\n\n`; + + md += `## Executive Summary\n\n`; + md += `- **Bloom's Demand:** ${result.summary?.bloomsLevel || 'N/A'}\n`; + md += `- **Constructive Alignment:** ${result.summary?.alignmentScore || 'N/A'}\n`; + md += `- **Key Takeaway:** ${result.summary?.keyTakeaway || 'No summary provided.'}\n\n`; + + if (Array.isArray(result.findings) && result.findings.length > 0) { + md += `## Evidence-Based Findings & Recommendations\n\n`; + result.findings.forEach((f, idx) => { + md += `### ${idx + 1}. [${f.tier || 'T1'}] ${f.citation || 'Citation'}\n\n`; + md += `- **Severity:** \`${(f.severity || 'info').toUpperCase()}\`\n`; + md += `- **Observation:** ${f.observation || ''}\n`; + md += `- **Empirical Evidence:** ${f.evidence || ''}\n`; + md += `- **Actionable Recommendation:** ${f.recommendation || ''}\n\n`; + }); + } + + if (result.improvedDraft && result.improvedDraft.content) { + md += `## ${result.improvedDraft.title || 'Improved Draft & Alignment Matrix'}\n\n`; + md += `${result.improvedDraft.content}\n\n`; + } + + md += `---\n*Generated by [idstack](https://github.com/savvides/idstack) — Evidence-Based Course Design.*`; + return md; +} + +function compileDossierToMarkdown(dossierItems, courseTitle = 'Canvas Course') { + if (!Array.isArray(dossierItems) || dossierItems.length === 0) { + return `# idstack Course Audit Dossier: ${courseTitle}\n\n*No audit materials in dossier.*`; + } + + const timestamp = new Date().toLocaleString(); + let md = `# idstack Course Audit Dossier: ${courseTitle}\n\n`; + md += `> **Course:** ${courseTitle} \n`; + md += `> **Total Audited Materials:** ${dossierItems.length} components \n`; + md += `> **Compiled Date:** ${timestamp} \n`; + md += `> **Engine:** idstack Evidence-Based Instructional Design Co-Pilot\n\n`; + + md += `## Table of Audited Materials\n\n`; + md += `| # | Component Title | Type | Bloom's Demand | Alignment Score |\n`; + md += `| :--- | :--- | :--- | :--- | :--- |\n`; + dossierItems.forEach((item, idx) => { + const s = item.result?.summary || {}; + md += `| ${idx + 1} | ${item.title || 'Untitled'} | ${item.pageType || 'Page'} | ${s.bloomsLevel || 'N/A'} | ${s.alignmentScore || 'N/A'} |\n`; + }); + md += `\n---\n\n`; + + dossierItems.forEach((item, idx) => { + md += `## Section ${idx + 1}: ${item.title || 'Component'}\n\n`; + md += `> **Type:** ${item.pageType || 'Page'} | **Source:** ${item.url || 'N/A'}\n\n`; + + const r = item.result || {}; + md += `### Summary\n`; + md += `- **Cognitive Demand:** ${r.summary?.bloomsLevel || 'N/A'}\n`; + md += `- **Alignment Rating:** ${r.summary?.alignmentScore || 'N/A'}\n`; + md += `- **Takeaway:** ${r.summary?.keyTakeaway || 'N/A'}\n\n`; + + if (Array.isArray(r.findings) && r.findings.length > 0) { + md += `### Findings & Evidence\n\n`; + r.findings.forEach((f, fIdx) => { + md += `#### ${idx + 1}.${fIdx + 1} [${f.tier || 'T1'}] ${f.citation || 'Citation'}\n`; + md += `- **Observation:** ${f.observation || ''}\n`; + md += `- **Evidence:** ${f.evidence || ''}\n`; + md += `- **Recommendation:** ${f.recommendation || ''}\n\n`; + }); + } + + if (r.improvedDraft && r.improvedDraft.content) { + md += `### ${r.improvedDraft.title || 'Revised Draft'}\n\n`; + md += `${r.improvedDraft.content}\n\n`; + } + + md += `---\n\n`; + }); + + md += `*Generated by [idstack](https://github.com/savvides/idstack) — Evidence-Based Course Design.*`; + return md; +} + +module.exports = { + compileSingleAuditToMarkdown, + compileDossierToMarkdown +}; diff --git a/extension/shared/dossier-compiler.js b/extension/shared/dossier-compiler.js new file mode 100644 index 0000000..6ad0cca --- /dev/null +++ b/extension/shared/dossier-compiler.js @@ -0,0 +1,91 @@ +export function compileSingleAuditToMarkdown(item) { + if (!item || !item.result) return ''; + const result = item.result; + const title = item.title || 'Course Material'; + const pageType = item.pageType || 'Web Page'; + const url = item.url || ''; + const timestamp = item.timestamp ? new Date(item.timestamp).toLocaleString() : new Date().toLocaleString(); + + let md = `# idstack Instructional Design Audit: ${title}\n\n`; + md += `> **Audited Component:** ${pageType} \n`; + if (url) md += `> **Source URL:** [${url}](${url}) \n`; + md += `> **Date:** ${timestamp} \n`; + md += `> **Evaluator:** idstack Evidence-Based Course Design Engine (Manifest V3)\n\n`; + + md += `## Executive Summary\n\n`; + md += `- **Bloom's Demand:** ${result.summary?.bloomsLevel || 'N/A'}\n`; + md += `- **Constructive Alignment:** ${result.summary?.alignmentScore || 'N/A'}\n`; + md += `- **Key Takeaway:** ${result.summary?.keyTakeaway || 'No summary provided.'}\n\n`; + + if (Array.isArray(result.findings) && result.findings.length > 0) { + md += `## Evidence-Based Findings & Recommendations\n\n`; + result.findings.forEach((f, idx) => { + md += `### ${idx + 1}. [${f.tier || 'T1'}] ${f.citation || 'Citation'}\n\n`; + md += `- **Severity:** \`${(f.severity || 'info').toUpperCase()}\`\n`; + md += `- **Observation:** ${f.observation || ''}\n`; + md += `- **Empirical Evidence:** ${f.evidence || ''}\n`; + md += `- **Actionable Recommendation:** ${f.recommendation || ''}\n\n`; + }); + } + + if (result.improvedDraft && result.improvedDraft.content) { + md += `## ${result.improvedDraft.title || 'Improved Draft & Alignment Matrix'}\n\n`; + md += `${result.improvedDraft.content}\n\n`; + } + + md += `---\n*Generated by [idstack](https://github.com/savvides/idstack) — Evidence-Based Course Design.*`; + return md; +} + +export function compileDossierToMarkdown(dossierItems, courseTitle = 'Canvas Course') { + if (!Array.isArray(dossierItems) || dossierItems.length === 0) { + return `# idstack Course Audit Dossier: ${courseTitle}\n\n*No audit materials in dossier.*`; + } + + const timestamp = new Date().toLocaleString(); + let md = `# idstack Course Audit Dossier: ${courseTitle}\n\n`; + md += `> **Course:** ${courseTitle} \n`; + md += `> **Total Audited Materials:** ${dossierItems.length} components \n`; + md += `> **Compiled Date:** ${timestamp} \n`; + md += `> **Engine:** idstack Evidence-Based Instructional Design Co-Pilot\n\n`; + + md += `## Table of Audited Materials\n\n`; + md += `| # | Component Title | Type | Bloom's Demand | Alignment Score |\n`; + md += `| :--- | :--- | :--- | :--- | :--- |\n`; + dossierItems.forEach((item, idx) => { + const s = item.result?.summary || {}; + md += `| ${idx + 1} | ${item.title || 'Untitled'} | ${item.pageType || 'Page'} | ${s.bloomsLevel || 'N/A'} | ${s.alignmentScore || 'N/A'} |\n`; + }); + md += `\n---\n\n`; + + dossierItems.forEach((item, idx) => { + md += `## Section ${idx + 1}: ${item.title || 'Component'}\n\n`; + md += `> **Type:** ${item.pageType || 'Page'} | **Source:** ${item.url || 'N/A'}\n\n`; + + const r = item.result || {}; + md += `### Summary\n`; + md += `- **Cognitive Demand:** ${r.summary?.bloomsLevel || 'N/A'}\n`; + md += `- **Alignment Rating:** ${r.summary?.alignmentScore || 'N/A'}\n`; + md += `- **Takeaway:** ${r.summary?.keyTakeaway || 'N/A'}\n\n`; + + if (Array.isArray(r.findings) && r.findings.length > 0) { + md += `### Findings & Evidence\n\n`; + r.findings.forEach((f, fIdx) => { + md += `#### ${idx + 1}.${fIdx + 1} [${f.tier || 'T1'}] ${f.citation || 'Citation'}\n`; + md += `- **Observation:** ${f.observation || ''}\n`; + md += `- **Evidence:** ${f.evidence || ''}\n`; + md += `- **Recommendation:** ${f.recommendation || ''}\n\n`; + }); + } + + if (r.improvedDraft && r.improvedDraft.content) { + md += `### ${r.improvedDraft.title || 'Revised Draft'}\n\n`; + md += `${r.improvedDraft.content}\n\n`; + } + + md += `---\n\n`; + }); + + md += `*Generated by [idstack](https://github.com/savvides/idstack) — Evidence-Based Course Design.*`; + return md; +} diff --git a/extension/shared/evidence-base.cjs b/extension/shared/evidence-base.cjs new file mode 100644 index 0000000..0bb8374 --- /dev/null +++ b/extension/shared/evidence-base.cjs @@ -0,0 +1,26 @@ +const TIER_METADATA = { + T1: { label: 'Meta-analysis', description: 'Systematic reviews / meta-analyses with large effect sizes', color: '#2f7a4a' }, + T2: { label: 'Controlled trial', description: 'Peer-reviewed empirical randomized or quasi-experimental studies', color: '#2864a8' }, + T3: { label: 'Observational', description: 'Correlational, cohort, or longitudinal learning studies', color: '#a87726' }, + T4: { label: 'Case study', description: 'Single-institution or discipline-specific qualitative studies', color: '#b35a1f' }, + T5: { label: 'Expert guidance', description: 'Established instructional design frameworks (QM, OLC, Bloom)', color: '#6b6b6b' } +}; + +const EVIDENCE_DOMAINS = [ + { code: 'Models', name: 'Instructional Design Models & Frameworks', keyStudies: ['Abuhassna et al. (2024) [T3]', 'Kalonde et al. (2025) [T3]', 'Crompton et al. (2023) [T3]'] }, + { code: 'Alignment', name: 'Constructive Alignment & Learning Objectives', keyStudies: ['Biggs (1996) [T5]', 'Anderson & Krathwohl (2001) [T5]', 'Agarwal (2019) [T1]'] }, + { code: 'Needs', name: 'Needs Analysis', keyStudies: ['Markaki et al. (2021) [T3]', 'Garavan et al. (2019) [T3]', 'Alsalamah & Callinan (2021) [T3]'] }, + { code: 'Cognitive', name: 'Cognitive Load Theory & Sequencing', keyStudies: ['Sweller (1994) [T5]', 'Costley et al. (2023) [T1]', 'Paas & van Merrienboer (2020) [T5]', 'Chen et al. (2018) [T1]'] }, + { code: 'Assessment', name: 'Formative Assessment & Feedback', keyStudies: ['Wisniewski et al. (2020) [T1]', 'Hattie & Timperley (2007) [T1]', 'Black & Wiliam (1998) [T1]', 'Double et al. (2019) [T1]'] }, + { code: 'Multimedia', name: 'Multimedia Learning Principles', keyStudies: ['Mayer (2024) [T5]', 'Moreno & Mayer (1999) [T1]', 'Noetel et al. (2021) [T1]'] }, + { code: 'Learner', name: 'Learner Analysis & Differentiation', keyStudies: ['Deunk et al. (2018) [T1]', 'Puzio et al. (2020) [T1]', 'Liou et al. (2023) [T2]'] }, + { code: 'Evaluation', name: 'Evaluation Models', keyStudies: ['Kirkpatrick / Alsalamah & Callinan (2021) [T3]', 'Frye & Hemmer (2012) [T5]', 'Allen et al. (2021) [T3]'] }, + { code: 'Prototype', name: 'Rapid Prototyping & Design-Based Research', keyStudies: ['Tripp & Bichelmeyer (1990) [T5]', 'Shakeel et al. (2022) [T2]', 'Design-Based Research Collective (2003) [T5]'] }, + { code: 'Online', name: 'Online Course Quality Frameworks', keyStudies: ['Quality Matters / Zimmerman et al. (2020) [T4]', 'Castro & Tumibay (2019) [T1]', 'Swan et al. (2012) [T2]'] }, + { code: 'Accessibility', name: 'Universal Design for Learning & A11y', keyStudies: ['CAST UDL Guidelines (2024) [T5]', 'WCAG 2.1/2.2 AA [T5]', 'Puzio et al. (2020) [T1]'] } +]; + +module.exports = { + TIER_METADATA, + EVIDENCE_DOMAINS +}; diff --git a/extension/shared/evidence-base.js b/extension/shared/evidence-base.js new file mode 100644 index 0000000..6c01133 --- /dev/null +++ b/extension/shared/evidence-base.js @@ -0,0 +1,21 @@ +export const TIER_METADATA = { + T1: { label: 'Meta-analysis', description: 'Systematic reviews / meta-analyses with large effect sizes', color: '#2f7a4a' }, + T2: { label: 'Controlled trial', description: 'Peer-reviewed empirical randomized or quasi-experimental studies', color: '#2864a8' }, + T3: { label: 'Observational', description: 'Correlational, cohort, or longitudinal learning studies', color: '#a87726' }, + T4: { label: 'Case study', description: 'Single-institution or discipline-specific qualitative studies', color: '#b35a1f' }, + T5: { label: 'Expert guidance', description: 'Established instructional design frameworks (QM, OLC, Bloom)', color: '#6b6b6b' } +}; + +export const EVIDENCE_DOMAINS = [ + { code: 'Models', name: 'Instructional Design Models & Frameworks', keyStudies: ['Abuhassna et al. (2024) [T3]', 'Kalonde et al. (2025) [T3]', 'Crompton et al. (2023) [T3]'] }, + { code: 'Alignment', name: 'Constructive Alignment & Learning Objectives', keyStudies: ['Biggs (1996) [T5]', 'Anderson & Krathwohl (2001) [T5]', 'Agarwal (2019) [T1]'] }, + { code: 'Needs', name: 'Needs Analysis', keyStudies: ['Markaki et al. (2021) [T3]', 'Garavan et al. (2019) [T3]', 'Alsalamah & Callinan (2021) [T3]'] }, + { code: 'Cognitive', name: 'Cognitive Load Theory & Sequencing', keyStudies: ['Sweller (1994) [T5]', 'Costley et al. (2023) [T1]', 'Paas & van Merrienboer (2020) [T5]', 'Chen et al. (2018) [T1]'] }, + { code: 'Assessment', name: 'Formative Assessment & Feedback', keyStudies: ['Wisniewski et al. (2020) [T1]', 'Hattie & Timperley (2007) [T1]', 'Black & Wiliam (1998) [T1]', 'Double et al. (2019) [T1]'] }, + { code: 'Multimedia', name: 'Multimedia Learning Principles', keyStudies: ['Mayer (2024) [T5]', 'Moreno & Mayer (1999) [T1]', 'Noetel et al. (2021) [T1]'] }, + { code: 'Learner', name: 'Learner Analysis & Differentiation', keyStudies: ['Deunk et al. (2018) [T1]', 'Puzio et al. (2020) [T1]', 'Liou et al. (2023) [T2]'] }, + { code: 'Evaluation', name: 'Evaluation Models', keyStudies: ['Kirkpatrick / Alsalamah & Callinan (2021) [T3]', 'Frye & Hemmer (2012) [T5]', 'Allen et al. (2021) [T3]'] }, + { code: 'Prototype', name: 'Rapid Prototyping & Design-Based Research', keyStudies: ['Tripp & Bichelmeyer (1990) [T5]', 'Shakeel et al. (2022) [T2]', 'Design-Based Research Collective (2003) [T5]'] }, + { code: 'Online', name: 'Online Course Quality Frameworks', keyStudies: ['Quality Matters / Zimmerman et al. (2020) [T4]', 'Castro & Tumibay (2019) [T1]', 'Swan et al. (2012) [T2]'] }, + { code: 'Accessibility', name: 'Universal Design for Learning & A11y', keyStudies: ['CAST UDL Guidelines (2024) [T5]', 'WCAG 2.1/2.2 AA [T5]', 'Puzio et al. (2020) [T1]'] } +]; diff --git a/extension/shared/prompts.cjs b/extension/shared/prompts.cjs new file mode 100644 index 0000000..3192726 --- /dev/null +++ b/extension/shared/prompts.cjs @@ -0,0 +1,93 @@ +const { EVIDENCE_DOMAINS, TIER_METADATA } = require('./evidence-base.cjs'); + +function buildAuditPrompt({ title, pageType, content = '' }) { + return `You are idstack, an evidence-based instructional design co-pilot. +Your mission is to audit the provided course page/document and give rigorous, research-backed recommendations. + +Target Document Title: "${title || 'Untitled Course Page'}" +Detected Document Type: ${pageType || 'Course Content'} + +Document Content: +""" +${(content || '').slice(0, 10000)} +""" + +Please audit this material against peer-reviewed instructional design evidence: +1. Classify learning objectives or implied cognitive depth using Bloom's Revised Taxonomy (Remember, Understand, Apply, Analyze, Evaluate, Create). +2. Check Constructive Alignment: Do activities and assessments match the stated or necessary cognitive depth? +3. Flag Cognitive Load, Elaborated Feedback gaps, and Accessibility considerations. +4. Rate each recommendation with an evidence tier [T1] to [T5]. +5. Provide a ready-to-use, improved version (rewritten rubric, upgraded learning outcome verbs, or enhanced prompt). + +You MUST respond strictly with valid JSON conforming to this schema: +{ + "summary": { + "bloomsLevel": "Remember | Understand | Apply | Analyze | Evaluate | Create", + "alignmentScore": "Strong | Moderate | Weak", + "keyTakeaway": "1-2 sentence executive summary of findings" + }, + "findings": [ + { + "severity": "critical | warning | suggestion", + "tier": "T1 | T2 | T3 | T4 | T5", + "citation": "[Domain-Code] Short Citation", + "observation": "What is present in the current material", + "evidence": "What peer-reviewed research indicates", + "recommendation": "Specific actionable suggestion" + } + ], + "improvedDraft": { + "title": "Improved Rubric / Learning Objective / Assignment Prompt", + "content": "Full markdown text ready for the instructor to copy-paste into Canvas" + } +}`; +} + +function buildCourseAuditPrompt(courseData = {}) { + const assignmentsSummary = (courseData.assignments || []) + .map((a, i) => `Assignment ${i+1}: ${a.title} (${a.points || 0} pts)\nDescription: ${(a.description || '').slice(0, 500)}`) + .join('\n\n'); + + return `You are an expert instructional designer and cognitive scientist using the idstack evidence base. +Perform a full-course Constructive Alignment audit (courseAudit) for the following course: + +COURSE TITLE: ${courseData.title || 'Canvas Course'} +SYLLABUS & LEARNING OBJECTIVES: +${(courseData.syllabus || 'No syllabus provided').slice(0, 8000)} + +COURSE ASSIGNMENTS & ASSESSMENTS (${(courseData.assignments || []).length} items): +${assignmentsSummary.slice(0, 20000)} + +Evaluate whether the assessment system constructively aligns with stated learning outcomes (Biggs 1996 [T2], Liou et al. 2023 [T2]). +Identify cognitive load bottlenecks (Sweller 2011 [T1]), scaffolding gaps (Wood et al. 1976 [T2]), and formative feedback quality (Wisniewski et al. 2020 [T1]). + +Respond with ONLY a valid JSON object matching this schema: +{ + "summary": { + "bloomsLevel": "Overall Cognitive Demand (e.g. Apply / Analyze)", + "alignmentScore": "High (90%) | Moderate (70%) | Low (40%)", + "keyTakeaway": "1-2 sentence executive summary of course-wide curriculum alignment." + }, + "findings": [ + { + "severity": "critical" | "warning" | "info", + "tier": "T1" | "T2" | "T3" | "T4" | "T5", + "citation": "[Domain-ID] Citation Name", + "observation": "What was identified across the course syllabus and assignments.", + "evidence": "Author (Year) [Tier description]: Empirical finding.", + "recommendation": "Concrete actionable curriculum fix." + } + ], + "improvedDraft": { + "title": "Course Alignment & Scaffolding Matrix", + "content": "Markdown formatted course roadmap and revised assessment scaffolding." + } +}`; +} + +module.exports = { + EVIDENCE_DOMAINS, + TIER_METADATA, + buildAuditPrompt, + buildCourseAuditPrompt +}; diff --git a/extension/shared/prompts.js b/extension/shared/prompts.js new file mode 100644 index 0000000..9822f9f --- /dev/null +++ b/extension/shared/prompts.js @@ -0,0 +1,88 @@ +import { EVIDENCE_DOMAINS, TIER_METADATA } from './evidence-base.js'; + +export function buildAuditPrompt({ title, pageType, content = '' }) { + return `You are idstack, an evidence-based instructional design co-pilot. +Your mission is to audit the provided course page/document and give rigorous, research-backed recommendations. + +Target Document Title: "${title || 'Untitled Course Page'}" +Detected Document Type: ${pageType || 'Course Content'} + +Document Content: +""" +${(content || '').slice(0, 10000)} +""" + +Please audit this material against peer-reviewed instructional design evidence: +1. Classify learning objectives or implied cognitive depth using Bloom's Revised Taxonomy (Remember, Understand, Apply, Analyze, Evaluate, Create). +2. Check Constructive Alignment: Do activities and assessments match the stated or necessary cognitive depth? +3. Flag Cognitive Load, Elaborated Feedback gaps, and Accessibility considerations. +4. Rate each recommendation with an evidence tier [T1] to [T5]. +5. Provide a ready-to-use, improved version (rewritten rubric, upgraded learning outcome verbs, or enhanced prompt). + +You MUST respond strictly with valid JSON conforming to this schema: +{ + "summary": { + "bloomsLevel": "Remember | Understand | Apply | Analyze | Evaluate | Create", + "alignmentScore": "Strong | Moderate | Weak", + "keyTakeaway": "1-2 sentence executive summary of findings" + }, + "findings": [ + { + "severity": "critical | warning | suggestion", + "tier": "T1 | T2 | T3 | T4 | T5", + "citation": "[Domain-Code] Short Citation", + "observation": "What is present in the current material", + "evidence": "What peer-reviewed research indicates", + "recommendation": "Specific actionable suggestion" + } + ], + "improvedDraft": { + "title": "Improved Rubric / Learning Objective / Assignment Prompt", + "content": "Full markdown text ready for the instructor to copy-paste into Canvas" + } +}`; +} + +export function buildCourseAuditPrompt(courseData = {}) { + const assignmentsSummary = (courseData.assignments || []) + .map((a, i) => `Assignment ${i+1}: ${a.title} (${a.points || 0} pts)\nDescription: ${(a.description || '').slice(0, 500)}`) + .join('\n\n'); + + return `You are an expert instructional designer and cognitive scientist using the idstack evidence base. +Perform a full-course Constructive Alignment audit (courseAudit) for the following course: + +COURSE TITLE: ${courseData.title || 'Canvas Course'} +SYLLABUS & LEARNING OBJECTIVES: +${(courseData.syllabus || 'No syllabus provided').slice(0, 8000)} + +COURSE ASSIGNMENTS & ASSESSMENTS (${(courseData.assignments || []).length} items): +${assignmentsSummary.slice(0, 20000)} + +Evaluate whether the assessment system constructively aligns with stated learning outcomes (Biggs 1996 [T2], Liou et al. 2023 [T2]). +Identify cognitive load bottlenecks (Sweller 2011 [T1]), scaffolding gaps (Wood et al. 1976 [T2]), and formative feedback quality (Wisniewski et al. 2020 [T1]). + +Respond with ONLY a valid JSON object matching this schema: +{ + "summary": { + "bloomsLevel": "Overall Cognitive Demand (e.g. Apply / Analyze)", + "alignmentScore": "High (90%) | Moderate (70%) | Low (40%)", + "keyTakeaway": "1-2 sentence executive summary of course-wide curriculum alignment." + }, + "findings": [ + { + "severity": "critical" | "warning" | "info", + "tier": "T1" | "T2" | "T3" | "T4" | "T5", + "citation": "[Domain-ID] Citation Name", + "observation": "What was identified across the course syllabus and assignments.", + "evidence": "Author (Year) [Tier description]: Empirical finding.", + "recommendation": "Concrete actionable curriculum fix." + } + ], + "improvedDraft": { + "title": "Course Alignment & Scaffolding Matrix", + "content": "Markdown formatted course roadmap and revised assessment scaffolding." + } +}`; +} + +export { EVIDENCE_DOMAINS, TIER_METADATA }; diff --git a/extension/shared/storage.js b/extension/shared/storage.js new file mode 100644 index 0000000..4ee794c --- /dev/null +++ b/extension/shared/storage.js @@ -0,0 +1,86 @@ +/** + * idstack storage helper for Chrome sync and local storage. + */ +export async function getSettings() { + return new Promise((resolve) => { + chrome.storage.sync.get(['apiKey', 'apiEndpoint', 'autoAudit'], (result) => { + resolve({ + apiKey: result.apiKey || '', + apiEndpoint: result.apiEndpoint || 'https://api.idstack.org/v1/audit', + autoAudit: result.autoAudit ?? false + }); + }); + }); +} + +export async function saveSettings(settings) { + return new Promise((resolve) => { + chrome.storage.sync.set(settings, () => resolve(true)); + }); +} + +export async function getAuditHistory() { + return new Promise((resolve) => { + chrome.storage.local.get(['auditHistory'], (result) => { + resolve(result.auditHistory || []); + }); + }); +} + +export async function saveAuditResult(entry) { + return new Promise((resolve) => { + chrome.storage.local.get(['auditHistory'], (result) => { + const history = result.auditHistory || []; + history.unshift({ + ...entry, + timestamp: new Date().toISOString() + }); + // Keep last 20 audits + chrome.storage.local.set({ auditHistory: history.slice(0, 20) }, () => resolve(true)); + }); + }); +} + +export async function getDossier() { + return new Promise((resolve) => { + chrome.storage.local.get(['activeDossier'], (result) => { + resolve(result.activeDossier || []); + }); + }); +} + +export async function addToDossier(item) { + return new Promise((resolve) => { + chrome.storage.local.get(['activeDossier'], (result) => { + let dossier = result.activeDossier || []; + const id = item.id || (item.url ? item.url : `dossier-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`); + const newItem = { + ...item, + id, + timestamp: item.timestamp || new Date().toISOString() + }; + const existingIdx = dossier.findIndex((d) => (item.id && d.id === item.id) || (item.url && d.url && d.url === item.url)); + if (existingIdx >= 0) { + dossier[existingIdx] = newItem; + } else { + dossier.push(newItem); + } + chrome.storage.local.set({ activeDossier: dossier }, () => resolve(dossier)); + }); + }); +} + +export async function removeFromDossier(id) { + return new Promise((resolve) => { + chrome.storage.local.get(['activeDossier'], (result) => { + const dossier = (result.activeDossier || []).filter((d) => d.id !== id); + chrome.storage.local.set({ activeDossier: dossier }, () => resolve(dossier)); + }); + }); +} + +export async function clearDossier() { + return new Promise((resolve) => { + chrome.storage.local.set({ activeDossier: [] }, () => resolve(true)); + }); +} diff --git a/extension/sidepanel/index.html b/extension/sidepanel/index.html new file mode 100644 index 0000000..989d843 --- /dev/null +++ b/extension/sidepanel/index.html @@ -0,0 +1,116 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>idstack — Course Co-Pilot + + + + + + +
+
+ + v1.0 +
+
+ + +
+
+ +
+ +
+
+
+ Detecting page... +

Loading page title...

+
+

Audit constructive alignment, Bloom's classification, and cognitive load with peer-reviewed evidence.

+
+ + +
+
+
+ + +
+
+
+

Analyzing course structure...

+ +
+
+ + +
+
+ + +
+
+
+ + +
+
+

Course Audit Dossier

+ +
+
+

Collected audits for compiled course-level markdown export.

+
+
+ +
+ + +
+
+
+
+ + +
+
+

Settings & Privacy

+ +
+
+
+ + + Get your free Google AI Studio key → +

Leave blank to use the built-in free demo tier.

+
+ +
+
+

Privacy & FERPA Commitment

+

idstack processes only curriculum and assignment text. No student PII is ever collected, retained, or used for model training.

+
+
+
+
+ + + + + diff --git a/extension/sidepanel/renderer-helper.cjs b/extension/sidepanel/renderer-helper.cjs new file mode 100644 index 0000000..d2b4438 --- /dev/null +++ b/extension/sidepanel/renderer-helper.cjs @@ -0,0 +1,116 @@ +/** + * idstack Side Panel HTML Renderer Helper (CommonJS) + */ + +function escapeHtml(str) { + if (str === null || str === undefined) return ''; + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function renderAuditHTML(data) { + if (!data) return ''; + const summary = data.summary || { bloomsLevel: 'N/A', alignmentScore: 'N/A', keyTakeaway: '' }; + const findings = data.findings || []; + const improvedDraft = data.improvedDraft || { title: 'Improved Draft', content: '' }; + + const findingsHTML = findings.map(f => { + const tier = escapeHtml(f.tier || 'T1'); + const tierClass = escapeHtml((f.tier || 'T1').toLowerCase()); + const severity = escapeHtml(f.severity || 'info'); + const citation = escapeHtml(f.citation || ''); + const observation = escapeHtml(f.observation || ''); + const evidence = escapeHtml(f.evidence || ''); + const recommendation = escapeHtml(f.recommendation || ''); + + return ` +
+
+ ${tier} + ${citation} +
+

Observation: ${observation}

+

Evidence: ${evidence}

+

Recommendation: ${recommendation}

+
+ `; + }).join(''); + + const bloomsLevel = escapeHtml(summary.bloomsLevel || 'N/A'); + const alignmentScore = escapeHtml(summary.alignmentScore || 'N/A'); + const keyTakeaway = escapeHtml(summary.keyTakeaway || ''); + const draftTitle = escapeHtml(improvedDraft.title || 'Improved Draft'); + const draftContent = escapeHtml(improvedDraft.content || ''); + + return ` +
+
+ Bloom's: ${bloomsLevel} + Alignment: ${alignmentScore} +
+

${keyTakeaway}

+
+ +

Evidence-Based Findings (${findings.length})

+
${findingsHTML}
+ +
+
+

${draftTitle}

+ +
+
${draftContent}
+
+ + + `; +} + +function renderDossierListHTML(dossierItems) { + if (!Array.isArray(dossierItems) || dossierItems.length === 0) { + return ` +
+

No items in dossier yet.

+

Audit pages and click “Add to Dossier” to build your course dossier.

+
+ `; + } + + return dossierItems.map((item) => { + const id = escapeHtml(item.id || ''); + const title = escapeHtml(item.title || 'Untitled Material'); + const pageType = escapeHtml(item.pageType || 'Page'); + const blooms = escapeHtml(item.result?.summary?.bloomsLevel || 'N/A'); + const alignment = escapeHtml(item.result?.summary?.alignmentScore || 'N/A'); + + return ` +
+
+ ${pageType} + +
+

${title}

+
+ Bloom's: ${blooms} + Alignment: ${alignment} +
+
+ `; + }).join(''); +} + +module.exports = { + escapeHtml, + renderAuditHTML, + renderDossierListHTML +}; + diff --git a/extension/sidepanel/renderer-helper.js b/extension/sidepanel/renderer-helper.js new file mode 100644 index 0000000..0c3a85d --- /dev/null +++ b/extension/sidepanel/renderer-helper.js @@ -0,0 +1,110 @@ +/** + * idstack Side Panel HTML Renderer Helper (ES Module) + */ + +export function escapeHtml(str) { + if (str === null || str === undefined) return ''; + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +export function renderAuditHTML(data) { + if (!data) return ''; + const summary = data.summary || { bloomsLevel: 'N/A', alignmentScore: 'N/A', keyTakeaway: '' }; + const findings = data.findings || []; + const improvedDraft = data.improvedDraft || { title: 'Improved Draft', content: '' }; + + const findingsHTML = findings.map(f => { + const tier = escapeHtml(f.tier || 'T1'); + const tierClass = escapeHtml((f.tier || 'T1').toLowerCase()); + const severity = escapeHtml(f.severity || 'info'); + const citation = escapeHtml(f.citation || ''); + const observation = escapeHtml(f.observation || ''); + const evidence = escapeHtml(f.evidence || ''); + const recommendation = escapeHtml(f.recommendation || ''); + + return ` +
+
+ ${tier} + ${citation} +
+

Observation: ${observation}

+

Evidence: ${evidence}

+

Recommendation: ${recommendation}

+
+ `; + }).join(''); + + const bloomsLevel = escapeHtml(summary.bloomsLevel || 'N/A'); + const alignmentScore = escapeHtml(summary.alignmentScore || 'N/A'); + const keyTakeaway = escapeHtml(summary.keyTakeaway || ''); + const draftTitle = escapeHtml(improvedDraft.title || 'Improved Draft'); + const draftContent = escapeHtml(improvedDraft.content || ''); + + return ` +
+
+ Bloom's: ${bloomsLevel} + Alignment: ${alignmentScore} +
+

${keyTakeaway}

+
+ +

Evidence-Based Findings (${findings.length})

+
${findingsHTML}
+ +
+
+

${draftTitle}

+ +
+
${draftContent}
+
+ + + `; +} + +export function renderDossierListHTML(dossierItems) { + if (!Array.isArray(dossierItems) || dossierItems.length === 0) { + return ` +
+

No items in dossier yet.

+

Audit pages and click “Add to Dossier” to build your course dossier.

+
+ `; + } + + return dossierItems.map((item) => { + const id = escapeHtml(item.id || ''); + const title = escapeHtml(item.title || 'Untitled Material'); + const pageType = escapeHtml(item.pageType || 'Page'); + const blooms = escapeHtml(item.result?.summary?.bloomsLevel || 'N/A'); + const alignment = escapeHtml(item.result?.summary?.alignmentScore || 'N/A'); + + return ` +
+
+ ${pageType} + +
+

${title}

+
+ Bloom's: ${blooms} + Alignment: ${alignment} +
+
+ `; + }).join(''); +} + diff --git a/extension/sidepanel/sidepanel.css b/extension/sidepanel/sidepanel.css new file mode 100644 index 0000000..b826e2b --- /dev/null +++ b/extension/sidepanel/sidepanel.css @@ -0,0 +1,771 @@ +:root { + /* Ivory publication theme from DESIGN.md */ + --bg: #faf8f3; + --raised: #ffffff; + --ink: #1a1815; + --ink-soft: #3a352e; + --ink-muted: #6b6358; + --rule: #e6e0d2; + --rule-strong: #d4cdb9; + + /* Annotation accents */ + --accent: #7a1f1f; + --accent-blue: #1d4a5e; + --accent-soft: #f4eae8; + + /* Canonical evidence-tier palette from DESIGN.md */ + --tier-t1: #2f7a4a; + --tier-t2: #2864a8; + --tier-t3: #a87726; + --tier-t4: #b35a1f; + --tier-t5: #6b6b6b; + + --tier-1: #2f7a4a; + --tier-2: #2864a8; + --tier-3: #a87726; + --tier-4: #b35a1f; + --tier-5: #6b6b6b; + + /* Severity palette */ + --severity-critical: #8c2515; + --severity-warning: #7a4f0c; + --severity-suggestion: #2f7a4a; + --severity-info: #344566; + + --sev-critical-bg: #f5dcd5; + --sev-warning-bg: #fbf0d9; + --sev-suggestion-bg: #eef6f0; + --sev-info-bg: #e7eaf1; + + /* Typography */ + --font-display: 'Source Serif 4', 'Source Serif Pro', 'Charter', Georgia, serif; + --font-body: 'Source Serif 4', 'Source Serif Pro', 'Charter', Georgia, serif; + --font-ui: 'Public Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + --font-mono: 'JetBrains Mono', 'SF Mono', Consolas, monospace; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #16140f; + --raised: #1f1d18; + --ink: #ebe7dd; + --ink-soft: #cfc9bc; + --ink-muted: #968f80; + --rule: #2c2a23; + --rule-strong: #3d3a30; + --accent: #d97461; + --accent-blue: #6aa6e3; + --accent-soft: #2a1d1a; + + --tier-t1: #5fc185; + --tier-t2: #6aa6e3; + --tier-t3: #d6a64c; + --tier-t4: #e58f53; + --tier-t5: #a3a3a3; + + --tier-1: #5fc185; + --tier-2: #6aa6e3; + --tier-3: #d6a64c; + --tier-4: #e58f53; + --tier-5: #a3a3a3; + + --severity-critical: #e89786; + --severity-warning: #e3b25c; + --severity-suggestion: #5fc185; + --severity-info: #9bafd2; + + --sev-critical-bg: #2e1612; + --sev-warning-bg: #2e2210; + --sev-suggestion-bg: #14281b; + --sev-info-bg: #1a2030; + } +} + +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background-color: var(--bg); + color: var(--ink); + font-family: var(--font-body); + font-size: 14px; + line-height: 1.6; + -webkit-font-smoothing: antialiased; +} + +/* Header & Brand */ +.app-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid var(--rule); + background: var(--raised); + position: sticky; + top: 0; + z-index: 10; +} + +.brand { + display: flex; + align-items: baseline; + gap: 6px; +} + +.brand .logo { + font-family: var(--font-display); + font-weight: 600; + font-size: 18px; + letter-spacing: -0.02em; + color: var(--ink); +} + +.badge-version { + font-family: var(--font-mono); + font-size: 11px; + color: var(--ink-muted); +} + +.header-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.dossier-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + background: var(--bg); + border: 1px solid var(--rule-strong); + border-radius: 4px; + font-family: var(--font-ui); + font-size: 12px; + font-weight: 500; + color: var(--ink); + cursor: pointer; + transition: all 0.15s ease; +} + +.dossier-pill:hover { + background: var(--raised); + border-color: var(--ink-muted); +} + +.dossier-count-badge { + display: inline-block; + font-family: var(--font-mono); + font-size: 11px; + font-weight: 600; + padding: 0 6px; + background: var(--ink); + color: var(--raised); + border-radius: 10px; + min-width: 18px; + text-align: center; + line-height: 1.5; +} + +.icon-btn { + background: transparent; + border: 1px solid transparent; + border-radius: 4px; + font-size: 16px; + cursor: pointer; + padding: 4px 8px; + color: var(--ink-soft); + transition: all 0.15s ease; +} + +.icon-btn:hover { + background: var(--bg); + border-color: var(--rule); + color: var(--ink); +} + +/* Main Content & State Panels */ +.content-body { + padding: 0; + position: relative; +} + +.state-panel { + display: none; + padding: 16px; +} + +.state-panel.active { + display: block; +} + +/* Cards & Surfaces */ +.context-card, .finding-card, .improved-box, .loader-box { + background: var(--raised); + border: 1px solid var(--rule); + border-radius: 4px; + padding: 16px; + margin-bottom: 16px; +} + +.page-meta { + margin-bottom: 12px; +} + +.page-heading { + font-family: var(--font-display); + font-size: 18px; + font-weight: 600; + line-height: 1.3; + margin-top: 6px; + color: var(--ink); +} + +.summary-hint { + font-size: 13px; + color: var(--ink-soft); + margin-bottom: 14px; + line-height: 1.5; +} + +/* Chips & Badges */ +.chip { + display: inline-block; + font-family: var(--font-mono); + font-size: 11px; + background: #ede8dc; + color: var(--ink-soft); + padding: 2px 8px; + border-radius: 3px; +} + +@media (prefers-color-scheme: dark) { + .chip { + background: #2c2a23; + color: var(--ink-soft); + } +} + +.tier-badge { + display: inline-block; + font-family: var(--font-mono); + font-size: 11px; + font-weight: 600; + padding: 2px 7px; + border-radius: 3px; + color: #ffffff; + letter-spacing: 0.02em; +} + +.tier-t1, .tier-1 { background: var(--tier-t1); } +.tier-t2, .tier-2 { background: var(--tier-t2); } +.tier-t3, .tier-3 { background: var(--tier-t3); } +.tier-t4, .tier-4 { background: var(--tier-t4); } +.tier-t5, .tier-5 { background: var(--tier-t5); } + +.citation { + font-family: var(--font-mono); + font-size: 12px; + color: var(--accent-blue); +} + +/* Severity Finding Cards */ +.finding-card { + border-left: 4px solid var(--rule-strong); +} + +.finding-card.severity-critical { + border-left-color: var(--severity-critical); +} + +.finding-card.severity-warning { + border-left-color: var(--severity-warning); +} + +.finding-card.severity-suggestion { + border-left-color: var(--severity-suggestion); +} + +.finding-card.severity-info { + border-left-color: var(--severity-info); +} + +.finding-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 10px; +} + +.finding-obs, .finding-evi, .finding-rec { + font-size: 13px; + margin-bottom: 8px; + line-height: 1.5; +} + +.finding-rec { + margin-bottom: 0; +} + +/* Inline Error Card */ +.error-card { + border-left: 4px solid var(--severity-critical); + background: var(--raised); +} + +.error-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + +.error-icon { + font-size: 16px; +} + +.error-header h4 { + font-family: var(--font-display); + font-size: 15px; + font-weight: 600; + color: var(--severity-critical); +} + +.error-msg { + font-size: 13px; + color: var(--ink-soft); + margin-bottom: 16px; + line-height: 1.5; +} + +.error-actions { + display: flex; + gap: 8px; +} + +.error-actions button { + flex: 1; +} + +/* Buttons */ + +.primary-btn { + width: 100%; + padding: 10px 16px; + background: var(--ink); + color: var(--raised); + font-family: var(--font-ui); + font-size: 13px; + font-weight: 600; + border: none; + border-radius: 4px; + cursor: pointer; + transition: opacity 0.15s ease, background 0.15s ease; +} + +.primary-btn:hover { + background: var(--ink-soft); +} + +.secondary-btn { + padding: 6px 12px; + background: var(--raised); + color: var(--ink); + font-family: var(--font-ui); + font-size: 12px; + font-weight: 500; + border: 1px solid var(--rule-strong); + border-radius: 4px; + cursor: pointer; + transition: all 0.15s ease; +} + +.secondary-btn:hover { + background: var(--bg); + border-color: var(--ink-muted); +} + +.link-btn { + background: none; + border: none; + color: var(--accent-blue); + font-family: var(--font-ui); + font-size: 12px; + cursor: pointer; + text-decoration: underline; + padding: 4px 6px; +} + +.link-btn:hover { + color: var(--ink); +} + +.feedback-btn { + background: var(--raised); + border: 1px solid var(--rule); + border-radius: 4px; + padding: 4px 8px; + cursor: pointer; + font-size: 13px; + transition: background 0.15s ease; +} + +.feedback-btn:hover { + background: var(--bg); +} + +.action-buttons { + display: flex; + flex-direction: column; + gap: 8px; +} + +.action-buttons button { + width: 100%; +} + +/* Loader Box & Spinner */ +.loader-box { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px 16px; + text-align: center; +} + +.spinner { + width: 28px; + height: 28px; + border: 3px solid var(--rule); + border-top-color: var(--ink); + border-radius: 50%; + animation: spin 0.8s linear infinite; + margin-bottom: 16px; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.loader-status-text { + font-family: var(--font-ui); + font-size: 13px; + color: var(--ink-soft); +} + +/* Progress Card & Animated Indicator */ +.progress-card { + width: 100%; + margin-top: 16px; + padding: 12px; + background: var(--bg); + border: 1px solid var(--rule); + border-radius: 4px; + text-align: left; +} + +.progress-bar-container { + width: 100%; + height: 6px; + background: var(--rule-strong); + border-radius: 3px; + overflow: hidden; + margin-bottom: 8px; +} + +.progress-bar-fill { + height: 100%; + width: 0%; + background: var(--accent-blue); + border-radius: 3px; + transition: width 0.3s cubic-bezier(0.2, 0, 0, 1); +} + +.progress-status-text { + font-family: var(--font-mono); + font-size: 11px; + color: var(--ink-soft); +} + + +/* Results State Structure */ +.summary-card .score-row { + display: flex; + gap: 8px; + margin-bottom: 10px; +} + +.key-takeaway { + font-size: 14px; + line-height: 1.5; + color: var(--ink); +} + +.section-title { + font-family: var(--font-display); + font-size: 16px; + font-weight: 600; + margin: 18px 0 12px; + padding-bottom: 4px; + border-bottom: 1px solid var(--rule); +} + +.improved-box { + margin-top: 16px; +} + +.improved-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.improved-header h4 { + font-family: var(--font-display); + font-size: 14px; + font-weight: 600; +} + +.improved-content { + background: var(--bg); + border: 1px solid var(--rule); + border-radius: 4px; + padding: 12px; + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.5; + overflow-x: auto; + white-space: pre-wrap; + word-break: break-word; +} + +.feedback-row { + display: flex; + align-items: center; + gap: 8px; + margin-top: 18px; + padding-top: 12px; + border-top: 1px solid var(--rule); + font-family: var(--font-ui); + font-size: 12px; + color: var(--ink-soft); +} + +.result-actions-bar { + display: flex; + gap: 8px; + margin-bottom: 16px; +} + +.result-actions-bar button { + flex: 1; +} + +/* Settings Drawer */ +.drawer { + display: none; + background: var(--raised); + border-top: 1px solid var(--rule); + padding: 16px; +} + +.drawer.open { + display: block; +} + +.drawer-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 1px solid var(--rule); +} + +.drawer-header h3 { + font-family: var(--font-display); + font-size: 16px; + font-weight: 600; +} + +.form-group { + margin-bottom: 14px; +} + +.form-group label { + display: block; + font-family: var(--font-ui); + font-size: 12px; + font-weight: 600; + margin-bottom: 6px; + color: var(--ink); +} + +.form-group input { + width: 100%; + padding: 8px 10px; + font-family: var(--font-mono); + font-size: 12px; + border: 1px solid var(--rule-strong); + border-radius: 4px; + background: var(--bg); + color: var(--ink); +} + +.form-group input:focus { + outline: 2px solid var(--accent-blue); + outline-offset: -1px; +} + +.help-text { + font-size: 11px; + color: var(--ink-muted); + margin-top: 4px; +} + +.help-link { + display: inline-block; + font-family: var(--font-ui); + font-size: 12px; + color: var(--accent-blue); + text-decoration: none; + margin-top: 6px; + margin-bottom: 4px; + transition: color 0.15s ease; +} + +.help-link:hover { + text-decoration: underline; + color: var(--ink); +} + +.divider { + border: none; + border-top: 1px solid var(--rule); + margin: 16px 0; +} + +.privacy-note h4 { + font-family: var(--font-ui); + font-size: 12px; + font-weight: 600; + margin-bottom: 4px; + color: var(--ink); +} + +.privacy-note p { + font-size: 11px; + color: var(--ink-muted); + line-height: 1.4; +} + +/* Dossier Drawer Styles */ +.drawer-intro { + font-size: 12px; + color: var(--ink-soft); + margin-bottom: 12px; + line-height: 1.4; +} + +.dossier-list { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 16px; + max-height: 320px; + overflow-y: auto; +} + +.dossier-item { + background: var(--bg); + border: 1px solid var(--rule); + border-radius: 4px; + padding: 12px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.dossier-item-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.dossier-item-title { + font-family: var(--font-display); + font-size: 14px; + font-weight: 600; + color: var(--ink); + line-height: 1.3; +} + +.dossier-item-meta { + font-family: var(--font-ui); + font-size: 11px; + color: var(--ink-soft); + display: flex; + gap: 12px; +} + +.dossier-delete-btn { + background: transparent; + border: none; + color: var(--ink-muted); + cursor: pointer; + padding: 2px 6px; + font-size: 13px; + border-radius: 3px; + transition: all 0.15s ease; +} + +.dossier-delete-btn:hover { + color: var(--severity-critical); + background: var(--sev-critical-bg); +} + +.dossier-empty { + padding: 24px 16px; + text-align: center; + color: var(--ink-muted); + font-size: 12px; + background: var(--bg); + border: 1px dashed var(--rule-strong); + border-radius: 4px; + margin-bottom: 16px; +} + +.dossier-empty p { + margin-bottom: 4px; +} + +.dossier-drawer-actions { + display: flex; + flex-direction: column; + gap: 8px; +} + +.dossier-secondary-actions { + display: flex; + gap: 8px; +} + +.dossier-secondary-actions button { + flex: 1; +} + +.danger-btn { + color: var(--severity-critical); + border-color: var(--rule-strong); +} + +.danger-btn:hover { + border-color: var(--severity-critical); + background: var(--sev-critical-bg); +} + diff --git a/extension/sidepanel/sidepanel.js b/extension/sidepanel/sidepanel.js new file mode 100644 index 0000000..7b383c9 --- /dev/null +++ b/extension/sidepanel/sidepanel.js @@ -0,0 +1,483 @@ +import { getSettings, saveSettings, getDossier, addToDossier, removeFromDossier, clearDossier } from '../shared/storage.js'; +import { renderAuditHTML, renderDossierListHTML } from './renderer-helper.js'; +import { detectCourseContext } from '../content/extractor-core.js'; +import { compileSingleAuditToMarkdown, compileDossierToMarkdown } from '../shared/dossier-compiler.js'; + +let activePayload = null; +let activeCourseContext = null; +let activeAuditResult = null; +let activeAuditItem = null; + +function sanitizeFilename(name) { + return String(name || 'material') + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/(^-|-$)/g, '') || 'material'; +} + +function downloadMarkdownFile(filename, content) { + const blob = new Blob([content], { type: 'text/markdown;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +export async function updateDossierBadge() { + const countEl = document.getElementById('dossier-count'); + if (!countEl) return; + try { + const dossier = await getDossier(); + countEl.textContent = String(dossier ? dossier.length : 0); + } catch (e) { + countEl.textContent = '0'; + } +} + +export async function updateDossierUI() { + await updateDossierBadge(); + const listEl = document.getElementById('dossier-list'); + if (!listEl) return; + try { + const dossier = await getDossier(); + listEl.innerHTML = renderDossierListHTML(dossier); + + listEl.querySelectorAll('.dossier-delete-btn').forEach((btn) => { + btn.addEventListener('click', async (e) => { + e.stopPropagation(); + const id = btn.getAttribute('data-dossier-id'); + if (id) { + await removeFromDossier(id); + await updateDossierUI(); + } + }); + }); + } catch (e) { + listEl.innerHTML = '

Error loading dossier.

'; + } +} + +export async function refreshActiveTab() { + if (typeof chrome === 'undefined' || !chrome.tabs || !chrome.tabs.query) return; + + try { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + if (!tab || !tab.id) return; + + const url = tab.url || ''; + activeCourseContext = detectCourseContext(url); + const auditCourseBtn = document.getElementById('audit-course-btn'); + if (auditCourseBtn) { + auditCourseBtn.style.display = activeCourseContext.isCourseRoot ? 'block' : 'none'; + } + + try { + const response = await chrome.tabs.sendMessage(tab.id, { action: 'EXTRACT_CONTENT' }); + if (response) { + activePayload = response; + const pageTypeTag = document.getElementById('page-type-tag'); + const pageTitle = document.getElementById('page-title'); + if (pageTypeTag) pageTypeTag.textContent = activeCourseContext.isCourseRoot ? 'Canvas Course Root' : response.pageType; + if (pageTitle) pageTitle.textContent = response.title; + return; + } + } catch (err) { + // Content script may not be injected on pre-existing tabs. Attempt programmatic injection. + if (chrome.scripting && chrome.scripting.executeScript) { + try { + await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + files: ['content/extractor.js'] + }); + const retryResponse = await chrome.tabs.sendMessage(tab.id, { action: 'EXTRACT_CONTENT' }); + if (retryResponse) { + activePayload = retryResponse; + const pageTypeTag = document.getElementById('page-type-tag'); + const pageTitle = document.getElementById('page-title'); + if (pageTypeTag) pageTypeTag.textContent = activeCourseContext.isCourseRoot ? 'Canvas Course Root' : retryResponse.pageType; + if (pageTitle) pageTitle.textContent = retryResponse.title; + return; + } + } catch (injectionErr) { + // Tab may be a chrome:// or restricted URL + } + } + } + + // Fallback payload if content script extraction fails + const pageTypeTag = document.getElementById('page-type-tag'); + const pageTitle = document.getElementById('page-title'); + if (pageTypeTag) pageTypeTag.textContent = activeCourseContext.isCourseRoot ? 'Canvas Course Root' : 'Web Page'; + if (pageTitle) pageTitle.textContent = tab.title || 'Current Tab'; + activePayload = { + url: tab.url || '', + title: tab.title || 'Current Tab', + pageType: activeCourseContext.isCourseRoot ? 'Canvas Course Root' : 'Web Page', + content: '' + }; + } catch (e) { + console.warn('Error refreshing active tab:', e); + } +} + +export function showState(stateName) { + document.querySelectorAll('.state-panel').forEach(el => el.classList.remove('active')); + const target = document.getElementById(`${stateName}-state`); + if (target) target.classList.add('active'); +} + +export function renderResults(data) { + activeAuditResult = data; + activeAuditItem = { + id: activePayload?.url || `item-${Date.now()}`, + title: activePayload?.title || 'Course Material', + pageType: activePayload?.pageType || 'Web Page', + url: activePayload?.url || '', + result: data, + timestamp: new Date().toISOString() + }; + + const addToDossierBtn = document.getElementById('add-to-dossier-btn'); + if (addToDossierBtn) { + addToDossierBtn.textContent = '➕ Add to Dossier'; + } + + const container = document.getElementById('results-container'); + if (!container) return; + + container.innerHTML = renderAuditHTML(data); + + const copyBtn = document.getElementById('copy-improved-btn'); + if (copyBtn) { + copyBtn.addEventListener('click', () => { + const contentToCopy = (data && data.improvedDraft && data.improvedDraft.content) || ''; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(contentToCopy); + } + copyBtn.textContent = '✓ Copied!'; + setTimeout(() => { + copyBtn.textContent = '📋 Copy to Clipboard'; + }, 2000); + }); + } + + const reAuditBtn = document.getElementById('re-audit-btn'); + if (reAuditBtn) { + reAuditBtn.addEventListener('click', () => { + showState('ready'); + refreshActiveTab(); + }); + } + + document.querySelectorAll('.feedback-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + const parent = e.target.parentElement; + if (parent) { + parent.innerHTML = 'Thank you for your feedback!'; + } + }); + }); + + showState('results'); +} + +export function renderError(errorMessage) { + const container = document.getElementById('results-container'); + if (!container) return; + + const safeError = errorMessage + ? String(errorMessage) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + : 'Unknown error occurred during audit.'; + + container.innerHTML = ` +
+
+ ⚠️ +

Audit Encountered an Issue

+
+

${safeError}

+
+ + +
+
+ `; + + const retryBtn = document.getElementById('error-retry-btn'); + if (retryBtn) { + retryBtn.addEventListener('click', () => { + showState('ready'); + const btn = document.getElementById('audit-btn'); + if (btn) btn.click(); + }); + } + + const errorSettingsBtn = document.getElementById('error-settings-btn'); + if (errorSettingsBtn) { + errorSettingsBtn.addEventListener('click', () => { + const drawer = document.getElementById('settings-drawer'); + if (drawer) drawer.classList.add('open'); + }); + } + + showState('results'); +} + +// Result Action Bar: Add to Dossier & Export Single .md +const addToDossierBtn = document.getElementById('add-to-dossier-btn'); +if (addToDossierBtn) { + addToDossierBtn.addEventListener('click', async () => { + if (!activeAuditItem) return; + await addToDossier(activeAuditItem); + await updateDossierBadge(); + addToDossierBtn.textContent = '✓ Added to Dossier'; + setTimeout(() => { + addToDossierBtn.textContent = '➕ Add to Dossier'; + }, 2000); + }); +} + +const exportSingleMdBtn = document.getElementById('export-single-md-btn'); +if (exportSingleMdBtn) { + exportSingleMdBtn.addEventListener('click', () => { + if (!activeAuditItem) return; + const md = compileSingleAuditToMarkdown(activeAuditItem); + const safeTitle = sanitizeFilename(activeAuditItem.title); + downloadMarkdownFile(`idstack-audit-${safeTitle}.md`, md); + }); +} + +// Single-page Audit button click handler +const auditBtn = document.getElementById('audit-btn'); +if (auditBtn) { + auditBtn.addEventListener('click', async () => { + if (!activePayload) await refreshActiveTab(); + + const progressCard = document.getElementById('crawl-progress-card'); + if (progressCard) progressCard.style.display = 'none'; + + const loaderStatus = document.getElementById('loader-status'); + if (loaderStatus) loaderStatus.textContent = 'Analyzing page content...'; + + showState('loading'); + + if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.sendMessage) { + try { + chrome.runtime.sendMessage({ action: 'RUN_AUDIT', payload: activePayload }, (response) => { + if (chrome.runtime.lastError) { + renderError(chrome.runtime.lastError.message); + return; + } + if (response && response.success) { + renderResults(response.data); + } else { + renderError(response?.error || 'Unknown error occurred during audit.'); + } + }); + } catch (err) { + renderError(err.message); + } + } + }); +} + +// Course-level Audit button click handler +const auditCourseBtn = document.getElementById('audit-course-btn'); +if (auditCourseBtn) { + auditCourseBtn.addEventListener('click', async () => { + if (!activePayload) await refreshActiveTab(); + + const url = (activePayload && activePayload.url) || ''; + const courseCtx = activeCourseContext || detectCourseContext(url); + const origin = courseCtx.origin || (url ? new URL(url).origin : ''); + const courseId = courseCtx.courseId; + + const progressCard = document.getElementById('crawl-progress-card'); + const statusText = document.getElementById('crawl-status-text'); + const progressFill = document.getElementById('crawl-progress-fill'); + const loaderStatus = document.getElementById('loader-status'); + + if (loaderStatus) loaderStatus.textContent = 'Auditing Full Canvas Course...'; + if (progressCard) progressCard.style.display = 'block'; + if (statusText) statusText.textContent = 'Gathering syllabus...'; + if (progressFill) progressFill.style.width = '25%'; + + showState('loading'); + + const timer1 = setTimeout(() => { + if (statusText) statusText.textContent = 'Fetching assignments...'; + if (progressFill) progressFill.style.width = '60%'; + }, 600); + + const timer2 = setTimeout(() => { + if (statusText) statusText.textContent = 'Analyzing constructive alignment...'; + if (progressFill) progressFill.style.width = '85%'; + }, 1300); + + if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.sendMessage) { + try { + chrome.runtime.sendMessage({ + action: 'CRAWL_AND_AUDIT_COURSE', + payload: { origin, courseId } + }, (response) => { + clearTimeout(timer1); + clearTimeout(timer2); + if (progressFill) progressFill.style.width = '100%'; + if (chrome.runtime.lastError) { + renderError(chrome.runtime.lastError.message); + return; + } + if (response && response.success) { + renderResults(response.data); + } else { + renderError(response?.error || 'Unknown error occurred during course audit.'); + } + }); + } catch (err) { + clearTimeout(timer1); + clearTimeout(timer2); + renderError(err.message); + } + } + }); +} + +// Dossier Drawer Management & Actions +const dossierToggleBtn = document.getElementById('dossier-toggle-btn'); +const closeDossier = document.getElementById('close-dossier'); +const dossierDrawer = document.getElementById('dossier-drawer'); +const exportDossierMdBtn = document.getElementById('export-dossier-md-btn'); +const copyDossierMdBtn = document.getElementById('copy-dossier-md-btn'); +const clearDossierBtn = document.getElementById('clear-dossier-btn'); + +if (dossierToggleBtn && dossierDrawer) { + dossierToggleBtn.addEventListener('click', async () => { + const settingsDrawer = document.getElementById('settings-drawer'); + if (settingsDrawer) settingsDrawer.classList.remove('open'); + dossierDrawer.classList.toggle('open'); + if (dossierDrawer.classList.contains('open')) { + await updateDossierUI(); + } + }); +} + +if (closeDossier && dossierDrawer) { + closeDossier.addEventListener('click', () => { + dossierDrawer.classList.remove('open'); + }); +} + +if (exportDossierMdBtn) { + exportDossierMdBtn.addEventListener('click', async () => { + const dossier = await getDossier(); + const courseTitle = (activeCourseContext && activeCourseContext.courseId) + ? `Course ${activeCourseContext.courseId}` + : (activePayload?.title || 'Canvas Course'); + const md = compileDossierToMarkdown(dossier, courseTitle); + const safeTitle = sanitizeFilename(courseTitle); + downloadMarkdownFile(`idstack-course-dossier-${safeTitle}.md`, md); + }); +} + +if (copyDossierMdBtn) { + copyDossierMdBtn.addEventListener('click', async () => { + const dossier = await getDossier(); + const courseTitle = (activeCourseContext && activeCourseContext.courseId) + ? `Course ${activeCourseContext.courseId}` + : (activePayload?.title || 'Canvas Course'); + const md = compileDossierToMarkdown(dossier, courseTitle); + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(md); + } + copyDossierMdBtn.textContent = '✓ Copied!'; + setTimeout(() => { + copyDossierMdBtn.textContent = '📋 Copy Markdown'; + }, 2000); + }); +} + +if (clearDossierBtn) { + clearDossierBtn.addEventListener('click', async () => { + await clearDossier(); + await updateDossierUI(); + }); +} + +// Settings Drawer Management +const settingsToggle = document.getElementById('settings-toggle'); +const closeSettings = document.getElementById('close-settings'); +const settingsDrawer = document.getElementById('settings-drawer'); +const saveSettingsBtn = document.getElementById('save-settings-btn'); +const apiKeyInput = document.getElementById('api-key-input'); + +if (settingsToggle && settingsDrawer) { + settingsToggle.addEventListener('click', () => { + if (dossierDrawer) dossierDrawer.classList.remove('open'); + settingsDrawer.classList.toggle('open'); + }); +} + +if (closeSettings && settingsDrawer) { + closeSettings.addEventListener('click', () => { + settingsDrawer.classList.remove('open'); + }); +} + +if (saveSettingsBtn && apiKeyInput) { + saveSettingsBtn.addEventListener('click', async () => { + const key = apiKeyInput.value.trim(); + await saveSettings({ apiKey: key }); + saveSettingsBtn.textContent = 'Saved!'; + setTimeout(() => { + saveSettingsBtn.textContent = 'Save Settings'; + if (settingsDrawer) settingsDrawer.classList.remove('open'); + }, 1200); + }); +} + +// Load initial settings & dossier count +(async () => { + if (apiKeyInput) { + try { + const settings = await getSettings(); + if (settings && settings.apiKey) { + apiKeyInput.value = settings.apiKey; + } + } catch (e) { + // Ignored if storage not initialized + } + } + await updateDossierBadge(); +})(); + +// Listen for tab switching / updates +if (typeof chrome !== 'undefined' && chrome.tabs) { + if (chrome.tabs.onActivated) { + chrome.tabs.onActivated.addListener(() => { + refreshActiveTab(); + }); + } + if (chrome.tabs.onUpdated) { + chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { + if (changeInfo.status === 'complete' && tab.active) { + refreshActiveTab(); + } + }); + } +} + +// Initial tab detection on load +if (typeof chrome !== 'undefined' && chrome.tabs) { + refreshActiveTab(); +} + + diff --git a/test/smoke-test.sh b/test/smoke-test.sh index 15ab2b8..07e96bf 100755 --- a/test/smoke-test.sh +++ b/test/smoke-test.sh @@ -306,6 +306,11 @@ if [ -x "$IDSTACK_DIR/test/test-setup.sh" ]; then check "setup behavioral tests pass" "'$IDSTACK_DIR/test/test-setup.sh' '$IDSTACK_DIR'" fi +# Chrome extension unit and integration tests +if [ -x "$IDSTACK_DIR/test/test-extension.sh" ]; then + check "chrome extension tests pass" "'$IDSTACK_DIR/test/test-extension.sh'" +fi + # Check generated files have auto-generated header for skill in $SKILLS; do check "$skill SKILL.md has auto-generated header" "grep -q 'AUTO-GENERATED from SKILL.md.tmpl' '$IDSTACK_DIR/skills/$skill/SKILL.md'" @@ -412,7 +417,7 @@ check "preamble embeds no ~/.agents fallbacks" "! grep -q '\.agents/' '$IDSTACK_ CLI_LEAK_RE='codex|gemini' # IDSTACK_CLI_LEAK_ALLOW CLI_LEAK="$(grep -rIiE "$CLI_LEAK_RE" "$IDSTACK_DIR" \ --exclude-dir=.git --exclude-dir=.gstack --exclude-dir=.idstack \ - --exclude-dir=.claude --exclude-dir=.superpowers --exclude=CHANGELOG.md 2>/dev/null || true)" + --exclude-dir=.claude --exclude-dir=.superpowers --exclude-dir=superpowers --exclude=CHANGELOG.md 2>/dev/null || true)" CLI_LEAK="$(printf '%s' "$CLI_LEAK" | grep -vF 'IDSTACK_CLI_LEAK_ALLOW' || true)" # Printed through the command itself, not tested with -z, so a failure names # the offending lines instead of just saying the string was non-empty. diff --git a/test/test-crawler.js b/test/test-crawler.js new file mode 100644 index 0000000..6c8f325 --- /dev/null +++ b/test/test-crawler.js @@ -0,0 +1,50 @@ +const assert = require('assert'); +const { crawlCanvasCourse, stripHtml } = require('../extension/background/canvas-crawler.cjs'); +const { getDemoCourseAuditResult } = require('../extension/background/parser-helper.cjs'); + +// Test 1: HTML Tag Stripping +const clean = stripHtml('

Hello World & Students

'); +assert.strictEqual(clean, 'Hello World & Students'); + +// Test 2: Mock Canvas Crawler +let fetchOptionsPassed = []; +const mockFetch = async (url, options) => { + if (options) fetchOptionsPassed.push(options); + if (url.includes('include[]=syllabus_body')) { + return { + ok: true, + json: async () => ({ + name: 'Biology 101: Cell Systems', + syllabus_body: '

Welcome to Biology 101. Objectives: Analyze cellular metabolism.

' + }) + }; + } + if (url.includes('/assignments')) { + return { + ok: true, + json: async () => [ + { name: 'Quiz 1', description: '

Recall organelles

', points_possible: 10 }, + { name: 'Lab Report 1', description: '

Analyze enzyme kinetics

', points_possible: 50 } + ] + }; + } + throw new Error('Not found: ' + url); +}; + +(async () => { + const courseData = await crawlCanvasCourse('https://canvas.instructure.com', '12345', mockFetch); + assert.strictEqual(courseData.title, 'Biology 101: Cell Systems'); + assert.ok(fetchOptionsPassed.every(opt => opt && opt.credentials === 'include'), 'fetchImpl must include credentials'); + assert.ok(courseData.syllabus.includes('Analyze cellular metabolism')); + assert.strictEqual(courseData.assignments.length, 2); + assert.strictEqual(courseData.assignments[0].title, 'Quiz 1'); + assert.strictEqual(courseData.assignments[0].description, 'Recall organelles'); + + // Test 3: Demo Course Audit Fallback + const demoResult = getDemoCourseAuditResult({ title: 'Biology 101: Cell Systems' }); + assert.ok(demoResult.summary.keyTakeaway.includes('Course-Level Demo')); + assert.ok(demoResult.findings.length >= 2); + assert.ok(demoResult.improvedDraft.title.includes('Matrix')); + + console.log('✅ Task 2 Canvas crawler tests passed.'); +})(); diff --git a/test/test-dossier-compiler.js b/test/test-dossier-compiler.js new file mode 100644 index 0000000..8db95e7 --- /dev/null +++ b/test/test-dossier-compiler.js @@ -0,0 +1,98 @@ +const fs = require('fs'); +const path = require('path'); +const assert = require('assert'); +const { compileDossierToMarkdown, compileSingleAuditToMarkdown } = require('../extension/shared/dossier-compiler.cjs'); + +// Test 1: Single Audit Markdown Compilation +const singleItem = { + title: 'Lab 1: Enzymes', + pageType: 'Canvas Assignment', + url: 'https://canvas.instructure.com/courses/123/assignments/456', + timestamp: '2026-08-16T10:00:00Z', + result: { + summary: { + bloomsLevel: 'Analyze (Level 4)', + alignmentScore: 'Moderate (75%)', + keyTakeaway: 'Focus on rubric transparency.' + }, + findings: [ + { + severity: 'warning', + tier: 'T1', + citation: '[Assessment-8] Formative Feedback', + observation: 'Rubric lacks milestone descriptors.', + evidence: 'Wisniewski et al. (2020) [T1]: Rubrics boost self-regulation.', + recommendation: 'Add milestone criteria.' + } + ], + improvedDraft: { + title: 'Improved Lab Rubric', + content: '| Criterion | Proficient | Novice |\n| --- | --- | --- |' + } + } +}; + +const singleMd = compileSingleAuditToMarkdown(singleItem); +assert.ok(singleMd.includes('# idstack Instructional Design Audit: Lab 1: Enzymes')); +assert.ok(singleMd.includes('**Bloom\'s Demand:** Analyze (Level 4)')); +assert.ok(singleMd.includes('Wisniewski et al. (2020)')); +assert.ok(singleMd.includes('Improved Lab Rubric')); + +// Test 2: Multi-Audit Dossier Compilation +const dossierItems = [ + singleItem, + { + title: 'Course Syllabus', + pageType: 'Canvas Syllabus', + url: 'https://canvas.instructure.com/courses/123/syllabus', + timestamp: '2026-08-16T09:30:00Z', + result: { + summary: { + bloomsLevel: 'Understand (Level 2)', + alignmentScore: 'High (90%)', + keyTakeaway: 'Clear policy structure.' + }, + findings: [ + { + severity: 'info', + tier: 'T2', + citation: '[Alignment-3] Direct Constructive Alignment', + observation: 'Objectives align with module outcomes.', + evidence: 'Biggs (1996) [T2]: Clear alignment supports deep learning.', + recommendation: 'Maintain alignment across quizzes.' + } + ], + improvedDraft: { + title: 'Revised Objectives', + content: '- Analyze core enzyme mechanisms.' + } + } + } +]; + +const dossierMd = compileDossierToMarkdown(dossierItems, 'Biology 101: Cell Systems'); +assert.ok(dossierMd.includes('# idstack Course Audit Dossier: Biology 101: Cell Systems')); +assert.ok(dossierMd.includes('**Total Audited Materials:** 2 components')); +assert.ok(dossierMd.includes('## Section 1: Lab 1: Enzymes')); +assert.ok(dossierMd.includes('## Section 2: Course Syllabus')); +assert.ok(dossierMd.includes('Wisniewski et al. (2020)')); +assert.ok(dossierMd.includes('Biggs (1996)')); + +// Test 3: Edge cases (empty or null inputs) +assert.strictEqual(compileSingleAuditToMarkdown(null), ''); +assert.strictEqual(compileSingleAuditToMarkdown({}), ''); +const emptyDossierMd = compileDossierToMarkdown([], 'Empty Course'); +assert.ok(emptyDossierMd.includes('*No audit materials in dossier.*')); +const nullDossierMd = compileDossierToMarkdown(null); +assert.ok(nullDossierMd.includes('*No audit materials in dossier.*')); + +// Test 4: Verify storage.js methods exist +const storagePath = path.join(__dirname, '../extension/shared/storage.js'); +assert.ok(fs.existsSync(storagePath), 'storage.js must exist'); +const storageContent = fs.readFileSync(storagePath, 'utf8'); +assert.ok(storageContent.includes('export async function getDossier()'), 'getDossier export required'); +assert.ok(storageContent.includes('export async function addToDossier('), 'addToDossier export required'); +assert.ok(storageContent.includes('export async function removeFromDossier('), 'removeFromDossier export required'); +assert.ok(storageContent.includes('export async function clearDossier()'), 'clearDossier export required'); + +console.log('✅ Task 1 Dossier compiler tests passed.'); diff --git a/test/test-extension.sh b/test/test-extension.sh new file mode 100755 index 0000000..a5e5f49 --- /dev/null +++ b/test/test-extension.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +DIR="$(cd "$(dirname "$0")" && pwd)" + +echo "==> Running idstack Chrome Extension test suite..." +node "$DIR/test-manifest.js" +node "$DIR/test-prompts.js" +node "$DIR/test-extractor.js" +node "$DIR/test-crawler.js" +node "$DIR/test-service-worker.js" +node "$DIR/test-dossier-compiler.js" +node "$DIR/test-sidepanel-dom.js" +node "$DIR/test-sidepanel-logic.js" + +echo "==> All Chrome Extension tests passed!" diff --git a/test/test-extractor.js b/test/test-extractor.js new file mode 100644 index 0000000..9081df9 --- /dev/null +++ b/test/test-extractor.js @@ -0,0 +1,338 @@ +const assert = require('assert'); + +// Support JSDOM if available, or fallback to built-in DOM mock for standalone node environments +let JSDOM; +try { + JSDOM = require('jsdom').JSDOM; +} catch { + class MockElement { + constructor(tagName = 'div', attrs = {}, text = '') { + this.tagName = tagName.toLowerCase(); + this.attrs = attrs; + this.children = []; + this._text = text; + } + get innerText() { + if (this.children.length > 0) { + return this.children.map(c => typeof c === 'string' ? c : c.innerText).join(' ').trim(); + } + return this._text.trim(); + } + get textContent() { + return this.innerText; + } + querySelector(selector) { + const selectors = selector.split(',').map(s => s.trim()); + for (const sel of selectors) { + const found = this._querySingle(sel); + if (found) return found; + } + return null; + } + _querySingle(selector) { + const parts = selector.split(/\s+/); + let current = [this]; + for (const part of parts) { + const next = []; + for (const el of current) { + next.push(...el._findDescendants(part)); + } + current = next; + if (current.length === 0) return null; + } + return current[0] || null; + } + _findDescendants(part) { + const results = []; + const check = (el) => { + if (el !== this) { + let match = true; + if (part.startsWith('#')) { + if (el.attrs.id !== part.slice(1)) match = false; + } else if (part.startsWith('.')) { + const requiredClasses = part.split('.').filter(Boolean); + const elClasses = (el.attrs.class || '').split(/\s+/).filter(Boolean); + if (!requiredClasses.every(cls => elClasses.includes(cls))) match = false; + } else if (part.startsWith('[')) { + const m = part.match(/\[([a-zA-Z0-9_-]+)(?:="([^"]*)")?\]/); + if (m) { + const [, attrName, attrVal] = m; + if (attrVal !== undefined) { + if (el.attrs[attrName] !== attrVal) match = false; + } else { + if (!(attrName in el.attrs)) match = false; + } + } + } else { + if (el.tagName !== part.toLowerCase()) match = false; + } + if (match) results.push(el); + } + for (const child of el.children) { + if (typeof child !== 'string') check(child); + } + }; + check(this); + return results; + } + } + + function parseHtmlToTree(html) { + const root = new MockElement('root'); + const stack = [root]; + const tagRegex = /<(\/)?([a-zA-Z0-9]+)([^>]*)>|([^<]+)/g; + let match; + + while ((match = tagRegex.exec(html)) !== null) { + const [full, isClose, tagName, attrStr, text] = match; + if (text) { + const trimmed = text.trim(); + if (trimmed) { + const current = stack[stack.length - 1]; + current.children.push(trimmed); + } + } else if (tagName) { + if (isClose) { + if (stack.length > 1 && stack[stack.length - 1].tagName === tagName.toLowerCase()) { + stack.pop(); + } + } else { + const attrs = {}; + if (attrStr) { + const attrRegex = /([a-zA-Z0-9_-]+)(?:=["']([^"']*)["'])?/g; + let attrMatch; + while ((attrMatch = attrRegex.exec(attrStr)) !== null) { + attrs[attrMatch[1]] = attrMatch[2] || ''; + } + } + const el = new MockElement(tagName, attrs); + stack[stack.length - 1].children.push(el); + if (!['meta', 'link', 'img', 'br', 'hr', 'input'].includes(tagName.toLowerCase())) { + stack.push(el); + } + } + } + } + return root; + } + + class SimpleJSDOM { + constructor(html, { url = 'http://localhost' } = {}) { + const tree = parseHtmlToTree(html); + const titleEl = tree.querySelector('title'); + const titleText = titleEl ? titleEl.innerText : ''; + const bodyEl = tree.querySelector('body') || tree; + + this.window = { + location: { href: url }, + document: { + title: titleText, + body: bodyEl, + querySelector: (sel) => tree.querySelector(sel) + } + }; + } + } + JSDOM = SimpleJSDOM; +} + +const { detectPageType, extractContentFromDOM, extractPageContent } = require('../extension/content/extractor-core.cjs'); + +// Test 1: Canvas Assignment fixture +const canvasAssignmentHTML = ` + + Module 3: Enzymes Assignment + +
+

Enzymes Lab Analysis

+
+

Read chapter 4 and answer the 5 review questions.

+
+
+ + +`; +const domAssignment = new JSDOM(canvasAssignmentHTML, { url: 'https://canvas.instructure.com/courses/101/assignments/202' }); +const extractedAssignment = extractContentFromDOM(domAssignment.window.document, domAssignment.window.location.href); + +assert.strictEqual(extractedAssignment.pageType, 'Canvas Assignment'); +assert.strictEqual(extractedAssignment.title, 'Enzymes Lab Analysis'); +assert.ok(extractedAssignment.content.includes('Read chapter 4')); +assert.strictEqual(extractedAssignment.wordCount, 9); + +// Test 2: Canvas Syllabus fixture +const canvasSyllabusHTML = ` + + Course Syllabus - CS 101 + +

CS 101 Syllabus

+
+

This course covers algorithms and data structures.

+
+ + +`; +const domSyllabus = new JSDOM(canvasSyllabusHTML, { url: 'https://canvas.instructure.com/courses/101/assignments/syllabus' }); +const extractedSyllabus = extractContentFromDOM(domSyllabus.window.document, domSyllabus.window.location.href); + +assert.strictEqual(extractedSyllabus.pageType, 'Canvas Syllabus'); +assert.strictEqual(extractedSyllabus.title, 'CS 101 Syllabus'); +assert.ok(extractedSyllabus.content.includes('algorithms and data structures')); + +// Test 3: Canvas Modules fixture +const canvasModulesHTML = ` + + Course Modules - BIO 200 + +

BIO 200 Modules

+
+ Week 1: Cellular Respiration +
+ + +`; +const domModules = new JSDOM(canvasModulesHTML, { url: 'https://canvas.instructure.com/courses/101/modules' }); +const extractedModules = extractContentFromDOM(domModules.window.document, domModules.window.location.href); + +assert.strictEqual(extractedModules.pageType, 'Canvas Modules'); +assert.strictEqual(extractedModules.title, 'BIO 200 Modules'); +assert.ok(extractedModules.content.includes('Week 1: Cellular Respiration')); + +// Test 4: Canvas Rubrics fixture +const canvasRubricsHTML = ` + + Course Rubrics + +

Grading Criteria

+
+

Criteria 1: Clarity of argument (10 pts)

+
+ + +`; +const domRubrics = new JSDOM(canvasRubricsHTML, { url: 'https://canvas.instructure.com/courses/101/rubrics' }); +const extractedRubrics = extractContentFromDOM(domRubrics.window.document, domRubrics.window.location.href); + +assert.strictEqual(extractedRubrics.pageType, 'Canvas Rubric'); +assert.strictEqual(extractedRubrics.title, 'Grading Criteria'); + +// Test 5: Canvas Generic LMS Page +const canvasGenericHTML = ` + + Course Overview + +

Welcome to PHY 101

+
+

General introduction to physics principles.

+
+ + +`; +const domGeneric = new JSDOM(canvasGenericHTML, { url: 'https://canvas.instructure.com/courses/101/pages/overview' }); +const extractedGeneric = extractContentFromDOM(domGeneric.window.document, domGeneric.window.location.href); + +assert.strictEqual(extractedGeneric.pageType, 'Canvas LMS Page'); +assert.strictEqual(extractedGeneric.title, 'Welcome to PHY 101'); +assert.ok(extractedGeneric.content.includes('physics principles')); + +// Test 6: Google Docs fixture +const gdocHTML = ` + + Instructional Design Principles - Google Docs + +
+

Course Outline: 1. Assessment Design 2. Cognitive Load Management

+
+ + +`; +const domGdoc = new JSDOM(gdocHTML, { url: 'https://docs.google.com/document/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit' }); +const extractedGdoc = extractContentFromDOM(domGdoc.window.document, domGdoc.window.location.href); + +assert.strictEqual(extractedGdoc.pageType, 'Google Doc Syllabus'); +assert.ok(extractedGdoc.content.includes('Assessment Design')); + +// Test 7: Generic Web Page fixture +const webHTML = ` + + Open Courseware Syllabus + +
+

Introduction to Machine Learning

+

Course schedule and grading policy.

+
+ + +`; +const domWeb = new JSDOM(webHTML, { url: 'https://ocw.mit.edu/syllabus/intro-ml' }); +const extractedWeb = extractContentFromDOM(domWeb.window.document, domWeb.window.location.href); + +assert.strictEqual(extractedWeb.pageType, 'Web Syllabus / Course Page'); +assert.strictEqual(extractedWeb.title, 'Open Courseware Syllabus'); +assert.ok(extractedWeb.content.includes('Machine Learning')); + +// Test 8: Content length cap at 15000 characters +const longText = 'word '.repeat(4000); // 20,000 characters +const longHTML = `Long Document

${longText}

`; +const domLong = new JSDOM(longHTML, { url: 'https://example.com/long' }); +const extractedLong = extractContentFromDOM(domLong.window.document, domLong.window.location.href); + +assert.strictEqual(extractedLong.content.length, 15000, 'Content should be capped at 15000 chars'); + +// Test 9: detectPageType helper directly +assert.strictEqual(detectPageType('https://canvas.instructure.com/courses/1/assignments/2', { querySelector: (s) => s === '#assignment_show' ? {} : null }), 'Canvas Assignment'); +assert.strictEqual(detectPageType('https://docs.google.com/document/d/123', {}), 'Google Doc Syllabus'); +assert.strictEqual(detectPageType('https://harvard.edu/syllabus', {}), 'Web Syllabus / Course Page'); + +// Test 10: Chrome Runtime onMessage listener handling +let messageListener = null; +global.chrome = { + runtime: { + onMessage: { + addListener: (fn) => { + messageListener = fn; + } + } + } +}; +global.window = domAssignment.window; +global.document = domAssignment.window.document; + +delete require.cache[require.resolve('../extension/content/extractor.js')]; +require('../extension/content/extractor.js'); + +assert.ok(typeof messageListener === 'function', 'chrome.runtime.onMessage listener should be registered'); +let responseData = null; +messageListener({ action: 'EXTRACT_CONTENT' }, {}, (data) => { + responseData = data; +}); +assert.ok(responseData, 'Response data must be returned on EXTRACT_CONTENT action'); +assert.strictEqual(responseData.pageType, 'Canvas Assignment'); +assert.strictEqual(responseData.title, 'Enzymes Lab Analysis'); + +// Clean up globals +delete global.chrome; +delete global.window; +delete global.document; + +// Test 11: Canvas Course Root Detection +const { detectCourseContext } = require('../extension/content/extractor-core.cjs'); +const rootCtx = detectCourseContext('https://canvas.instructure.com/courses/987654', 'Biology 101'); +assert.strictEqual(rootCtx.isCourseRoot, true); +assert.strictEqual(rootCtx.courseId, '987654'); +assert.strictEqual(rootCtx.origin, 'https://canvas.instructure.com'); + +const modulesCtx = detectCourseContext('https://canvas.instructure.com/courses/987654/modules', 'Modules'); +assert.strictEqual(modulesCtx.isCourseRoot, true); +assert.strictEqual(modulesCtx.courseId, '987654'); +assert.strictEqual(modulesCtx.origin, 'https://canvas.instructure.com'); + +const queryParamCtx = detectCourseContext('https://canvas.instructure.com/courses/987654/modules?view=feed', 'Modules Feed'); +assert.strictEqual(queryParamCtx.isCourseRoot, true); +assert.strictEqual(queryParamCtx.courseId, '987654'); + +const subpageCtx = detectCourseContext('https://canvas.instructure.com/courses/987654/assignments/123', 'Lab 1'); +assert.strictEqual(subpageCtx.isCourseRoot, false); +assert.strictEqual(subpageCtx.courseId, '987654'); + +console.log('✅ Task 3 extractor tests passed.'); diff --git a/test/test-manifest.js b/test/test-manifest.js new file mode 100644 index 0000000..8f7e3bf --- /dev/null +++ b/test/test-manifest.js @@ -0,0 +1,32 @@ +const fs = require('fs'); +const path = require('path'); +const assert = require('assert'); + +// Test manifest.json +const manifestPath = path.join(__dirname, '../extension/manifest.json'); +assert.ok(fs.existsSync(manifestPath), 'manifest.json must exist'); +const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + +assert.strictEqual(manifest.manifest_version, 3); +assert.strictEqual(manifest.name, 'idstack — Evidence-Based Course Design'); +assert.ok(manifest.permissions.includes('sidePanel')); +assert.ok(manifest.permissions.includes('storage')); +assert.ok(manifest.permissions.includes('activeTab')); +assert.ok(manifest.host_permissions && manifest.host_permissions.includes('https://generativelanguage.googleapis.com/*'), 'host_permissions for AI API required'); +assert.ok(manifest.host_permissions && manifest.host_permissions.includes('*://*.instructure.com/*'), 'host_permissions for Canvas API required'); + +// Storage helper check +const storagePath = path.join(__dirname, '../extension/shared/storage.js'); +assert.ok(fs.existsSync(storagePath), 'storage.js must exist'); +const storageContent = fs.readFileSync(storagePath, 'utf8'); +assert.ok(storageContent.includes('export async function getSettings()'), 'getSettings export required'); +assert.ok(storageContent.includes('export async function saveSettings('), 'saveSettings export required'); +assert.ok(storageContent.includes('export async function getAuditHistory()'), 'getAuditHistory export required'); +assert.ok(storageContent.includes('export async function saveAuditResult('), 'saveAuditResult export required'); + +// Icons check +assert.ok(fs.existsSync(path.join(__dirname, '../extension/icons/icon-16.png')), 'icon-16.png must exist'); +assert.ok(fs.existsSync(path.join(__dirname, '../extension/icons/icon-48.png')), 'icon-48.png must exist'); +assert.ok(fs.existsSync(path.join(__dirname, '../extension/icons/icon-128.png')), 'icon-128.png must exist'); + +console.log('✅ Task 1 manifest checks passed.'); diff --git a/test/test-prompts.js b/test/test-prompts.js new file mode 100644 index 0000000..e9a5cf9 --- /dev/null +++ b/test/test-prompts.js @@ -0,0 +1,38 @@ +const assert = require('assert'); +const { EVIDENCE_DOMAINS, TIER_METADATA, buildAuditPrompt } = require('../extension/shared/prompts.cjs'); + +assert.ok(EVIDENCE_DOMAINS.length >= 10, 'Should include all core idstack research domains'); +assert.ok(TIER_METADATA.T1, 'Tier 1 metadata must exist'); +assert.strictEqual(TIER_METADATA.T1.label, 'Meta-analysis'); +assert.strictEqual(TIER_METADATA.T1.color, '#2f7a4a', 'T1 must match canonical DESIGN.md color'); +assert.strictEqual(TIER_METADATA.T2.color, '#2864a8', 'T2 must match canonical DESIGN.md color'); +assert.strictEqual(TIER_METADATA.T3.color, '#a87726', 'T3 must match canonical DESIGN.md color'); +assert.strictEqual(TIER_METADATA.T4.color, '#b35a1f', 'T4 must match canonical DESIGN.md color'); +assert.strictEqual(TIER_METADATA.T5.color, '#6b6b6b', 'T5 must match canonical DESIGN.md color'); + + +const prompt = buildAuditPrompt({ + title: 'Biology 101 - Cell Division Assignment', + pageType: 'assignment', + content: 'Students will list the phases of mitosis and take a 5-question multiple choice quiz.' +}); + +assert.ok(prompt.includes('Biology 101'), 'Prompt should include content title'); +assert.ok(prompt.includes("Bloom's"), "Prompt should require Bloom's classification"); +assert.ok(prompt.includes('JSON'), 'Prompt should enforce JSON format'); + +const { buildCourseAuditPrompt } = require('../extension/shared/prompts.cjs'); +assert.strictEqual(typeof buildCourseAuditPrompt, 'function'); +const coursePrompt = buildCourseAuditPrompt({ + title: 'Biology 101', + syllabus: 'Course objectives and grading policy...', + assignments: [ + { title: 'Quiz 1', description: 'Recall cell parts', points: 10 }, + { title: 'Final Project', description: 'Design an experiment', points: 100 } + ] +}); +assert.ok(coursePrompt.includes('Biology 101')); +assert.ok(coursePrompt.includes('Constructive Alignment')); +assert.ok(coursePrompt.includes('courseAudit')); + +console.log('✅ Task 2 prompt engine tests passed.'); diff --git a/test/test-service-worker.js b/test/test-service-worker.js new file mode 100644 index 0000000..6d41846 --- /dev/null +++ b/test/test-service-worker.js @@ -0,0 +1,65 @@ +const assert = require('assert'); +const { parseAuditResponse, cleanJsonResponse, getDemoAuditResult, getDemoCourseAuditResult } = require('../extension/background/parser-helper.cjs'); + +// Test 1: Markdown fenced JSON with '```json' +const rawLlmResponse = "```json\n{\n \"summary\": {\n \"bloomsLevel\": \"Remember\",\n \"alignmentScore\": \"Moderate\",\n \"keyTakeaway\": \"Quiz focuses only on memorization.\"\n },\n \"findings\": [],\n \"improvedDraft\": {\n \"title\": \"Analysis Prompt\",\n \"content\": \"Compare and contrast\"\n }\n}\n```"; + +const parsed = parseAuditResponse(rawLlmResponse); +assert.strictEqual(parsed.summary.bloomsLevel, 'Remember'); +assert.strictEqual(parsed.improvedDraft.title, 'Analysis Prompt'); +assert.strictEqual(parsed.summary.alignmentScore, 'Moderate'); + +// Test 2: Markdown fenced JSON with '```' (without json tag) +const rawFencedNoTag = "```\n{\n \"summary\": { \"bloomsLevel\": \"Analyze\" }\n}\n```"; +const parsedNoTag = parseAuditResponse(rawFencedNoTag); +assert.strictEqual(parsedNoTag.summary.bloomsLevel, 'Analyze'); + +// Test 3: Raw clean JSON without fences +const rawPlainJson = '{"summary": {"bloomsLevel": "Create"}}'; +const parsedPlain = parseAuditResponse(rawPlainJson); +assert.strictEqual(parsedPlain.summary.bloomsLevel, 'Create'); + +// Test 4: JSON with leading/trailing whitespace and fences +const rawWithWhitespace = ' \n```json\n{"summary": {"bloomsLevel": "Evaluate"}}\n```\n '; +const parsedWhitespace = parseAuditResponse(rawWithWhitespace); +assert.strictEqual(parsedWhitespace.summary.bloomsLevel, 'Evaluate'); + +// Test 5: cleanJsonResponse alias +assert.strictEqual(typeof cleanJsonResponse, 'function'); +const cleaned = cleanJsonResponse('{"key": "value"}'); +assert.strictEqual(cleaned.key, 'value'); + +// Test 6: Demo fallback when API key is not configured +assert.strictEqual(typeof getDemoAuditResult, 'function', 'getDemoAuditResult must be a function'); +const demoData = getDemoAuditResult({ + title: 'Enzymes Lab Analysis', + pageType: 'Canvas Assignment' +}); + +assert.ok(demoData, 'Demo audit result must be returned'); +assert.ok(demoData.summary.bloomsLevel.includes('Analyze'), "Demo result must demonstrate Bloom's classification"); +assert.ok(demoData.summary.keyTakeaway.includes('Settings'), 'Demo result must note that Settings unlocks live audits'); +assert.ok(demoData.findings.length >= 2, 'Demo result must provide multiple evidence-based findings'); + +const hasT1 = demoData.findings.some(f => f.tier === 'T1'); +const hasT2 = demoData.findings.some(f => f.tier === 'T2'); +assert.ok(hasT1, 'Demo findings must include T1 evidence tier badge'); +assert.ok(hasT2, 'Demo findings must include T2 evidence tier badge'); + +assert.ok(demoData.improvedDraft.title, 'Demo draft must include title'); +assert.ok(demoData.improvedDraft.content.includes('Settings'), 'Demo draft must remind user to configure API key in Settings'); +assert.ok(demoData.improvedDraft.content.includes('Rubric'), 'Demo draft must provide an improved rubric draft'); + +// Test 7: Demo course-level audit fallback +assert.strictEqual(typeof getDemoCourseAuditResult, 'function', 'getDemoCourseAuditResult must be a function'); +const demoCourseData = getDemoCourseAuditResult({ + title: 'Biology 101: Cell Systems' +}); +assert.ok(demoCourseData, 'Demo course audit result must be returned'); +assert.ok(demoCourseData.summary.keyTakeaway.includes('Course-Level Demo')); +assert.ok(demoCourseData.findings.length >= 2); +assert.ok(demoCourseData.improvedDraft.title.includes('Matrix')); + +console.log('✅ Task 4 response parser & demo fallback tests passed.'); + + diff --git a/test/test-sidepanel-dom.js b/test/test-sidepanel-dom.js new file mode 100644 index 0000000..88abd49 --- /dev/null +++ b/test/test-sidepanel-dom.js @@ -0,0 +1,84 @@ +const fs = require('fs'); +const path = require('path'); +const assert = require('assert'); + +const htmlPath = path.join(__dirname, '../extension/sidepanel/index.html'); +const cssPath = path.join(__dirname, '../extension/sidepanel/sidepanel.css'); + +assert.ok(fs.existsSync(htmlPath), 'index.html must exist'); +assert.ok(fs.existsSync(cssPath), 'sidepanel.css must exist'); + +const html = fs.readFileSync(htmlPath, 'utf8'); +const css = fs.readFileSync(cssPath, 'utf8'); + +// HTML structure assertions +assert.ok(html.includes('id="ready-state"'), 'Ready state panel must exist'); +assert.ok(html.includes('id="loading-state"'), 'Loading state panel must exist'); +assert.ok(html.includes('id="results-state"'), 'Results state panel must exist'); +assert.ok(html.includes('id="settings-drawer"'), 'Settings drawer must exist'); +assert.ok(html.includes('id="audit-btn"'), 'Audit button must exist in HTML'); +assert.ok(html.includes('id="results-container"'), 'Results container must exist'); +assert.ok(html.includes('id="settings-toggle"'), 'Settings toggle button must exist'); +assert.ok(html.includes('id="page-type-tag"'), 'Page type tag must exist'); +assert.ok(html.includes('id="page-title"'), 'Page title heading must exist'); +assert.ok(html.includes('id="loader-status"'), 'Loader status text must exist'); + +// Typography and external font loading +assert.ok(html.includes('Source+Serif+4') || html.includes('Source Serif 4'), 'HTML must load Source Serif 4'); +assert.ok(html.includes('Public+Sans') || html.includes('Public Sans'), 'HTML must load Public Sans'); +assert.ok(html.includes('JetBrains+Mono') || html.includes('JetBrains Mono'), 'HTML must load JetBrains Mono'); + +// CSS Token assertions from DESIGN.md +assert.ok(css.includes('--bg: #faf8f3'), 'CSS must include ivory background token (#faf8f3) from DESIGN.md'); +assert.ok(css.includes('--raised: #ffffff'), 'CSS must include raised surface token (#ffffff) from DESIGN.md'); +assert.ok(css.includes('--ink: #1a1815'), 'CSS must include primary ink token (#1a1815) from DESIGN.md'); +assert.ok(css.includes('--rule: #e6e0d2'), 'CSS must include hairline rule token (#e6e0d2) from DESIGN.md'); +assert.ok(css.includes('--rule-strong: #d4cdb9'), 'CSS must include stronger divider token (#d4cdb9) from DESIGN.md'); + +// Canonical Tier Tokens from DESIGN.md +assert.ok(css.includes('#2f7a4a'), 'CSS must use canonical T1 color #2f7a4a'); +assert.ok(css.includes('#2864a8'), 'CSS must use canonical T2 color #2864a8'); +assert.ok(css.includes('#a87726'), 'CSS must use canonical T3 color #a87726'); +assert.ok(css.includes('#b35a1f'), 'CSS must use canonical T4 color #b35a1f'); +assert.ok(css.includes('#6b6b6b'), 'CSS must use canonical T5 color #6b6b6b'); + +// CSS font family variables +assert.ok(css.includes('Source Serif 4'), 'CSS must use Source Serif 4 typography'); +assert.ok(css.includes('Public Sans'), 'CSS must use Public Sans typography'); +assert.ok(css.includes('JetBrains Mono'), 'CSS must use JetBrains Mono for citations'); + +// Error Card Styles +assert.ok(css.includes('.error-card'), 'CSS must include .error-card class'); +assert.ok(css.includes('.error-actions'), 'CSS must include .error-actions class'); + +// Course Audit Button & Progress Bar in HTML +assert.ok(html.includes('id="audit-course-btn"'), 'audit-course-btn must exist in index.html'); +assert.ok(html.includes('id="crawl-progress-card"'), 'crawl-progress-card must exist in index.html'); +assert.ok(html.includes('id="crawl-status-text"'), 'crawl-status-text must exist in index.html'); +assert.ok(html.includes('https://aistudio.google.com/app/apikey'), 'Link to free Google AI Studio key must exist in Settings'); + +// Progress Card & Help Link Styles +assert.ok(css.includes('.progress-card'), 'CSS must include .progress-card class'); +assert.ok(css.includes('.progress-bar-container'), 'CSS must include .progress-bar-container class'); +assert.ok(css.includes('.progress-bar-fill'), 'CSS must include .progress-bar-fill class'); +assert.ok(css.includes('.help-link'), 'CSS must include .help-link class'); + +// Dossier UI Elements & Styles +assert.ok(html.includes('id="dossier-toggle-btn"'), 'dossier-toggle-btn must exist in header'); +assert.ok(html.includes('id="dossier-count"'), 'dossier-count element must exist'); +assert.ok(html.includes('id="add-to-dossier-btn"'), 'add-to-dossier-btn must exist'); +assert.ok(html.includes('id="export-single-md-btn"'), 'export-single-md-btn must exist'); +assert.ok(html.includes('id="dossier-drawer"'), 'dossier-drawer must exist'); +assert.ok(html.includes('id="export-dossier-md-btn"'), 'export-dossier-md-btn must exist in dossier drawer'); +assert.ok(html.includes('id="copy-dossier-md-btn"'), 'copy-dossier-md-btn must exist in dossier drawer'); +assert.ok(html.includes('id="clear-dossier-btn"'), 'clear-dossier-btn must exist in dossier drawer'); + +assert.ok(css.includes('.dossier-pill'), 'CSS must include .dossier-pill class'); +assert.ok(css.includes('.result-actions-bar'), 'CSS must include .result-actions-bar class'); +assert.ok(css.includes('.dossier-list'), 'CSS must include .dossier-list class'); +assert.ok(css.includes('.dossier-item'), 'CSS must include .dossier-item class'); +assert.ok(css.includes('.dossier-delete-btn'), 'CSS must include .dossier-delete-btn class'); + +console.log('✅ Side panel DOM, progress bar, and CSS token tests passed.'); + + diff --git a/test/test-sidepanel-logic.js b/test/test-sidepanel-logic.js new file mode 100644 index 0000000..06e6568 --- /dev/null +++ b/test/test-sidepanel-logic.js @@ -0,0 +1,180 @@ +const assert = require('assert'); +const { renderAuditHTML, escapeHtml } = require('../extension/sidepanel/renderer-helper.cjs'); + +// Test 1: Full mock audit data rendering +const mockData = { + summary: { + bloomsLevel: 'Analyze', + alignmentScore: 'Strong', + keyTakeaway: 'Great constructive alignment between rubric and lab analysis.' + }, + findings: [ + { + severity: 'suggestion', + tier: 'T1', + citation: '[Assessment-8] Elaborated Feedback', + observation: 'Rubric uses generic grading bands.', + evidence: 'Elaborated criteria increase metacognitive monitoring.', + recommendation: 'Add milestone descriptions for each level.' + }, + { + severity: 'critical', + tier: 'T2', + citation: '[Alignment-3] Direct Assessment', + observation: 'Objectives not measured in final quiz.', + evidence: 'Direct assessment ensures learning outcome achievement.', + recommendation: 'Align quiz items with analysis objectives.' + } + ], + improvedDraft: { + title: 'Rewritten Rubric Matrix', + content: '| Criterion | Exemplary | Developing |\n|---|---|---|' + } +}; + +const rendered = renderAuditHTML(mockData); + +// Check summary rendering +assert.ok(rendered.includes('Analyze'), "Must render Bloom's level"); +assert.ok(rendered.includes('Strong'), "Must render alignment score"); +assert.ok(rendered.includes('Great constructive alignment between rubric and lab analysis.'), "Must render key takeaway"); + +// Check findings count & citations +assert.ok(rendered.includes('Evidence-Based Findings (2)'), 'Must render correct findings count heading'); +assert.ok(rendered.includes('[Assessment-8]'), 'Must render citation for finding 1'); +assert.ok(rendered.includes('[Alignment-3]'), 'Must render citation for finding 2'); + +// Check tier badges & severity classes +assert.ok(rendered.includes('tier-badge tier-t1'), 'Must include tier-t1 badge'); +assert.ok(rendered.includes('tier-badge tier-t2'), 'Must include tier-t2 badge'); +assert.ok(rendered.includes('severity-suggestion'), 'Must include severity-suggestion class'); +assert.ok(rendered.includes('severity-critical'), 'Must include severity-critical class'); + +// Check observation, evidence, recommendation +assert.ok(rendered.includes('Rubric uses generic grading bands.'), 'Must render observation'); +assert.ok(rendered.includes('Elaborated criteria increase metacognitive monitoring.'), 'Must render evidence'); +assert.ok(rendered.includes('Add milestone descriptions for each level.'), 'Must render recommendation'); + +// Check improved draft & 1-click copy button +assert.ok(rendered.includes('Rewritten Rubric Matrix'), 'Must render improved draft title'); +assert.ok(rendered.includes('copy-improved-btn'), 'Must include 1-click copy button'); +assert.ok(rendered.includes('| Criterion | Exemplary | Developing |'), 'Must include draft content in pre/code block'); + +// Check feedback & re-audit controls +assert.ok(rendered.includes('feedback-btn'), 'Must include feedback voting buttons'); +assert.ok(rendered.includes('re-audit-btn'), 'Must include re-audit button'); + +// Test 2: Empty findings list rendering +const emptyData = { + summary: { + bloomsLevel: 'Remember', + alignmentScore: 'Developing', + keyTakeaway: 'Basic knowledge check.' + }, + findings: [], + improvedDraft: { + title: 'No Changes Needed', + content: 'Content is aligned.' + } +}; +const emptyRendered = renderAuditHTML(emptyData); +assert.ok(emptyRendered.includes('Evidence-Based Findings (0)'), 'Must handle 0 findings gracefully'); +assert.ok(emptyRendered.includes('Remember'), 'Must render Bloom\'s level for empty findings data'); + +// Test 3: Null / undefined resilience +assert.strictEqual(renderAuditHTML(null), '', 'Must return empty string for null data'); +assert.strictEqual(renderAuditHTML(undefined), '', 'Must return empty string for undefined data'); + +// Test 4: Partial data resilience +const partialData = { + summary: { + bloomsLevel: 'Evaluate', + alignmentScore: 'Moderate', + keyTakeaway: 'Evaluation needs more rubric detail.' + } +}; +const partialRendered = renderAuditHTML(partialData); +assert.ok(partialRendered.includes('Evaluate'), 'Must handle missing findings and improvedDraft'); +assert.ok(partialRendered.includes('Evidence-Based Findings (0)'), 'Must default findings to 0'); + +// Test 5: escapeHtml unit tests +assert.strictEqual(escapeHtml(''), '<script>alert("xss")</script>'); +assert.strictEqual(escapeHtml("Tom & Jerry's"), 'Tom & Jerry's'); +assert.strictEqual(escapeHtml(null), ''); +assert.strictEqual(escapeHtml(undefined), ''); + +// Test 6: HTML Entity Escaping in all dynamic interpolations +const unsafeData = { + summary: { + bloomsLevel: '', + alignmentScore: '100%', + keyTakeaway: ' & "quotes"' + }, + findings: [ + { + severity: 'warning', + tier: 'T1', + citation: '[Cite-1]', + observation: 'Raw in observation & "quotes"', + evidence: ' in evidence', + recommendation: '

Unsafe Title

', + content: '' + } +}; + +const renderedUnsafe = renderAuditHTML(unsafeData); +assert.ok(!renderedUnsafe.includes('', + pageType: '', + result: { summary: { bloomsLevel: 'Bold', alignmentScore: 'Italic' } } + } +]; +const escapedDossierHtml = renderDossierListHTML(unsafeDossier); +assert.ok(!escapedDossierHtml.includes('