From e1f291155f15ad1ec0ff4a0f44bc0e92f854c763 Mon Sep 17 00:00:00 2001 From: NiloCK Date: Mon, 10 Nov 2025 13:09:02 -0400 Subject: [PATCH 01/13] working docs --- agent/BACKPORT_CHECKLIST.md | 273 ++++++++++++++++++++++++++ agent/a.1.assessment.md | 210 ++++++++++++++++++++ agent/a.2.plan.md | 379 ++++++++++++++++++++++++++++++++++++ agent/a.3.todo.md | 218 +++++++++++++++++++++ 4 files changed, 1080 insertions(+) create mode 100644 agent/BACKPORT_CHECKLIST.md create mode 100644 agent/a.1.assessment.md create mode 100644 agent/a.2.plan.md create mode 100644 agent/a.3.todo.md diff --git a/agent/BACKPORT_CHECKLIST.md b/agent/BACKPORT_CHECKLIST.md new file mode 100644 index 000000000..11f05fd9e --- /dev/null +++ b/agent/BACKPORT_CHECKLIST.md @@ -0,0 +1,273 @@ +# Backport Checklist for Scaffolding Templates + +Changes tested in `/flutor` that need to be propagated to vue-skuilder template packages. + +**Note:** This document is self-contained. All necessary code examples and rationale are included below. + +## How to Use This Document + +1. **Copy this file** to the vue-skuilder monorepo for reference +2. **Apply changes 1-6** to the template packages (standalone-ui, sk-contributor) +3. **Test** using the testing checklist at the bottom +4. **Ignore** the "Infrastructure Changes" section - those are already in the monorepo + +**Files to Update:** +- `/packages/standalone-ui/vite.config.ts` +- `/packages/standalone-ui/src/questions/index.ts` +- `/packages/standalone-ui/src/main.ts` +- `/packages/standalone-ui/src/questions/*QuestionView.vue` (all view components) +- `/packages/sk-contributor/` (same files as above) +- `/packages/standalone-ui/src/questions/README.md` (examples) + +## Context + +These fixes enable custom questions to work in both: +- **Standalone mode** (`yarn dev`) - already worked +- **Studio mode** (`yarn studio`) - now works after these fixes + +The root cause was a combination of: +1. Browser unable to resolve bare imports in dynamically loaded modules +2. Missing view component names for runtime lookup +3. Side-effect import not running (views array staying empty) +4. Incorrect views export format for studio-ui consumption + +## 1. Vite Config - External Dependencies & Terser +**File:** `vite.config.ts` +**Location:** Library build configuration +**Change:** +```typescript +build: buildMode === 'library' + ? { + // ... + terserOptions: { + keep_classnames: true, + keep_fnames: true, // ADD: Preserve function names + mangle: { + properties: false, // ADD: Don't mangle static properties like seedData + }, + }, + rollupOptions: { + // CRITICAL: Do NOT externalize dependencies for studio mode + // Browser cannot resolve bare imports without import maps + external: [ + // Leave empty - bundle everything for browser compatibility + ], + } + } +``` +**Why:** +- Terser options prevent minification from mangling static properties (seedData, views, dataShapes) +- Empty externals array bundles all dependencies so browser can load the module without import maps +- Previously tried externalizing to avoid duplication, but browser can't resolve `import { X } from "@vue-skuilder/courseware"` at runtime +**Affects:** +- `/packages/standalone-ui/vite.config.ts` +- CLI template generation for new projects + +--- + +## 2. Question Index - Views Export Format +**File:** `src/questions/index.ts` +**Location:** `allCustomQuestions()` function +**Change:** +```typescript +// OLD (broken): +const views: ViewComponent[] = []; +questionClasses.forEach((questionClass) => { + if (questionClass.views) { + questionClass.views.forEach((view) => { + views.push(view); + }); + } +}); + +// NEW (working): +const views: Array<{ name: string; component: ViewComponent }> = []; +questionClasses.forEach((questionClass) => { + if (questionClass.views) { + questionClass.views.forEach((view) => { + const viewName = (view as any).name || (view as any).__name; + if (viewName) { + if (!views.find((existing) => existing.name === viewName)) { + views.push({ name: viewName, component: view }); + } + } else { + console.warn('[allCustomQuestions] View component missing name property:', view); + } + }); + } +}); +``` +**Why:** Studio-ui expects `{ name, component }` format (see studio-ui/src/main.ts:183-189) +**Affects:** +- `/packages/standalone-ui/src/questions/index.ts` +- `/packages/sk-contributor/src/questions/index.ts` + +--- + +## 3. Question Index - TypeScript Interface +**File:** `src/questions/index.ts` +**Location:** `CustomQuestionsExport` interface +**Change:** +```typescript +export interface CustomQuestionsExport { + courses: CourseWare[]; + questionClasses: Array; + dataShapes: DataShape[]; + views: Array<{ name: string; component: ViewComponent }>; // CHANGED from ViewComponent[] + inlineComponents: Record; + meta: { /* ... */ }; +} +``` +**Why:** Matches runtime format +**Affects:** +- `/packages/standalone-ui/src/questions/index.ts` +- `/packages/sk-contributor/src/questions/index.ts` + +--- + +## 4. Main.ts - Import Questions Index +**File:** `src/main.ts` +**Location:** Before `allCourseWare.courses.push(exampleCourse)` +**Change:** +```typescript +// Import allCourseWare singleton and exampleCourse +import { allCourseWare } from '@vue-skuilder/courseware'; +// Import from index.ts to ensure view setup code runs +import './questions/index'; // ADD THIS LINE +import { exampleCourse } from './questions/exampleCourse'; +``` +**Why:** Ensures static view setup runs before course registration +**Affects:** +- `/packages/standalone-ui/src/main.ts` +- `/packages/sk-contributor/src/main.ts` + +--- + +## 5. Question Class - Direct Inline Views +**File:** `src/questions/MyQuestion.ts` +**Location:** Question class static properties +**Change:** +```typescript +// Import at top +import { markRaw } from 'vue'; +import MyQuestionView from './MyQuestionView.vue'; + +export class MyQuestion extends Question { + public static dataShapes = [/* ... */]; + + // Direct inline registration - no external setup needed + public static views = [markRaw(MyQuestionView)]; // CHANGED from empty array + + // ... +} +``` +**Why:** Self-contained, works immediately, no fragile side-effects +**Affects:** +- Template examples in `/packages/standalone-ui/src/questions/README.md` +- Template examples in `/packages/sk-contributor/src/questions/README.md` +- Existing template questions (NumberRangeQuestion, etc.) + +--- + +## 6. Vue Component - DefineOptions Name +**File:** `src/questions/MyQuestionView.vue` +**Location:** Script setup +**Change:** +```typescript + - - diff --git a/packages/sk-contributor/logs/.a74d0b06286c8a4cf196682f1943432de0472db1-audit.json b/packages/sk-contributor/logs/.a74d0b06286c8a4cf196682f1943432de0472db1-audit.json deleted file mode 100644 index 1905f9a7f..000000000 --- a/packages/sk-contributor/logs/.a74d0b06286c8a4cf196682f1943432de0472db1-audit.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "keep": { - "days": true, - "amount": 7 - }, - "auditLog": "logs/.a74d0b06286c8a4cf196682f1943432de0472db1-audit.json", - "files": [ - { - "date": 1754318839444, - "name": "logs/combined-2025-08-04.log", - "hash": "ac657d834f87bd70823c7c054d01b0a549febfe17a45d1a51e4ca7f2e588ccaa" - }, - { - "date": 1754387825313, - "name": "logs/combined-2025-08-05.log", - "hash": "2cbc2fa0ed1ba4fdd98f30d7bb99dda4367a9ba8e52bd9110ced0e5a9275e7f8" - } - ], - "hashType": "sha256" -} \ No newline at end of file diff --git a/packages/sk-contributor/logs/.d3dff8304a896044ec8db08dd8e9c1056ae5fb50-audit.json b/packages/sk-contributor/logs/.d3dff8304a896044ec8db08dd8e9c1056ae5fb50-audit.json deleted file mode 100644 index edabae232..000000000 --- a/packages/sk-contributor/logs/.d3dff8304a896044ec8db08dd8e9c1056ae5fb50-audit.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "keep": { - "days": true, - "amount": 7 - }, - "auditLog": "logs/.d3dff8304a896044ec8db08dd8e9c1056ae5fb50-audit.json", - "files": [ - { - "date": 1754318839439, - "name": "logs/error-2025-08-04.log", - "hash": "0d820416f020343dd9d2e77f550e8147f6133977feec0415d2489a1c6c002150" - }, - { - "date": 1754387825307, - "name": "logs/error-2025-08-05.log", - "hash": "e312b4e2e088a6e5f4e3087bff850227a3a7698a0a933eb770261c818fd33a09" - } - ], - "hashType": "sha256" -} \ No newline at end of file diff --git a/packages/sk-contributor/logs/combined-2025-08-04.log b/packages/sk-contributor/logs/combined-2025-08-04.log deleted file mode 100644 index d7b7d6868..000000000 --- a/packages/sk-contributor/logs/combined-2025-08-04.log +++ /dev/null @@ -1,7 +0,0 @@ -{"caller":"Format.transform (logger.js:4)","level":"info","message":"FFMPEG path: /home/colin/pn/vue-skuilder/df1/node_modules/ffmpeg-static/ffmpeg","timestamp":"2025-08-04T14:47:19.450Z"} -{"caller":"Format.transform (logger.js:4)","level":"info","message":"FFMPEG version: ffmpeg version 6.0-static https://johnvansickle.com/ffmpeg/ Copyright (c) 2000-2023 the FFmpeg developers","timestamp":"2025-08-04T14:47:19.527Z"} -{"caller":"Format.transform (logger.js:4)","level":"info","message":"Following all course databases for changes...","timestamp":"2025-08-04T14:49:16.848Z"} -{"caller":"Format.transform (logger.js:4)","level":"info","message":"Starting initCourseDBDesignDocInsert...","timestamp":"2025-08-04T14:49:16.851Z"} -{"caller":"Format.transform (logger.js:4)","level":"warn","message":"Course lookup database not found - skipping platform course discovery","timestamp":"2025-08-04T14:49:16.911Z"} -{"caller":"Format.transform (logger.js:4)","level":"error","message":"Error in initCourseDBDesignDocInsert background task: [object Object]","timestamp":"2025-08-04T14:49:16.931Z"} -{"caller":"Format.transform (logger.js:4)","level":"error","message":"Full error details in initCourseDBDesignDocInsert: {\"error\":\"not_found\",\"reason\":\"Database does not exist.\",\"status\":404,\"name\":\"not_found\",\"message\":\"Database does not exist.\",\"stack\":\"Error\\n at generateErrorFromResponse (/home/colin/pn/vue-skuilder/df1/node_modules/pouchdb/lib/index.js:553:18)\\n at fetchJSON (/home/colin/pn/vue-skuilder/df1/node_modules/pouchdb/lib/index.js:6886:19)\\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\\n at async /home/colin/pn/vue-skuilder/df1/node_modules/pouchdb/lib/index.js:7490:22\"}","timestamp":"2025-08-04T14:49:16.933Z"} diff --git a/packages/sk-contributor/logs/combined-2025-08-05.log b/packages/sk-contributor/logs/combined-2025-08-05.log deleted file mode 100644 index 02b32f018..000000000 --- a/packages/sk-contributor/logs/combined-2025-08-05.log +++ /dev/null @@ -1,21 +0,0 @@ -{"caller":"Format.transform (logger.js:4)","level":"info","message":"FFMPEG path: /home/colin/pn/vue-skuilder/df1/node_modules/ffmpeg-static/ffmpeg","timestamp":"2025-08-05T09:57:05.321Z"} -{"caller":"Format.transform (logger.js:4)","level":"info","message":"FFMPEG version: ffmpeg version 6.0-static https://johnvansickle.com/ffmpeg/ Copyright (c) 2000-2023 the FFmpeg developers","timestamp":"2025-08-05T09:57:05.416Z"} -{"caller":"Format.transform (logger.js:4)","level":"info","message":"Following all course databases for changes...","timestamp":"2025-08-05T09:57:13.800Z"} -{"caller":"Format.transform (logger.js:4)","level":"info","message":"Starting initCourseDBDesignDocInsert...","timestamp":"2025-08-05T09:57:13.802Z"} -{"caller":"Format.transform (logger.js:4)","level":"error","message":"Error in initCourseDBDesignDocInsert background task: [object Object]","timestamp":"2025-08-05T09:57:13.869Z"} -{"caller":"Format.transform (logger.js:4)","level":"error","message":"Full error details in initCourseDBDesignDocInsert: {\"error\":\"not_found\",\"reason\":\"Database does not exist.\",\"status\":404,\"name\":\"not_found\",\"message\":\"Database does not exist.\",\"stack\":\"Error\\n at generateErrorFromResponse (/home/colin/pn/vue-skuilder/df1/node_modules/pouchdb/lib/index.js:553:18)\\n at fetchJSON (/home/colin/pn/vue-skuilder/df1/node_modules/pouchdb/lib/index.js:6886:19)\\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\\n at async /home/colin/pn/vue-skuilder/df1/node_modules/pouchdb/lib/index.js:7490:22\"}","timestamp":"2025-08-05T09:57:13.871Z"} -{"caller":"Format.transform (logger.js:4)","level":"warn","message":"Course lookup database not found - skipping platform course discovery","timestamp":"2025-08-05T09:57:13.875Z"} -{"caller":"Format.transform (logger.js:4)","level":"info","message":"PACK_COURSE request from undefined...","timestamp":"2025-08-05T17:55:21.560Z"} -{"caller":"Format.transform (logger.js:4)","level":"info","message":"Studio mode: bypassing authentication for local development","timestamp":"2025-08-05T17:55:21.563Z"} -{"caller":"Format.transform (logger.js:4)","level":"info","message":"Authorized PACK_COURSE request made...","timestamp":"2025-08-05T17:55:21.565Z"} -{"caller":"Format.transform (logger.js:4)","level":"info","message":"Starting PACK_COURSE for unpacked_c01c9577-8f54-4102-9d43-d2ae0591ddd8_20250805_oksk24...","timestamp":"2025-08-05T17:55:21.568Z"} -{"caller":"Format.transform (logger.js:4)","level":"info","message":"Pack request data: {\n \"courseId\": \"unpacked_c01c9577-8f54-4102-9d43-d2ae0591ddd8_20250805_oksk24\",\n \"outputPath\": \"./public/static-courses/c01c9577-8f54-4102-9d43-d2ae0591ddd8\",\n \"couchdbUrl\": \"http://admin:password@localhost:5985/coursedb-unpacked_c01c9577-8f54-4102-9d43-d2ae0591ddd8_20250805_oksk24\"\n}","timestamp":"2025-08-05T17:55:21.572Z"} -{"caller":"Format.transform (logger.js:4)","level":"info","message":"Using provided CouchDB URL: \"http://admin:password@localhost:5985/coursedb-unpacked_c01c9577-8f54-4102-9d43-d2ae0591ddd8_20250805_oksk24\"","timestamp":"2025-08-05T17:55:21.573Z"} -{"caller":"Format.transform (logger.js:4)","level":"info","message":"Packing course unpacked_c01c9577-8f54-4102-9d43-d2ae0591ddd8_20250805_oksk24 from http://admin:password@localhost:5985/coursedb-unpacked_c01c9577-8f54-4102-9d43-d2ae0591ddd8_20250805_oksk24 to /home/colin/pn/vue-skuilder/df1/packages/sk-contributor/public/static-courses/c01c9577-8f54-4102-9d43-d2ae0591ddd8","timestamp":"2025-08-05T17:55:21.575Z"} -{"caller":"Format.transform (logger.js:4)","level":"info","message":"Removing existing directory: /home/colin/pn/vue-skuilder/df1/packages/sk-contributor/public/static-courses/c01c9577-8f54-4102-9d43-d2ae0591ddd8","timestamp":"2025-08-05T17:55:21.583Z"} -{"caller":"Format.transform (logger.js:4)","level":"info","message":"Creating PouchDB instance with URL: http://admin:password@localhost:5985/coursedb-unpacked_c01c9577-8f54-4102-9d43-d2ae0591ddd8_20250805_oksk24","timestamp":"2025-08-05T17:55:21.602Z"} -{"caller":"Format.transform (logger.js:4)","level":"info","message":"PouchDB constructor available: function","timestamp":"2025-08-05T17:55:21.604Z"} -{"caller":"Format.transform (logger.js:4)","level":"info","message":"PouchDB adapters: [\"leveldb\",\"http\",\"https\"]","timestamp":"2025-08-05T17:55:21.606Z"} -{"caller":"Format.transform (logger.js:4)","level":"info","message":"PouchDB instance created, adapter: http","timestamp":"2025-08-05T17:55:21.609Z"} -{"caller":"Format.transform (logger.js:4)","level":"info","message":"Pack completed in 210ms. Attachments: 0, Files written: 6","timestamp":"2025-08-05T17:55:21.780Z"} -{"caller":"Format.transform (logger.js:4)","level":"info","message":"::ffff:127.0.0.1 - - [05/Aug/2025:17:55:21 +0000] \"POST / HTTP/1.1\" 200 223 \"http://localhost:7174/\" \"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0\"","timestamp":"2025-08-05T17:55:21.794Z"} diff --git a/packages/sk-contributor/logs/error-2025-08-04.log b/packages/sk-contributor/logs/error-2025-08-04.log deleted file mode 100644 index a5a17d9be..000000000 --- a/packages/sk-contributor/logs/error-2025-08-04.log +++ /dev/null @@ -1,2 +0,0 @@ -{"caller":"Format.transform (logger.js:4)","level":"error","message":"Error in initCourseDBDesignDocInsert background task: [object Object]","timestamp":"2025-08-04T14:49:16.931Z"} -{"caller":"Format.transform (logger.js:4)","level":"error","message":"Full error details in initCourseDBDesignDocInsert: {\"error\":\"not_found\",\"reason\":\"Database does not exist.\",\"status\":404,\"name\":\"not_found\",\"message\":\"Database does not exist.\",\"stack\":\"Error\\n at generateErrorFromResponse (/home/colin/pn/vue-skuilder/df1/node_modules/pouchdb/lib/index.js:553:18)\\n at fetchJSON (/home/colin/pn/vue-skuilder/df1/node_modules/pouchdb/lib/index.js:6886:19)\\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\\n at async /home/colin/pn/vue-skuilder/df1/node_modules/pouchdb/lib/index.js:7490:22\"}","timestamp":"2025-08-04T14:49:16.933Z"} diff --git a/packages/sk-contributor/logs/error-2025-08-05.log b/packages/sk-contributor/logs/error-2025-08-05.log deleted file mode 100644 index 262ee4ca2..000000000 --- a/packages/sk-contributor/logs/error-2025-08-05.log +++ /dev/null @@ -1,2 +0,0 @@ -{"caller":"Format.transform (logger.js:4)","level":"error","message":"Error in initCourseDBDesignDocInsert background task: [object Object]","timestamp":"2025-08-05T09:57:13.869Z"} -{"caller":"Format.transform (logger.js:4)","level":"error","message":"Full error details in initCourseDBDesignDocInsert: {\"error\":\"not_found\",\"reason\":\"Database does not exist.\",\"status\":404,\"name\":\"not_found\",\"message\":\"Database does not exist.\",\"stack\":\"Error\\n at generateErrorFromResponse (/home/colin/pn/vue-skuilder/df1/node_modules/pouchdb/lib/index.js:553:18)\\n at fetchJSON (/home/colin/pn/vue-skuilder/df1/node_modules/pouchdb/lib/index.js:6886:19)\\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\\n at async /home/colin/pn/vue-skuilder/df1/node_modules/pouchdb/lib/index.js:7490:22\"}","timestamp":"2025-08-05T09:57:13.871Z"} diff --git a/packages/sk-contributor/package.json b/packages/sk-contributor/package.json deleted file mode 100644 index 4c1c0d052..000000000 --- a/packages/sk-contributor/package.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "name": "sk-contributor", - "version": "1.0.0", - "type": "module", - "main": "./dist-lib/questions.cjs.js", - "module": "./dist-lib/questions.mjs", - "types": "./dist-lib/index.d.ts", - "exports": { - ".": { - "types": "./dist-lib/index.d.ts", - "import": "./dist-lib/questions.mjs", - "require": "./dist-lib/questions.cjs.js" - }, - "./questions": { - "types": "./dist-lib/index.d.ts", - "import": "./dist-lib/questions.mjs", - "require": "./dist-lib/questions.cjs.js" - }, - "./style": "./dist-lib/assets/index.css" - }, - "files": [ - "dist/", - "dist-lib/" - ], - "scripts": { - "dev": "vite", - "build": "npm run build:webapp && npm run build:lib", - "build:webapp": "vite build", - "build:lib": "BUILD_MODE=library vite build", - "preview": "vite preview", - "test:e2e": "cypress open", - "test:e2e:headless": "cypress run", - "ci:e2e": "vite dev & wait-on http://localhost:6173 && cypress run", - "studio": "skuilder studio" - }, - "dependencies": { - "@mdi/font": "^7.3.67", - "@vue-skuilder/common": "^0.1.14", - "@vue-skuilder/common-ui": "^0.1.14", - "@vue-skuilder/courseware": "^0.1.14", - "@vue-skuilder/db": "^0.1.14", - "events": "^3.3.0", - "pinia": "^2.3.0", - "vue": "^3.5.13", - "vue-router": "^4.2.0", - "vuetify": "^3.7.0" - }, - "devDependencies": { - "@types/cypress": "1.1.6", - "@types/events": "^3", - "@vitejs/plugin-vue": "^5.2.1", - "@vue-skuilder/cli": "^0.1.14", - "cypress": "14.1.0", - "terser": "^5.39.0", - "typescript": "^5.7.2", - "vite": "^6.0.9", - "vite-plugin-dts": "^4.3.0", - "vue-tsc": "^1.8.0", - "wait-on": "8.0.2" - }, - "stableVersion": "0.1.10", - "description": "Skuilder course application: sk-contributor" -} diff --git a/packages/sk-contributor/skuilder.config.json b/packages/sk-contributor/skuilder.config.json deleted file mode 100644 index 8af907e8e..000000000 --- a/packages/sk-contributor/skuilder.config.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "title": "Contributing", - "dataLayerType": "static", - "course": "c01c9577-8f54-4102-9d43-d2ae0591ddd8", - "theme": { - "name": "default", - "defaultMode": "light", - "light": { - "dark": false, - "colors": { - "primary": "#1976D2", - "secondary": "#424242", - "accent": "#82B1FF", - "error": "#F44336", - "info": "#2196F3", - "success": "#4CAF50", - "warning": "#FF9800", - "background": "#FFFFFF", - "surface": "#FFFFFF", - "surface-bright": "#FFFFFF", - "surface-light": "#EEEEEE", - "surface-variant": "#E3F2FD", - "on-surface-variant": "#1976D2", - "primary-darken-1": "#1565C0", - "secondary-darken-1": "#212121", - "on-primary": "#FFFFFF", - "on-secondary": "#FFFFFF", - "on-background": "#212121", - "on-surface": "#212121" - } - }, - "dark": { - "dark": true, - "colors": { - "primary": "#2196F3", - "secondary": "#90A4AE", - "accent": "#82B1FF", - "error": "#FF5252", - "info": "#2196F3", - "success": "#4CAF50", - "warning": "#FFC107", - "background": "#121212", - "surface": "#1E1E1E", - "surface-bright": "#2C2C2C", - "surface-light": "#2C2C2C", - "surface-variant": "#1A237E", - "on-surface-variant": "#82B1FF", - "primary-darken-1": "#1976D2", - "secondary-darken-1": "#546E7A", - "on-primary": "#000000", - "on-secondary": "#000000", - "on-background": "#FFFFFF", - "on-surface": "#FFFFFF" - } - } - } -} \ No newline at end of file diff --git a/packages/sk-contributor/src/App.vue b/packages/sk-contributor/src/App.vue deleted file mode 100644 index 94dea955f..000000000 --- a/packages/sk-contributor/src/App.vue +++ /dev/null @@ -1,62 +0,0 @@ - - - diff --git a/packages/sk-contributor/src/ENVIRONMENT_VARS.ts b/packages/sk-contributor/src/ENVIRONMENT_VARS.ts deleted file mode 100644 index 66cffe8d9..000000000 --- a/packages/sk-contributor/src/ENVIRONMENT_VARS.ts +++ /dev/null @@ -1,76 +0,0 @@ -// vue-skuilder/packages/standalone-ui/src/ENVIRONMENT_VARS.ts -type ProtocolString = 'http' | 'https'; - -import config from '../skuilder.config.json'; - -export interface Environment { - /** - * URL to the remote couchDB instance that the app connects to. - * Loaded from VITE_COUCHDB_SERVER_URL environment variable. - */ - COUCHDB_SERVER_URL: string; - - /** - * Protocol for the CouchDB server. - * Loaded from VITE_COUCHDB_SERVER_PROTOCOL environment variable. - */ - COUCHDB_SERVER_PROTOCOL: ProtocolString; - - /** - * Static course IDs to load. - * Loaded from VITE_STATIC_COURSE_IDS environment variable (comma-separated string). - */ - STATIC_COURSE_ID: string; - - /** - * Type of data layer to use - couchdb live backend or - * statically built and served from the app. - */ - DATALAYER_TYPE: 'couch' | 'static'; - - /** - * A global flag to enable debug messaging mode. - * Loaded from VITE_DEBUG environment variable ('true' or 'false'). - */ - DEBUG: boolean; -} - -// Default fallback values if environment variables are not set -const defaultEnv: Environment = { - COUCHDB_SERVER_URL: 'localhost:5984/', // Sensible default for local dev - COUCHDB_SERVER_PROTOCOL: 'http', - STATIC_COURSE_ID: 'not_set', - DATALAYER_TYPE: 'couch', - DEBUG: false, // Default to false if VITE_DEBUG is not 'true' -}; - -// --- Read Environment Variables using Vite's import.meta.env --- -// Vite replaces these variables at build time with the values from your .env files. - -if (config.dataLayerType !== 'couch' && config.dataLayerType !== 'static') { - throw new Error('Invalid data layer type'); -} - -const ENV: Environment = { - // Use the value from import.meta.env if available, otherwise use the default - COUCHDB_SERVER_URL: import.meta.env.VITE_COUCHDB_SERVER || defaultEnv.COUCHDB_SERVER_URL, - - // Ensure the protocol is one of the allowed types - COUCHDB_SERVER_PROTOCOL: (import.meta.env.VITE_COUCHDB_PROTOCOL === 'https' - ? 'https' - : 'http') as ProtocolString, - - STATIC_COURSE_ID: config.course, - - DATALAYER_TYPE: config.dataLayerType, - - // Environment variables are always strings, so compare VITE_DEBUG to 'true' - DEBUG: import.meta.env.VITE_DEBUG === 'true', -}; - -// Optional: Log the resolved environment in development mode for debugging -if (import.meta.env.DEV) { - console.log('Resolved Environment Variables:', ENV); -} - -export default ENV; diff --git a/packages/sk-contributor/src/components/CourseFooter.vue b/packages/sk-contributor/src/components/CourseFooter.vue deleted file mode 100644 index 6f94fbe3c..000000000 --- a/packages/sk-contributor/src/components/CourseFooter.vue +++ /dev/null @@ -1,33 +0,0 @@ - - - diff --git a/packages/sk-contributor/src/components/CourseHeader.vue b/packages/sk-contributor/src/components/CourseHeader.vue deleted file mode 100644 index b2b39ec5f..000000000 --- a/packages/sk-contributor/src/components/CourseHeader.vue +++ /dev/null @@ -1,55 +0,0 @@ - - - diff --git a/packages/sk-contributor/src/composables/useCourseConfig.ts b/packages/sk-contributor/src/composables/useCourseConfig.ts deleted file mode 100644 index 07840df13..000000000 --- a/packages/sk-contributor/src/composables/useCourseConfig.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ref, readonly } from 'vue'; -import config from '../../skuilder.config.json'; - -// This would be replaced by actual course configuration in a real implementation -const defaultConfig = { - title: config.title ? config.title : '[UNSET] Course Title', - description: 'This is the devenv test course setup.', - logo: '', - darkMode: false, - links: [ - { text: 'About', url: '/about' }, - { text: 'Help', url: '/help' }, - ], - copyright: '', -}; - -export function useCourseConfig() { - const courseConfig = ref(defaultConfig); - - // Later this would load from a configuration file or API - const loadConfig = async () => { - // In a real implementation, this would load configuration - // courseConfig.value = await loadCourseConfig(); - }; - - // Initialize - loadConfig(); - - return { - courseConfig: readonly(courseConfig), - loadConfig, - }; -} diff --git a/packages/sk-contributor/src/main.ts b/packages/sk-contributor/src/main.ts deleted file mode 100644 index bdde9c521..000000000 --- a/packages/sk-contributor/src/main.ts +++ /dev/null @@ -1,201 +0,0 @@ -import ENV from './ENVIRONMENT_VARS'; -import '@mdi/font/css/materialdesignicons.css'; - -import { createApp } from 'vue'; -import { createPinia } from 'pinia'; -import App from './App.vue'; -import router from './router'; - -// Vuetify -import 'vuetify/styles'; -import { createVuetify } from 'vuetify'; -import * as components from 'vuetify/components'; -import * as directives from 'vuetify/directives'; -import { aliases, mdi } from 'vuetify/iconsets/mdi'; - -// data layer -import { initializeDataLayer, getDataLayer } from '@vue-skuilder/db'; - -// auth store -import { useAuthStore } from '@vue-skuilder/common-ui'; - -// styles from component library packages -import '@vue-skuilder/courseware/style'; -import '@vue-skuilder/common-ui/style'; - -// Import allCourseWare singleton and exampleCourse -import { allCourseWare } from '@vue-skuilder/courseware'; -import { exampleCourse } from './questions/exampleCourse'; - -// Add the example course to the allCourseWare singleton -allCourseWare.courses.push(exampleCourse); - -// theme configuration -import config from '../skuilder.config.json'; - -(async () => { - // For static data layer, load manifest - let dataLayerOptions: any = { - COUCHDB_SERVER_URL: ENV.COUCHDB_SERVER_URL, - COUCHDB_SERVER_PROTOCOL: ENV.COUCHDB_SERVER_PROTOCOL, - COURSE_IDS: [config.course ? config.course : 'default-course'], - }; - - if (config.dataLayerType === 'static') { - // Load manifest for static mode - const courseId = config.course; - if (!courseId) { - throw new Error('Course ID required for static data layer'); - } - - try { - const manifestResponse = await fetch(`/static-courses/${courseId}/manifest.json`); - if (!manifestResponse.ok) { - throw new Error( - `Failed to load manifest: ${manifestResponse.status} ${manifestResponse.statusText}` - ); - } - const manifest = await manifestResponse.json(); - console.log(`Loaded manifest for course ${courseId}`); - console.log(JSON.stringify(manifest)); - - dataLayerOptions = { - staticContentPath: '/static-courses', - manifests: { - [courseId]: manifest, - }, - }; - } catch (error) { - console.error('[DEBUG] Failed to load course manifest:', error); - throw new Error(`Could not load course manifest for ${courseId}: ${error}`); - } - } - - try { - await initializeDataLayer({ - type: (config.dataLayerType || 'couch') as 'couch' | 'static', - options: dataLayerOptions, - }); - console.log('[DEBUG] Data layer initialized successfully'); - } catch (error) { - console.error('[DEBUG] Data layer initialization failed:', error); - throw error; - } - const pinia = createPinia(); - - // Apply theme configuration from skuilder.config.json - const themeConfig = config.theme - ? { - defaultTheme: config.theme.defaultMode || 'light', - themes: { - light: config.theme.light, - dark: config.theme.dark, - }, - } - : { - defaultTheme: 'light', - themes: { - light: { - dark: false, - colors: { - primary: '#1976D2', - secondary: '#424242', - accent: '#82B1FF', - error: '#F44336', - info: '#2196F3', - success: '#4CAF50', - warning: '#FF9800', - background: '#FFFFFF', - surface: '#FFFFFF', - 'surface-bright': '#FFFFFF', - 'surface-light': '#EEEEEE', - 'surface-variant': '#E3F2FD', - 'on-surface-variant': '#1976D2', - 'primary-darken-1': '#1565C0', - 'secondary-darken-1': '#212121', - 'on-primary': '#FFFFFF', - 'on-secondary': '#FFFFFF', - 'on-background': '#212121', - 'on-surface': '#212121', - }, - }, - dark: { - dark: true, - colors: { - primary: '#2196F3', - secondary: '#90A4AE', - accent: '#82B1FF', - error: '#FF5252', - info: '#2196F3', - success: '#4CAF50', - warning: '#FFC107', - background: '#121212', - surface: '#1E1E1E', - 'surface-bright': '#2C2C2C', - 'surface-light': '#2C2C2C', - 'surface-variant': '#1A237E', - 'on-surface-variant': '#82B1FF', - 'primary-darken-1': '#1976D2', - 'secondary-darken-1': '#546E7A', - 'on-primary': '#000000', - 'on-secondary': '#000000', - 'on-background': '#FFFFFF', - 'on-surface': '#FFFFFF', - }, - }, - }, - }; - - const vuetify = createVuetify({ - components, - directives, - theme: themeConfig, - icons: { - defaultSet: 'mdi', - aliases, - sets: { - mdi, - }, - }, - }); - - const app = createApp(App); - - app.use(router); - app.use(vuetify); - app.use(pinia); - - const { piniaPlugin } = await import('@vue-skuilder/common-ui'); - app.use(piniaPlugin, { pinia }); - - await useAuthStore().init(); - - // Initialize config store to load user settings (including dark mode) - const { useConfigStore } = await import('@vue-skuilder/common-ui'); - await useConfigStore().init(); - - // Auto-register user for the course in standalone mode - if (config.course) { - try { - const authStore = useAuthStore(); - const user = getDataLayer().getUserDB(); - - // Check if user is already registered for the course - const courseRegistrations = await user.getCourseRegistrationsDoc(); - const isRegistered = courseRegistrations.courses.some(c => c.courseID === config.course); - - if (!isRegistered) { - console.log(`[Standalone] Auto-registering user for course: ${config.course}`); - await user.registerForCourse(config.course, false); // non-preview mode - console.log(`[Standalone] Auto-registration completed for course: ${config.course}`); - } else { - console.log(`[Standalone] User already registered for course: ${config.course}`); - } - } catch (error) { - console.warn(`[Standalone] Failed to auto-register for course ${config.course}:`, error); - // Don't block app startup on registration failure - } - } - - app.mount('#app'); -})(); diff --git a/packages/sk-contributor/src/questions/MultipleChoiceQuestion.ts b/packages/sk-contributor/src/questions/MultipleChoiceQuestion.ts deleted file mode 100644 index 07c3d04a0..000000000 --- a/packages/sk-contributor/src/questions/MultipleChoiceQuestion.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { ViewData, Answer, Question } from '@vue-skuilder/courseware'; -import { FieldType, DataShape, DataShapeName } from '@vue-skuilder/common'; -import MultipleChoiceQuestionView from './MultipleChoiceQuestionView.vue'; - -export class MultipleChoiceQuestion extends Question { - public static dataShapes: DataShape[] = [ - { - name: 'MultipleChoiceQuestion' as DataShapeName, - fields: [ - { name: 'questionText', type: FieldType.STRING }, - { name: 'options', type: FieldType.STRING }, // Comma-separated string of options - { name: 'correctAnswer', type: FieldType.STRING }, - ], - }, - ]; - - public static views = [ - { name: 'MultipleChoiceQuestionView', component: MultipleChoiceQuestionView }, - ]; - - // @ts-expect-error TS6133: Used in Vue template - private _questionText: string; - // @ts-expect-error TS6133: Used in Vue template - private options: string[]; - private correctAnswer: string; - - constructor(data: ViewData[]) { - super(data); - this._questionText = data[0].questionText as string; - this.options = (data[0].options as string).split(',').map((s) => s.trim()); - this.correctAnswer = data[0].correctAnswer as string; - } - - public dataShapes(): DataShape[] { - return MultipleChoiceQuestion.dataShapes; - } - - public views() { - // This will be dynamically populated or imported - return MultipleChoiceQuestion.views; - } - - protected isCorrect(answer: Answer): boolean { - return (answer.response as string) === this.correctAnswer; - } -} diff --git a/packages/sk-contributor/src/questions/MultipleChoiceQuestionView.vue b/packages/sk-contributor/src/questions/MultipleChoiceQuestionView.vue deleted file mode 100644 index c9cceba07..000000000 --- a/packages/sk-contributor/src/questions/MultipleChoiceQuestionView.vue +++ /dev/null @@ -1,50 +0,0 @@ - - - - - diff --git a/packages/sk-contributor/src/questions/NumberRangeQuestion.ts b/packages/sk-contributor/src/questions/NumberRangeQuestion.ts deleted file mode 100644 index d5b8b082d..000000000 --- a/packages/sk-contributor/src/questions/NumberRangeQuestion.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ViewData, Answer, Question } from '@vue-skuilder/courseware'; -import { FieldType, DataShape, DataShapeName } from '@vue-skuilder/common'; -import NumberRangeQuestionView from './NumberRangeQuestionView.vue'; - -export class NumberRangeQuestion extends Question { - public static dataShapes: DataShape[] = [ - { - name: 'NumberRangeQuestion' as DataShapeName, - fields: [ - { name: 'questionText', type: FieldType.STRING }, - { name: 'min', type: FieldType.NUMBER }, - { name: 'max', type: FieldType.NUMBER }, - ], - }, - ]; - - public static views = [{ name: 'NumberRangeQuestionView', component: NumberRangeQuestionView }]; - - // @ts-expect-error TS6133: Used in Vue template - private questionText: string; - private min: number; - private max: number; - - constructor(data: ViewData[]) { - super(data); - this.questionText = data[0].questionText as string; - this.min = data[0].min as number; - this.max = data[0].max as number; - } - - public dataShapes(): DataShape[] { - return NumberRangeQuestion.dataShapes; - } - - public views() { - // This will be dynamically populated or imported - return NumberRangeQuestion.views; - } - - protected isCorrect(answer: Answer): boolean { - const userAnswer = answer.response as number; - return userAnswer >= this.min && userAnswer <= this.max; - } -} diff --git a/packages/sk-contributor/src/questions/NumberRangeQuestionView.vue b/packages/sk-contributor/src/questions/NumberRangeQuestionView.vue deleted file mode 100644 index 137da7535..000000000 --- a/packages/sk-contributor/src/questions/NumberRangeQuestionView.vue +++ /dev/null @@ -1,43 +0,0 @@ - - - - - diff --git a/packages/sk-contributor/src/questions/README.md b/packages/sk-contributor/src/questions/README.md deleted file mode 100644 index 231162d8c..000000000 --- a/packages/sk-contributor/src/questions/README.md +++ /dev/null @@ -1,129 +0,0 @@ -# Custom Questions in Standalone UI - -This directory contains example implementations of custom question types for the Vue Skuilder platform. These examples demonstrate how to create new `Question` subclasses and their corresponding Vue components, and how to integrate them into your application. - -## Example Questions Provided - -- **SimpleTextQuestion**: A basic question that asks for a text input and checks for an exact string match. -- **MultipleChoiceQuestion**: Presents a question with multiple options and checks for the correct selection. -- **NumberRangeQuestion**: Asks for a numeric input and validates if it falls within a specified range. - -## How to Use These Examples - -Each question type consists of two main parts: -1. A TypeScript file (`.ts`) defining the `Question` subclass, which handles the question logic, data shapes, and answer evaluation. -2. A Vue component file (`.vue`) that provides the user interface for the question. - -These examples are already integrated into the `exampleCourse.ts` file, which you can use to see them in action. - -## Integrating Custom Questions into Your Course at Runtime - -To use your custom questions in a course, you need to: - -1. **Define your Question Class**: Create a new TypeScript file (e.g., `MyCustomQuestion.ts`) that extends the `Question` class from `@vue-skuilder/courseware`. Define its `dataShapes` and `views` static properties. - - ```typescript - // MyCustomQuestion.ts - import { Question, DataShape, ViewData, Answer } from '@vue-skuilder/courseware'; - import { FieldType } from '@vue-skuilder/common'; - import MyCustomQuestionView from './MyCustomQuestionView.vue'; - - export class MyCustomQuestion extends Question { - public static dataShapes: DataShape[] = [ - new DataShape('MyCustomQuestion', [ - { name: 'myField', type: FieldType.STRING }, - ]), - ]; - - public static views = [ - { name: 'MyCustomQuestionView', component: MyCustomQuestionView }, - ]; - - constructor(data: ViewData[]) { - super(data); - // Initialize your question data from `data` - } - - public dataShapes(): DataShape[] { - return MyCustomQuestion.dataShapes; - } - - public views() { - return MyCustomQuestion.views; - } - - protected isCorrect(answer: Answer): boolean { - // Implement your answer evaluation logic here - return false; - } - } - ``` - -2. **Create Your Vue Component**: Create a Vue component (e.g., `MyCustomQuestionView.vue`) that will render your question and allow user interaction. This component will receive props based on the `ViewData` you define for your question. - - ```vue - - - - - ``` - -3. **Register Your Question and Course**: In your application's entry point (e.g., `src/main.ts` or `src/App.vue`), you need to import your custom question and include it in a `Course` instance. Then, register this course with the `allCourses` list. - - ```typescript - // src/main.ts (example) - import { createApp } from 'vue'; - import App from './App.vue'; - import { createPinia } from 'pinia'; - import { allCourses, Course } from '@vue-skuilder/courseware'; - - // Import your custom question - import { MyCustomQuestion } from './questions/MyCustomQuestion'; - - // Create a new Course instance with your custom question - const myCustomCourse = new Course('MyCustomCourse', [ - new MyCustomQuestion([{ myField: 'Hello Custom Question!' }]), - ]); - - // Add your custom course to the global allCourses list - allCourses.courses.push(myCustomCourse); - - const app = createApp(App); - app.use(createPinia()); - app.mount('#app'); - ``` - - **Note**: The `allCourses` object is a singleton that manages all available courses and their associated questions and views. By adding your custom course to `allCourses.courses`, it becomes discoverable by the `CardViewer` and other components that rely on the course registry. - -## Developing New Questions - -When developing new questions, consider the following: - -- **DataShape Definition**: Carefully define the `DataShape` for your question. This dictates the structure of the data that will be passed to your question's constructor and Vue component. -- **Answer Evaluation**: Implement the `isCorrect` method in your `Question` subclass to define how user answers are evaluated. -- **Vue Component Props**: Ensure your Vue component's `props` match the data fields defined in your `DataShape` and any additional data you pass from your `Question` instance. -- **StudySessionStore**: Use the `useStudySessionStore()` composable from `@vue-skuilder/common-ui` to submit user answers and interact with the study session logic. - -Feel free to modify and extend the provided examples to suit your needs. diff --git a/packages/sk-contributor/src/questions/SimpleTextQuestion.test.ts b/packages/sk-contributor/src/questions/SimpleTextQuestion.test.ts deleted file mode 100644 index 4729d36d2..000000000 --- a/packages/sk-contributor/src/questions/SimpleTextQuestion.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { SimpleTextQuestion } from './SimpleTextQuestion'; - -describe('SimpleTextQuestion', () => { - it('should correctly evaluate a correct answer', () => { - const question = new SimpleTextQuestion([ - { questionText: 'What is the capital of France?', correctAnswer: 'Paris' }, - ]); - expect(question.evaluate({ response: 'Paris' }, 0).isCorrect).toBe(true); - }); - - it('should correctly evaluate an incorrect answer', () => { - const question = new SimpleTextQuestion([ - { questionText: 'What is the capital of France?', correctAnswer: 'Paris' }, - ]); - expect(question.evaluate({ response: 'London' }, 0).isCorrect).toBe(false); - }); - - it('should be case-insensitive', () => { - const question = new SimpleTextQuestion([ - { questionText: 'What is the capital of France?', correctAnswer: 'Paris' }, - ]); - expect(question.evaluate({ response: 'paris' }, 0).isCorrect).toBe(true); - }); -}); diff --git a/packages/sk-contributor/src/questions/SimpleTextQuestion.ts b/packages/sk-contributor/src/questions/SimpleTextQuestion.ts deleted file mode 100644 index 1455b5b07..000000000 --- a/packages/sk-contributor/src/questions/SimpleTextQuestion.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { ViewData, Answer, Question } from '@vue-skuilder/courseware'; -import { FieldType, DataShape, DataShapeName } from '@vue-skuilder/common'; -import SimpleTextQuestionView from './SimpleTextQuestionView.vue'; - -export class SimpleTextQuestion extends Question { - public static dataShapes: DataShape[] = [ - { - name: 'SimpleTextQuestion' as DataShapeName, - fields: [ - { name: 'questionText', type: FieldType.STRING }, - { name: 'correctAnswer', type: FieldType.STRING }, - ], - }, - ]; - - public static views = [{ name: 'SimpleTextQuestionView', component: SimpleTextQuestionView }]; - - // @ts-expect-error TS6133: Used in Vue template - private questionText: string; - private correctAnswer: string; - - constructor(data: ViewData[]) { - super(data); - this.questionText = data[0].questionText as string; - this.correctAnswer = data[0].correctAnswer as string; - } - - public dataShapes(): DataShape[] { - return SimpleTextQuestion.dataShapes; - } - - public views() { - // This will be dynamically populated or imported - return SimpleTextQuestion.views; - } - - protected isCorrect(answer: Answer): boolean { - return (answer.response as string).toLowerCase() === this.correctAnswer.toLowerCase(); - } -} diff --git a/packages/sk-contributor/src/questions/SimpleTextQuestionView.vue b/packages/sk-contributor/src/questions/SimpleTextQuestionView.vue deleted file mode 100644 index 8c1ca66f8..000000000 --- a/packages/sk-contributor/src/questions/SimpleTextQuestionView.vue +++ /dev/null @@ -1,46 +0,0 @@ - - - - - diff --git a/packages/sk-contributor/src/questions/exampleCourse.ts b/packages/sk-contributor/src/questions/exampleCourse.ts deleted file mode 100644 index 7029f2424..000000000 --- a/packages/sk-contributor/src/questions/exampleCourse.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { CourseWare } from '@vue-skuilder/courseware'; -import { SimpleTextQuestion } from './SimpleTextQuestion'; -import { MultipleChoiceQuestion } from './MultipleChoiceQuestion'; -import { NumberRangeQuestion } from './NumberRangeQuestion'; - -export const exampleCourse = new CourseWare('ExampleCourse', [ - SimpleTextQuestion, - MultipleChoiceQuestion, - NumberRangeQuestion, -]); diff --git a/packages/sk-contributor/src/questions/index.ts b/packages/sk-contributor/src/questions/index.ts deleted file mode 100644 index 390e63b42..000000000 --- a/packages/sk-contributor/src/questions/index.ts +++ /dev/null @@ -1,117 +0,0 @@ -// Library entry point for custom questions in standalone-ui -// This file exports question types and components for consumption by studio-ui - -import { CourseWare } from '@vue-skuilder/courseware'; -import { DataShape } from '@vue-skuilder/common'; -import { ViewComponent } from '@vue-skuilder/common-ui'; - -// [ ] todo: simplify exports here. Only the final 'bundle' is strictly required. - -// Export individual question classes -export { SimpleTextQuestion } from './SimpleTextQuestion'; -export { MultipleChoiceQuestion } from './MultipleChoiceQuestion'; -export { NumberRangeQuestion } from './NumberRangeQuestion'; - -// Export example course -export { exampleCourse } from './exampleCourse'; - -// Import components for re-export -import SimpleTextQuestionView from './SimpleTextQuestionView.vue'; -import MultipleChoiceQuestionView from './MultipleChoiceQuestionView.vue'; -import NumberRangeQuestionView from './NumberRangeQuestionView.vue'; - -// Export Vue components -export { SimpleTextQuestionView, MultipleChoiceQuestionView, NumberRangeQuestionView }; - -// Import classes for analysis -import { SimpleTextQuestion } from './SimpleTextQuestion'; -import { MultipleChoiceQuestion } from './MultipleChoiceQuestion'; -import { NumberRangeQuestion } from './NumberRangeQuestion'; -import { exampleCourse } from './exampleCourse'; - -/** - * Main function to export all custom questions for studio-ui consumption - * This provides a standardized interface for the CLI to discover and integrate - * custom question types into studio-ui builds - */ -export function allCustomQuestions() { - // Collect all question classes - const questionClasses = [SimpleTextQuestion, MultipleChoiceQuestion, NumberRangeQuestion]; - - // Collect all data shapes from questions - const dataShapes: DataShape[] = []; - questionClasses.forEach((questionClass) => { - if (questionClass.dataShapes) { - questionClass.dataShapes.forEach((shape) => { - // Avoid duplicates - if (!dataShapes.find((existing) => existing.name === shape.name)) { - dataShapes.push(shape); - } - }); - } - }); - - // Collect all view components from questions - const views: ViewComponent[] = []; - questionClasses.forEach((questionClass) => { - if (questionClass.views) { - questionClass.views.forEach((view) => { - // Avoid duplicates by name - if (!views.find((existing) => existing.name === view.name)) { - views.push(view); - } - }); - } - }); - - const courses = [exampleCourse]; - - // Return structured data for studio-ui integration - return { - // CourseWare instances with question instances - courses, - - // Question class constructors for registration - questionClasses, - - // Available data shapes for studio-ui CreateCardView - dataShapes, - - // Vue components for runtime registration - views, - - // Metadata for debugging and analysis - meta: { - questionCount: questionClasses.length, - dataShapeCount: dataShapes.length, - viewCount: views.length, - courseCount: courses.length, - packageName: '@vue-skuilder/standalone-ui', - sourceDirectory: 'src/questions', - }, - }; -} - -/** - * Type definitions for the custom questions export structure - * This provides TypeScript support for CLI and studio-ui integration - */ -export interface CustomQuestionsExport { - courses: CourseWare[]; - questionClasses: Array< - typeof SimpleTextQuestion | typeof MultipleChoiceQuestion | typeof NumberRangeQuestion - >; - dataShapes: DataShape[]; - views: ViewComponent[]; - meta: { - questionCount: number; - dataShapeCount: number; - viewCount: number; - courseCount: number; - packageName: string; - sourceDirectory: string; - }; -} - -// Default export for convenience -export default allCustomQuestions; diff --git a/packages/sk-contributor/src/router/index.ts b/packages/sk-contributor/src/router/index.ts deleted file mode 100644 index 76b516bbf..000000000 --- a/packages/sk-contributor/src/router/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; -import HomeView from '../views/HomeView.vue'; -import StudyView from '../views/StudyView.vue'; -import ProgressView from '../views/ProgressView.vue'; -import BrowseView from '../views/BrowseView.vue'; -import UserStatsView from '../views/UserStatsView.vue'; -import UserSettingsView from '../views/UserSettingsView.vue'; -import { UserLogin, UserRegistration } from '@vue-skuilder/common-ui'; - -const routes: Array = [ - { - path: '/', - name: 'home', - component: HomeView, - }, - { - path: '/study', - name: 'study', - component: StudyView, - }, - { - path: '/progress', - name: 'progress', - component: ProgressView, - }, - { - path: '/browse', - name: 'browse', - component: BrowseView, - }, - { - path: '/login', - name: 'login', - component: UserLogin, - }, - { - path: '/register', - alias: '/signup', - name: 'Register', - component: UserRegistration, - }, - { - path: '/u/:username', - name: 'UserSettings', - component: UserSettingsView, - }, - { - path: '/u/:username/stats', - name: 'UserStats', - component: UserStatsView, - }, -]; - -const router = createRouter({ - history: createWebHistory(), - routes, -}); - -export default router; diff --git a/packages/sk-contributor/src/views/BrowseView.vue b/packages/sk-contributor/src/views/BrowseView.vue deleted file mode 100644 index 154178644..000000000 --- a/packages/sk-contributor/src/views/BrowseView.vue +++ /dev/null @@ -1,41 +0,0 @@ - - - diff --git a/packages/sk-contributor/src/views/HomeView.vue b/packages/sk-contributor/src/views/HomeView.vue deleted file mode 100644 index 256453750..000000000 --- a/packages/sk-contributor/src/views/HomeView.vue +++ /dev/null @@ -1,35 +0,0 @@ - - - diff --git a/packages/sk-contributor/src/views/ProgressView.vue b/packages/sk-contributor/src/views/ProgressView.vue deleted file mode 100644 index 57eb37c55..000000000 --- a/packages/sk-contributor/src/views/ProgressView.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/packages/sk-contributor/src/views/StudyView.vue b/packages/sk-contributor/src/views/StudyView.vue deleted file mode 100644 index d73a9885c..000000000 --- a/packages/sk-contributor/src/views/StudyView.vue +++ /dev/null @@ -1,84 +0,0 @@ - - - diff --git a/packages/sk-contributor/src/views/UserSettingsView.vue b/packages/sk-contributor/src/views/UserSettingsView.vue deleted file mode 100644 index 0923a8b27..000000000 --- a/packages/sk-contributor/src/views/UserSettingsView.vue +++ /dev/null @@ -1,175 +0,0 @@ - - - \ No newline at end of file diff --git a/packages/sk-contributor/src/views/UserStatsView.vue b/packages/sk-contributor/src/views/UserStatsView.vue deleted file mode 100644 index a41d9e1eb..000000000 --- a/packages/sk-contributor/src/views/UserStatsView.vue +++ /dev/null @@ -1,76 +0,0 @@ - - - \ No newline at end of file diff --git a/packages/sk-contributor/tsconfig.json b/packages/sk-contributor/tsconfig.json deleted file mode 100644 index d253dee15..000000000 --- a/packages/sk-contributor/tsconfig.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "useDefineForClassFields": true, - "module": "ESNext", - "moduleResolution": "bundler", - "jsx": "preserve", - "resolveJsonModule": true, - "isolatedModules": true, - "lib": [ - "ESNext", - "DOM" - ], - "noEmit": true, - "baseUrl": ".", - "types": [ - "vite/client" - ], - "strict": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true - }, - "include": [ - "src/**/*.ts", - "src/**/*.d.ts", - "src/**/*.tsx", - "src/**/*.vue" - ] -} \ No newline at end of file diff --git a/packages/sk-contributor/vite.config.ts b/packages/sk-contributor/vite.config.ts deleted file mode 100644 index f2a0f571e..000000000 --- a/packages/sk-contributor/vite.config.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { defineConfig } from 'vite'; -import vue from '@vitejs/plugin-vue'; -import dts from 'vite-plugin-dts'; -import { resolve } from 'path'; -import { fileURLToPath, URL } from 'node:url'; - -// Determine build mode from environment variable -const buildMode = process.env.BUILD_MODE || 'webapp'; - -export default defineConfig({ - plugins: [ - vue(), - // Only include dts plugin for library builds - ...(buildMode === 'library' - ? [dts({ - insertTypesEntry: true, - include: ['src/questions/**/*.ts', 'src/questions/**/*.vue'], - exclude: ['**/*.spec.ts', '**/*.test.ts'], - outDir: 'dist-lib', - })] - : [] - ) - ], - resolve: { - alias: { - // Alias for internal src paths - '@': fileURLToPath(new URL('./src', import.meta.url)), - // Add events alias if needed (often required by dependencies) - events: 'events', - }, - extensions: ['.js', '.ts', '.json', '.vue'], - dedupe: [ - // Ensure single instances of core libs and published packages - 'vue', - 'vuetify', - 'pinia', - 'vue-router', - '@vue-skuilder/db', - '@vue-skuilder/common', - '@vue-skuilder/common-ui', - '@vue-skuilder/courseware', - ], - }, - // --- Dependencies optimization --- - optimizeDeps: { - include: [ - 'events', - '@vue-skuilder/common-ui', - '@vue-skuilder/db', - '@vue-skuilder/common', - '@vue-skuilder/courseware', - ], - }, - server: { - port: 5173, // Use standard Vite port for standalone projects - }, - build: buildMode === 'library' - ? { - // Library build configuration - sourcemap: true, - target: 'es2020', - minify: 'terser', - terserOptions: { - keep_classnames: true, - }, - lib: { - entry: resolve(__dirname, 'src/questions/index.ts'), - name: 'VueSkuilderStandaloneQuestions', - fileName: (format) => `questions.${format === 'es' ? 'mjs' : 'cjs.js'}`, - }, - rollupOptions: { - // External packages that shouldn't be bundled in library mode - // For studio integration, we bundle vue-skuilder packages to avoid npm resolution issues - external: [ - // Bundle everything for studio integration - no externals - ], - output: { - // Global variables for UMD build - globals: { - vue: 'Vue', - 'vue-router': 'VueRouter', - vuetify: 'Vuetify', - pinia: 'Pinia', - // Remove globals for bundled packages - // '@vue-skuilder/common': 'VueSkuilderCommon', - // '@vue-skuilder/common-ui': 'VueSkuilderCommonUI', - // '@vue-skuilder/courseware': 'VueSkuilderCourseWare', - // '@vue-skuilder/db': 'VueSkuilderDB', - }, - exports: 'named', - // Preserve CSS in the output bundle - assetFileNames: 'assets/[name].[ext]', - }, - }, - // Output to separate directory for library build - outDir: 'dist-lib', - // Allow CSS code splitting for component libraries - cssCodeSplit: true, - } - : { - // Webapp build configuration (existing) - sourcemap: true, - target: 'es2020', - minify: 'terser', - terserOptions: { - keep_classnames: true, - }, - // Standard webapp output directory - outDir: 'dist', - }, - // Add define block for process polyfills - define: { - global: 'window', - 'process.env': process.env, - 'process.browser': true, - 'process.version': JSON.stringify(process.version), - }, -}); From 0ae6ab06462fd48354c31761ca60c18a2dba0a93 Mon Sep 17 00:00:00 2001 From: NiloCK Date: Mon, 10 Nov 2025 13:19:48 -0400 Subject: [PATCH 06/13] update working doc --- agent/a.3.todo.md | 57 +++++++++++++++++------------------------------ 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/agent/a.3.todo.md b/agent/a.3.todo.md index 1d468206d..d00371b87 100644 --- a/agent/a.3.todo.md +++ b/agent/a.3.todo.md @@ -62,50 +62,36 @@ - [ ] Verify build succeeds - [ ] Check that component names are preserved in output -## Phase 3: Side-Effect Import to standalone-ui +## Phase 3: Side-Effect Import to standalone-ui (OMITTED) + +**DECISION:** This phase was omitted because the side-effect import is not needed. + +**Rationale:** +- Question classes already have inline view registration (e.g., `SimpleTextQuestion.views = [{ name, component }]`) +- No additional setup code exists in `index.ts` that needs to run +- BACKPORT_CHECKLIST #7 even recommends removing side-effects as "future" work +- We're already using the preferred direct inline pattern (#5) + +**Risk:** If tests fail with "view not found" errors, we may need to add this back. But current analysis indicates it's redundant. ### 3.1 Main.ts Import -- [ ] Open `packages/standalone-ui/src/main.ts` -- [ ] Locate line 27-28 (imports from courseware) -- [ ] Add line after line 27: `import './questions/index';` -- [ ] Ensure it's before: `import { exampleCourse } from './questions/exampleCourse';` +- [x] ~~Add `import './questions/index';`~~ OMITTED (not needed) -### 3.2 Test Dev Mode +### 3.2 Test Dev Mode (DEFERRED) - [ ] Run: `yarn workspace @vue-skuilder/standalone-ui dev` - [ ] Navigate to http://localhost:6173 - [ ] Verify app loads without errors - [ ] Check browser console for warnings -## Phase 4: Replicate to sk-contributor - -### 4.1 Vite Config -- [ ] Open `packages/sk-contributor/vite.config.ts` -- [ ] Apply same terser options changes as 1.1 -- [ ] Verify syntax +## Phase 4: sk-contributor Package (REMOVED) -### 4.2 Views Export Format -- [ ] Open `packages/sk-contributor/src/questions/index.ts` -- [ ] Apply same changes as 1.2 (views array and logic) +**DECISION:** The sk-contributor package never made it off the ground and is being removed from the monorepo. -### 4.3 TypeScript Interface -- [ ] Apply same changes as 1.3 to interface +### 4.1 Remove Package +- [ ] User will execute: `git rm -r packages/sk-contributor` +- [ ] Update workspace references if needed (package.json, tsconfig, etc.) -### 4.4 Component Names -- [ ] Open `packages/sk-contributor/src/questions/SimpleTextQuestionView.vue` -- [ ] Add defineOptions with name (same as 2.1) -- [ ] Open `packages/sk-contributor/src/questions/MultipleChoiceQuestionView.vue` -- [ ] Add defineOptions with name (same as 2.2) -- [ ] Open `packages/sk-contributor/src/questions/NumberRangeQuestionView.vue` -- [ ] Add defineOptions with name (same as 2.3) - -### 4.5 Side-Effect Import -- [ ] Open `packages/sk-contributor/src/main.ts` -- [ ] Apply same change as 3.1 - -### 4.6 Test Build -- [ ] Run: `BUILD_MODE=library yarn workspace @vue-skuilder/sk-contributor build` -- [ ] Verify build succeeds -- [ ] Spot check output structure +**Note:** All backport changes were only applied to standalone-ui. If sk-contributor is revived in the future, it would need the same changes applied. ## Phase 5: Update Documentation @@ -115,10 +101,7 @@ - [ ] Update examples to show direct inline view registration - [ ] Add note about `markRaw()` for Vue components - [ ] Add note about `{ name, component }` format - -### 5.2 sk-contributor README (if exists) -- [ ] Check if `packages/sk-contributor/src/questions/README.md` exists -- [ ] If yes, apply same documentation updates +- [ ] Note that side-effect import is NOT needed (inline pattern preferred) ## Phase 6: CI Enhancement - Standalone Mode Tests From c8d73e296272c1d2999fe77345c8d081c4a9fc30 Mon Sep 17 00:00:00 2001 From: NiloCK Date: Mon, 10 Nov 2025 13:22:07 -0400 Subject: [PATCH 07/13] followups on rming sk-contributor --- scripts/vtag.js | 1 - yarn.lock | 38 +++++--------------------------------- 2 files changed, 5 insertions(+), 34 deletions(-) diff --git a/scripts/vtag.js b/scripts/vtag.js index bf6862f7b..cb726cb91 100755 --- a/scripts/vtag.js +++ b/scripts/vtag.js @@ -18,7 +18,6 @@ const PACKAGES = [ 'express', 'mcp', 'cli', - 'sk-contributor', ]; function main() { diff --git a/yarn.lock b/yarn.lock index c607b1d88..0454d96c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6453,7 +6453,7 @@ __metadata: languageName: node linkType: hard -"@vue-skuilder/cli@npm:^0.1.14, @vue-skuilder/cli@workspace:packages/cli": +"@vue-skuilder/cli@workspace:packages/cli": version: 0.0.0-use.local resolution: "@vue-skuilder/cli@workspace:packages/cli" dependencies: @@ -6515,7 +6515,7 @@ __metadata: languageName: unknown linkType: soft -"@vue-skuilder/common-ui@npm:^0.1.14, @vue-skuilder/common-ui@workspace:*, @vue-skuilder/common-ui@workspace:packages/common-ui": +"@vue-skuilder/common-ui@workspace:*, @vue-skuilder/common-ui@workspace:packages/common-ui": version: 0.0.0-use.local resolution: "@vue-skuilder/common-ui@workspace:packages/common-ui" dependencies: @@ -6557,7 +6557,7 @@ __metadata: languageName: unknown linkType: soft -"@vue-skuilder/common@npm:^0.1.14, @vue-skuilder/common@workspace:*, @vue-skuilder/common@workspace:^, @vue-skuilder/common@workspace:packages/common": +"@vue-skuilder/common@workspace:*, @vue-skuilder/common@workspace:^, @vue-skuilder/common@workspace:packages/common": version: 0.0.0-use.local resolution: "@vue-skuilder/common@workspace:packages/common" dependencies: @@ -6568,7 +6568,7 @@ __metadata: languageName: unknown linkType: soft -"@vue-skuilder/courseware@npm:^0.1.14, @vue-skuilder/courseware@workspace:*, @vue-skuilder/courseware@workspace:packages/courseware": +"@vue-skuilder/courseware@workspace:*, @vue-skuilder/courseware@workspace:packages/courseware": version: 0.0.0-use.local resolution: "@vue-skuilder/courseware@workspace:packages/courseware" dependencies: @@ -6591,7 +6591,7 @@ __metadata: languageName: unknown linkType: soft -"@vue-skuilder/db@npm:^0.1.14, @vue-skuilder/db@workspace:*, @vue-skuilder/db@workspace:^, @vue-skuilder/db@workspace:packages/db": +"@vue-skuilder/db@workspace:*, @vue-skuilder/db@workspace:^, @vue-skuilder/db@workspace:packages/db": version: 0.0.0-use.local resolution: "@vue-skuilder/db@workspace:packages/db" dependencies: @@ -17939,34 +17939,6 @@ __metadata: languageName: node linkType: hard -"sk-contributor@workspace:packages/sk-contributor": - version: 0.0.0-use.local - resolution: "sk-contributor@workspace:packages/sk-contributor" - dependencies: - "@mdi/font": "npm:^7.3.67" - "@types/cypress": "npm:1.1.6" - "@types/events": "npm:^3" - "@vitejs/plugin-vue": "npm:^5.2.1" - "@vue-skuilder/cli": "npm:^0.1.14" - "@vue-skuilder/common": "npm:^0.1.14" - "@vue-skuilder/common-ui": "npm:^0.1.14" - "@vue-skuilder/courseware": "npm:^0.1.14" - "@vue-skuilder/db": "npm:^0.1.14" - cypress: "npm:14.1.0" - events: "npm:^3.3.0" - pinia: "npm:^2.3.0" - terser: "npm:^5.39.0" - typescript: "npm:^5.7.2" - vite: "npm:^6.0.9" - vite-plugin-dts: "npm:^4.3.0" - vue: "npm:^3.5.13" - vue-router: "npm:^4.2.0" - vue-tsc: "npm:^1.8.0" - vuetify: "npm:^3.7.0" - wait-on: "npm:8.0.2" - languageName: unknown - linkType: soft - "skin-tone@npm:^2.0.0": version: 2.0.0 resolution: "skin-tone@npm:2.0.0" From 8ada1580a34d8a80f2c7536e9f2c05f6125692d6 Mon Sep 17 00:00:00 2001 From: NiloCK Date: Mon, 10 Nov 2025 13:30:54 -0400 Subject: [PATCH 08/13] update standalone-ui readme --- agent/a.3.todo.md | 13 +++--- .../standalone-ui/src/questions/README.md | 42 ++++++++++++------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/agent/a.3.todo.md b/agent/a.3.todo.md index d00371b87..da2ab7a82 100644 --- a/agent/a.3.todo.md +++ b/agent/a.3.todo.md @@ -96,12 +96,13 @@ ## Phase 5: Update Documentation ### 5.1 Standalone-ui README -- [ ] Open `packages/standalone-ui/src/questions/README.md` -- [ ] Update examples to show `defineOptions({ name: '...' })` pattern -- [ ] Update examples to show direct inline view registration -- [ ] Add note about `markRaw()` for Vue components -- [ ] Add note about `{ name, component }` format -- [ ] Note that side-effect import is NOT needed (inline pattern preferred) +- [x] Open `packages/standalone-ui/src/questions/README.md` +- [x] Update examples to show `defineOptions({ name: '...' })` pattern +- [x] Update examples to show direct inline view registration with `markRaw()` +- [x] Add note about `markRaw()` for Vue components +- [x] Add note about `{ name, component }` format for studio compatibility +- [x] Note that side-effect import is NOT needed (inline pattern preferred) +- [x] Add "Best Practices" section with key guidelines ## Phase 6: CI Enhancement - Standalone Mode Tests diff --git a/packages/standalone-ui/src/questions/README.md b/packages/standalone-ui/src/questions/README.md index 231162d8c..4142829ef 100644 --- a/packages/standalone-ui/src/questions/README.md +++ b/packages/standalone-ui/src/questions/README.md @@ -24,19 +24,24 @@ To use your custom questions in a course, you need to: ```typescript // MyCustomQuestion.ts + import { markRaw } from 'vue'; import { Question, DataShape, ViewData, Answer } from '@vue-skuilder/courseware'; import { FieldType } from '@vue-skuilder/common'; import MyCustomQuestionView from './MyCustomQuestionView.vue'; export class MyCustomQuestion extends Question { public static dataShapes: DataShape[] = [ - new DataShape('MyCustomQuestion', [ - { name: 'myField', type: FieldType.STRING }, - ]), + { + name: 'MyCustomQuestion' as DataShapeName, + fields: [ + { name: 'myField', type: FieldType.STRING }, + ], + }, ]; + // Direct inline view registration - use markRaw() to prevent Vue reactivity public static views = [ - { name: 'MyCustomQuestionView', component: MyCustomQuestionView }, + { name: 'MyCustomQuestionView', component: markRaw(MyCustomQuestionView) }, ]; constructor(data: ViewData[]) { @@ -59,6 +64,11 @@ To use your custom questions in a course, you need to: } ``` + **Important Notes:** + - Use `markRaw()` to wrap component imports (prevents unnecessary reactivity) + - Views must be `{ name: string, component: ViewComponent }` format for studio mode + - The `name` field must match your component's `defineOptions({ name: '...' })` + 2. **Create Your Vue Component**: Create a Vue component (e.g., `MyCustomQuestionView.vue`) that will render your question and allow user interaction. This component will receive props based on the `ViewData` you define for your question. ```vue @@ -72,9 +82,14 @@ To use your custom questions in a course, you need to: ``` + **Important:** Component name must match Question class `views` array name + 3. **Register Your Question and Course**: In your application's entry point (e.g., `src/main.ts` or `src/App.vue`), you need to import your custom question and include it in a `Course` instance. Then, register this course with the `allCourses` list. ```typescript @@ -117,13 +134,10 @@ To use your custom questions in a course, you need to: **Note**: The `allCourses` object is a singleton that manages all available courses and their associated questions and views. By adding your custom course to `allCourses.courses`, it becomes discoverable by the `CardViewer` and other components that rely on the course registry. -## Developing New Questions - -When developing new questions, consider the following: - -- **DataShape Definition**: Carefully define the `DataShape` for your question. This dictates the structure of the data that will be passed to your question's constructor and Vue component. -- **Answer Evaluation**: Implement the `isCorrect` method in your `Question` subclass to define how user answers are evaluated. -- **Vue Component Props**: Ensure your Vue component's `props` match the data fields defined in your `DataShape` and any additional data you pass from your `Question` instance. -- **StudySessionStore**: Use the `useStudySessionStore()` composable from `@vue-skuilder/common-ui` to submit user answers and interact with the study session logic. +## Key Requirements -Feel free to modify and extend the provided examples to suit your needs. +- **DataShape Definition**: Defines data structure passed to constructor and Vue component +- **Answer Evaluation**: Implement `isCorrect()` method in your Question subclass +- **Component Names**: Use `defineOptions({ name: '...' })` - must match Question class `views` array +- **View Registration**: Register views inline with `markRaw()` - no separate setup files needed +- **Format**: Use `{ name: string, component: ViewComponent }` format for studio mode compatibility From 0096713219e87e8600ff2ad4950349270cbd2626 Mon Sep 17 00:00:00 2001 From: NiloCK Date: Mon, 10 Nov 2025 13:46:04 -0400 Subject: [PATCH 09/13] mirror changes from vite config... see https://github.com/patched-network/vue-skuilder/actions/runs/19240733395/job/55002462664?pr=958 --- packages/cli/src/utils/template.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/cli/src/utils/template.ts b/packages/cli/src/utils/template.ts index beecb3e6d..0dd8fe025 100644 --- a/packages/cli/src/utils/template.ts +++ b/packages/cli/src/utils/template.ts @@ -192,6 +192,10 @@ export default defineConfig({ minify: 'terser', terserOptions: { keep_classnames: true, + keep_fnames: true, + mangle: { + properties: false, + }, }, lib: { entry: resolve(__dirname, 'src/questions/index.ts'), From d6345fd46402760a39e382ff7c24698042b8cf4f Mon Sep 17 00:00:00 2001 From: NiloCK Date: Mon, 10 Nov 2025 14:47:05 -0400 Subject: [PATCH 10/13] add test skeletons for custom Q e2e assert --- .../cypress/e2e/custom-questions-dev.cy.js | 46 ++++++++++ .../cypress/e2e/custom-questions-studio.cy.js | 87 +++++++++++++++++++ packages/cli/package.json | 9 +- 3 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 packages/cli/cypress/e2e/custom-questions-dev.cy.js create mode 100644 packages/cli/cypress/e2e/custom-questions-studio.cy.js diff --git a/packages/cli/cypress/e2e/custom-questions-dev.cy.js b/packages/cli/cypress/e2e/custom-questions-dev.cy.js new file mode 100644 index 000000000..48ce83067 --- /dev/null +++ b/packages/cli/cypress/e2e/custom-questions-dev.cy.js @@ -0,0 +1,46 @@ +// Custom Questions Workflow - Dev Mode +// Tests that card created in studio mode and flushed to static +// renders correctly in standalone dev mode + +describe('Custom Questions - Dev Mode', () => { + it('should load dev mode successfully', () => { + cy.visit('http://localhost:6173'); + + // Verify basic page load + cy.get('body').should('be.visible'); + }); + + it('should render flushed card in study view', () => { + cy.visit('http://localhost:6173/study'); + + // Wait for study view to load + cy.get('body', { timeout: 15000 }).should('be.visible'); + + // Verify card with SimpleTextQuestionView renders + cy.get('[data-viewable*="SimpleTextQuestionView"]', { timeout: 15000 }) + .should('exist') + .and('be.visible'); + + // Verify the card content that was created in studio + cy.contains('What is 2+2?').should('exist'); + + // Verify the input element is present (characteristic of SimpleTextQuestionView) + cy.get('input[type="text"], input[placeholder*="answer"]') + .should('be.visible'); + }); + + it('should allow interaction with the custom question', () => { + cy.visit('http://localhost:6173/study'); + + // Wait for card to render + cy.get('[data-viewable*="SimpleTextQuestionView"]', { timeout: 15000 }) + .should('exist'); + + // Type an answer + cy.get('input[type="text"], input[placeholder*="answer"]') + .type('4'); + + // Submit (look for submit button) + cy.contains('button', /submit/i).should('be.visible'); + }); +}); diff --git a/packages/cli/cypress/e2e/custom-questions-studio.cy.js b/packages/cli/cypress/e2e/custom-questions-studio.cy.js new file mode 100644 index 000000000..5c72a7804 --- /dev/null +++ b/packages/cli/cypress/e2e/custom-questions-studio.cy.js @@ -0,0 +1,87 @@ +// Custom Questions Workflow - Studio Mode +// Tests custom question DataShapes in studio mode: +// - CreateCardView shows custom question types +// - Can create card with custom DataShape +// - Card renders in browse view +// - Flush to static works + +describe('Custom Questions - Studio Mode', () => { + it('should load studio UI successfully', () => { + cy.visit('http://localhost:7174'); + + // Verify basic page load + cy.get('body').should('be.visible'); + }); + + it('should show custom DataShapes in CreateCardView', () => { + cy.visit('http://localhost:7174/create'); + + // Wait for page to load + cy.get('body', { timeout: 15000 }).should('be.visible'); + + // Verify custom question types appear as options + // (Exact selectors will depend on studio-ui CreateCardView implementation) + cy.contains('SimpleTextQuestion', { timeout: 15000 }).should('exist'); + cy.contains('MultipleChoiceQuestion').should('exist'); + cy.contains('NumberRangeQuestion').should('exist'); + }); + + it('should create a card with SimpleTextQuestion DataShape', () => { + cy.visit('http://localhost:7174/create'); + + // Wait for page load + cy.get('body', { timeout: 15000 }).should('be.visible'); + + // Select SimpleTextQuestion DataShape + // (Exact interaction will depend on studio-ui form implementation) + cy.contains('SimpleTextQuestion').click(); + + // Fill in required fields + // Field names/selectors depend on studio-ui form structure + // This is a placeholder - adjust based on actual UI + cy.get('input[name="questionText"], input[placeholder*="question"]') + .type('What is 2+2?'); + cy.get('input[name="correctAnswer"], input[placeholder*="answer"]') + .type('4'); + + // Submit the card + cy.contains('button', /create|save|submit/i).click(); + + // Verify success (redirect, message, etc.) + // Adjust based on studio-ui behavior + cy.url({ timeout: 10000 }).should('not.include', '/create'); + }); + + it('should render created card in studio browse view', () => { + cy.visit('http://localhost:7174/browse'); + + // Wait for cards to load + cy.get('.cardView, [data-viewable]', { timeout: 15000 }) + .should('exist') + .and('be.visible'); + + // Verify our created card appears + // Check for SimpleTextQuestionView component + cy.get('[data-viewable*="SimpleTextQuestionView"]', { timeout: 15000 }) + .should('exist'); + + // Verify card content + cy.contains('What is 2+2?').should('exist'); + }); + + it('should flush changes to static files', () => { + // Navigate to wherever the flush button is located + // (Could be in settings, admin panel, or main nav) + cy.visit('http://localhost:7174'); + + // Find and click "Flush to Static" button + // Adjust selector based on actual studio-ui implementation + cy.contains('button', /flush.*static|save.*static|export/i, { timeout: 10000 }) + .click(); + + // Wait for flush operation to complete + // Look for success message or confirmation + cy.contains(/flushed|saved|exported|success/i, { timeout: 30000 }) + .should('exist'); + }); +}); diff --git a/packages/cli/package.json b/packages/cli/package.json index bf68a8368..5401b39ef 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -27,8 +27,13 @@ "lint:fix": "npx eslint . --fix", "lint:check": "npx eslint . --max-warnings 0", "try:init": "node dist/cli.js init testproject --dangerously-clobber --no-interactive --data-layer static --import-course-data --import-server-url http://localhost:5984 --import-username admin --import-password password --import-course-ids 2aeb8315ef78f3e89ca386992d00825b && cd testproject && npm i && npm install --save-dev @vue-skuilder/cli@file:.. && npm install @vue-skuilder/db@file:../../db @vue-skuilder/courseware@file:../../courseware @vue-skuilder/common-ui@file:../../common-ui @vue-skuilder/express@file:../../express", - "test:e2e": "cypress open", - "test:e2e:headless": "cypress run" + "try:init:empty": "node dist/cli.js init testproject-empty --dangerously-clobber --no-interactive --data-layer static && cd testproject-empty && npm i && npm install --save-dev @vue-skuilder/cli@file:.. && npm install @vue-skuilder/db@file:../../db @vue-skuilder/courseware@file:../../courseware @vue-skuilder/common-ui@file:../../common-ui @vue-skuilder/express@file:../../express", + "test:e2e": "cypress open --spec 'cypress/e2e/scaffolded-app.cy.js'", + "test:e2e:headless": "cypress run --spec 'cypress/e2e/scaffolded-app.cy.js'", + "test:e2e:custom:studio": "cypress open --spec 'cypress/e2e/custom-questions-studio.cy.js'", + "test:e2e:custom:studio:headless": "cypress run --spec 'cypress/e2e/custom-questions-studio.cy.js'", + "test:e2e:custom:dev": "cypress open --spec 'cypress/e2e/custom-questions-dev.cy.js'", + "test:e2e:custom:dev:headless": "cypress run --spec 'cypress/e2e/custom-questions-dev.cy.js'" }, "keywords": [ "cli", From 50fd90306c9107506ce541f09a1cfd9d6bcb2b91 Mon Sep 17 00:00:00 2001 From: NiloCK Date: Mon, 10 Nov 2025 14:47:17 -0400 Subject: [PATCH 11/13] update working doc --- agent/a.3.todo.md | 133 ++++++++++++++++++++++++++-------------------- 1 file changed, 74 insertions(+), 59 deletions(-) diff --git a/agent/a.3.todo.md b/agent/a.3.todo.md index da2ab7a82..9f81b5940 100644 --- a/agent/a.3.todo.md +++ b/agent/a.3.todo.md @@ -8,6 +8,9 @@ - [x] Add `keep_fnames: true` - [x] Add `mangle: { properties: false }` - [x] Verify syntax is correct +- [x] Sync same changes to `packages/cli/src/utils/template.ts` (line ~193) + - **Why:** CLI uses template.ts to generate vite.config.ts for scaffolded projects + - **CI Check:** Automated check ensures these stay in sync ### 1.2 Views Export Format - index.ts - [x] Open `packages/standalone-ui/src/questions/index.ts` @@ -104,65 +107,77 @@ - [x] Note that side-effect import is NOT needed (inline pattern preferred) - [x] Add "Best Practices" section with key guidelines -## Phase 6: CI Enhancement - Standalone Mode Tests - -### 6.1 Enhanced Scaffolded App Test -- [ ] Open `packages/cli/cypress/e2e/scaffolded-app.cy.js` -- [ ] Add new describe block: 'Custom Questions - Standalone Mode' -- [ ] Add test: 'should render custom question types in study view' - - Check for `[data-viewable*="SimpleTextQuestionView"]` - - Check for input elements -- [ ] Add test: 'should display all three question types' - - Verify SimpleText, MultipleChoice, NumberRange can appear -- [ ] Run local test to verify: `cd packages/cli && yarn test:e2e:headless` - -### 6.2 Verify Existing CI Passes -- [ ] Check CI workflow will trigger on these changes -- [ ] Ensure `ci-pkg-cli.yml` includes relevant paths -- [ ] Plan to monitor CI after push - -## Phase 7: CI Enhancement - Studio Mode Tests (Optional/Future) - -### 7.1 Studio Mode Test File -- [ ] Create `packages/cli/cypress/e2e/scaffolded-app-studio.cy.js` -- [ ] Add test: 'should load studio UI' - - Visit studio port (7174) - - Check for page load -- [ ] Add test: 'should show custom data shapes in CreateCardView' - - Look for SimpleTextQuestion, MultipleChoiceQuestion, NumberRangeQuestion - -### 7.2 Cypress Config for Studio -- [ ] Create `packages/cli/cypress.studio.config.js` -- [ ] Configure baseUrl: 'http://localhost:7174' -- [ ] Configure specPattern for `-studio.cy.js` files - -### 7.3 Package Scripts -- [ ] Add to `packages/cli/package.json`: - - `"test:e2e:studio": "cypress open --config-file cypress.studio.config.js"` - - `"test:e2e:studio:headless": "cypress run --config-file cypress.studio.config.js"` - -### 7.4 CI Workflow Addition -- [ ] Open `.github/workflows/ci-pkg-cli.yml` -- [ ] Add step after "Run try:init": - ```yaml - - name: Start studio mode and wait - working-directory: packages/cli/testproject - run: | - npx skuilder studio --no-browser & - npx wait-on http://localhost:7174 --timeout 120000 - ``` -- [ ] Add step for studio tests: - ```yaml - - name: Run E2E tests on studio mode - working-directory: packages/cli - run: yarn test:e2e:studio:headless - ``` -- [ ] Update cleanup step to kill studio ports: - ```yaml - kill $(lsof -t -i:7174) || true - kill $(lsof -t -i:3001) || true - kill $(lsof -t -i:5985) || true - ``` +## Phase 6: CI Enhancement - Custom Questions Workflow Test + +**Strategy:** Create end-to-end test of custom questions workflow: +1. Scaffold empty static course +2. Start studio mode +3. Create card using custom DataShape via Cypress +4. Verify card renders in studio browse view +5. Flush to static via Cypress button click +6. Start dev mode +7. Verify card renders in dev study view + +**Key Decisions:** +- Leave existing `scaffolded-app.cy.js` alone (tests packed courses) +- New script: `try:init:empty` (no --import-course-data) +- **TWO Cypress test files** (studio tests, then dev tests - cleaner CI workflow) +- Clean up between existing and new test +- Studio mode manages its own CouchDB (no conflicts) +- Flush via Cypress clicking studio UI button + +### 6.1 CLI Script - Empty Project Init +- [x] Add to `packages/cli/package.json`: + - `try:init:empty` - scaffolds empty project with template custom questions + - Includes npm install and file:.. dependencies + +### 6.2 Cypress Tests - Custom Questions Workflow +- [x] Create `packages/cli/cypress/e2e/custom-questions-studio.cy.js` + - Test 1: Studio UI loads + - Test 2: Custom DataShapes appear in CreateCardView + - Test 3: Create card with SimpleTextQuestion + - Test 4: Card renders in studio browse view + - Test 5: Flush to static button works +- [x] Create `packages/cli/cypress/e2e/custom-questions-dev.cy.js` + - Test 1: Dev mode loads + - Test 2: Flushed card renders in study view + - Test 3: Can interact with custom question + +### 6.3 CI Workflow Updates +- [x] Update `.github/workflows/ci-pkg-cli.yml` +- [x] Add after existing test cleanup: + - Create empty project + - Start studio mode (wait on 7174) + - Run studio tests + - Shutdown studio + - Start dev mode (wait on 6173) + - Run dev tests + - Final cleanup (all ports) + +### 6.4 Package Scripts +- [x] Update existing scripts to be specific: + - `test:e2e` - now targets only `scaffolded-app.cy.js` + - `test:e2e:headless` - now targets only `scaffolded-app.cy.js` +- [x] Add new custom questions scripts: + - `test:e2e:custom:studio` - open studio tests + - `test:e2e:custom:studio:headless` - run studio tests + - `test:e2e:custom:dev` - open dev tests + - `test:e2e:custom:dev:headless` - run dev tests + +### 6.5 Local Testing (DEFERRED) +- [ ] Build CLI: `yarn workspace @vue-skuilder/cli build` +- [ ] Run empty init: `cd packages/cli && yarn try:init:empty` +- [ ] Test studio: `cd testproject-empty && npx skuilder studio` +- [ ] Manually verify custom questions appear in CreateCardView +- [ ] Run Cypress studio tests: `cd packages/cli && yarn test:e2e:custom:studio` +- [ ] After flush, run dev tests: `yarn test:e2e:custom:dev` + +## Phase 7: Obsolete (Merged into Phase 6) + +Studio mode tests are now fully covered in Phase 6 with the two-file Cypress approach: +- `custom-questions-studio.cy.js` tests studio mode functionality +- `custom-questions-dev.cy.js` tests dev mode after flush +- CI workflow runs both sequentially with proper server lifecycle management ## Phase 8: Final Verification From 25fea27f1036b87eac48d4b3da6cd3e5931f8001 Mon Sep 17 00:00:00 2001 From: NiloCK Date: Mon, 10 Nov 2025 14:51:40 -0400 Subject: [PATCH 12/13] ignore testproject-empty --- packages/cli/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli/.gitignore b/packages/cli/.gitignore index 54ad7d8d7..7d64d151f 100644 --- a/packages/cli/.gitignore +++ b/packages/cli/.gitignore @@ -1,2 +1,3 @@ testproject +testproject-empty userdb-Guest From 4e9ccbec769c890fd81f4afe5bdd08b92af38320 Mon Sep 17 00:00:00 2001 From: NiloCK Date: Mon, 10 Nov 2025 16:10:23 -0400 Subject: [PATCH 13/13] improve types generation from common-ui... using DTS plugin as used in `courseware` pkg --- packages/common-ui/package.json | 4 ++-- packages/common-ui/tsconfig.types.json | 11 ----------- packages/common-ui/vite.config.js | 12 +++++++++++- yarn.lock | 1 + 4 files changed, 14 insertions(+), 14 deletions(-) delete mode 100644 packages/common-ui/tsconfig.types.json diff --git a/packages/common-ui/package.json b/packages/common-ui/package.json index 7b844f049..410bd6d18 100644 --- a/packages/common-ui/package.json +++ b/packages/common-ui/package.json @@ -22,8 +22,7 @@ ], "scripts": { "dev": "vite build --watch", - "build": "vite build && yarn build-types", - "build-types": "tsc --project tsconfig.types.json", + "build": "vite build", "lint": "eslint . --fix", "lint:check": "eslint .", "test:unit": "vitest run", @@ -68,6 +67,7 @@ "sass": "^1.83.0", "typescript": "~5.7.2", "vite": "^6.0.9", + "vite-plugin-dts": "^4.5.3", "vitest": "^3.0.5" }, "stableVersion": "0.1.14" diff --git a/packages/common-ui/tsconfig.types.json b/packages/common-ui/tsconfig.types.json deleted file mode 100644 index a50948ce6..000000000 --- a/packages/common-ui/tsconfig.types.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "dist", - "declaration": true, - "declarationDir": "dist", - "emitDeclarationOnly": true - }, - "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"], - "exclude": ["node_modules", "**/*.spec.ts", "dist"] -} diff --git a/packages/common-ui/vite.config.js b/packages/common-ui/vite.config.js index 6319ab319..78667b4b8 100644 --- a/packages/common-ui/vite.config.js +++ b/packages/common-ui/vite.config.js @@ -1,6 +1,7 @@ // packages/common-ui/vite.config.js import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; +import dts from 'vite-plugin-dts'; import { resolve } from 'path'; import { createBaseResolve } from '../../vite.config.base.js'; @@ -53,7 +54,16 @@ export default defineConfig({ // This is crucial for component libraries - allow CSS to be in chunks cssCodeSplit: true, }, - plugins: [vue()], + plugins: [ + vue(), + dts({ + insertTypesEntry: true, + // Exclude test files from type generation + exclude: ['**/*.spec.ts', '**/*.test.ts'], + // Include only necessary files + include: ['src/**/*.ts', 'src/**/*.d.ts', 'src/**/*.vue'], + }), + ], resolve: createBaseResolve(resolve(__dirname, '../..'), { '@cui': resolve(__dirname, 'src'), // Override for self-imports during build }), diff --git a/yarn.lock b/yarn.lock index 0454d96c2..99c7e0d6c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6547,6 +6547,7 @@ __metadata: sass: "npm:^1.83.0" typescript: "npm:~5.7.2" vite: "npm:^6.0.9" + vite-plugin-dts: "npm:^4.5.3" vitest: "npm:^3.0.5" vue: "npm:^3.5.13" vuetify: "npm:^3.7.0"