@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,379 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="relative-priority-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
|
+
<!-- Configuration Options -->
|
|
24
|
+
<div class="config-options mb-4">
|
|
25
|
+
<h4 class="text-subtitle-1 mb-2">Priority Configuration</h4>
|
|
26
|
+
|
|
27
|
+
<v-slider
|
|
28
|
+
:model-value="config.defaultPriority !== undefined ? config.defaultPriority : 0.5"
|
|
29
|
+
@update:model-value="updateDefaultPriority"
|
|
30
|
+
label="Default Priority"
|
|
31
|
+
:min="0"
|
|
32
|
+
:max="1"
|
|
33
|
+
:step="0.05"
|
|
34
|
+
thumb-label
|
|
35
|
+
hint="Priority for tags not explicitly listed (0.5 = neutral)"
|
|
36
|
+
persistent-hint
|
|
37
|
+
class="mb-3"
|
|
38
|
+
>
|
|
39
|
+
<template #append>
|
|
40
|
+
<v-text-field
|
|
41
|
+
:model-value="config.defaultPriority !== undefined ? config.defaultPriority : 0.5"
|
|
42
|
+
@update:model-value="updateDefaultPriority"
|
|
43
|
+
type="number"
|
|
44
|
+
style="width: 80px"
|
|
45
|
+
density="compact"
|
|
46
|
+
hide-details
|
|
47
|
+
:min="0"
|
|
48
|
+
:max="1"
|
|
49
|
+
:step="0.05"
|
|
50
|
+
></v-text-field>
|
|
51
|
+
</template>
|
|
52
|
+
</v-slider>
|
|
53
|
+
|
|
54
|
+
<v-select
|
|
55
|
+
:model-value="config.combineMode || 'max'"
|
|
56
|
+
@update:model-value="updateCombineMode"
|
|
57
|
+
label="Combine Mode"
|
|
58
|
+
:items="combineModes"
|
|
59
|
+
hint="How to combine priorities when a card has multiple tags"
|
|
60
|
+
persistent-hint
|
|
61
|
+
density="compact"
|
|
62
|
+
class="mb-3"
|
|
63
|
+
></v-select>
|
|
64
|
+
|
|
65
|
+
<v-slider
|
|
66
|
+
:model-value="config.priorityInfluence !== undefined ? config.priorityInfluence : 0.5"
|
|
67
|
+
@update:model-value="updatePriorityInfluence"
|
|
68
|
+
label="Priority Influence"
|
|
69
|
+
:min="0"
|
|
70
|
+
:max="1"
|
|
71
|
+
:step="0.05"
|
|
72
|
+
thumb-label
|
|
73
|
+
hint="How strongly priority affects scoring (0 = no effect, 1 = maximum effect)"
|
|
74
|
+
persistent-hint
|
|
75
|
+
>
|
|
76
|
+
<template #append>
|
|
77
|
+
<v-text-field
|
|
78
|
+
:model-value="config.priorityInfluence !== undefined ? config.priorityInfluence : 0.5"
|
|
79
|
+
@update:model-value="updatePriorityInfluence"
|
|
80
|
+
type="number"
|
|
81
|
+
style="width: 80px"
|
|
82
|
+
density="compact"
|
|
83
|
+
hide-details
|
|
84
|
+
:min="0"
|
|
85
|
+
:max="1"
|
|
86
|
+
:step="0.05"
|
|
87
|
+
></v-text-field>
|
|
88
|
+
</template>
|
|
89
|
+
</v-slider>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<!-- Tag Priorities -->
|
|
93
|
+
<div class="tag-priorities-section">
|
|
94
|
+
<h4 class="text-subtitle-1 mb-2">Tag Priorities</h4>
|
|
95
|
+
<p class="text-caption mb-3">Set priority for each tag (1.0 = highest priority, 0.0 = lowest)</p>
|
|
96
|
+
|
|
97
|
+
<v-alert v-if="loadingTags" type="info" density="compact">
|
|
98
|
+
Loading tags...
|
|
99
|
+
</v-alert>
|
|
100
|
+
|
|
101
|
+
<v-alert v-else-if="availableTags.length === 0" type="warning" density="compact">
|
|
102
|
+
No tags available in course
|
|
103
|
+
</v-alert>
|
|
104
|
+
|
|
105
|
+
<div v-else class="tag-priority-list">
|
|
106
|
+
<div
|
|
107
|
+
v-for="tag in availableTags"
|
|
108
|
+
:key="tag"
|
|
109
|
+
class="tag-priority-item mb-3"
|
|
110
|
+
>
|
|
111
|
+
<v-slider
|
|
112
|
+
:model-value="getTagPriority(tag)"
|
|
113
|
+
@update:model-value="(val) => updateTagPriority(tag, val)"
|
|
114
|
+
:label="tag"
|
|
115
|
+
:min="0"
|
|
116
|
+
:max="1"
|
|
117
|
+
:step="0.05"
|
|
118
|
+
thumb-label
|
|
119
|
+
density="compact"
|
|
120
|
+
>
|
|
121
|
+
<template #append>
|
|
122
|
+
<v-text-field
|
|
123
|
+
:model-value="getTagPriority(tag)"
|
|
124
|
+
@update:model-value="(val) => updateTagPriority(tag, val)"
|
|
125
|
+
type="number"
|
|
126
|
+
style="width: 80px"
|
|
127
|
+
density="compact"
|
|
128
|
+
hide-details
|
|
129
|
+
:min="0"
|
|
130
|
+
:max="1"
|
|
131
|
+
:step="0.05"
|
|
132
|
+
></v-text-field>
|
|
133
|
+
</template>
|
|
134
|
+
</v-slider>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</v-window-item>
|
|
139
|
+
|
|
140
|
+
<!-- JSON Editor Mode -->
|
|
141
|
+
<v-window-item value="json">
|
|
142
|
+
<v-textarea
|
|
143
|
+
:model-value="jsonText"
|
|
144
|
+
@update:model-value="updateFromJson"
|
|
145
|
+
label="Configuration JSON"
|
|
146
|
+
rows="15"
|
|
147
|
+
placeholder='{"tagPriorities": {"letter-s": 0.95, "letter-t": 0.90}, "defaultPriority": 0.5, "combineMode": "max", "priorityInfluence": 0.5, "delegateStrategy": "elo"}'
|
|
148
|
+
hint="Paste or edit JSON configuration directly"
|
|
149
|
+
persistent-hint
|
|
150
|
+
auto-grow
|
|
151
|
+
></v-textarea>
|
|
152
|
+
|
|
153
|
+
<v-alert v-if="jsonError" type="error" density="compact" class="mt-2">
|
|
154
|
+
{{ jsonError }}
|
|
155
|
+
</v-alert>
|
|
156
|
+
|
|
157
|
+
<v-alert v-else-if="jsonText" type="success" density="compact" class="mt-2">
|
|
158
|
+
Valid configuration
|
|
159
|
+
</v-alert>
|
|
160
|
+
</v-window-item>
|
|
161
|
+
</v-window>
|
|
162
|
+
|
|
163
|
+
<!-- Validation Summary -->
|
|
164
|
+
<v-alert v-if="validationError" type="error" density="compact" class="mt-3">
|
|
165
|
+
{{ validationError }}
|
|
166
|
+
</v-alert>
|
|
167
|
+
</div>
|
|
168
|
+
</template>
|
|
169
|
+
|
|
170
|
+
<script lang="ts">
|
|
171
|
+
import { defineComponent, ref, computed, watch, onMounted } from 'vue';
|
|
172
|
+
import { getDataLayer } from '@vue-skuilder/db';
|
|
173
|
+
|
|
174
|
+
export interface RelativePriorityConfig {
|
|
175
|
+
tagPriorities: { [tagId: string]: number };
|
|
176
|
+
defaultPriority?: number;
|
|
177
|
+
combineMode?: 'max' | 'average' | 'min';
|
|
178
|
+
priorityInfluence?: number;
|
|
179
|
+
delegateStrategy?: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export default defineComponent({
|
|
183
|
+
name: 'RelativePriorityConfigForm',
|
|
184
|
+
|
|
185
|
+
props: {
|
|
186
|
+
modelValue: {
|
|
187
|
+
type: Object as () => RelativePriorityConfig,
|
|
188
|
+
required: true,
|
|
189
|
+
},
|
|
190
|
+
courseId: {
|
|
191
|
+
type: String,
|
|
192
|
+
required: true,
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
emits: ['update:modelValue'],
|
|
197
|
+
|
|
198
|
+
setup(props, { emit }) {
|
|
199
|
+
const inputMode = ref<'ui' | 'json'>('ui');
|
|
200
|
+
const availableTags = ref<string[]>([]);
|
|
201
|
+
const validationError = ref<string | null>(null);
|
|
202
|
+
const jsonError = ref<string | null>(null);
|
|
203
|
+
const loadingTags = ref(true);
|
|
204
|
+
|
|
205
|
+
const delegateStrategies = ['elo', 'srs', 'hardcoded'];
|
|
206
|
+
const combineModes = [
|
|
207
|
+
{ title: 'Max (highest priority wins)', value: 'max' },
|
|
208
|
+
{ title: 'Average (average all priorities)', value: 'average' },
|
|
209
|
+
{ title: 'Min (lowest priority wins)', value: 'min' },
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
const config = computed(() => props.modelValue);
|
|
213
|
+
|
|
214
|
+
const jsonText = computed(() => {
|
|
215
|
+
try {
|
|
216
|
+
return JSON.stringify(config.value, null, 2);
|
|
217
|
+
} catch {
|
|
218
|
+
return '';
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Load available tags from course
|
|
223
|
+
async function loadCourseTags() {
|
|
224
|
+
loadingTags.value = true;
|
|
225
|
+
try {
|
|
226
|
+
const dataLayer = getDataLayer();
|
|
227
|
+
const courseDB = dataLayer.getCourseDB(props.courseId);
|
|
228
|
+
const tags = await courseDB.getCourseTagStubs();
|
|
229
|
+
availableTags.value = tags.rows.map((row) => row.id.replace('TAG-', ''));
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error('Failed to load course tags:', error);
|
|
232
|
+
validationError.value = 'Failed to load course tags';
|
|
233
|
+
} finally {
|
|
234
|
+
loadingTags.value = false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function updateDelegateStrategy(value: string) {
|
|
239
|
+
emit('update:modelValue', {
|
|
240
|
+
...config.value,
|
|
241
|
+
delegateStrategy: value,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function updateDefaultPriority(value: string | number) {
|
|
246
|
+
const numValue = typeof value === 'string' ? parseFloat(value) : value;
|
|
247
|
+
if (isNaN(numValue)) return;
|
|
248
|
+
|
|
249
|
+
emit('update:modelValue', {
|
|
250
|
+
...config.value,
|
|
251
|
+
defaultPriority: Math.max(0, Math.min(1, numValue)),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function updateCombineMode(value: 'max' | 'average' | 'min') {
|
|
256
|
+
emit('update:modelValue', {
|
|
257
|
+
...config.value,
|
|
258
|
+
combineMode: value,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function updatePriorityInfluence(value: string | number) {
|
|
263
|
+
const numValue = typeof value === 'string' ? parseFloat(value) : value;
|
|
264
|
+
if (isNaN(numValue)) return;
|
|
265
|
+
|
|
266
|
+
emit('update:modelValue', {
|
|
267
|
+
...config.value,
|
|
268
|
+
priorityInfluence: Math.max(0, Math.min(1, numValue)),
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function getTagPriority(tag: string): number {
|
|
273
|
+
return config.value.tagPriorities[tag] !== undefined
|
|
274
|
+
? config.value.tagPriorities[tag]
|
|
275
|
+
: config.value.defaultPriority !== undefined
|
|
276
|
+
? config.value.defaultPriority
|
|
277
|
+
: 0.5;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function updateTagPriority(tag: string, value: string | number) {
|
|
281
|
+
const numValue = typeof value === 'string' ? parseFloat(value) : value;
|
|
282
|
+
if (isNaN(numValue)) return;
|
|
283
|
+
|
|
284
|
+
emit('update:modelValue', {
|
|
285
|
+
...config.value,
|
|
286
|
+
tagPriorities: {
|
|
287
|
+
...config.value.tagPriorities,
|
|
288
|
+
[tag]: Math.max(0, Math.min(1, numValue)),
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function updateFromJson(text: string) {
|
|
294
|
+
jsonError.value = null;
|
|
295
|
+
try {
|
|
296
|
+
const parsed = JSON.parse(text);
|
|
297
|
+
if (!parsed.tagPriorities || typeof parsed.tagPriorities !== 'object') {
|
|
298
|
+
throw new Error('Config must have "tagPriorities" object');
|
|
299
|
+
}
|
|
300
|
+
emit('update:modelValue', parsed);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
jsonError.value = error instanceof Error ? error.message : 'Invalid JSON';
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function validateConfig() {
|
|
307
|
+
validationError.value = null;
|
|
308
|
+
try {
|
|
309
|
+
if (!config.value.tagPriorities || typeof config.value.tagPriorities !== 'object') {
|
|
310
|
+
throw new Error('Tag priorities must be an object');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (const [tag, priority] of Object.entries(config.value.tagPriorities)) {
|
|
314
|
+
if (typeof priority !== 'number' || priority < 0 || priority > 1) {
|
|
315
|
+
throw new Error(`Priority for ${tag} must be between 0 and 1`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return true;
|
|
320
|
+
} catch (error) {
|
|
321
|
+
validationError.value = error instanceof Error ? error.message : 'Invalid configuration';
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
onMounted(() => {
|
|
327
|
+
loadCourseTags();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
watch(
|
|
331
|
+
() => config.value,
|
|
332
|
+
() => {
|
|
333
|
+
validateConfig();
|
|
334
|
+
},
|
|
335
|
+
{ deep: true }
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
inputMode,
|
|
340
|
+
config,
|
|
341
|
+
availableTags,
|
|
342
|
+
loadingTags,
|
|
343
|
+
delegateStrategies,
|
|
344
|
+
combineModes,
|
|
345
|
+
jsonText,
|
|
346
|
+
jsonError,
|
|
347
|
+
validationError,
|
|
348
|
+
updateDelegateStrategy,
|
|
349
|
+
updateDefaultPriority,
|
|
350
|
+
updateCombineMode,
|
|
351
|
+
updatePriorityInfluence,
|
|
352
|
+
getTagPriority,
|
|
353
|
+
updateTagPriority,
|
|
354
|
+
updateFromJson,
|
|
355
|
+
};
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
</script>
|
|
359
|
+
|
|
360
|
+
<style scoped>
|
|
361
|
+
.relative-priority-config-form {
|
|
362
|
+
padding: 16px 0;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.config-options,
|
|
366
|
+
.tag-priorities-section {
|
|
367
|
+
margin-top: 16px;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.tag-priority-list {
|
|
371
|
+
max-height: 400px;
|
|
372
|
+
overflow-y: auto;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.tag-priority-item {
|
|
376
|
+
padding: 8px;
|
|
377
|
+
border-left: 3px solid #e0e0e0;
|
|
378
|
+
}
|
|
379
|
+
</style>
|