diff --git a/.github/workflows/ci-pkg-cli.yml b/.github/workflows/ci-pkg-cli.yml index ddfe50088..124161e7d 100644 --- a/.github/workflows/ci-pkg-cli.yml +++ b/.github/workflows/ci-pkg-cli.yml @@ -85,6 +85,63 @@ jobs: # Clean up CouchDB yarn couchdb:stop + # Custom Questions Workflow Test + - name: Create empty project for custom questions test + working-directory: packages/cli + run: yarn try:init:empty + + - name: Start studio mode for custom questions workflow + working-directory: packages/cli/testproject-empty + run: | + npm run studio --no-browser > studio.log 2>&1 & + echo "Studio process started with PID: $!" + sleep 2 + + - name: Wait for studio server to be ready + working-directory: packages/cli/testproject-empty + run: | + echo "Giving studio 3 minutes to build and start..." + sleep 180 + echo "Done waiting, checking studio log:" + tail -20 studio.log || echo "No log file" + + - name: Run custom questions studio mode tests + working-directory: packages/cli + run: yarn test:e2e:custom:studio:headless + + - name: Shutdown studio mode + if: always() + run: | + kill $(lsof -t -i:7174) || true + kill $(lsof -t -i:3001) || true + kill $(lsof -t -i:5985) || true + + - name: Start dev mode for custom questions verification + working-directory: packages/cli/testproject-empty + run: | + npm run dev & + npx wait-on http://localhost:5173 --timeout 60000 + + - name: Run custom questions dev mode tests + working-directory: packages/cli + run: yarn test:e2e:custom:dev:headless + + - name: Final cleanup + if: always() + run: | + kill $(lsof -t -i:6173) || true + kill $(lsof -t -i:7174) || true + kill $(lsof -t -i:3001) || true + kill $(lsof -t -i:5985) || true + + - name: Upload studio log on failure + uses: actions/upload-artifact@v4 + if: failure() + with: + name: studio-log + path: packages/cli/testproject-empty/studio.log + retention-days: 7 + - name: Upload screenshots on failure uses: actions/upload-artifact@v4 if: failure() 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), - }, -}); diff --git a/packages/standalone-ui/src/questions/MultipleChoiceQuestion.ts b/packages/standalone-ui/src/questions/MultipleChoiceQuestion.ts index 07c3d04a0..c753cc24c 100644 --- a/packages/standalone-ui/src/questions/MultipleChoiceQuestion.ts +++ b/packages/standalone-ui/src/questions/MultipleChoiceQuestion.ts @@ -1,5 +1,6 @@ import { ViewData, Answer, Question } from '@vue-skuilder/courseware'; import { FieldType, DataShape, DataShapeName } from '@vue-skuilder/common'; +import { markRaw } from 'vue'; import MultipleChoiceQuestionView from './MultipleChoiceQuestionView.vue'; export class MultipleChoiceQuestion extends Question { @@ -14,9 +15,7 @@ export class MultipleChoiceQuestion extends Question { }, ]; - public static views = [ - { name: 'MultipleChoiceQuestionView', component: MultipleChoiceQuestionView }, - ]; + public static views = [markRaw(MultipleChoiceQuestionView)]; // @ts-expect-error TS6133: Used in Vue template private _questionText: string; diff --git a/packages/standalone-ui/src/questions/MultipleChoiceQuestionView.vue b/packages/standalone-ui/src/questions/MultipleChoiceQuestionView.vue index c9cceba07..092b0905e 100644 --- a/packages/standalone-ui/src/questions/MultipleChoiceQuestionView.vue +++ b/packages/standalone-ui/src/questions/MultipleChoiceQuestionView.vue @@ -10,34 +10,48 @@ ``` + **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 diff --git a/packages/standalone-ui/src/questions/SimpleTextQuestion.ts b/packages/standalone-ui/src/questions/SimpleTextQuestion.ts index 1455b5b07..b72b91daf 100644 --- a/packages/standalone-ui/src/questions/SimpleTextQuestion.ts +++ b/packages/standalone-ui/src/questions/SimpleTextQuestion.ts @@ -1,5 +1,6 @@ import { ViewData, Answer, Question } from '@vue-skuilder/courseware'; import { FieldType, DataShape, DataShapeName } from '@vue-skuilder/common'; +import { markRaw } from 'vue'; import SimpleTextQuestionView from './SimpleTextQuestionView.vue'; export class SimpleTextQuestion extends Question { @@ -13,7 +14,7 @@ export class SimpleTextQuestion extends Question { }, ]; - public static views = [{ name: 'SimpleTextQuestionView', component: SimpleTextQuestionView }]; + public static views = [markRaw(SimpleTextQuestionView)]; // @ts-expect-error TS6133: Used in Vue template private questionText: string; diff --git a/packages/standalone-ui/src/questions/SimpleTextQuestionView.vue b/packages/standalone-ui/src/questions/SimpleTextQuestionView.vue index 8c1ca66f8..51c232ed3 100644 --- a/packages/standalone-ui/src/questions/SimpleTextQuestionView.vue +++ b/packages/standalone-ui/src/questions/SimpleTextQuestionView.vue @@ -1,5 +1,6 @@