@vue-skuilder/edit-ui 0.1.17 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.17",
6
+ "version": "0.1.20",
7
7
  "main": "./dist/edit-ui.umd.js",
8
8
  "module": "./dist/edit-ui.es.js",
9
9
  "types": "./dist/index.d.ts",
@@ -31,10 +31,10 @@
31
31
  "cypress:run": "cypress run --component"
32
32
  },
33
33
  "dependencies": {
34
- "@vue-skuilder/common": "0.1.17",
35
- "@vue-skuilder/common-ui": "0.1.17",
36
- "@vue-skuilder/courseware": "0.1.17",
37
- "@vue-skuilder/db": "0.1.17",
34
+ "@vue-skuilder/common": "0.1.20",
35
+ "@vue-skuilder/common-ui": "0.1.20",
36
+ "@vue-skuilder/courseware": "0.1.20",
37
+ "@vue-skuilder/db": "0.1.20",
38
38
  "pinia": "^2.3.0",
39
39
  "vue": "^3.5.13",
40
40
  "vuetify": "^3.7.0"
@@ -49,9 +49,9 @@
49
49
  "@testing-library/jest-dom": "^6.6.3",
50
50
  "@testing-library/vue": "^8.1.0",
51
51
  "@types/pouchdb": "^6.4.2",
52
- "@typescript-eslint/eslint-plugin": "^8.25.0",
53
- "@typescript-eslint/parser": "^8.25.0",
54
- "@vitejs/plugin-vue": "^5.2.1",
52
+ "@typescript-eslint/eslint-plugin": "^8.48.1",
53
+ "@typescript-eslint/parser": "^8.48.1",
54
+ "@vitejs/plugin-vue": "^6.0.0",
55
55
  "@vue/eslint-config-typescript": "^14.4.0",
56
56
  "@vue/test-utils": "^2.4.6",
57
57
  "cypress": "^15.6.0",
@@ -61,9 +61,9 @@
61
61
  "eslint-plugin-vue": "^9.32.0",
62
62
  "jsdom": "^25.0.1",
63
63
  "sass": "^1.83.0",
64
- "typescript": "~5.7.2",
65
- "vite": "^6.4.1",
66
- "vitest": "^3.0.5"
64
+ "typescript": "~5.9.3",
65
+ "vite": "^7.0.0",
66
+ "vitest": "^4.0.15"
67
67
  },
68
- "stableVersion": "0.1.17"
68
+ "stableVersion": "0.1.20"
69
69
  }
