From 89d78eb01e3a2e703d1d167ef9c3bc73c99decde Mon Sep 17 00:00:00 2001 From: juliamuiruri4 <40116776+juliamuiruri4@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:14:43 +0300 Subject: [PATCH] add brainmax canvas extension --- .github/plugin/marketplace.json | 6 + .../.github/plugin/plugin.json | 19 + extensions/brainmax-canvas/README.md | 38 + extensions/brainmax-canvas/assets/preview.png | Bin 0 -> 122685 bytes extensions/brainmax-canvas/extension.mjs | 536 +++++++++++ .../brainmax-canvas/lib/http-server.mjs | 164 ++++ extensions/brainmax-canvas/lib/state.mjs | 76 ++ extensions/brainmax-canvas/package.json | 15 + extensions/brainmax-canvas/public/app.js | 428 +++++++++ extensions/brainmax-canvas/public/index.html | 155 +++ extensions/brainmax-canvas/public/styles.css | 906 ++++++++++++++++++ 11 files changed, 2343 insertions(+) create mode 100644 extensions/brainmax-canvas/.github/plugin/plugin.json create mode 100644 extensions/brainmax-canvas/README.md create mode 100644 extensions/brainmax-canvas/assets/preview.png create mode 100644 extensions/brainmax-canvas/extension.mjs create mode 100644 extensions/brainmax-canvas/lib/http-server.mjs create mode 100644 extensions/brainmax-canvas/lib/state.mjs create mode 100644 extensions/brainmax-canvas/package.json create mode 100644 extensions/brainmax-canvas/public/app.js create mode 100644 extensions/brainmax-canvas/public/index.html create mode 100644 extensions/brainmax-canvas/public/styles.css diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index b77c57253..34a1a124d 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -160,6 +160,12 @@ "description": "Quickly swipe through backlog issues to triage decisions like assign, needs-info, defer, close, or ignore.", "version": "1.0.2" }, + { + "name": "brainmax-canvas", + "source": "extensions/brainmax-canvas", + "description": "Interactive concept-mastery dashboard for codebase-grounded BrainMax quizzes, scores and competency reports.", + "version": "1.0.0" + }, { "name": "cast-imaging", "source": "plugins/cast-imaging", diff --git a/extensions/brainmax-canvas/.github/plugin/plugin.json b/extensions/brainmax-canvas/.github/plugin/plugin.json new file mode 100644 index 000000000..5189f9889 --- /dev/null +++ b/extensions/brainmax-canvas/.github/plugin/plugin.json @@ -0,0 +1,19 @@ +{ + "name": "brainmax-canvas", + "description": "Interactive concept-mastery dashboard for codebase-grounded BrainMax quizzes, scores and competency reports.", + "version": "1.0.0", + "author": { + "name": "Julia Muiruri", + "url": "https://github.com/juliamuiruri4" + }, + "keywords": [ + "assessment", + "student-developer", + "developer-education", + "quiz", + "conceptual-understanding", + "skills-mastery" + ], + "logo": "assets/preview.png", + "extensions": "." +} diff --git a/extensions/brainmax-canvas/README.md b/extensions/brainmax-canvas/README.md new file mode 100644 index 000000000..bac09ae9e --- /dev/null +++ b/extensions/brainmax-canvas/README.md @@ -0,0 +1,38 @@ +# BrainMax Canvas + +BrainMax Canvas is the interactive dashboard for codebase-grounded concept-mastery quizes. It presents detected knowledge domains, accepts freeform answers, shows rubric scores, and compiles completed domains into a competency report. + +The Canvas is an optional visual layer. Question generation and scoring stay with the [BrainMaxxing Agent Skills](https://gh.io/brainmaxxing/skills), and the skills continue to work in clients without Canvas support. + +![BrainMax domain selection](assets/preview.png) + +## Install + +Install the Brainmax Canvas extension through GitHub Copilot or place this directory at `.github/extensions/brainmax-canvas/` in a project. + +Install the companion Brainmaxxing Agent Skills separately: + +```bash +npx skills add juliamuiruri4/brainmaxxing +``` + +**Reload your extensions and skills.** + +## How to use + +1. In a new agent session, invoke `/brainmax` in a repository. +1. Wait while BrainMax detects the knowledge domains represented in the codebase, and opens the Canvas. +1. Select a domain in the Canvas. +1. Answer each code-grounded question in the Canvas. Answers are relayed to the active session for scoring. +1. Pick another domain or compile a cross-domain report. + +## Architecture + +- `extension.mjs` declares the Canvas and validates state transitions. +- `lib/state.mjs` owns per-instance quiz state and score-tier helpers. +- `lib/http-server.mjs` serves the UI and streams state over server-sent events. +- `public/` contains the dependency-free browser interface. + +## License + +MIT diff --git a/extensions/brainmax-canvas/assets/preview.png b/extensions/brainmax-canvas/assets/preview.png new file mode 100644 index 0000000000000000000000000000000000000000..682f6bd7931acdc79e1767df522a83e3144df34f GIT binary patch literal 122685 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 literal 0 HcmV?d00001 diff --git a/extensions/brainmax-canvas/extension.mjs b/extensions/brainmax-canvas/extension.mjs new file mode 100644 index 000000000..19b13b68f --- /dev/null +++ b/extensions/brainmax-canvas/extension.mjs @@ -0,0 +1,536 @@ +// Extension: brainmax-canvas +// +// Canvas UI for the BrainMax quiz orchestrator (https://gh.io/brainmaxxing/skills). +// The canvas displays quiz state and accepts freeform answers, but it never +// scores answers itself. The agent drives it through the actions declared +// below; canvas answers are relayed into normal chat, where the domain skill +// scores them and reports back via `record_score` / `complete_domain` / +// `show_report`. +// +// See README.md in this directory for the full action contract with +// example payloads. + +import { joinSession, createCanvas } from "@github/copilot-sdk/extension"; +import { randomUUID } from "node:crypto"; +import { startInstanceServer } from "./lib/http-server.mjs"; +import { getState, deleteState, tierForScore, tierForPercentage, TIER_LABELS } from "./lib/state.mjs"; + +/** @type {Map>>} */ +const servers = new Map(); +/** @type {Map} */ +const stateCleanupTimers = new Map(); +const STATE_RETENTION_MS = 30 * 60 * 1000; +const QUESTION_TYPES = new Set(["Explain", "Predict", "Refactor", "Debug"]); + +function isNonEmptyString(value) { + return typeof value === "string" && value.trim().length > 0; +} + +function isValidQuestion(question, total, expectedIndex) { + return Boolean( + question + && Number.isInteger(question.index) + && question.index === expectedIndex + && question.total === total + && isNonEmptyString(question.prompt) + && QUESTION_TYPES.has(question.type), + ); +} + +async function ensureServer(instanceId, session) { + let entry = servers.get(instanceId); + if (entry) return entry; + + const cleanupTimer = stateCleanupTimers.get(instanceId); + if (cleanupTimer) { + clearTimeout(cleanupTimer); + stateCleanupTimers.delete(instanceId); + } + + entry = await startInstanceServer( + instanceId, + () => getState(instanceId), + (event) => handleClientEvent(instanceId, session, event), + ); + servers.set(instanceId, entry); + return entry; +} + +/** Handle an interaction posted from the browser back into chat. */ +function handleClientEvent(instanceId, session, event) { + switch (event.type) { + case "select-domain": { + const state = getState(instanceId); + if (state.view !== "domains") { + return { ok: false, error: "Return to domain selection before starting another quiz." }; + } + const domain = state.domains.find((candidate) => candidate.id === event.domainId); + if (!domain) return { ok: false, error: "That knowledge area is not available." }; + if (state.domainSelectionStatus === "submitting") { + return { ok: false, error: "A quiz is already being started." }; + } + + const prompt = `Start the ${domain.name} quiz.`; + state.domainSelectionStatus = "submitting"; + state.domainSelectionError = null; + state.announcement = `Starting ${domain.name} quiz.`; + servers.get(instanceId)?.broadcastState(); + // Avoid calling session.send synchronously from an event-loop tick + // that originated outside the normal turn flow. + setTimeout(async () => { + try { + await session.send({ prompt }); + } catch (err) { + const currentState = getState(instanceId); + if (currentState.domainSelectionStatus !== "submitting") return; + currentState.domainSelectionStatus = "error"; + currentState.domainSelectionError = "The quiz could not be started. Try selecting the domain again."; + currentState.announcement = currentState.domainSelectionError; + servers.get(instanceId)?.broadcastState(); + console.error("Failed to start BrainMax quiz", err); + } + }, 0); + return { ok: true }; + } + case "submit-answer": { + const state = getState(instanceId); + const answer = typeof event.answer === "string" ? event.answer.trim() : ""; + if (state.view !== "quiz" || !state.question || event.questionId !== state.question.id) { + return { ok: false, error: "This question is no longer active. Review the current question and try again." }; + } + if (!answer) { + return { ok: false, error: "Enter an answer before submitting." }; + } + if (answer.length > 8000) { + return { ok: false, error: "Keep your answer under 8,000 characters." }; + } + if (state.answerStatus === "submitting" || state.answerStatus === "scored") { + return { ok: false, error: "An answer has already been submitted for this question." }; + } + + const question = state.question; + const domainName = state.quiz?.domainName || "BrainMax"; + state.answerStatus = "submitting"; + state.answerError = null; + state.lastScore = null; + state.announcement = `Answer submitted for question ${question.index}.`; + servers.get(instanceId)?.broadcastState(); + + setTimeout(async () => { + try { + await session.send({ + prompt: `Answer to ${domainName} question ${question.index} of ${question.total}:\n\n${answer}`, + }); + } catch (err) { + const currentState = getState(instanceId); + if (currentState.question?.id !== question.id || currentState.answerStatus !== "submitting") return; + currentState.answerStatus = "error"; + currentState.answerError = "Your answer could not be sent to Copilot. Try again."; + currentState.announcement = currentState.answerError; + servers.get(instanceId)?.broadcastState(); + console.error("Failed to submit BrainMax answer", err); + } + }, 0); + return { ok: true }; + } + case "choose-another-domain": { + const state = getState(instanceId); + if (state.view !== "summary" && state.view !== "report") { + return { ok: false, error: "Finish the active quiz before choosing another domain." }; + } + state.view = "domains"; + state.question = null; + state.lastScore = null; + state.quiz = null; + state.domainSelectionStatus = "idle"; + state.domainSelectionError = null; + state.reportRequestStatus = "idle"; + state.reportRequestError = null; + state.announcement = "Back to domain selection. Choose a knowledge area to start another quiz."; + servers.get(instanceId)?.broadcastState(); + return { ok: true }; + } + case "compile-report": { + const state = getState(instanceId); + if (state.view !== "summary" || state.completed.length === 0) { + return { ok: false, error: "Complete a domain before compiling the report." }; + } + if (state.reportRequestStatus === "submitting") { + return { ok: false, error: "The report is already being compiled." }; + } + state.reportRequestStatus = "submitting"; + state.reportRequestError = null; + state.announcement = "Compiling competency report."; + servers.get(instanceId)?.broadcastState(); + setTimeout(async () => { + try { + await session.send({ prompt: "Compile report" }); + } catch (err) { + const currentState = getState(instanceId); + if (currentState.reportRequestStatus !== "submitting") return; + currentState.reportRequestStatus = "error"; + currentState.reportRequestError = "The report could not be requested. Try again."; + currentState.announcement = currentState.reportRequestError; + servers.get(instanceId)?.broadcastState(); + console.error("Failed to compile BrainMax report", err); + } + }, 0); + return { ok: true }; + } + default: + return { ok: false, error: "Unknown canvas event." }; + } +} + +const session = await joinSession({ + canvases: [ + createCanvas({ + id: "brainmax-canvas", + displayName: "BrainMax", + description: + "Interactive dashboard for the BrainMax concept-mastery quiz: shows detected domains, accepts freeform answers, tracks live quiz progress and running score, and displays the final competency report. Opening this Canvas does not populate it. After opening, invoke set_domains before responding in chat; then drive it via start_quiz / set_question / record_score / complete_domain / show_report.", + actions: [ + { + name: "set_domains", + description: + "MANDATORY immediately after opening BrainMax: render the detected-domain selection screen before sending the domain list in chat. Opening the Canvas alone does not populate it. Only include domains that were actually detected.", + inputSchema: { + type: "object", + required: ["domains"], + properties: { + domains: { + type: "array", + items: { + type: "object", + required: ["id", "name"], + properties: { + id: { type: "string", description: "Skill slug, e.g. 'api-design'" }, + name: { type: "string", description: "Display name, e.g. 'API Design'" }, + }, + }, + }, + }, + }, + handler: async (ctx) => { + const state = getState(ctx.instanceId); + const domains = ctx.input?.domains; + if (!Array.isArray(domains) || domains.some((domain) => !isNonEmptyString(domain?.id) || !isNonEmptyString(domain?.name))) { + return { ok: false, error: "Each detected domain requires a non-empty id and name." }; + } + const normalizedDomains = domains.map((domain) => ({ id: domain.id.trim(), name: domain.name.trim() })); + if (new Set(normalizedDomains.map((domain) => domain.id)).size !== normalizedDomains.length) { + return { ok: false, error: "Detected domain ids must be unique." }; + } + state.domains = normalizedDomains; + state.view = "domains"; + state.domainSelectionStatus = "idle"; + state.domainSelectionError = null; + state.reportRequestStatus = "idle"; + state.reportRequestError = null; + state.announcement = `${state.domains.length} knowledge areas detected.`; + servers.get(ctx.instanceId)?.broadcastState(); + return { ok: true }; + }, + }, + { + name: "show_domains", + description: + "Return to the domain selection screen using the domains already sent via set_domains (e.g. after a student finishes one domain and wants to pick another).", + handler: async (ctx) => { + const state = getState(ctx.instanceId); + state.view = "domains"; + state.question = null; + state.lastScore = null; + state.quiz = null; + state.domainSelectionStatus = "idle"; + state.domainSelectionError = null; + state.reportRequestStatus = "idle"; + state.reportRequestError = null; + servers.get(ctx.instanceId)?.broadcastState(); + return { ok: true }; + }, + }, + { + name: "start_quiz", + description: "MANDATORY before presenting question 1 in chat: atomically start the selected quiz and display the exact first question. Wait for success before sending the same question in chat.", + inputSchema: { + type: "object", + required: ["domainId", "domainName", "total", "firstQuestion"], + properties: { + domainId: { type: "string" }, + domainName: { type: "string" }, + total: { type: "number", description: "Total questions in this quiz, typically 5." }, + firstQuestion: { + type: "object", + required: ["index", "total", "prompt", "type"], + properties: { + index: { type: "number", description: "Must be 1." }, + total: { type: "number", description: "Must match the quiz total." }, + prompt: { type: "string", minLength: 1 }, + type: { type: "string", enum: ["Explain", "Predict", "Refactor", "Debug"] }, + }, + }, + }, + }, + handler: async (ctx) => { + const state = getState(ctx.instanceId); + const { domainId, domainName, total, firstQuestion } = ctx.input || {}; + const domain = state.domains.find((candidate) => candidate.id === domainId); + if (!domain || domain.name !== domainName) { + return { ok: false, error: "start_quiz must target a detected domain." }; + } + if (!Number.isInteger(total) || total < 1 || total > 20 || !isValidQuestion(firstQuestion, total, 1)) { + return { ok: false, error: "start_quiz requires a valid Question 1 and a matching total from 1 to 20." }; + } + state.quiz = { + domainId, + domainName, + total: total ?? 5, + index: 1, + runningScore: 0, + runningMax: 0, + history: [], + }; + state.question = { ...firstQuestion, id: randomUUID() }; + state.domainSelectionStatus = "idle"; + state.domainSelectionError = null; + state.answerStatus = "idle"; + state.answerError = null; + state.lastScore = null; + state.summary = null; + state.view = "quiz"; + state.announcement = `Starting ${domainName} quiz.`; + servers.get(ctx.instanceId)?.broadcastState(); + return { ok: true }; + }, + }, + { + name: "set_question", + description: "MANDATORY before presenting each question in chat: show the exact same current question in the Canvas and wait for this action to succeed.", + inputSchema: { + type: "object", + required: ["index", "total", "prompt", "type"], + properties: { + index: { type: "number", description: "1-based question number." }, + total: { type: "number" }, + prompt: { type: "string", description: "The question text, grounded in the student's code." }, + type: { type: "string", enum: ["Explain", "Predict", "Refactor", "Debug"] }, + }, + }, + handler: async (ctx) => { + const state = getState(ctx.instanceId); + const { index, total, prompt, type } = ctx.input || {}; + const expectedIndex = (state.quiz?.history.length ?? 0) + 1; + const question = { index, total, prompt, type }; + if (!state.quiz || state.answerStatus !== "scored" || index !== expectedIndex) { + return { ok: false, error: "Score the active question before advancing to the next one." }; + } + if (index > state.quiz.total || !isValidQuestion(question, state.quiz.total, expectedIndex)) { + return { ok: false, error: "The next question must use the quiz total and next in-range index." }; + } + state.question = { id: randomUUID(), ...question }; + state.answerStatus = "idle"; + state.answerError = null; + if (state.quiz) state.quiz.index = index; + state.view = "quiz"; + state.announcement = `Question ${index} of ${total}: ${type}.`; + servers.get(ctx.instanceId)?.broadcastState(); + return { ok: true }; + }, + }, + { + name: "record_score", + description: + "Reveal the score for the question just answered (0-3 rubric) with a one-sentence explanation. For questions 1-4, immediately follow this successful action with set_question for the next question before responding in chat. For question 5, follow with complete_domain.", + inputSchema: { + type: "object", + required: ["index", "score", "feedback"], + properties: { + index: { type: "number" }, + score: { type: "number", minimum: 0, maximum: 3 }, + feedback: { type: "string", description: "One-sentence explanation of the score." }, + }, + }, + handler: async (ctx) => { + const state = getState(ctx.instanceId); + const { index, score, feedback } = ctx.input || {}; + if (!state.quiz || !state.question || index !== state.question.index || state.answerStatus !== "submitting") { + return { ok: false, error: "record_score must target the active question after its answer is submitted." }; + } + if (!Number.isInteger(score) || score < 0 || score > 3 || !isNonEmptyString(feedback)) { + return { ok: false, error: "record_score requires an integer score from 0 to 3 and non-empty feedback." }; + } + const tier = tierForScore(score); + state.lastScore = { index, score, max: 3, feedback, tier, tierLabel: TIER_LABELS[tier] }; + state.answerStatus = "scored"; + state.answerError = null; + if (state.quiz) { + state.quiz.runningScore += score; + state.quiz.runningMax += 3; + state.quiz.history.push({ index, score, tier }); + } + state.announcement = `Question ${index} scored ${score} of 3: ${TIER_LABELS[tier]}.`; + servers.get(ctx.instanceId)?.broadcastState(); + return { ok: true }; + }, + }, + { + name: "complete_domain", + description: "MANDATORY after Question 5: show the domain summary with non-empty strongestArea and gap analysis before presenting the summary in chat.", + inputSchema: { + type: "object", + required: ["domainId", "domainName", "total", "max", "percentage", "strongestArea", "gap"], + properties: { + domainId: { type: "string" }, + domainName: { type: "string" }, + total: { type: "number", description: "Total points earned." }, + max: { type: "number", description: "Max possible points." }, + percentage: { type: "number" }, + strongestArea: { type: "string", minLength: 1 }, + gap: { type: "string", minLength: 1 }, + }, + }, + handler: async (ctx) => { + const state = getState(ctx.instanceId); + const input = ctx.input || {}; + if ( + !state.quiz + || state.answerStatus !== "scored" + || state.quiz.domainId !== input.domainId + || state.quiz.domainName !== input.domainName + || state.quiz.history.length !== state.quiz.total + ) { + return { ok: false, error: "Complete and score every question before completing the domain." }; + } + if (input.total !== state.quiz.runningScore || input.max !== state.quiz.runningMax) { + return { ok: false, error: "Domain totals must match the recorded Canvas scores." }; + } + if (!isNonEmptyString(input.strongestArea) || !isNonEmptyString(input.gap)) { + return { ok: false, error: "Domain summaries require a strongest area and a gap analysis." }; + } + const percentage = input.max === 0 ? 0 : (input.total / input.max) * 100; + state.summary = { ...input, percentage, tier: tierForPercentage(percentage) }; + const existingIdx = state.completed.findIndex((d) => d.id === input.domainId); + const entry = { + id: input.domainId, + name: input.domainName, + score: input.total, + max: input.max, + percentage, + tier: tierForPercentage(percentage), + strongestArea: input.strongestArea, + gap: input.gap, + }; + if (existingIdx >= 0) state.completed[existingIdx] = entry; + else state.completed.push(entry); + state.view = "summary"; + state.announcement = `${input.domainName} complete: ${percentage}%.`; + servers.get(ctx.instanceId)?.broadcastState(); + return { ok: true }; + }, + }, + { + name: "show_report", + description: "MANDATORY after the student says 'compile report': display the complete competency report before presenting the same report in chat. Recorded Canvas scores are authoritative.", + inputSchema: { + type: "object", + required: ["overallScore", "overallMax", "overallPercentage", "domains", "strongestAreas", "priorityAreas", "recommendations", "nextChallenge"], + properties: { + overallScore: { type: "number" }, + overallMax: { type: "number" }, + overallPercentage: { type: "number" }, + domains: { + type: "array", + items: { + type: "object", + required: ["name", "score", "max", "percentage"], + properties: { + name: { type: "string" }, + score: { type: "number" }, + max: { type: "number" }, + percentage: { type: "number" }, + }, + }, + }, + strongestAreas: { type: "array", items: { type: "string" } }, + priorityAreas: { + type: "array", + items: { + type: "object", + required: ["name", "concepts"], + properties: { + name: { type: "string" }, + concepts: { type: "array", items: { type: "string" } }, + }, + }, + }, + recommendations: { type: "array", items: { type: "string" } }, + nextChallenge: { type: "string", minLength: 1 }, + }, + }, + handler: async (ctx) => { + const state = getState(ctx.instanceId); + if (state.completed.length === 0) { + return { ok: false, error: "Complete at least one domain before compiling a report." }; + } + if (!isNonEmptyString(ctx.input?.nextChallenge)) { + return { ok: false, error: "The report requires a concrete next challenge." }; + } + const domains = state.completed.map(({ name, score, max, percentage }) => ({ name, score, max, percentage })); + const overallScore = domains.reduce((sum, domain) => sum + domain.score, 0); + const overallMax = domains.reduce((sum, domain) => sum + domain.max, 0); + const overallPercentage = overallMax === 0 ? 0 : (overallScore / overallMax) * 100; + // The agent's qualitative lists are authoritative here: the schema + // already requires them and they mirror the chat report. Scores and + // the domain table stay server-derived from recorded Canvas results. + const strongestAreas = Array.isArray(ctx.input?.strongestAreas) + ? ctx.input.strongestAreas.filter(isNonEmptyString) + : []; + const priorityAreas = Array.isArray(ctx.input?.priorityAreas) + ? ctx.input.priorityAreas + .filter((area) => area && isNonEmptyString(area.name)) + .map((area) => ({ + name: area.name, + concepts: Array.isArray(area.concepts) ? area.concepts.filter(isNonEmptyString) : [], + })) + : []; + state.report = { + ...ctx.input, + overallScore, + overallMax, + overallPercentage, + domains, + strongestAreas, + priorityAreas, + }; + state.reportRequestStatus = "idle"; + state.reportRequestError = null; + state.view = "report"; + state.announcement = `Report ready: ${Math.round(overallPercentage)}% overall.`; + servers.get(ctx.instanceId)?.broadcastState(); + return { ok: true }; + }, + }, + ], + open: async (ctx) => { + const entry = await ensureServer(ctx.instanceId, session); + return { title: "BrainMax", url: entry.url }; + }, + onClose: async (ctx) => { + const entry = servers.get(ctx.instanceId); + if (entry) { + servers.delete(ctx.instanceId); + await entry.close(); + } + const existingTimer = stateCleanupTimers.get(ctx.instanceId); + if (existingTimer) clearTimeout(existingTimer); + const cleanupTimer = setTimeout(() => { + deleteState(ctx.instanceId); + stateCleanupTimers.delete(ctx.instanceId); + }, STATE_RETENTION_MS); + cleanupTimer.unref?.(); + stateCleanupTimers.set(ctx.instanceId, cleanupTimer); + }, + }), + ], +}); diff --git a/extensions/brainmax-canvas/lib/http-server.mjs b/extensions/brainmax-canvas/lib/http-server.mjs new file mode 100644 index 000000000..410709ec6 --- /dev/null +++ b/extensions/brainmax-canvas/lib/http-server.mjs @@ -0,0 +1,164 @@ +// Local HTTP server for one BrainMax canvas instance: serves the static +// frontend (public/), streams state updates to it over Server-Sent Events, +// and accepts interactions from the page to relay back into chat +// via session.send(). + +import { createServer } from "node:http"; +import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const PUBLIC_DIR = path.join(__dirname, "..", "public"); + +const MIME = { + ".html": "text/html; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", +}; +const MAX_EVENT_BODY_BYTES = 16 * 1024; +const SSE_HEARTBEAT_MS = 20_000; + +function writeHeaders(res, status, contentType, extra = {}) { + res.writeHead(status, { + "Content-Type": contentType, + "Cache-Control": "no-store", + "X-Content-Type-Options": "nosniff", + "Cross-Origin-Resource-Policy": "same-origin", + "Content-Security-Policy": "default-src 'self'; connect-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self'; base-uri 'none'; form-action 'self'", + ...extra, + }); +} + +/** + * @param {string} instanceId + * @param {import("./state.mjs").InstanceState} getState - fn returning current state + * @param {(event: {type: string, [k: string]: unknown}) => void} onClientEvent + */ +export async function startInstanceServer(instanceId, getStateFn, onClientEvent) { + /** @type {Set} */ + const sseClients = new Set(); + + function broadcastState() { + const payload = `data: ${JSON.stringify(getStateFn())}\n\n`; + for (const res of sseClients) { + res.write(payload); + } + } + + const heartbeat = setInterval(() => { + for (const res of sseClients) { + res.write(`: heartbeat ${Date.now()}\n\n`); + } + }, SSE_HEARTBEAT_MS); + heartbeat.unref?.(); + + const server = createServer(async (req, res) => { + try { + const url = new URL(req.url, "http://localhost"); + + if (url.pathname === "/events" && req.method === "GET") { + writeHeaders(res, 200, "text/event-stream", { Connection: "keep-alive" }); + res.write(`data: ${JSON.stringify(getStateFn())}\n\n`); + sseClients.add(res); + req.on("close", () => sseClients.delete(res)); + return; + } + + if (url.pathname === "/state" && req.method === "GET") { + writeHeaders(res, 200, MIME[".json"]); + res.end(JSON.stringify(getStateFn())); + return; + } + + if (url.pathname === "/event" && req.method === "POST") { + if (!String(req.headers["content-type"] || "").toLowerCase().startsWith("application/json")) { + writeHeaders(res, 415, MIME[".json"]); + res.end(JSON.stringify({ ok: false, error: "content type must be application/json" })); + return; + } + let body = ""; + for await (const chunk of req) { + body += chunk; + if (Buffer.byteLength(body, "utf8") > MAX_EVENT_BODY_BYTES) { + writeHeaders(res, 413, MIME[".json"]); + res.end(JSON.stringify({ ok: false, error: "event payload is too large" })); + return; + } + } + let event; + try { + event = JSON.parse(body || "{}"); + } catch { + writeHeaders(res, 400, MIME[".json"]); + res.end(JSON.stringify({ ok: false, error: "invalid JSON" })); + return; + } + if (!event || typeof event !== "object" || Array.isArray(event)) { + writeHeaders(res, 400, MIME[".json"]); + res.end(JSON.stringify({ ok: false, error: "event payload must be an object" })); + return; + } + const result = onClientEvent(event) || { ok: true }; + writeHeaders(res, result.ok === false ? 400 : 202, MIME[".json"]); + res.end(JSON.stringify(result)); + return; + } + + // Static file serving for everything else, defaulting to index.html. + if (req.method !== "GET" && req.method !== "HEAD") { + writeHeaders(res, 405, "text/plain; charset=utf-8", { Allow: "GET, HEAD" }); + res.end("Method not allowed"); + return; + } + let relPath = url.pathname === "/" ? "/index.html" : url.pathname; + const filePath = path.resolve(PUBLIC_DIR, `.${relPath}`); + const relativePath = path.relative(PUBLIC_DIR, filePath); + if (relativePath.startsWith(`..${path.sep}`) || relativePath === ".." || path.isAbsolute(relativePath)) { + writeHeaders(res, 403, "text/plain; charset=utf-8"); + res.end("Forbidden"); + return; + } + const ext = path.extname(filePath); + const data = await readFile(filePath); + writeHeaders(res, 200, MIME[ext] || "application/octet-stream"); + res.end(req.method === "HEAD" ? undefined : data); + } catch (err) { + if (err && err.code === "ENOENT") { + writeHeaders(res, 404, "text/plain; charset=utf-8"); + res.end("Not found"); + return; + } + console.error(`BrainMax canvas server error for ${instanceId}`, err); + writeHeaders(res, 500, "text/plain; charset=utf-8"); + res.end("Internal server error"); + } + }); + + try { + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + server.off("error", reject); + resolve(); + }); + }); + } catch (error) { + clearInterval(heartbeat); + throw error; + } + const address = server.address(); + const port = typeof address === "object" && address ? address.port : 0; + + return { + url: `http://127.0.0.1:${port}/`, + broadcastState, + close: () => + new Promise((resolve) => { + clearInterval(heartbeat); + for (const res of sseClients) res.end(); + server.close(() => resolve()); + }), + }; +} diff --git a/extensions/brainmax-canvas/lib/state.mjs b/extensions/brainmax-canvas/lib/state.mjs new file mode 100644 index 000000000..f52b2f5a1 --- /dev/null +++ b/extensions/brainmax-canvas/lib/state.mjs @@ -0,0 +1,76 @@ +// In-memory per-instance state for the BrainMax canvas. +// +// Each open canvas instance (identified by instanceId) gets its own state +// object. Action handlers in extension.mjs mutate this state; the HTTP +// server (lib/http-server.mjs) serializes it to connected SSE clients so the +// browser re-renders without polling. + +/** @type {Map} */ +const instances = new Map(); + +/** + * @typedef {Object} InstanceState + * @property {"domains"|"quiz"|"summary"|"report"} view + * @property {Array<{id: string, name: string}>} domains + * @property {Array<{id: string, name: string, score: number, max: number, percentage: number}>} completed + * @property {null|Object} quiz - { domainId, domainName, total, index, runningScore, runningMax, history: [] } + * @property {null|Object} question - { id, index, total, prompt, type } + * @property {"idle"|"submitting"|"error"} domainSelectionStatus + * @property {null|string} domainSelectionError + * @property {"idle"|"submitting"|"scored"|"error"} answerStatus + * @property {null|string} answerError + * @property {null|Object} lastScore - { index, score, max, feedback, tier } (transient reveal) + * @property {null|Object} summary - payload from complete_domain + * @property {null|Object} report - payload from show_report + * @property {"idle"|"submitting"|"error"} reportRequestStatus + * @property {null|string} reportRequestError + * @property {string} announcement - latest aria-live text + */ + +function freshState() { + return { + view: "domains", + domains: [], + completed: [], + quiz: null, + question: null, + domainSelectionStatus: "idle", + domainSelectionError: null, + answerStatus: "idle", + answerError: null, + lastScore: null, + summary: null, + report: null, + reportRequestStatus: "idle", + reportRequestError: null, + announcement: "", + }; +} + +export function getState(instanceId) { + let state = instances.get(instanceId); + if (!state) { + state = freshState(); + instances.set(instanceId, state); + } + return state; +} + +export function deleteState(instanceId) { + instances.delete(instanceId); +} + +/** Map a 0-100 percentage to a score tier index 0-3 (matches shared/scoring-rubric.md bands). */ +export function tierForPercentage(percentage) { + if (percentage >= 90) return 3; + if (percentage >= 70) return 2; + if (percentage >= 50) return 1; + return 0; +} + +/** Map a raw 0-3 question score directly to its tier (same scale, no banding needed). */ +export function tierForScore(score) { + return Math.max(0, Math.min(3, Math.round(score))); +} + +export const TIER_LABELS = ["No understanding", "Recognition", "Application", "Mastery"]; diff --git a/extensions/brainmax-canvas/package.json b/extensions/brainmax-canvas/package.json new file mode 100644 index 000000000..c94e44df3 --- /dev/null +++ b/extensions/brainmax-canvas/package.json @@ -0,0 +1,15 @@ +{ + "name": "brainmax-canvas", + "version": "1.0.0", + "private": true, + "type": "module", + "main": "extension.mjs", + "description": "Canvas UI for codebase-grounded BrainMax concept-mastery quizzes.", + "scripts": { + "check": "node --check extension.mjs && node --check lib/http-server.mjs && node --check lib/state.mjs && node --check public/app.js", + "test": "node --test" + }, + "engines": { + "node": ">=20" + } +} diff --git a/extensions/brainmax-canvas/public/app.js b/extensions/brainmax-canvas/public/app.js new file mode 100644 index 000000000..4d00d2b17 --- /dev/null +++ b/extensions/brainmax-canvas/public/app.js @@ -0,0 +1,428 @@ +// BrainMax canvas frontend — vanilla JS +// Renders whatever state the extension pushes over SSE; posts interactions +// back to the extension, which relays them into chat via session.send(). + +const DOMAIN_CODES = { + "api-design": "API", + "database-design": "DB", + "system-architecture": "SYS", + "implementation-patterns": "IMPL", + "testing-strategy": "TEST", + "security-fundamentals": "SEC", + "devops-and-ci-cd": "CI/CD", + "error-handling-and-resilience": "ERR", + "requirements-and-scope": "REQ", + "domain-modeling": "DDD", + "ui-and-frontend": "UI", + observability: "OBS", +}; + +function domainCode(id, name) { + if (DOMAIN_CODES[id]) return DOMAIN_CODES[id]; + return (name || id || "??").slice(0, 4).toUpperCase(); +} + +function tierClass(tier) { + return `tier-${Math.max(0, Math.min(3, tier ?? 0))}`; +} + +// Mirrors lib/state.mjs#tierForPercentage. Kept in sync deliberately: the +// frontend recomputes from `percentage` instead of trusting a passed-through +// `tier` field, so a payload that omits it still renders the correct color. +function tierForPercentage(percentage) { + if (percentage >= 90) return 3; + if (percentage >= 70) return 2; + if (percentage >= 50) return 1; + return 0; +} + +const TIER_LABELS = ["No understanding", "Recognition", "Application", "Mastery"]; + +async function postEvent(event) { + const response = await fetch("/event", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(event), + }); + const result = await response.json(); + if (!response.ok || result.ok === false) { + throw new Error(result.error || "The canvas event could not be sent."); + } + return result; +} + +function sendEvent(event) { + postEvent(event).catch((err) => console.error("Failed to post event", err)); +} + +const views = { + domains: document.getElementById("view-domains"), + quiz: document.getElementById("view-quiz"), + summary: document.getElementById("view-summary"), + report: document.getElementById("view-report"), +}; + +let lastAnnouncement = ""; +const liveRegion = document.getElementById("live-region"); +const answerForm = document.getElementById("answer-form"); +const answerInput = document.getElementById("answer-input"); +const answerStatus = document.getElementById("answer-status"); +const submitAnswerButton = document.getElementById("btn-submit-answer"); +let latestState = null; +let activeQuestionId = null; +let locallySubmitting = false; +let localAnswerError = ""; + +function announce(text) { + if (!text || text === lastAnnouncement) return; + lastAnnouncement = text; + liveRegion.textContent = text; +} + +function showView(name) { + for (const [key, el] of Object.entries(views)) { + el.hidden = key !== name; + } +} + +function updateAnswerControls() { + const question = latestState?.question; + const serverStatus = localAnswerError ? "error" : latestState?.answerStatus || "idle"; + if (serverStatus === "submitting" || serverStatus === "scored" || serverStatus === "error") { + locallySubmitting = false; + } + const isSubmitting = locallySubmitting || serverStatus === "submitting"; + const isScored = serverStatus === "scored"; + const canEdit = Boolean(question?.id) && !isSubmitting && !isScored; + + answerForm.hidden = !question; + answerInput.disabled = !canEdit; + submitAnswerButton.disabled = !canEdit || !answerInput.value.trim(); + answerStatus.dataset.state = serverStatus; + + if (isSubmitting) { + answerStatus.textContent = "Evaluating your answer…"; + } else if (serverStatus === "error") { + answerStatus.textContent = localAnswerError || latestState?.answerError || "Your answer could not be submitted. Try again."; + } else if (isScored) { + answerStatus.textContent = "Answer scored."; + } else { + answerStatus.textContent = ""; + } +} + +function updateScoreReveal() { + const revealEl = document.getElementById("score-reveal"); + const isSubmitting = locallySubmitting || latestState?.answerStatus === "submitting"; + const score = latestState?.lastScore; + if (!score || isSubmitting) { + revealEl.hidden = true; + return; + } + + revealEl.hidden = false; + const chip = document.getElementById("score-reveal-chip"); + chip.textContent = `${score.score} / 3`; + chip.className = `score-chip mono ${tierClass(score.tier)}`; + document.getElementById("score-reveal-feedback").textContent = + `Question ${score.index}: ${score.tierLabel} — ${score.feedback}`; +} + +function renderDomains(state) { + const grid = document.getElementById("domain-grid"); + const selectionStatus = document.getElementById("domain-selection-status"); + const isStarting = state.domainSelectionStatus === "submitting"; + grid.innerHTML = ""; + selectionStatus.dataset.state = state.domainSelectionStatus; + selectionStatus.textContent = isStarting + ? "Starting quiz…" + : state.domainSelectionStatus === "error" + ? state.domainSelectionError || "The quiz could not be started. Try again." + : ""; + for (const domain of state.domains) { + const completed = state.completed.find((d) => d.id === domain.id); + const btn = document.createElement("button"); + btn.type = "button"; + btn.className = "domain-tile"; + btn.disabled = isStarting; + btn.setAttribute("aria-label", `Start the ${domain.name} quiz`); + + const code = document.createElement("span"); + code.className = "domain-code"; + code.textContent = domainCode(domain.id, domain.name); + + const name = document.createElement("span"); + name.className = "domain-name"; + name.textContent = domain.name; + + btn.append(code, name); + + if (completed) { + const pill = document.createElement("span"); + pill.className = `domain-status-pill ${tierClass(tierForPercentage(completed.percentage))}`; + pill.textContent = `${Math.round(completed.percentage)}%`; + btn.appendChild(pill); + } else { + const status = document.createElement("span"); + status.className = "domain-status"; + status.textContent = "Not started"; + btn.appendChild(status); + } + + btn.addEventListener("click", async () => { + for (const tile of grid.querySelectorAll("button")) tile.disabled = true; + selectionStatus.dataset.state = "submitting"; + selectionStatus.textContent = `Starting ${domain.name} quiz…`; + try { + await postEvent({ type: "select-domain", domainId: domain.id }); + } catch (err) { + for (const tile of grid.querySelectorAll("button")) tile.disabled = false; + selectionStatus.dataset.state = "error"; + selectionStatus.textContent = err instanceof Error ? err.message : "The quiz could not be started. Try again."; + } + }); + + grid.appendChild(btn); + } +} + +function renderQuiz(state) { + const quiz = state.quiz; + if (!quiz) return; + + document.getElementById("quiz-domain-code").textContent = domainCode(quiz.domainId, quiz.domainName); + document.getElementById("quiz-domain-name").textContent = quiz.domainName; + + const question = state.question; + const total = question?.total ?? quiz.total; + const index = question?.index ?? quiz.index; + document.getElementById("quiz-counter").textContent = `Question ${index} of ${total}`; + + // Progress rail + const rail = document.getElementById("progress-rail"); + rail.innerHTML = ""; + const scoredByIndex = new Map(quiz.history.map((h) => [h.index, h.tier])); + for (let i = 1; i <= total; i++) { + const li = document.createElement("li"); + if (scoredByIndex.has(i)) { + li.dataset.state = "scored"; + li.classList.add(tierClass(scoredByIndex.get(i))); + } else if (i === index) { + li.dataset.state = "current"; + } else { + li.dataset.state = "upcoming"; + } + li.setAttribute("aria-label", `Question ${i}${scoredByIndex.has(i) ? " scored" : i === index ? " current" : ""}`); + rail.appendChild(li); + } + + // Question card + const questionCard = document.getElementById("question-card"); + if (question) { + questionCard.dataset.state = "ready"; + document.getElementById("question-type").textContent = question.type; + document.getElementById("question-prompt").textContent = question.prompt; + if (question.id !== activeQuestionId) { + activeQuestionId = question.id; + answerInput.value = ""; + locallySubmitting = false; + localAnswerError = ""; + } + } else { + questionCard.dataset.state = "preparing"; + document.getElementById("question-type").textContent = "Preparing"; + document.getElementById("question-prompt").textContent = "Preparing question…"; + activeQuestionId = null; + answerInput.value = ""; + } + updateAnswerControls(); + updateScoreReveal(); + + document.getElementById("quiz-running-score").textContent = `${quiz.runningScore} / ${quiz.runningMax}`; +} + +function renderSummary(state) { + const summary = state.summary; + if (!summary) return; + + document.getElementById("summary-domain-code").textContent = domainCode(summary.domainId, summary.domainName); + document.getElementById("summary-domain-name").textContent = summary.domainName; + document.getElementById("summary-percentage").textContent = `${Math.round(summary.percentage)}%`; + document.getElementById("summary-fraction").textContent = `${summary.total} / ${summary.max} pts`; + + const tierLabelEl = document.getElementById("summary-tier-label"); + tierLabelEl.textContent = TIER_LABELS[summary.tier] ?? ""; + tierLabelEl.className = `tier-label ${tierClass(summary.tier)}`; + + document.getElementById("summary-strongest").textContent = summary.strongestArea || "—"; + document.getElementById("summary-gap").textContent = summary.gap || "—"; + + const reportButton = document.getElementById("btn-compile-report"); + const reportStatus = document.getElementById("report-request-status"); + const isCompiling = state.reportRequestStatus === "submitting"; + reportButton.disabled = isCompiling; + reportButton.textContent = isCompiling ? "Compiling report…" : "Compile report"; + reportStatus.dataset.state = state.reportRequestStatus || "idle"; + reportStatus.textContent = isCompiling + ? "Building your competency report…" + : state.reportRequestStatus === "error" + ? state.reportRequestError || "The report could not be requested. Try again." + : ""; +} + +function renderReport(state) { + const report = state.report; + if (!report) return; + + const domainCount = report.domains?.length ?? 0; + const verdictEl = document.getElementById("report-verdict"); + const percentageEl = document.createElement("span"); + percentageEl.className = "mono"; + percentageEl.textContent = `${Math.round(report.overallPercentage)}%`; + verdictEl.replaceChildren( + "You scored ", + percentageEl, + ` (${report.overallScore} / ${report.overallMax} pts) across ${domainCount} domain${domainCount === 1 ? "" : "s"}.`, + ); + + const tbody = document.getElementById("report-table-body"); + tbody.innerHTML = ""; + for (const d of report.domains || []) { + const tr = document.createElement("tr"); + const nameCell = document.createElement("td"); + nameCell.textContent = d.name; + const scoreCell = document.createElement("td"); + scoreCell.className = "mono"; + scoreCell.textContent = `${d.score} / ${d.max}`; + const percentageCell = document.createElement("td"); + percentageCell.className = "mono"; + percentageCell.textContent = `${Math.round(d.percentage)}%`; + tr.append(nameCell, scoreCell, percentageCell); + tbody.appendChild(tr); + } + + const strongestEl = document.getElementById("report-strongest"); + strongestEl.innerHTML = ""; + for (const area of report.strongestAreas || []) { + const li = document.createElement("li"); + li.textContent = area; + strongestEl.appendChild(li); + } + + const priorityEl = document.getElementById("report-priority"); + priorityEl.innerHTML = ""; + for (const item of report.priorityAreas || []) { + const div = document.createElement("div"); + div.className = "report-priority-item"; + const h3 = document.createElement("h3"); + h3.textContent = item.name; + div.appendChild(h3); + if (item.concepts?.length) { + const ul = document.createElement("ul"); + for (const concept of item.concepts) { + const li = document.createElement("li"); + li.textContent = concept; + ul.appendChild(li); + } + div.appendChild(ul); + } + priorityEl.appendChild(div); + } + + const recEl = document.getElementById("report-recommendations"); + recEl.innerHTML = ""; + for (const rec of report.recommendations || []) { + const li = document.createElement("li"); + li.textContent = rec; + recEl.appendChild(li); + } + + document.getElementById("report-next-challenge").textContent = report.nextChallenge || ""; +} + +function render(state) { + latestState = state; + showView(state.view); + if (state.view === "domains") renderDomains(state); + if (state.view === "quiz") renderQuiz(state); + if (state.view === "summary") renderSummary(state); + if (state.view === "report") renderReport(state); + announce(state.announcement); +} + +answerInput.addEventListener("input", updateAnswerControls); +answerInput.addEventListener("keydown", (event) => { + if (event.key !== "Enter" || (!event.metaKey && !event.ctrlKey) || submitAnswerButton.disabled) return; + event.preventDefault(); + answerForm.requestSubmit(); +}); +answerForm.addEventListener("submit", async (event) => { + event.preventDefault(); + const answer = answerInput.value.trim(); + const question = latestState?.question; + if (!answer || !question?.id || locallySubmitting) return; + + locallySubmitting = true; + localAnswerError = ""; + updateAnswerControls(); + updateScoreReveal(); + try { + await postEvent({ type: "submit-answer", questionId: question.id, answer }); + } catch (err) { + locallySubmitting = false; + localAnswerError = err instanceof Error ? err.message : "Your answer could not be submitted. Try again."; + updateAnswerControls(); + updateScoreReveal(); + } +}); + +document.getElementById("btn-choose-another").addEventListener("click", () => { + sendEvent({ type: "choose-another-domain" }); +}); +document.getElementById("btn-report-choose-another").addEventListener("click", () => { + sendEvent({ type: "choose-another-domain" }); +}); +document.getElementById("btn-compile-report").addEventListener("click", () => { + sendEvent({ type: "compile-report" }); +}); + +// ---------- Theme toggle ---------- +// Local UI preference only — never sent to the extension/chat. Persisted per +// browser via localStorage so it survives canvas reopens. +const THEME_STORAGE_KEY = "brainmax-canvas-theme"; +const themeToggleBtn = document.getElementById("theme-toggle"); +const themeToggleLabel = document.getElementById("theme-toggle-label"); + +function applyTheme(theme) { + if (theme === "light" || theme === "dark") { + document.documentElement.dataset.theme = theme; + } else { + delete document.documentElement.dataset.theme; + } + const resolved = + theme === "light" || theme === "dark" + ? theme + : window.matchMedia("(prefers-color-scheme: light)").matches + ? "light" + : "dark"; + themeToggleLabel.textContent = resolved.toUpperCase(); +} + +const storedTheme = localStorage.getItem(THEME_STORAGE_KEY); +applyTheme(storedTheme); + +themeToggleBtn.addEventListener("click", () => { + const current = document.documentElement.dataset.theme || (window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark"); + const next = current === "light" ? "dark" : "light"; + localStorage.setItem(THEME_STORAGE_KEY, next); + applyTheme(next); +}); + +const source = new EventSource("/events"); +source.onmessage = (e) => { + const state = JSON.parse(e.data); + render(state); +}; +source.onerror = () => { + // EventSource auto-reconnects; nothing to do here besides letting it retry. +}; diff --git a/extensions/brainmax-canvas/public/index.html b/extensions/brainmax-canvas/public/index.html new file mode 100644 index 000000000..4459a714b --- /dev/null +++ b/extensions/brainmax-canvas/public/index.html @@ -0,0 +1,155 @@ + + + + + + BrainMax + + + +
+
+ + BrainMax + concept mastery check + +
+ +
+ + + + + + + + + + + +
+ +
+
+ + + + diff --git a/extensions/brainmax-canvas/public/styles.css b/extensions/brainmax-canvas/public/styles.css new file mode 100644 index 000000000..e4439b253 --- /dev/null +++ b/extensions/brainmax-canvas/public/styles.css @@ -0,0 +1,906 @@ +/* BrainMax canvas — "assessment terminal" visual system. Tokens mirror + DESIGN.md at the project root; keep them in sync if either changes. */ + +/* ---------- Theme tokens ---------- + Dark is the default identity ("assessment terminal" — a focused evening + check-in). Light is a fully-considered second theme, not an inversion: + different surface strategy (shadow-based elevation vs. lighter-surface + stacking) and darkened brand/tier colors so text-on-color still clears + WCAG AA on a bright surface. Resolution order: explicit [data-theme] + attribute (user toggle, persisted) > prefers-color-scheme > dark default. */ + +:root { + /* Surfaces */ + --bg: oklch(0.09 0 0); + --surface: oklch(0.15 0.012 294); + --surface-2: oklch(0.2 0.014 294); + --border: oklch(0.32 0.018 294); + --border-strong: oklch(0.46 0.03 294); + --shadow-sm: none; + --shadow-md: none; + + /* Text */ + --ink: oklch(0.95 0.006 294); + --muted: oklch(0.7 0.014 294); + --faint: oklch(0.52 0.014 294); + + /* Brand */ + --primary: oklch(0.62 0.16 294); + --primary-ink: oklch(0.99 0 0); + --primary-dim: oklch(0.3 0.05 294); + --accent: oklch(0.75 0.15 75); + --accent-ink: oklch(0.16 0.02 75); + + /* Score tiers (0-3) */ + --tier-0: oklch(0.62 0.19 25); + --tier-1: oklch(0.7 0.15 55); + --tier-2: oklch(0.74 0.12 190); + --tier-3: oklch(0.72 0.14 145); +} + +/* Auto light mode: only applies when the user hasn't explicitly toggled + (no [data-theme] attribute set yet by app.js). */ +@media (prefers-color-scheme: light) { + :root:not([data-theme]) { + --bg: oklch(1 0 0); + --surface: oklch(0.98 0.004 294); + --surface-2: oklch(0.955 0.008 294); + --border: oklch(0.87 0.01 294); + --border-strong: oklch(0.74 0.02 294); + --shadow-sm: 0 1px 2px oklch(0.2 0.02 294 / 0.06); + --shadow-md: 0 4px 16px oklch(0.2 0.02 294 / 0.08); + + --ink: oklch(0.2 0.012 294); + --muted: oklch(0.42 0.016 294); + --faint: oklch(0.55 0.016 294); + + --primary: oklch(0.5 0.17 294); + --primary-ink: oklch(0.99 0 0); + --primary-dim: oklch(0.93 0.025 294); + --accent: oklch(0.48 0.15 75); + --accent-ink: oklch(0.99 0 0); + + --tier-0: oklch(0.52 0.19 25); + --tier-1: oklch(0.55 0.15 55); + --tier-2: oklch(0.5 0.11 190); + --tier-3: oklch(0.48 0.13 145); + } +} + +[data-theme="light"] { + --bg: oklch(1 0 0); + --surface: oklch(0.98 0.004 294); + --surface-2: oklch(0.955 0.008 294); + --border: oklch(0.87 0.01 294); + --border-strong: oklch(0.74 0.02 294); + --shadow-sm: 0 1px 2px oklch(0.2 0.02 294 / 0.06); + --shadow-md: 0 4px 16px oklch(0.2 0.02 294 / 0.08); + + --ink: oklch(0.2 0.012 294); + --muted: oklch(0.42 0.016 294); + --faint: oklch(0.55 0.016 294); + + --primary: oklch(0.5 0.17 294); + --primary-ink: oklch(0.99 0 0); + --primary-dim: oklch(0.93 0.025 294); + --accent: oklch(0.48 0.15 75); + --accent-ink: oklch(0.99 0 0); + + --tier-0: oklch(0.52 0.19 25); + --tier-1: oklch(0.55 0.15 55); + --tier-2: oklch(0.5 0.11 190); + --tier-3: oklch(0.48 0.13 145); +} + +[data-theme="dark"] { + --bg: oklch(0.09 0 0); + --surface: oklch(0.15 0.012 294); + --surface-2: oklch(0.2 0.014 294); + --border: oklch(0.32 0.018 294); + --border-strong: oklch(0.46 0.03 294); + --shadow-sm: none; + --shadow-md: none; + + --ink: oklch(0.95 0.006 294); + --muted: oklch(0.7 0.014 294); + --faint: oklch(0.52 0.014 294); + + --primary: oklch(0.62 0.16 294); + --primary-ink: oklch(0.99 0 0); + --primary-dim: oklch(0.3 0.05 294); + --accent: oklch(0.75 0.15 75); + --accent-ink: oklch(0.16 0.02 75); + + --tier-0: oklch(0.62 0.19 25); + --tier-1: oklch(0.7 0.15 55); + --tier-2: oklch(0.74 0.12 190); + --tier-3: oklch(0.72 0.14 145); +} + +:root { + /* Type */ + --font-sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace; + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1rem; + --text-lg: 1.25rem; + --text-xl: 1.75rem; + --text-2xl: 2.5rem; + + /* Space (4pt scale) */ + --space-xs: 0.25rem; + --space-sm: 0.5rem; + --space-md: 1rem; + --space-lg: 1.5rem; + --space-xl: 3rem; + --space-2xl: 6rem; + + /* Motion */ + --ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); + --duration-fast: 150ms; + --duration-base: 220ms; + --duration-reveal: 260ms; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body { + height: 100%; +} + +:root { + color-scheme: dark; +} + +[data-theme="light"] { + color-scheme: light; +} + +body { + margin: 0; + background: var(--bg); + color: var(--ink); + font-family: var(--font-sans); + font-size: var(--text-base); + line-height: 1.5; + -webkit-font-smoothing: antialiased; + transition: background-color var(--duration-base) var(--ease-out-quart), color var(--duration-base) var(--ease-out-quart); +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +:focus-visible { + outline: 2px solid var(--primary); + outline-offset: 2px; + border-radius: 4px; +} + +.mono { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; +} + +/* ---------- App shell ---------- */ + +.app { + min-height: 100%; + display: flex; + flex-direction: column; +} + +.app-header { + display: flex; + align-items: baseline; + gap: var(--space-sm); + padding: var(--space-lg) var(--space-xl); + border-bottom: 1px solid var(--border); +} + +.app-mark { + color: var(--primary); + font-size: var(--text-lg); +} + +.app-title { + font-weight: 700; + font-size: var(--text-lg); + letter-spacing: -0.01em; +} + +.app-subtitle { + color: var(--muted); + font-size: var(--text-sm); +} + +.theme-toggle { + margin-left: auto; + align-self: center; + display: inline-flex; + align-items: center; + gap: var(--space-xs); + background: transparent; + border: 1px solid var(--border); + color: var(--muted); + font-family: var(--font-mono); + font-size: var(--text-xs); + letter-spacing: 0.04em; + border-radius: 999px; + padding: 0.35em 0.8em; + cursor: pointer; + transition: border-color var(--duration-fast) var(--ease-out-quart), color var(--duration-fast) var(--ease-out-quart); +} + +.theme-toggle:hover, +.theme-toggle:focus-visible { + border-color: var(--border-strong); + color: var(--ink); +} + +/* ---------- View stack (crossfade router) ---------- */ + +.view-stack { + flex: 1; + position: relative; + padding: var(--space-xl); +} + +.view { + max-width: 760px; + margin: 0 auto; + animation: view-enter var(--duration-base) var(--ease-out-quart); +} + +@keyframes view-enter { + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.view-heading { + font-size: var(--text-xl); + font-weight: 700; + letter-spacing: -0.01em; + margin: 0 0 var(--space-sm); +} + +.view-lede { + color: var(--muted); + max-width: 62ch; + margin: 0 0 var(--space-xl); +} + +.mono-tag { + display: inline-block; + font-family: var(--font-mono); + font-size: var(--text-xs); + font-weight: 600; + letter-spacing: 0.06em; + color: var(--primary); + background: var(--primary-dim); + border: 1px solid var(--border); + border-radius: 4px; + padding: 0.15em 0.5em; + margin-bottom: var(--space-sm); +} + +/* ---------- Domain grid ---------- */ + +.domain-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: var(--space-md); +} + +.domain-tile { + text-align: left; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 10px; + padding: var(--space-lg); + cursor: pointer; + color: inherit; + font: inherit; + display: flex; + flex-direction: column; + gap: var(--space-sm); + box-shadow: var(--shadow-sm); + transition: transform var(--duration-fast) var(--ease-out-quart), border-color var(--duration-fast) var(--ease-out-quart), + background var(--duration-fast) var(--ease-out-quart), box-shadow var(--duration-fast) var(--ease-out-quart); +} + +.domain-tile:hover, +.domain-tile:focus-visible { + border-color: var(--border-strong); + background: var(--surface-2); + transform: scale(1.02); + box-shadow: var(--shadow-md); +} + +.domain-tile:active { + transform: scale(0.99); +} + +.domain-code { + font-family: var(--font-mono); + font-size: var(--text-xs); + font-weight: 600; + letter-spacing: 0.08em; + color: var(--muted); +} + +.domain-name { + font-weight: 600; + font-size: var(--text-base); +} + +.domain-status { + font-size: var(--text-sm); + color: var(--faint); +} + +.domain-status-pill { + display: inline-flex; + align-items: center; + gap: var(--space-xs); + font-family: var(--font-mono); + font-size: var(--text-xs); + font-weight: 600; + color: var(--primary-ink); + padding: 0.2em 0.6em; + border-radius: 999px; + width: fit-content; +} + +.domain-tile:disabled { + cursor: wait; + opacity: 0.65; + transform: none; +} + +.domain-selection-status { + min-height: 1.5rem; + margin: var(--space-md) 0 0; + color: var(--muted); + font-size: var(--text-sm); +} + +.domain-selection-status[data-state="error"] { + color: var(--tier-0); +} + +.report-request-status { + min-height: 1.5rem; + margin: var(--space-sm) 0 0; + color: var(--muted); + font-size: var(--text-sm); + text-align: right; +} + +.report-request-status[data-state="error"] { + color: var(--tier-0); +} + +/* ---------- Quiz view ---------- */ + +.quiz-header { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: var(--space-md); + margin-bottom: var(--space-lg); +} + +.quiz-header-labels .view-heading { + margin-bottom: 0; +} + +.quiz-counter { + font-size: var(--text-sm); + color: var(--muted); + white-space: nowrap; +} + +.progress-rail { + display: flex; + gap: var(--space-sm); + list-style: none; + padding: 0; + margin: 0 0 var(--space-xl); +} + +.progress-rail li { + width: 14px; + height: 14px; + border-radius: 50%; + border: 2px solid var(--border-strong); + background: transparent; + transition: background var(--duration-base) var(--ease-out-quart), border-color var(--duration-base) var(--ease-out-quart), + transform var(--duration-base) var(--ease-out-quart); +} + +.progress-rail li[data-state="current"] { + border-color: var(--primary); + box-shadow: 0 0 0 3px var(--primary-dim); +} + +.progress-rail li[data-state="scored"] { + border-color: transparent; + transform: scale(1.05); +} + +.question-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 12px; + padding: var(--space-xl); + margin-bottom: var(--space-lg); + box-shadow: var(--shadow-sm); +} + +.question-card[data-state="preparing"] { + color: var(--muted); +} + +.question-type-tag { + display: inline-block; + font-family: var(--font-mono); + font-size: var(--text-xs); + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--accent); + margin-bottom: var(--space-md); +} + +.question-prompt { + font-size: var(--text-lg); + line-height: 1.5; + margin: 0; + max-width: 62ch; + overflow-wrap: anywhere; +} + +.answer-form { + margin-bottom: var(--space-lg); +} + +.answer-label { + display: block; + margin-bottom: var(--space-sm); + color: var(--muted); + font-size: var(--text-sm); + font-weight: 600; +} + +.answer-input { + display: block; + width: 100%; + min-height: 9rem; + resize: vertical; + padding: var(--space-md); + border: 1px solid var(--border-strong); + border-radius: 8px; + background: var(--surface); + color: var(--ink); + font: inherit; + line-height: 1.5; + box-shadow: var(--shadow-sm); + transition: border-color var(--duration-fast) var(--ease-out-quart), background var(--duration-fast) var(--ease-out-quart); +} + +.answer-input::placeholder { + color: var(--faint); +} + +.answer-input:hover:not(:disabled) { + border-color: var(--primary); +} + +.answer-input:disabled { + background: var(--surface-2); + color: var(--muted); + cursor: not-allowed; +} + +.answer-actions { + min-height: 2.75rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-md); + margin-top: var(--space-sm); +} + +.answer-status { + margin: 0; + color: var(--muted); + font-size: var(--text-sm); +} + +.answer-status[data-state="error"] { + color: var(--tier-0); +} + +.score-reveal { + display: flex; + align-items: center; + gap: var(--space-md); + background: var(--surface); + border: 1px solid var(--border); + border-radius: 12px; + padding: var(--space-md) var(--space-lg); + margin-bottom: var(--space-lg); + box-shadow: var(--shadow-md); + animation: score-reveal-in var(--duration-reveal) var(--ease-out-quart); +} + +@keyframes score-reveal-in { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.score-chip { + flex-shrink: 0; + font-size: var(--text-lg); + font-weight: 600; + color: var(--primary-ink); + padding: var(--space-sm) var(--space-md); + border-radius: 8px; +} + +.score-feedback { + margin: 0; + color: var(--muted); +} + +.quiz-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: var(--space-md); + border-top: 1px solid var(--border); +} + +.quiz-footer-label { + color: var(--muted); + font-size: var(--text-sm); +} + +.quiz-running-score { + font-weight: 600; + font-size: var(--text-lg); +} + +/* ---------- Summary view ---------- */ + +.summary-score-block { + display: flex; + align-items: baseline; + gap: var(--space-lg); + margin: var(--space-lg) 0 var(--space-xl); +} + +.summary-percentage { + font-size: var(--text-2xl); + font-weight: 600; + line-height: 1; +} + +.summary-score-detail { + display: flex; + flex-direction: column; + gap: var(--space-xs); +} + +.summary-fraction { + color: var(--muted); + font-size: var(--text-sm); +} + +.tier-label { + font-weight: 600; + font-size: var(--text-sm); + width: fit-content; + padding: 0.15em 0.6em; + border-radius: 999px; +} + +.summary-notes { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: var(--space-lg); + margin: 0 0 var(--space-xl); + padding: var(--space-lg) 0; + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); +} + +.summary-note dt { + font-size: var(--text-xs); + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--faint); + margin-bottom: var(--space-xs); +} + +.summary-note dd { + margin: 0; + color: var(--ink); +} + +/* ---------- Report view ---------- */ + +.report-verdict { + font-size: var(--text-lg); + max-width: 65ch; + margin: 0 0 var(--space-xl); +} + +.report-verdict .mono { + font-weight: 600; + color: var(--primary); +} + +.report-table { + width: 100%; + border-collapse: collapse; + margin-bottom: var(--space-xl); +} + +.report-table th, +.report-table td { + text-align: left; + padding: var(--space-sm) var(--space-md); + border-bottom: 1px solid var(--border); +} + +.report-table th { + font-size: var(--text-xs); + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--faint); + font-weight: 600; +} + +.report-table td.mono { + color: var(--muted); +} + +.report-columns { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: var(--space-xl); + margin-bottom: var(--space-xl); +} + +.report-col-heading { + font-size: var(--text-base); + font-weight: 700; + margin: 0 0 var(--space-md); +} + +.report-list { + margin: 0; + padding-left: var(--space-lg); + color: var(--muted); +} + +.report-list li { + margin-bottom: var(--space-sm); +} + +.report-priority { + display: flex; + flex-direction: column; + gap: var(--space-md); +} + +.report-priority-item { + border: 1px solid var(--border); + border-radius: 8px; + padding: var(--space-md); +} + +.report-priority-item h3 { + margin: 0 0 var(--space-xs); + font-size: var(--text-sm); + color: var(--tier-0); +} + +.report-priority-item ul { + margin: 0; + padding-left: var(--space-lg); + color: var(--muted); + font-size: var(--text-sm); +} + +.callout { + border: 1px solid var(--border-strong); + border-radius: 12px; + padding: var(--space-lg); + margin-bottom: var(--space-xl); + box-shadow: var(--shadow-sm); +} + +.callout-label { + display: block; + font-family: var(--font-mono); + font-size: var(--text-xs); + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--accent); + margin-bottom: var(--space-sm); +} + +.callout p { + margin: 0; + max-width: 65ch; +} + +/* ---------- Buttons ---------- */ + +.action-row { + display: flex; + gap: var(--space-md); + flex-wrap: wrap; +} + +.btn { + font-family: var(--font-sans); + font-size: var(--text-sm); + font-weight: 600; + padding: var(--space-sm) var(--space-lg); + border-radius: 8px; + border: 1px solid transparent; + cursor: pointer; + transition: transform var(--duration-fast) var(--ease-out-quart), border-color var(--duration-fast) var(--ease-out-quart), + background var(--duration-fast) var(--ease-out-quart); +} + +.btn:hover { + transform: scale(1.02); +} + +.btn:active { + transform: scale(0.98); +} + +.btn:disabled, +.btn:disabled:hover { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +.btn-primary { + background: var(--primary); + color: var(--primary-ink); +} + +.btn-primary:hover { + background: oklch(from var(--primary) calc(l + 0.05) c h); +} + +.btn-secondary { + background: transparent; + color: var(--ink); + border-color: var(--border-strong); +} + +.btn-secondary:hover { + background: var(--surface-2); +} + +/* ---------- Tier color utility classes (applied via JS) ---------- */ + +.tier-0 { + background: var(--tier-0); +} +.tier-1 { + background: var(--tier-1); +} +.tier-2 { + background: var(--tier-2); +} +.tier-3 { + background: var(--tier-3); +} +.progress-rail li.tier-0 { + background: var(--tier-0); +} +.progress-rail li.tier-1 { + background: var(--tier-1); +} +.progress-rail li.tier-2 { + background: var(--tier-2); +} +.progress-rail li.tier-3 { + background: var(--tier-3); +} +.tier-label.tier-0 { + color: var(--tier-0); + background: color-mix(in oklch, var(--tier-0) 16%, transparent); +} +.tier-label.tier-1 { + color: var(--tier-1); + background: color-mix(in oklch, var(--tier-1) 16%, transparent); +} +.tier-label.tier-2 { + color: var(--tier-2); + background: color-mix(in oklch, var(--tier-2) 16%, transparent); +} +.tier-label.tier-3 { + color: var(--tier-3); + background: color-mix(in oklch, var(--tier-3) 16%, transparent); +} + +/* ---------- Responsive ---------- */ + +@media (max-width: 640px) { + .app-header { + padding: var(--space-md) var(--space-lg); + } + .app-subtitle { + display: none; + } + .view-stack { + padding: var(--space-lg) var(--space-md); + } + .quiz-header { + flex-direction: column; + align-items: flex-start; + gap: var(--space-xs); + } + .summary-score-block { + flex-direction: column; + gap: var(--space-sm); + } + .answer-actions { + align-items: stretch; + flex-direction: column; + } + .answer-actions .btn { + width: 100%; + } +} + +/* ---------- Reduced motion ---------- */ + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +}