@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.
- package/dist/assets/index.css +1 -1
- package/dist/edit-ui.es.js +1 -71753
- package/dist/edit-ui.es.js.map +1 -1
- package/dist/edit-ui.umd.js +1 -82
- package/dist/edit-ui.umd.js.map +1 -1
- package/package.json +9 -9
- package/src/components/NavigationStrategy/HardcodedOrderConfigForm.vue +108 -0
- package/src/components/NavigationStrategy/HierarchyConfigForm.vue +453 -0
- package/src/components/NavigationStrategy/InterferenceConfigForm.vue +460 -0
- package/src/components/NavigationStrategy/NavigationStrategyEditor.vue +345 -84
- package/src/components/NavigationStrategy/NavigationStrategyList.vue +19 -22
- package/src/components/NavigationStrategy/RelativePriorityConfigForm.vue +379 -0
|
@@ -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>
|