@@ -0,0 +1,108 @@
1
+ <template>
2
+ <div class="hardcoded-order-config-form">
3
+ <v-textarea
4
+ :model-value="cardIdsText"
5
+ @update:model-value="updateCardIds"
6
+ label="Card IDs"
7
+ placeholder="Enter card IDs, one per line or separated by commas"
8
+ rows="10"
9
+ hint="Paste card IDs in the order they should be presented"
10
+ persistent-hint
11
+ required
12
+ ></v-textarea>
13
+
14
+ <v-alert v-if="cardCount > 0" type="info" density="compact" class="mt-2">
15
+ {{ cardCount }} card{{ cardCount === 1 ? '' : 's' }} configured
16
+ </v-alert>
17
+
18
+ <v-alert v-if="validationError" type="error" density="compact" class="mt-2">
19
+ {{ validationError }}
20
+ </v-alert>
21
+ </div>
22
+ </template>
23
+
24
+ <script lang="ts">
25
+ import { defineComponent, computed, ref, watch } from 'vue';
26
+
27
+ /**
28
+ * Configuration for hardcoded order strategy
29
+ * Serialized format: JSON array of card IDs
30
+ */
31
+ export interface HardcodedOrderConfig {
32
+ cardIds: string[];
33
+ }
34
+
35
+ export default defineComponent({
36
+ name: 'HardcodedOrderConfigForm',
37
+
38
+ props: {
39
+ modelValue: {
40
+ type: Object as () => HardcodedOrderConfig,
41
+ required: true,
42
+ },
43
+ },
44
+
45
+ emits: ['update:modelValue'],
46
+
47
+ setup(props, { emit }) {
48
+ const validationError = ref<string | null>(null);
49
+
50
+ // Convert card IDs array to text representation
51
+ const cardIdsText = computed(() => {
52
+ return props.modelValue.cardIds.join('\n');
53
+ });
54
+
55
+ // Count of configured cards
56
+ const cardCount = computed(() => {
57
+ return props.modelValue.cardIds.length;
58
+ });
59
+
60
+ /**
61
+ * Update card IDs from text input
62
+ * Splits on newlines or commas, trims, and filters empty strings
63
+ */
64
+ function updateCardIds(text: string) {
65
+ validationError.value = null;
66
+
67
+ try {
68
+ const cardIdArray = text
69
+ .split(/[\n,]+/)
70
+ .map((id) => id.trim())
71
+ .filter((id) => id);
72
+
73
+ emit('update:modelValue', {
74
+ cardIds: cardIdArray,
75
+ });
76
+ } catch (error) {
77
+ validationError.value = error instanceof Error ? error.message : 'Invalid input';
78
+ }
79
+ }
80
+
81
+ // Validate on mount and when config changes
82
+ watch(
83
+ () => props.modelValue,
84
+ () => {
85
+ if (props.modelValue.cardIds.length === 0) {
86
+ validationError.value = 'At least one card ID is required';
87
+ } else {
88
+ validationError.value = null;
89
+ }
90
+ },
91
+ { immediate: true }
92
+ );
93
+
94
+ return {
95
+ cardIdsText,
96
+ cardCount,
97
+ validationError,
98
+ updateCardIds,
99
+ };
100
+ },
101
+ });
102
+ </script>
103
+
104
+ <style scoped>
105
+ .hardcoded-order-config-form {
106
+ padding: 16px 0;
107
+ }
108
+ </style>
@@ -0,0 +1,453 @@
1
+ <template>
2
+ <div class="hierarchy-config-form">
3
+ <!-- Input Mode Toggle -->
4
+ <v-tabs v-model="inputMode" density="compact" class="mb-4">
5
+ <v-tab value="ui">Visual Editor</v-tab>
6
+ <v-tab value="json">JSON Editor</v-tab>
7
+ </v-tabs>
8
+
9
+ <v-window v-model="inputMode">
10
+ <!-- Visual Editor Mode -->
11
+ <v-window-item value="ui">
12
+ <!-- Delegate Strategy Selector -->
13
+ <v-select
14
+ :model-value="config.delegateStrategy || 'elo'"
15
+ @update:model-value="updateDelegateStrategy"
16
+ label="Delegate Strategy"
17
+ :items="delegateStrategies"
18
+ hint="Strategy used to generate candidate cards"
19
+ persistent-hint
20
+ class="mb-4"
21
+ ></v-select>
22
+
23
+ <!-- Prerequisites Builder -->
24
+ <div class="prerequisites-section">
25
+ <div class="d-flex align-center mb-2">
26
+ <h4 class="text-subtitle-1">Prerequisites</h4>
27
+ <v-spacer></v-spacer>
28
+ <v-btn size="small" color="primary" @click="addPrerequisiteRule">
29
+ <v-icon start>mdi-plus</v-icon>
30
+ Add Rule
31
+ </v-btn>
32
+ </div>
33
+
34
+ <v-alert v-if="Object.keys(config.prerequisites).length === 0" type="info" density="compact">
35
+ No prerequisites defined. Click "Add Rule" to gate a tag behind prerequisite mastery.
36
+ </v-alert>
37
+
38
+ <!-- Prerequisite Rules List -->
39
+ <v-card
40
+ v-for="(prereqs, tagId) in config.prerequisites"
41
+ :key="tagId"
42
+ variant="outlined"
43
+ class="mb-3 pa-3"
44
+ >
45
+ <div class="d-flex align-center mb-2">
46
+ <v-select
47
+ :model-value="tagId"
48
+ @update:model-value="(newTag) => renamePrerequisiteTag(tagId, newTag)"
49
+ :items="availableTags"
50
+ :loading="loadingTags"
51
+ :disabled="loadingTags"
52
+ label="Gated Tag"
53
+ hint="This tag will be locked until prerequisites are met"
54
+ persistent-hint
55
+ density="compact"
56
+ style="max-width: 300px"
57
+ ></v-select>
58
+ <v-spacer></v-spacer>
59
+ <v-btn icon size="small" color="error" @click="removePrerequisiteRule(tagId)">
60
+ <v-icon>mdi-delete</v-icon>
61
+ </v-btn>
62
+ </div>
63
+
64
+ <v-divider class="my-2"></v-divider>
65
+
66
+ <div class="text-caption mb-2">Requires mastery of:</div>
67
+
68
+ <!-- Prerequisites for this tag -->
69
+ <div
70
+ v-for="(prereq, idx) in prereqs"
71
+ :key="`${tagId}-${idx}`"
72
+ class="d-flex align-center gap-2 mb-2"
73
+ >
74
+ <v-select
75
+ :model-value="prereq.tag"
76
+ @update:model-value="(newTag) => updatePrerequisiteTag(tagId, idx, newTag)"
77
+ :items="availableTags"
78
+ :loading="loadingTags"
79
+ :disabled="loadingTags"
80
+ label="Prerequisite Tag"
81
+ density="compact"
82
+ style="flex: 1; max-width: 200px"
83
+ ></v-select>
84
+
85
+ <v-text-field
86
+ :model-value="prereq.masteryThreshold?.minCount"
87
+ @update:model-value="(val) => updateMinCount(tagId, idx, val)"
88
+ label="Min Count"
89
+ type="number"
90
+ density="compact"
91
+ style="max-width: 100px"
92
+ hint="Min interactions"
93
+ persistent-hint
94
+ ></v-text-field>
95
+
96
+ <v-text-field
97
+ :model-value="prereq.masteryThreshold?.minElo"
98
+ @update:model-value="(val) => updateMinElo(tagId, idx, val)"
99
+ label="Min ELO"
100
+ type="number"
101
+ density="compact"
102
+ style="max-width: 100px"
103
+ hint="Optional"
104
+ persistent-hint
105
+ ></v-text-field>
106
+
107
+ <v-btn icon size="small" @click="removePrerequisite(tagId, idx)">
108
+ <v-icon>mdi-minus-circle</v-icon>
109
+ </v-btn>
110
+ </div>
111
+
112
+ <v-btn size="small" variant="text" @click="addPrerequisite(tagId)">
113
+ <v-icon start>mdi-plus</v-icon>
114
+ Add Prerequisite
115
+ </v-btn>
116
+ </v-card>
117
+ </div>
118
+ </v-window-item>
119
+
120
+ <!-- JSON Editor Mode -->
121
+ <v-window-item value="json">
122
+ <v-textarea
123
+ :model-value="jsonText"
124
+ @update:model-value="updateFromJson"
125
+ label="Configuration JSON"
126
+ rows="15"
127
+ placeholder='{"prerequisites": {"cvc-words": [{"tag": "letter-sounds", "masteryThreshold": {"minCount": 10}}]}, "delegateStrategy": "elo"}'
128
+ hint="Paste or edit JSON configuration directly"
129
+ persistent-hint
130
+ auto-grow
131
+ ></v-textarea>
132
+
133
+ <v-alert v-if="jsonError" type="error" density="compact" class="mt-2">
134
+ {{ jsonError }}
135
+ </v-alert>
136
+
137
+ <v-alert v-else-if="jsonText" type="success" density="compact" class="mt-2">
138
+ Valid configuration
139
+ </v-alert>
140
+ </v-window-item>
141
+ </v-window>
142
+
143
+ <!-- Validation Summary -->
144
+ <v-alert v-if="validationError" type="error" density="compact" class="mt-3">
145
+ {{ validationError }}
146
+ </v-alert>
147
+ </div>
148
+ </template>
149
+
150
+ <script lang="ts">
151
+ import { defineComponent, ref, computed, watch, onMounted } from 'vue';
152
+ import { getDataLayer } from '@vue-skuilder/db';
153
+
154
+ interface TagPrerequisite {
155
+ tag: string;
156
+ masteryThreshold?: {
157
+ minElo?: number;
158
+ minCount?: number;
159
+ };
160
+ }
161
+
162
+ export interface HierarchyConfig {
163
+ prerequisites: {
164
+ [tagId: string]: TagPrerequisite[];
165
+ };
166
+ delegateStrategy?: string;
167
+ }
168
+
169
+ export default defineComponent({
170
+ name: 'HierarchyConfigForm',
171
+
172
+ props: {
173
+ modelValue: {
174
+ type: Object as () => HierarchyConfig,
175
+ required: true,
176
+ },
177
+ courseId: {
178
+ type: String,
179
+ required: true,
180
+ },
181
+ },
182
+
183
+ emits: ['update:modelValue'],
184
+
185
+ setup(props, { emit }) {
186
+ const inputMode = ref<'ui' | 'json'>('ui');
187
+ const availableTags = ref<string[]>([]);
188
+ const validationError = ref<string | null>(null);
189
+ const jsonError = ref<string | null>(null);
190
+ const loadingTags = ref(true);
191
+
192
+ const delegateStrategies = ['elo', 'srs', 'hardcoded'];
193
+
194
+ // Reactive copy of config for editing
195
+ const config = computed(() => props.modelValue);
196
+
197
+ // JSON representation
198
+ const jsonText = computed(() => {
199
+ try {
200
+ return JSON.stringify(config.value, null, 2);
201
+ } catch {
202
+ return '';
203
+ }
204
+ });
205
+
206
+ // Load available tags from course
207
+ async function loadCourseTags() {
208
+ loadingTags.value = true;
209
+ try {
210
+ const dataLayer = getDataLayer();
211
+ const courseDB = dataLayer.getCourseDB(props.courseId);
212
+ const tags = await courseDB.getCourseTagStubs();
213
+ availableTags.value = tags.rows.map((row) => row.id.replace('TAG-', ''));
214
+ console.log('Loaded tags:', availableTags.value);
215
+ } catch (error) {
216
+ console.error('Failed to load course tags:', error);
217
+ validationError.value = 'Failed to load course tags';
218
+ } finally {
219
+ loadingTags.value = false;
220
+ }
221
+ }
222
+
223
+ // Update delegate strategy
224
+ function updateDelegateStrategy(value: string) {
225
+ emit('update:modelValue', {
226
+ ...config.value,
227
+ delegateStrategy: value,
228
+ });
229
+ }
230
+
231
+ // Add a new prerequisite rule (new gated tag)
232
+ function addPrerequisiteRule() {
233
+ const newTag = availableTags.value[0] || 'new-tag';
234
+ emit('update:modelValue', {
235
+ ...config.value,
236
+ prerequisites: {
237
+ ...config.value.prerequisites,
238
+ [newTag]: [],
239
+ },
240
+ });
241
+ }
242
+
243
+ // Remove a prerequisite rule
244
+ function removePrerequisiteRule(tagId: string) {
245
+ const newPrereqs = { ...config.value.prerequisites };
246
+ delete newPrereqs[tagId];
247
+ emit('update:modelValue', {
248
+ ...config.value,
249
+ prerequisites: newPrereqs,
250
+ });
251
+ }
252
+
253
+ // Rename a prerequisite tag (change the key)
254
+ function renamePrerequisiteTag(oldTag: string, newTag: string) {
255
+ if (oldTag === newTag) return;
256
+ const newPrereqs = { ...config.value.prerequisites };
257
+ newPrereqs[newTag] = newPrereqs[oldTag];
258
+ delete newPrereqs[oldTag];
259
+ emit('update:modelValue', {
260
+ ...config.value,
261
+ prerequisites: newPrereqs,
262
+ });
263
+ }
264
+
265
+ // Add a prerequisite to a tag
266
+ function addPrerequisite(tagId: string) {
267
+ const newPrereqTag = availableTags.value.find((t) => t !== tagId) || 'prerequisite-tag';
268
+ const newPrereqs = { ...config.value.prerequisites };
269
+ newPrereqs[tagId] = [
270
+ ...newPrereqs[tagId],
271
+ {
272
+ tag: newPrereqTag,
273
+ masteryThreshold: { minCount: 3 },
274
+ },
275
+ ];
276
+ emit('update:modelValue', {
277
+ ...config.value,
278
+ prerequisites: newPrereqs,
279
+ });
280
+ }
281
+
282
+ // Remove a prerequisite
283
+ function removePrerequisite(tagId: string, idx: number) {
284
+ const newPrereqs = { ...config.value.prerequisites };
285
+ newPrereqs[tagId] = newPrereqs[tagId].filter((_, i) => i !== idx);
286
+ emit('update:modelValue', {
287
+ ...config.value,
288
+ prerequisites: newPrereqs,
289
+ });
290
+ }
291
+
292
+ // Update prerequisite tag
293
+ function updatePrerequisiteTag(tagId: string, idx: number, newTag: string) {
294
+ const newPrereqs = { ...config.value.prerequisites };
295
+ newPrereqs[tagId][idx] = {
296
+ ...newPrereqs[tagId][idx],
297
+ tag: newTag,
298
+ };
299
+ emit('update:modelValue', {
300
+ ...config.value,
301
+ prerequisites: newPrereqs,
302
+ });
303
+ }
304
+
305
+ // Update min count
306
+ function updateMinCount(tagId: string, idx: number, value: string | number) {
307
+ const numValue = typeof value === 'string' ? parseInt(value) : value;
308
+ if (isNaN(numValue)) return;
309
+
310
+ const newPrereqs = { ...config.value.prerequisites };
311
+ const prereq = newPrereqs[tagId][idx];
312
+ newPrereqs[tagId][idx] = {
313
+ ...prereq,
314
+ masteryThreshold: {
315
+ ...prereq.masteryThreshold,
316
+ minCount: numValue,
317
+ },
318
+ };
319
+ emit('update:modelValue', {
320
+ ...config.value,
321
+ prerequisites: newPrereqs,
322
+ });
323
+ }
324
+
325
+ // Update min ELO
326
+ function updateMinElo(tagId: string, idx: number, value: string | number) {
327
+ if (value === '' || value === null || value === undefined) {
328
+ // Remove minElo if cleared
329
+ const newPrereqs = { ...config.value.prerequisites };
330
+ const prereq = newPrereqs[tagId][idx];
331
+ const newThreshold = { ...prereq.masteryThreshold };
332
+ delete newThreshold.minElo;
333
+ newPrereqs[tagId][idx] = {
334
+ ...prereq,
335
+ masteryThreshold: newThreshold,
336
+ };
337
+ emit('update:modelValue', {
338
+ ...config.value,
339
+ prerequisites: newPrereqs,
340
+ });
341
+ return;
342
+ }
343
+
344
+ const numValue = typeof value === 'string' ? parseInt(value) : value;
345
+ if (isNaN(numValue)) return;
346
+
347
+ const newPrereqs = { ...config.value.prerequisites };
348
+ const prereq = newPrereqs[tagId][idx];
349
+ newPrereqs[tagId][idx] = {
350
+ ...prereq,
351
+ masteryThreshold: {
352
+ ...prereq.masteryThreshold,
353
+ minElo: numValue,
354
+ },
355
+ };
356
+ emit('update:modelValue', {
357
+ ...config.value,
358
+ prerequisites: newPrereqs,
359
+ });
360
+ }
361
+
362
+ // Update from JSON
363
+ function updateFromJson(text: string) {
364
+ jsonError.value = null;
365
+ try {
366
+ const parsed = JSON.parse(text);
367
+ // Basic validation
368
+ if (!parsed.prerequisites || typeof parsed.prerequisites !== 'object') {
369
+ throw new Error('Config must have "prerequisites" object');
370
+ }
371
+ emit('update:modelValue', parsed);
372
+ } catch (error) {
373
+ jsonError.value = error instanceof Error ? error.message : 'Invalid JSON';
374
+ }
375
+ }
376
+
377
+ // Validate using navigator's parseConfig
378
+ function validateConfig() {
379
+ validationError.value = null;
380
+ try {
381
+ // Basic validation
382
+ if (!config.value.prerequisites || typeof config.value.prerequisites !== 'object') {
383
+ throw new Error('Prerequisites must be an object');
384
+ }
385
+
386
+ // Check for circular dependencies (basic check)
387
+ const allTags = new Set(Object.keys(config.value.prerequisites));
388
+ for (const [tagId, prereqs] of Object.entries(config.value.prerequisites)) {
389
+ for (const prereq of prereqs) {
390
+ if (prereq.tag === tagId) {
391
+ throw new Error(`Circular dependency: ${tagId} requires itself`);
392
+ }
393
+ }
394
+ }
395
+
396
+ return true;
397
+ } catch (error) {
398
+ validationError.value = error instanceof Error ? error.message : 'Invalid configuration';
399
+ return false;
400
+ }
401
+ }
402
+
403
+ // Load tags on mount
404
+ onMounted(() => {
405
+ loadCourseTags();
406
+ });
407
+
408
+ // Validate on config change
409
+ watch(
410
+ () => config.value,
411
+ () => {
412
+ validateConfig();
413
+ },
414
+ { deep: true }
415
+ );
416
+
417
+ return {
418
+ inputMode,
419
+ config,
420
+ availableTags,
421
+ loadingTags,
422
+ delegateStrategies,
423
+ jsonText,
424
+ jsonError,
425
+ validationError,
426
+ updateDelegateStrategy,
427
+ addPrerequisiteRule,
428
+ removePrerequisiteRule,
429
+ renamePrerequisiteTag,
430
+ addPrerequisite,
431
+ removePrerequisite,
432
+ updatePrerequisiteTag,
433
+ updateMinCount,
434
+ updateMinElo,
435
+ updateFromJson,
436
+ };
437
+ },
438
+ });
439
+ </script>
440
+
441
+ <style scoped>
442
+ .hierarchy-config-form {
443
+ padding: 16px 0;
444
+ }
445
+
446
+ .prerequisites-section {
447
+ margin-top: 16px;
448
+ }
449
+
450
+ .gap-2 {
451
+ gap: 8px;
452
+ }
453
+ </style>