@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/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 +12 -12
- 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
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.1.
|
|
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.
|
|
35
|
-
"@vue-skuilder/common-ui": "0.1.
|
|
36
|
-
"@vue-skuilder/courseware": "0.1.
|
|
37
|
-
"@vue-skuilder/db": "0.1.
|
|
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.
|
|
53
|
-
"@typescript-eslint/parser": "^8.
|
|
54
|
-
"@vitejs/plugin-vue": "^
|
|
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.
|
|
65
|
-
"vite": "^
|
|
66
|
-
"vitest": "^
|
|
64
|
+
"typescript": "~5.9.3",
|
|
65
|
+
"vite": "^7.0.0",
|
|
66
|
+
"vitest": "^4.0.15"
|
|
67
67
|
},
|
|
68
|
-
"stableVersion": "0.1.
|
|
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>
|