@vue-skuilder/edit-ui 0.1.16 → 0.1.18

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.
@@ -0,0 +1,460 @@
1
+ <template>
2
+ <div class="interference-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
+ <!-- Maturity Threshold -->
24
+ <div class="maturity-section mb-4">
25
+ <h4 class="text-subtitle-1 mb-2">Maturity Threshold</h4>
26
+ <p class="text-caption mb-3">Tags below this threshold are considered "immature" (still being learned)</p>
27
+
28
+ <v-text-field
29
+ :model-value="config.maturityThreshold?.minCount"
30
+ @update:model-value="updateMinCount"
31
+ label="Min Count"
32
+ type="number"
33
+ hint="Minimum interactions required (default: 10)"
34
+ persistent-hint
35
+ density="compact"
36
+ class="mb-2"
37
+ ></v-text-field>
38
+
39
+ <v-text-field
40
+ :model-value="config.maturityThreshold?.minElo"
41
+ @update:model-value="updateMinElo"
42
+ label="Min ELO (optional)"
43
+ type="number"
44
+ hint="Minimum ELO score for maturity"
45
+ persistent-hint
46
+ density="compact"
47
+ class="mb-2"
48
+ ></v-text-field>
49
+
50
+ <v-text-field
51
+ :model-value="config.maturityThreshold?.minElapsedDays"
52
+ @update:model-value="updateMinElapsedDays"
53
+ label="Min Elapsed Days"
54
+ type="number"
55
+ hint="Minimum time since first interaction (default: 3)"
56
+ persistent-hint
57
+ density="compact"
58
+ ></v-text-field>
59
+ </div>
60
+
61
+ <!-- Default Decay -->
62
+ <div class="mb-4">
63
+ <v-slider
64
+ :model-value="config.defaultDecay || 0.8"
65
+ @update:model-value="updateDefaultDecay"
66
+ label="Default Decay"
67
+ :min="0"
68
+ :max="1"
69
+ :step="0.05"
70
+ thumb-label
71
+ hint="Default interference strength for groups without explicit decay (0 = no effect, 1 = maximum avoidance)"
72
+ persistent-hint
73
+ >
74
+ <template #append>
75
+ <v-text-field
76
+ :model-value="config.defaultDecay || 0.8"
77
+ @update:model-value="updateDefaultDecay"
78
+ type="number"
79
+ style="width: 80px"
80
+ density="compact"
81
+ hide-details
82
+ :min="0"
83
+ :max="1"
84
+ :step="0.05"
85
+ ></v-text-field>
86
+ </template>
87
+ </v-slider>
88
+ </div>
89
+
90
+ <!-- Interference Groups -->
91
+ <div class="interference-groups-section">
92
+ <div class="d-flex align-center mb-2">
93
+ <h4 class="text-subtitle-1">Interference Groups</h4>
94
+ <v-spacer></v-spacer>
95
+ <v-btn size="small" color="primary" @click="addInterferenceGroup">
96
+ <v-icon start>mdi-plus</v-icon>
97
+ Add Group
98
+ </v-btn>
99
+ </div>
100
+
101
+ <v-alert v-if="config.interferenceSets.length === 0" type="info" density="compact">
102
+ No interference groups defined. Click "Add Group" to create a set of tags that interfere with each other.
103
+ </v-alert>
104
+
105
+ <!-- Interference Groups List -->
106
+ <v-card
107
+ v-for="(group, idx) in config.interferenceSets"
108
+ :key="idx"
109
+ variant="outlined"
110
+ class="mb-3 pa-3"
111
+ >
112
+ <div class="d-flex align-center mb-2">
113
+ <h5 class="text-subtitle-2">Group {{ idx + 1 }}</h5>
114
+ <v-spacer></v-spacer>
115
+ <v-btn icon size="small" color="error" @click="removeInterferenceGroup(idx)">
116
+ <v-icon>mdi-delete</v-icon>
117
+ </v-btn>
118
+ </div>
119
+
120
+ <v-select
121
+ :model-value="group.tags"
122
+ @update:model-value="(tags) => updateGroupTags(idx, tags)"
123
+ :items="availableTags"
124
+ :loading="loadingTags"
125
+ :disabled="loadingTags"
126
+ label="Interfering Tags"
127
+ hint="Tags that interfere with each other"
128
+ persistent-hint
129
+ multiple
130
+ chips
131
+ closable-chips
132
+ density="compact"
133
+ class="mb-3"
134
+ ></v-select>
135
+
136
+ <v-slider
137
+ :model-value="group.decay !== undefined ? group.decay : config.defaultDecay || 0.8"
138
+ @update:model-value="(val) => updateGroupDecay(idx, val)"
139
+ label="Decay Strength"
140
+ :min="0"
141
+ :max="1"
142
+ :step="0.05"
143
+ thumb-label
144
+ hint="How strongly these tags interfere (higher = stronger avoidance)"
145
+ persistent-hint
146
+ >
147
+ <template #append>
148
+ <v-text-field
149
+ :model-value="group.decay !== undefined ? group.decay : config.defaultDecay || 0.8"
150
+ @update:model-value="(val) => updateGroupDecay(idx, val)"
151
+ type="number"
152
+ style="width: 80px"
153
+ density="compact"
154
+ hide-details
155
+ :min="0"
156
+ :max="1"
157
+ :step="0.05"
158
+ ></v-text-field>
159
+ </template>
160
+ </v-slider>
161
+ </v-card>
162
+ </div>
163
+ </v-window-item>
164
+
165
+ <!-- JSON Editor Mode -->
166
+ <v-window-item value="json">
167
+ <v-textarea
168
+ :model-value="jsonText"
169
+ @update:model-value="updateFromJson"
170
+ label="Configuration JSON"
171
+ rows="20"
172
+ placeholder='{"interferenceSets": [{"tags": ["letter-b", "letter-d"], "decay": 0.9}], "maturityThreshold": {"minCount": 10}, "defaultDecay": 0.8, "delegateStrategy": "elo"}'
173
+ hint="Paste or edit JSON configuration directly"
174
+ persistent-hint
175
+ auto-grow
176
+ ></v-textarea>
177
+
178
+ <v-alert v-if="jsonError" type="error" density="compact" class="mt-2">
179
+ {{ jsonError }}
180
+ </v-alert>
181
+
182
+ <v-alert v-else-if="jsonText" type="success" density="compact" class="mt-2">
183
+ Valid configuration
184
+ </v-alert>
185
+ </v-window-item>
186
+ </v-window>
187
+
188
+ <!-- Validation Summary -->
189
+ <v-alert v-if="validationError" type="error" density="compact" class="mt-3">
190
+ {{ validationError }}
191
+ </v-alert>
192
+ </div>
193
+ </template>
194
+
195
+ <script lang="ts">
196
+ import { defineComponent, ref, computed, watch, onMounted } from 'vue';
197
+ import { getDataLayer } from '@vue-skuilder/db';
198
+
199
+ interface InterferenceGroup {
200
+ tags: string[];
201
+ decay?: number;
202
+ }
203
+
204
+ export interface InterferenceConfig {
205
+ interferenceSets: InterferenceGroup[];
206
+ maturityThreshold?: {
207
+ minCount?: number;
208
+ minElo?: number;
209
+ minElapsedDays?: number;
210
+ };
211
+ defaultDecay?: number;
212
+ delegateStrategy?: string;
213
+ }
214
+
215
+ export default defineComponent({
216
+ name: 'InterferenceConfigForm',
217
+
218
+ props: {
219
+ modelValue: {
220
+ type: Object as () => InterferenceConfig,
221
+ required: true,
222
+ },
223
+ courseId: {
224
+ type: String,
225
+ required: true,
226
+ },
227
+ },
228
+
229
+ emits: ['update:modelValue'],
230
+
231
+ setup(props, { emit }) {
232
+ const inputMode = ref<'ui' | 'json'>('ui');
233
+ const availableTags = ref<string[]>([]);
234
+ const validationError = ref<string | null>(null);
235
+ const jsonError = ref<string | null>(null);
236
+ const loadingTags = ref(true);
237
+
238
+ const delegateStrategies = ['elo', 'srs', 'hardcoded'];
239
+
240
+ const config = computed(() => props.modelValue);
241
+
242
+ const jsonText = computed(() => {
243
+ try {
244
+ return JSON.stringify(config.value, null, 2);
245
+ } catch {
246
+ return '';
247
+ }
248
+ });
249
+
250
+ // Load available tags from course
251
+ async function loadCourseTags() {
252
+ loadingTags.value = true;
253
+ try {
254
+ const dataLayer = getDataLayer();
255
+ const courseDB = dataLayer.getCourseDB(props.courseId);
256
+ const tags = await courseDB.getCourseTagStubs();
257
+ availableTags.value = tags.rows.map((row) => row.id.replace('TAG-', ''));
258
+ } catch (error) {
259
+ console.error('Failed to load course tags:', error);
260
+ validationError.value = 'Failed to load course tags';
261
+ } finally {
262
+ loadingTags.value = false;
263
+ }
264
+ }
265
+
266
+ function updateDelegateStrategy(value: string) {
267
+ emit('update:modelValue', {
268
+ ...config.value,
269
+ delegateStrategy: value,
270
+ });
271
+ }
272
+
273
+ function updateMinCount(value: string | number) {
274
+ const numValue = typeof value === 'string' ? parseInt(value) : value;
275
+ if (isNaN(numValue)) return;
276
+
277
+ emit('update:modelValue', {
278
+ ...config.value,
279
+ maturityThreshold: {
280
+ ...config.value.maturityThreshold,
281
+ minCount: numValue,
282
+ },
283
+ });
284
+ }
285
+
286
+ function updateMinElo(value: string | number) {
287
+ if (value === '' || value === null || value === undefined) {
288
+ const newThreshold = { ...config.value.maturityThreshold };
289
+ delete newThreshold.minElo;
290
+ emit('update:modelValue', {
291
+ ...config.value,
292
+ maturityThreshold: newThreshold,
293
+ });
294
+ return;
295
+ }
296
+
297
+ const numValue = typeof value === 'string' ? parseInt(value) : value;
298
+ if (isNaN(numValue)) return;
299
+
300
+ emit('update:modelValue', {
301
+ ...config.value,
302
+ maturityThreshold: {
303
+ ...config.value.maturityThreshold,
304
+ minElo: numValue,
305
+ },
306
+ });
307
+ }
308
+
309
+ function updateMinElapsedDays(value: string | number) {
310
+ const numValue = typeof value === 'string' ? parseInt(value) : value;
311
+ if (isNaN(numValue)) return;
312
+
313
+ emit('update:modelValue', {
314
+ ...config.value,
315
+ maturityThreshold: {
316
+ ...config.value.maturityThreshold,
317
+ minElapsedDays: numValue,
318
+ },
319
+ });
320
+ }
321
+
322
+ function updateDefaultDecay(value: string | number) {
323
+ const numValue = typeof value === 'string' ? parseFloat(value) : value;
324
+ if (isNaN(numValue)) return;
325
+
326
+ emit('update:modelValue', {
327
+ ...config.value,
328
+ defaultDecay: Math.max(0, Math.min(1, numValue)),
329
+ });
330
+ }
331
+
332
+ function addInterferenceGroup() {
333
+ emit('update:modelValue', {
334
+ ...config.value,
335
+ interferenceSets: [
336
+ ...config.value.interferenceSets,
337
+ {
338
+ tags: [],
339
+ decay: config.value.defaultDecay || 0.8,
340
+ },
341
+ ],
342
+ });
343
+ }
344
+
345
+ function removeInterferenceGroup(idx: number) {
346
+ emit('update:modelValue', {
347
+ ...config.value,
348
+ interferenceSets: config.value.interferenceSets.filter((_, i) => i !== idx),
349
+ });
350
+ }
351
+
352
+ function updateGroupTags(idx: number, tags: string[]) {
353
+ const newSets = [...config.value.interferenceSets];
354
+ newSets[idx] = {
355
+ ...newSets[idx],
356
+ tags,
357
+ };
358
+ emit('update:modelValue', {
359
+ ...config.value,
360
+ interferenceSets: newSets,
361
+ });
362
+ }
363
+
364
+ function updateGroupDecay(idx: number, value: string | number) {
365
+ const numValue = typeof value === 'string' ? parseFloat(value) : value;
366
+ if (isNaN(numValue)) return;
367
+
368
+ const newSets = [...config.value.interferenceSets];
369
+ newSets[idx] = {
370
+ ...newSets[idx],
371
+ decay: Math.max(0, Math.min(1, numValue)),
372
+ };
373
+ emit('update:modelValue', {
374
+ ...config.value,
375
+ interferenceSets: newSets,
376
+ });
377
+ }
378
+
379
+ function updateFromJson(text: string) {
380
+ jsonError.value = null;
381
+ try {
382
+ const parsed = JSON.parse(text);
383
+ if (!parsed.interferenceSets || !Array.isArray(parsed.interferenceSets)) {
384
+ throw new Error('Config must have "interferenceSets" array');
385
+ }
386
+ emit('update:modelValue', parsed);
387
+ } catch (error) {
388
+ jsonError.value = error instanceof Error ? error.message : 'Invalid JSON';
389
+ }
390
+ }
391
+
392
+ function validateConfig() {
393
+ validationError.value = null;
394
+ try {
395
+ if (!config.value.interferenceSets || !Array.isArray(config.value.interferenceSets)) {
396
+ throw new Error('Interference sets must be an array');
397
+ }
398
+
399
+ for (const group of config.value.interferenceSets) {
400
+ if (!Array.isArray(group.tags) || group.tags.length < 2) {
401
+ throw new Error('Each interference group must have at least 2 tags');
402
+ }
403
+ if (group.decay !== undefined && (group.decay < 0 || group.decay > 1)) {
404
+ throw new Error('Decay values must be between 0 and 1');
405
+ }
406
+ }
407
+
408
+ return true;
409
+ } catch (error) {
410
+ validationError.value = error instanceof Error ? error.message : 'Invalid configuration';
411
+ return false;
412
+ }
413
+ }
414
+
415
+ onMounted(() => {
416
+ loadCourseTags();
417
+ });
418
+
419
+ watch(
420
+ () => config.value,
421
+ () => {
422
+ validateConfig();
423
+ },
424
+ { deep: true }
425
+ );
426
+
427
+ return {
428
+ inputMode,
429
+ config,
430
+ availableTags,
431
+ loadingTags,
432
+ delegateStrategies,
433
+ jsonText,
434
+ jsonError,
435
+ validationError,
436
+ updateDelegateStrategy,
437
+ updateMinCount,
438
+ updateMinElo,
439
+ updateMinElapsedDays,
440
+ updateDefaultDecay,
441
+ addInterferenceGroup,
442
+ removeInterferenceGroup,
443
+ updateGroupTags,
444
+ updateGroupDecay,
445
+ updateFromJson,
446
+ };
447
+ },
448
+ });
449
+ </script>
450
+
451
+ <style scoped>
452
+ .interference-config-form {
453
+ padding: 16px 0;
454
+ }
455
+
456
+ .maturity-section,
457
+ .interference-groups-section {
458
+ margin-top: 16px;
459
+ }
460
+ </style>