@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,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>