@vue-skuilder/common-ui 0.2.34 → 0.2.36

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.
Files changed (207) hide show
  1. package/dist/_virtual/_@oxc-project_runtime@0.115.0/helpers/defineProperty.mjs +13 -0
  2. package/dist/_virtual/_@oxc-project_runtime@0.115.0/helpers/toPrimitive.mjs +15 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.115.0/helpers/toPropertyKey.mjs +10 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.115.0/helpers/typeof.mjs +12 -0
  5. package/dist/_virtual/_plugin-vue_export-helper.mjs +9 -0
  6. package/dist/_virtual/_rolldown/runtime.mjs +7 -0
  7. package/dist/admin/admin.css +1 -1
  8. package/dist/admin/common-ui-admin.es.js +1074 -473
  9. package/dist/admin/common-ui-admin.es.js.map +1 -1
  10. package/dist/admin/components/EChart.vue.d.ts +14 -0
  11. package/dist/admin/components/EChart.vue.d.ts.map +1 -0
  12. package/dist/admin/components/LearnerDiagnostics.vue.d.ts +3 -3
  13. package/dist/admin/components/LearnerDiagnostics.vue.d.ts.map +1 -1
  14. package/dist/admin/components/LearnerTagElo.vue.d.ts +14 -0
  15. package/dist/admin/components/LearnerTagElo.vue.d.ts.map +1 -0
  16. package/dist/admin/index.d.ts +1 -0
  17. package/dist/admin/index.d.ts.map +1 -1
  18. package/dist/assets/index.css +2 -1
  19. package/dist/components/CardBrowser.mjs +63 -0
  20. package/dist/components/CardBrowser.mjs.map +1 -0
  21. package/dist/components/CardBrowser.vue_vue_type_script_lang.mjs +50 -0
  22. package/dist/components/CardBrowser.vue_vue_type_script_lang.mjs.map +1 -0
  23. package/dist/components/CardHistoryViewer.mjs +70 -0
  24. package/dist/components/CardHistoryViewer.mjs.map +1 -0
  25. package/dist/components/CardHistoryViewer.vue_vue_type_script_lang.mjs +108 -0
  26. package/dist/components/CardHistoryViewer.vue_vue_type_script_lang.mjs.map +1 -0
  27. package/dist/components/CardSearch.mjs +26 -0
  28. package/dist/components/CardSearch.mjs.map +1 -0
  29. package/dist/components/CardSearch.vue_vue_type_script_lang.mjs +17 -0
  30. package/dist/components/CardSearch.vue_vue_type_script_lang.mjs.map +1 -0
  31. package/dist/components/CardSearchResults.mjs +32 -0
  32. package/dist/components/CardSearchResults.mjs.map +1 -0
  33. package/dist/components/CardSearchResults.vue_vue_type_script_lang.mjs +71 -0
  34. package/dist/components/CardSearchResults.vue_vue_type_script_lang.mjs.map +1 -0
  35. package/dist/components/CourseCardBrowser.mjs +175 -0
  36. package/dist/components/CourseCardBrowser.mjs.map +1 -0
  37. package/dist/components/CourseCardBrowser.vue.d.ts +3 -3
  38. package/dist/components/CourseCardBrowser.vue_vue_type_script_lang.mjs +173 -0
  39. package/dist/components/CourseCardBrowser.vue_vue_type_script_lang.mjs.map +1 -0
  40. package/dist/components/CourseInformation.mjs +132 -0
  41. package/dist/components/CourseInformation.mjs.map +1 -0
  42. package/dist/components/CourseInformation.vue.d.ts +3 -3
  43. package/dist/components/CourseInformation.vue_vue_type_script_lang.mjs +56 -0
  44. package/dist/components/CourseInformation.vue_vue_type_script_lang.mjs.map +1 -0
  45. package/dist/components/CourseTagFilterWidget.mjs +114 -0
  46. package/dist/components/CourseTagFilterWidget.mjs.map +1 -0
  47. package/dist/components/CourseTagFilterWidget.vue_vue_type_script_lang.mjs +65 -0
  48. package/dist/components/CourseTagFilterWidget.vue_vue_type_script_lang.mjs.map +1 -0
  49. package/dist/components/HeatMap.mjs +40 -0
  50. package/dist/components/HeatMap.mjs.map +1 -0
  51. package/dist/components/HeatMap.vue_vue_type_script_lang.mjs +211 -0
  52. package/dist/components/HeatMap.vue_vue_type_script_lang.mjs.map +1 -0
  53. package/dist/components/PaginatingToolbar.mjs +94 -0
  54. package/dist/components/PaginatingToolbar.mjs.map +1 -0
  55. package/dist/components/PaginatingToolbar.vue.d.ts +3 -3
  56. package/dist/components/PaginatingToolbar.vue_vue_type_script_lang.mjs +36 -0
  57. package/dist/components/PaginatingToolbar.vue_vue_type_script_lang.mjs.map +1 -0
  58. package/dist/components/SessionControllerDebug.mjs +130 -0
  59. package/dist/components/SessionControllerDebug.mjs.map +1 -0
  60. package/dist/components/SessionControllerDebug.vue_vue_type_script_lang.mjs +45 -0
  61. package/dist/components/SessionControllerDebug.vue_vue_type_script_lang.mjs.map +1 -0
  62. package/dist/components/SkMouseTrap.mjs +66 -0
  63. package/dist/components/SkMouseTrap.mjs.map +1 -0
  64. package/dist/components/SkMouseTrap.vue_vue_type_script_lang.mjs +31 -0
  65. package/dist/components/SkMouseTrap.vue_vue_type_script_lang.mjs.map +1 -0
  66. package/dist/components/SkMouseTrapToolTip.mjs +28 -0
  67. package/dist/components/SkMouseTrapToolTip.mjs.map +1 -0
  68. package/dist/components/SkMouseTrapToolTip.vue.d.ts +1 -1
  69. package/dist/components/SkMouseTrapToolTip.vue_vue_type_script_lang.mjs +91 -0
  70. package/dist/components/SkMouseTrapToolTip.vue_vue_type_script_lang.mjs.map +1 -0
  71. package/dist/components/SnackbarService.mjs +24 -0
  72. package/dist/components/SnackbarService.mjs.map +1 -0
  73. package/dist/components/SnackbarService.vue_vue_type_script_lang.mjs +34 -0
  74. package/dist/components/SnackbarService.vue_vue_type_script_lang.mjs.map +1 -0
  75. package/dist/components/SnackbarService2.mjs +40 -0
  76. package/dist/components/SnackbarService2.mjs.map +1 -0
  77. package/dist/components/StudySession.mjs +123 -0
  78. package/dist/components/StudySession.mjs.map +1 -0
  79. package/dist/components/StudySession.vue_vue_type_script_lang.mjs +280 -0
  80. package/dist/components/StudySession.vue_vue_type_script_lang.mjs.map +1 -0
  81. package/dist/components/StudySessionTimer.mjs +50 -0
  82. package/dist/components/StudySessionTimer.mjs.map +1 -0
  83. package/dist/components/StudySessionTimer.vue_vue_type_script_lang.mjs +43 -0
  84. package/dist/components/StudySessionTimer.vue_vue_type_script_lang.mjs.map +1 -0
  85. package/dist/components/TagsInput.mjs +45 -0
  86. package/dist/components/TagsInput.mjs.map +1 -0
  87. package/dist/components/TagsInput.vue_vue_type_script_lang.mjs +117 -0
  88. package/dist/components/TagsInput.vue_vue_type_script_lang.mjs.map +1 -0
  89. package/dist/components/UserTagPreferences.mjs +205 -0
  90. package/dist/components/UserTagPreferences.mjs.map +1 -0
  91. package/dist/components/UserTagPreferences.vue_vue_type_script_lang.mjs +139 -0
  92. package/dist/components/UserTagPreferences.vue_vue_type_script_lang.mjs.map +1 -0
  93. package/dist/components/auth/RequestPasswordReset.mjs +96 -0
  94. package/dist/components/auth/RequestPasswordReset.mjs.map +1 -0
  95. package/dist/components/auth/RequestPasswordReset.vue_vue_type_script_lang.mjs +47 -0
  96. package/dist/components/auth/RequestPasswordReset.vue_vue_type_script_lang.mjs.map +1 -0
  97. package/dist/components/auth/ResetPassword.mjs +129 -0
  98. package/dist/components/auth/ResetPassword.mjs.map +1 -0
  99. package/dist/components/auth/ResetPassword.vue_vue_type_script_lang.mjs +69 -0
  100. package/dist/components/auth/ResetPassword.vue_vue_type_script_lang.mjs.map +1 -0
  101. package/dist/components/auth/UserChip.mjs +9 -0
  102. package/dist/components/auth/UserChip.mjs.map +1 -0
  103. package/dist/components/auth/UserChip.vue_vue_type_script_setup_true_lang.mjs +213 -0
  104. package/dist/components/auth/UserChip.vue_vue_type_script_setup_true_lang.mjs.map +1 -0
  105. package/dist/components/auth/UserLogin.mjs +9 -0
  106. package/dist/components/auth/UserLogin.mjs.map +1 -0
  107. package/dist/components/auth/UserLogin.vue_vue_type_script_setup_true_lang.mjs +155 -0
  108. package/dist/components/auth/UserLogin.vue_vue_type_script_setup_true_lang.mjs.map +1 -0
  109. package/dist/components/auth/UserLoginAndRegistrationContainer.mjs +9 -0
  110. package/dist/components/auth/UserLoginAndRegistrationContainer.mjs.map +1 -0
  111. package/dist/components/auth/UserLoginAndRegistrationContainer.vue_vue_type_script_setup_true_lang.mjs +126 -0
  112. package/dist/components/auth/UserLoginAndRegistrationContainer.vue_vue_type_script_setup_true_lang.mjs.map +1 -0
  113. package/dist/components/auth/UserRegistration.mjs +147 -0
  114. package/dist/components/auth/UserRegistration.mjs.map +1 -0
  115. package/dist/components/auth/UserRegistration.vue_vue_type_script_lang.mjs +130 -0
  116. package/dist/components/auth/UserRegistration.vue_vue_type_script_lang.mjs.map +1 -0
  117. package/dist/components/auth/VerifyEmail.mjs +118 -0
  118. package/dist/components/auth/VerifyEmail.mjs.map +1 -0
  119. package/dist/components/auth/VerifyEmail.vue_vue_type_script_lang.mjs +59 -0
  120. package/dist/components/auth/VerifyEmail.vue_vue_type_script_lang.mjs.map +1 -0
  121. package/dist/components/cardRendering/AudioAutoPlayer.mjs +9 -0
  122. package/dist/components/cardRendering/AudioAutoPlayer.mjs.map +1 -0
  123. package/dist/components/cardRendering/AudioAutoPlayer.vue_vue_type_script_setup_true_lang.mjs +69 -0
  124. package/dist/components/cardRendering/AudioAutoPlayer.vue_vue_type_script_setup_true_lang.mjs.map +1 -0
  125. package/dist/components/cardRendering/CardLoader.mjs +31 -0
  126. package/dist/components/cardRendering/CardLoader.mjs.map +1 -0
  127. package/dist/components/cardRendering/CardLoader.vue_vue_type_script_lang.mjs +75 -0
  128. package/dist/components/cardRendering/CardLoader.vue_vue_type_script_lang.mjs.map +1 -0
  129. package/dist/components/cardRendering/CardViewer.mjs +39 -0
  130. package/dist/components/cardRendering/CardViewer.mjs.map +1 -0
  131. package/dist/components/cardRendering/CardViewer.vue_vue_type_script_lang.mjs +66 -0
  132. package/dist/components/cardRendering/CardViewer.vue_vue_type_script_lang.mjs.map +1 -0
  133. package/dist/components/cardRendering/CodeBlockRenderer.mjs +8 -0
  134. package/dist/components/cardRendering/CodeBlockRenderer.mjs.map +1 -0
  135. package/dist/components/cardRendering/CodeBlockRenderer.vue_vue_type_script_setup_true_lang.mjs +44 -0
  136. package/dist/components/cardRendering/CodeBlockRenderer.vue_vue_type_script_setup_true_lang.mjs.map +1 -0
  137. package/dist/components/cardRendering/MarkdownRenderer.mjs +21 -0
  138. package/dist/components/cardRendering/MarkdownRenderer.mjs.map +1 -0
  139. package/dist/components/cardRendering/MarkdownRenderer.vue_vue_type_script_lang.mjs +24 -0
  140. package/dist/components/cardRendering/MarkdownRenderer.vue_vue_type_script_lang.mjs.map +1 -0
  141. package/dist/components/cardRendering/MarkdownRendererHelpers.mjs +79 -0
  142. package/dist/components/cardRendering/MarkdownRendererHelpers.mjs.map +1 -0
  143. package/dist/components/cardRendering/MdTokenRenderer.mjs +9 -0
  144. package/dist/components/cardRendering/MdTokenRenderer.mjs.map +1 -0
  145. package/dist/components/cardRendering/MdTokenRenderer.vue_vue_type_script_setup_true_lang.mjs +176 -0
  146. package/dist/components/cardRendering/MdTokenRenderer.vue_vue_type_script_setup_true_lang.mjs.map +1 -0
  147. package/dist/components/studentInputs/BaseUserInput.mjs +54 -0
  148. package/dist/components/studentInputs/BaseUserInput.mjs.map +1 -0
  149. package/dist/components/studentInputs/MultipleChoiceOption.mjs +26 -0
  150. package/dist/components/studentInputs/MultipleChoiceOption.mjs.map +1 -0
  151. package/dist/components/studentInputs/MultipleChoiceOption.vue_vue_type_script_lang.mjs +76 -0
  152. package/dist/components/studentInputs/MultipleChoiceOption.vue_vue_type_script_lang.mjs.map +1 -0
  153. package/dist/components/studentInputs/RadioMultipleChoice.mjs +33 -0
  154. package/dist/components/studentInputs/RadioMultipleChoice.mjs.map +1 -0
  155. package/dist/components/studentInputs/RadioMultipleChoice.vue_vue_type_script_lang.mjs +105 -0
  156. package/dist/components/studentInputs/RadioMultipleChoice.vue_vue_type_script_lang.mjs.map +1 -0
  157. package/dist/components/studentInputs/TrueFalse.mjs +19 -0
  158. package/dist/components/studentInputs/TrueFalse.mjs.map +1 -0
  159. package/dist/components/studentInputs/TrueFalse.vue_vue_type_script_lang.mjs +21 -0
  160. package/dist/components/studentInputs/TrueFalse.vue_vue_type_script_lang.mjs.map +1 -0
  161. package/dist/components/studentInputs/UserInputNumber.mjs +33 -0
  162. package/dist/components/studentInputs/UserInputNumber.mjs.map +1 -0
  163. package/dist/components/studentInputs/UserInputNumber.vue_vue_type_script_lang.mjs +25 -0
  164. package/dist/components/studentInputs/UserInputNumber.vue_vue_type_script_lang.mjs.map +1 -0
  165. package/dist/components/studentInputs/UserInputString.mjs +22 -0
  166. package/dist/components/studentInputs/UserInputString.mjs.map +1 -0
  167. package/dist/components/studentInputs/UserInputString.vue_vue_type_script_lang.mjs +23 -0
  168. package/dist/components/studentInputs/UserInputString.vue_vue_type_script_lang.mjs.map +1 -0
  169. package/dist/components/studentInputs/fillInInput.mjs +25 -0
  170. package/dist/components/studentInputs/fillInInput.mjs.map +1 -0
  171. package/dist/components/studentInputs/fillInInput.vue_vue_type_script_lang.mjs +26 -0
  172. package/dist/components/studentInputs/fillInInput.vue_vue_type_script_lang.mjs.map +1 -0
  173. package/dist/composables/CompositionViewable.mjs +65 -0
  174. package/dist/composables/CompositionViewable.mjs.map +1 -0
  175. package/dist/composables/Displayable.mjs +36 -0
  176. package/dist/composables/Displayable.mjs.map +1 -0
  177. package/dist/composables/useAuthUI.mjs +38 -0
  178. package/dist/composables/useAuthUI.mjs.map +1 -0
  179. package/dist/composables/useEntitlements.mjs +58 -0
  180. package/dist/composables/useEntitlements.mjs.map +1 -0
  181. package/dist/index.mjs +49 -0
  182. package/dist/plugins/pinia.mjs +12 -0
  183. package/dist/plugins/pinia.mjs.map +1 -0
  184. package/dist/services/authAPI.mjs +159 -0
  185. package/dist/services/authAPI.mjs.map +1 -0
  186. package/dist/stores/useAuthStore.mjs +68 -0
  187. package/dist/stores/useAuthStore.mjs.map +1 -0
  188. package/dist/stores/useCardPreviewModeStore.mjs +20 -0
  189. package/dist/stores/useCardPreviewModeStore.mjs.map +1 -0
  190. package/dist/stores/useConfigStore.mjs +60 -0
  191. package/dist/stores/useConfigStore.mjs.map +1 -0
  192. package/dist/utils/SkldrMouseTrap.mjs +48 -0
  193. package/dist/utils/SkldrMouseTrap.mjs.map +1 -0
  194. package/dist/utils/passwordValidation.mjs +12 -0
  195. package/dist/utils/passwordValidation.mjs.map +1 -0
  196. package/package.json +8 -8
  197. package/src/admin/components/EChart.vue +59 -0
  198. package/src/admin/components/LearnerDiagnostics.vue +8 -4
  199. package/src/admin/components/LearnerTagElo.vue +563 -0
  200. package/src/admin/index.ts +1 -0
  201. package/dist/MarkdownRenderer-U_iPiA2x.mjs +0 -4966
  202. package/dist/MarkdownRenderer-U_iPiA2x.mjs.map +0 -1
  203. package/dist/assets/MarkdownRenderer.css +0 -1
  204. package/dist/common-ui.es.js +0 -5090
  205. package/dist/common-ui.es.js.map +0 -1
  206. package/dist/common-ui.umd.js +0 -77
  207. package/dist/common-ui.umd.js.map +0 -1
@@ -0,0 +1,117 @@
1
+ import "../_virtual/_rolldown/runtime.mjs";
2
+ import { defineComponent as e } from "vue";
3
+ import { getDataLayer as t } from "@vue-skuilder/db";
4
+ import { VueTagsInput as n } from "@vojtechlanka/vue-tags-input";
5
+ //#region src/components/TagsInput.vue?vue&type=script&lang.ts
6
+ var r = e({
7
+ name: "SkTagsInput",
8
+ components: { VueTagsInput: n },
9
+ props: {
10
+ courseID: {
11
+ type: String,
12
+ required: !0,
13
+ default: ""
14
+ },
15
+ cardID: {
16
+ type: String,
17
+ required: !1,
18
+ default: ""
19
+ },
20
+ hideSubmit: {
21
+ type: Boolean,
22
+ required: !1,
23
+ default: !1
24
+ }
25
+ },
26
+ data() {
27
+ return {
28
+ loading: !0,
29
+ tag: "",
30
+ tags: [],
31
+ initialTags: [],
32
+ availableCourseTags: [],
33
+ separators: [
34
+ ";",
35
+ ",",
36
+ " "
37
+ ],
38
+ courseDB: null
39
+ };
40
+ },
41
+ computed: { autoCompleteSuggestions() {
42
+ return this.availableCourseTags.filter((e) => e.name.toLowerCase().indexOf(this.tag.toLowerCase()) !== -1).map((e) => ({
43
+ text: e.name,
44
+ data: { snippet: e.snippet }
45
+ }));
46
+ } },
47
+ watch: {
48
+ async cardID() {
49
+ await this.getAppliedTags();
50
+ },
51
+ async courseID() {
52
+ this.courseDB = t().getCourseDB(this.courseID), await this.updateAvailableCourseTags();
53
+ }
54
+ },
55
+ async created() {
56
+ this.courseDB = t().getCourseDB(this.courseID), await this.updateAvailableCourseTags(), await this.getAppliedTags();
57
+ },
58
+ methods: {
59
+ tagsChanged(e) {
60
+ console.log(`[TagsInput] Tags changing: ${JSON.stringify(e)}`), this.tags = e;
61
+ },
62
+ async getAppliedTags() {
63
+ this.initialTags = [], this.tags = [];
64
+ try {
65
+ (await this.courseDB.getAppliedTags(this.cardID)).rows.forEach((e) => {
66
+ console.log(`[TagsInput] The following tag is applied:
67
+ \t${JSON.stringify(e)}`), this.tags.push({
68
+ text: e.value.name,
69
+ style: "",
70
+ classes: ""
71
+ });
72
+ }), this.initialTags = this.tags.map((e) => e.text);
73
+ } catch (e) {
74
+ console.error(`Error in init-getAppliedTags: ${JSON.stringify(e)}, ${e}`);
75
+ } finally {
76
+ this.loading = !1;
77
+ }
78
+ },
79
+ async updateAvailableCourseTags() {
80
+ try {
81
+ this.availableCourseTags = (await this.courseDB.getCourseTagStubs()).rows.map((e) => e.doc);
82
+ } catch (e) {
83
+ console.error(`Error in init-availableCourseTags: ${JSON.stringify(e)}`);
84
+ }
85
+ },
86
+ async submit() {
87
+ console.log("[TagsInput] tagsInput is submitting..."), this.loading = !0;
88
+ try {
89
+ await Promise.all(this.tags.map(async (e) => {
90
+ if (!this.initialTags.includes(e.text)) try {
91
+ await this.courseDB.addTagToCard(this.cardID, e.text), console.log(`[TagsInput] Successfully added tag: ${e.text}`);
92
+ } catch (t) {
93
+ console.error(`Failed to add tag ${e.text}:`, t);
94
+ }
95
+ }));
96
+ } catch (e) {
97
+ console.error(`Exception adding tags: ${JSON.stringify(e)}`);
98
+ }
99
+ try {
100
+ await Promise.all(this.initialTags.map(async (e) => {
101
+ if (this.tags.filter((t) => t.text === e).length === 0) try {
102
+ await this.courseDB.removeTagFromCard(this.cardID, e), console.log(`[TagsInput] Successfully removed tag: ${e}`);
103
+ } catch (t) {
104
+ console.error(`Failed to remove tag ${e}:`, t);
105
+ }
106
+ }));
107
+ } catch (e) {
108
+ console.error(`Exception removing tags: ${JSON.stringify(e)}`);
109
+ }
110
+ this.loading = !1;
111
+ }
112
+ }
113
+ });
114
+ //#endregion
115
+ export { r as default };
116
+
117
+ //# sourceMappingURL=TagsInput.vue_vue_type_script_lang.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TagsInput.vue_vue_type_script_lang.mjs","names":[],"sources":["../../src/components/TagsInput.vue"],"sourcesContent":["<template>\n <div data-cy=\"tags-input\">\n <vue-tags-input\n v-model=\"tag\"\n :tags=\"tags\"\n :autocomplete-items=\"autoCompleteSuggestions\"\n :separators=\"separators\"\n :add-on-key=\"separators\"\n @tags-changed=\"tagsChanged\"\n >\n <template #autocomplete-item=\"props\">\n <div class=\"autocomplete-item\" :class=\"{ 'is-active': props.selected }\">\n <span class=\"tag-name\">{{ props.item.text }}</span>\n <span v-if=\"props.item.data && props.item.data.snippet\" class=\"tag-snippet\">\n - {{ props.item.data.snippet }}\n </span>\n </div>\n </template>\n </vue-tags-input>\n\n <v-btn v-if=\"!hideSubmit\" color=\"success\" :loading=\"loading\" @click=\"submit\">Save Changes</v-btn>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { getDataLayer, Tag, CourseDBInterface } from '@vue-skuilder/db';\n// @ts-expect-error - suppress TS error for VueTagsInput - no types available\nimport { VueTagsInput } from '@vojtechlanka/vue-tags-input';\n\nexport interface TagObject {\n text: string;\n style?: string;\n classes?: string;\n data?: {\n snippet: string;\n };\n}\n\nexport interface TagsInputInstance {\n tags: Array<{\n text: string;\n style?: string;\n classes?: string;\n data?: {\n snippet: string;\n };\n }>;\n submit: () => Promise<void>;\n updateAvailableCourseTags: () => Promise<void>;\n}\n\nexport default defineComponent({\n name: 'SkTagsInput',\n\n components: {\n VueTagsInput,\n },\n\n props: {\n courseID: {\n type: String,\n required: true,\n default: '',\n },\n cardID: {\n type: String,\n required: false,\n default: '',\n },\n hideSubmit: {\n type: Boolean,\n required: false,\n default: false,\n },\n },\n\n data() {\n return {\n loading: true,\n tag: '',\n tags: [] as TagObject[],\n initialTags: [] as string[],\n availableCourseTags: [] as Tag[],\n separators: [';', ',', ' '] as string[],\n courseDB: null as CourseDBInterface | null,\n };\n },\n\n computed: {\n autoCompleteSuggestions(): TagObject[] {\n return this.availableCourseTags\n .filter((availableTag) => {\n return availableTag.name.toLowerCase().indexOf(this.tag.toLowerCase()) !== -1;\n })\n .map((availableTag) => {\n return {\n text: availableTag.name,\n data: {\n snippet: availableTag.snippet,\n },\n };\n });\n },\n },\n\n watch: {\n async cardID() {\n await this.getAppliedTags();\n },\n async courseID() {\n this.courseDB = getDataLayer().getCourseDB(this.courseID);\n await this.updateAvailableCourseTags();\n },\n },\n\n async created() {\n this.courseDB = getDataLayer().getCourseDB(this.courseID);\n\n await this.updateAvailableCourseTags();\n await this.getAppliedTags();\n },\n\n methods: {\n tagsChanged(newTags: TagObject[]) {\n console.log(`[TagsInput] Tags changing: ${JSON.stringify(newTags)}`);\n this.tags = newTags;\n },\n\n async getAppliedTags() {\n this.initialTags = [];\n this.tags = [];\n try {\n const appliedDocsFindResult = await this.courseDB!.getAppliedTags(this.cardID);\n appliedDocsFindResult.rows.forEach((row) => {\n console.log(`[TagsInput] The following tag is applied:\n\\t${JSON.stringify(row)}`);\n this.tags.push({\n text: row!.value.name,\n style: '',\n classes: '',\n });\n });\n this.initialTags = this.tags.map((tag) => tag.text);\n } catch (e) {\n console.error(`Error in init-getAppliedTags: ${JSON.stringify(e)}, ${e}`);\n } finally {\n this.loading = false;\n }\n },\n\n async updateAvailableCourseTags() {\n try {\n this.availableCourseTags = (await this.courseDB!.getCourseTagStubs()).rows.map((row) => {\n // console.log(`[TagsInput] available tag: ${JSON.stringify(row)}`);\n return row.doc! as Tag;\n });\n } catch (e) {\n console.error(`Error in init-availableCourseTags: ${JSON.stringify(e)}`);\n }\n },\n\n async submit() {\n console.log(`[TagsInput] tagsInput is submitting...`);\n this.loading = true;\n\n try {\n await Promise.all(\n this.tags.map(async (currentTag) => {\n if (!this.initialTags.includes(currentTag.text)) {\n try {\n await this.courseDB!.addTagToCard(this.cardID, currentTag.text);\n console.log(`[TagsInput] Successfully added tag: ${currentTag.text}`);\n } catch (error) {\n console.error(`Failed to add tag ${currentTag.text}:`, error);\n }\n }\n })\n );\n } catch (e) {\n console.error(`Exception adding tags: ${JSON.stringify(e)}`);\n }\n\n try {\n await Promise.all(\n this.initialTags.map(async (initialTag) => {\n if (\n this.tags.filter((tag) => {\n return tag.text === initialTag;\n }).length === 0\n ) {\n try {\n await this.courseDB!.removeTagFromCard(this.cardID, initialTag);\n console.log(`[TagsInput] Successfully removed tag: ${initialTag}`);\n } catch (error) {\n console.error(`Failed to remove tag ${initialTag}:`, error);\n }\n }\n })\n );\n } catch (e) {\n console.error(`Exception removing tags: ${JSON.stringify(e)}`);\n }\n this.loading = false;\n },\n },\n});\n</script>\n\n<style scoped>\n.vue-tags-input {\n max-width: 100%;\n}\n\n.autocomplete-item {\n display: flex;\n align-items: center;\n padding: 5px;\n}\n\n.autocomplete-item.is-active {\n background-color: #e8e8e8;\n}\n\n.autocomplete-item.is-active > .tag-name {\n background-color: #5c6bc0;\n color: white;\n}\n\n.tag-name {\n background-color: #e0e0e0;\n color: #333;\n padding: 2px 6px;\n border-radius: 3px;\n font-weight: bold;\n margin-right: 5px;\n}\n\n.tag-snippet {\n color: #666;\n font-size: 0.9em;\n}\n\n.autocomplete-item.is-active .tag-snippet {\n color: #333; /* Ensure snippet text is visible when item is active */\n}\n</style>\n"],"mappings":";;;;;AAoDA,IAAA,IAAe,EAAgB;CAC7B,MAAM;CAEN,YAAY,EACV,iBACD;CAED,OAAO;EACL,UAAU;GACR,MAAM;GACN,UAAU;GACV,SAAS;GACV;EACD,QAAQ;GACN,MAAM;GACN,UAAU;GACV,SAAS;GACV;EACD,YAAY;GACV,MAAM;GACN,UAAU;GACV,SAAS;GACV;EACF;CAED,OAAO;AACL,SAAO;GACL,SAAS;GACT,KAAK;GACL,MAAM,EAAC;GACP,aAAa,EAAC;GACd,qBAAqB,EAAC;GACtB,YAAY;IAAC;IAAK;IAAK;IAAG;GAC1B,UAAU;GACX;;CAGH,UAAU,EACR,0BAAuC;AACrC,SAAO,KAAK,oBACT,QAAQ,MACA,EAAa,KAAK,aAAa,CAAC,QAAQ,KAAK,IAAI,aAAa,CAAA,KAAM,GAC5E,CACA,KAAK,OACG;GACL,MAAM,EAAa;GACnB,MAAM,EACJ,SAAS,EAAa,SACvB;GACF,EACD;IAEP;CAED,OAAO;EACL,MAAM,SAAS;AACb,SAAM,KAAK,gBAAgB;;EAE7B,MAAM,WAAW;AAEf,GADA,KAAK,WAAW,GAAc,CAAC,YAAY,KAAK,SAAS,EACzD,MAAM,KAAK,2BAA2B;;EAEzC;CAED,MAAM,UAAU;AAId,EAHA,KAAK,WAAW,GAAc,CAAC,YAAY,KAAK,SAAS,EAEzD,MAAM,KAAK,2BAA2B,EACtC,MAAM,KAAK,gBAAgB;;CAG7B,SAAS;EACP,YAAY,GAAsB;AAEhC,GADA,QAAQ,IAAI,8BAA8B,KAAK,UAAU,EAAQ,GAAG,EACpE,KAAK,OAAO;;EAGd,MAAM,iBAAiB;AAErB,GADA,KAAK,cAAc,EAAE,EACrB,KAAK,OAAO,EAAE;AACd,OAAI;AAWF,KAV8B,MAAM,KAAK,SAAU,eAAe,KAAK,OAAO,EACxD,KAAK,SAAS,MAAQ;AAG1C,KAFA,QAAQ,IAAI;IAClB,KAAK,UAAU,EAAI,GAAG,EAChB,KAAK,KAAK,KAAK;MACb,MAAM,EAAK,MAAM;MACjB,OAAO;MACP,SAAS;MACV,CAAC;MACF,EACF,KAAK,cAAc,KAAK,KAAK,KAAK,MAAQ,EAAI,KAAK;YAC5C,GAAG;AACV,YAAQ,MAAM,iCAAiC,KAAK,UAAU,EAAE,CAAC,IAAI,IAAI;aACjE;AACR,SAAK,UAAU;;;EAInB,MAAM,4BAA4B;AAChC,OAAI;AACF,SAAK,uBAAuB,MAAM,KAAK,SAAU,mBAAmB,EAAE,KAAK,KAAK,MAEvE,EAAI,IACX;YACK,GAAG;AACV,YAAQ,MAAM,sCAAsC,KAAK,UAAU,EAAE,GAAG;;;EAI5E,MAAM,SAAS;AAEb,GADA,QAAQ,IAAI,yCAAyC,EACrD,KAAK,UAAU;AAEf,OAAI;AACF,UAAM,QAAQ,IACZ,KAAK,KAAK,IAAI,OAAO,MAAe;AAClC,SAAI,CAAC,KAAK,YAAY,SAAS,EAAW,KAAK,CAC7C,KAAI;AAEF,MADA,MAAM,KAAK,SAAU,aAAa,KAAK,QAAQ,EAAW,KAAK,EAC/D,QAAQ,IAAI,uCAAuC,EAAW,OAAO;cAC9D,GAAO;AACd,cAAQ,MAAM,qBAAqB,EAAW,KAAK,IAAI,EAAM;;MAGlE,CACF;YACM,GAAG;AACV,YAAQ,MAAM,0BAA0B,KAAK,UAAU,EAAE,GAAG;;AAG9D,OAAI;AACF,UAAM,QAAQ,IACZ,KAAK,YAAY,IAAI,OAAO,MAAe;AACzC,SACE,KAAK,KAAK,QAAQ,MACT,EAAI,SAAS,EACpB,CAAC,WAAW,EAEd,KAAI;AAEF,MADA,MAAM,KAAK,SAAU,kBAAkB,KAAK,QAAQ,EAAW,EAC/D,QAAQ,IAAI,yCAAyC,IAAa;cAC3D,GAAO;AACd,cAAQ,MAAM,wBAAwB,EAAW,IAAI,EAAM;;MAGhE,CACF;YACM,GAAG;AACV,YAAQ,MAAM,4BAA4B,KAAK,UAAU,EAAE,GAAG;;AAEhE,QAAK,UAAU;;EAElB;CACF,CAAC"}
@@ -0,0 +1,205 @@
1
+ import "../_virtual/_rolldown/runtime.mjs";
2
+ import e from "../_virtual/_plugin-vue_export-helper.mjs";
3
+ import t from "./UserTagPreferences.vue_vue_type_script_lang.mjs";
4
+ /* empty css */
5
+ import { Fragment as n, createBlock as r, createCommentVNode as i, createElementBlock as a, createElementVNode as o, createTextVNode as s, createVNode as c, guardReactiveProps as l, normalizeProps as u, openBlock as d, renderList as f, resolveComponent as p, toDisplayString as m, withCtx as h } from "vue";
6
+ //#region src/components/UserTagPreferences.vue
7
+ var g = { key: 1 }, _ = { class: "mb-6" }, v = { class: "text-subtitle-1 font-weight-bold mb-2" }, y = { class: "d-flex align-center ga-3 w-100" }, b = {
8
+ class: "text-body-2 flex-shrink-0",
9
+ style: {
10
+ "min-width": "60px",
11
+ "text-align": "right"
12
+ }
13
+ }, x = {
14
+ key: 2,
15
+ class: "d-flex justify-center py-8"
16
+ };
17
+ function _sfc_render(e, t, S, C, w, T) {
18
+ let E = p("v-icon"), D = p("v-card-title"), O = p("v-card-subtitle"), k = p("v-alert"), A = p("v-list-item"), j = p("v-autocomplete"), M = p("v-chip"), N = p("v-slider"), P = p("v-btn"), F = p("v-list"), I = p("v-progress-circular"), L = p("v-card-text"), R = p("v-spacer"), z = p("v-card-actions"), B = p("v-card");
19
+ return d(), r(B, { loading: e.loading }, {
20
+ default: h(() => [
21
+ c(D, null, {
22
+ default: h(() => [c(E, { start: "" }, {
23
+ default: h(() => t[3] || (t[3] = [s("mdi-tune")])),
24
+ _: 1
25
+ }), t[4] || (t[4] = s(" Learning Preferences "))]),
26
+ _: 1
27
+ }),
28
+ e.courseId ? (d(), r(O, { key: 0 }, {
29
+ default: h(() => t[5] || (t[5] = [s(" Customize how content is presented in this course ")])),
30
+ _: 1
31
+ })) : i("", !0),
32
+ c(L, null, {
33
+ default: h(() => [e.courseId ? e.loading ? (d(), a("div", x, [c(I, {
34
+ indeterminate: "",
35
+ color: "primary"
36
+ })])) : (d(), a("div", g, [
37
+ o("div", _, [
38
+ o("h3", v, [c(E, {
39
+ start: "",
40
+ size: "small"
41
+ }, {
42
+ default: h(() => t[7] || (t[7] = [s("mdi-tune-variant")])),
43
+ _: 1
44
+ }), t[8] || (t[8] = s(" Tag Preferences "))]),
45
+ t[10] || (t[10] = o("p", { class: "text-body-2 text-medium-emphasis mb-3" }, " Adjust how much you want to see cards with specific tags. 0 = exclude, 1 = neutral, higher = prefer more. ", -1)),
46
+ c(j, {
47
+ modelValue: e.tagToAdd,
48
+ "onUpdate:modelValue": [t[0] || (t[0] = (t) => e.tagToAdd = t), e.addTag],
49
+ items: e.availableTagsToAdd,
50
+ "item-title": "name",
51
+ "item-value": "name",
52
+ label: "Add tag preference",
53
+ placeholder: "Search tags...",
54
+ density: "compact",
55
+ variant: "outlined",
56
+ clearable: "",
57
+ "hide-details": "",
58
+ class: "mb-4"
59
+ }, {
60
+ item: h(({ props: e, item: t }) => [c(A, u(l(e)), {
61
+ subtitle: h(() => [s(m(t.raw.snippet), 1)]),
62
+ _: 2
63
+ }, 1040)]),
64
+ _: 1
65
+ }, 8, [
66
+ "modelValue",
67
+ "items",
68
+ "onUpdate:modelValue"
69
+ ]),
70
+ e.tagNames.length > 0 ? (d(), r(F, {
71
+ key: 0,
72
+ class: "mt-4"
73
+ }, {
74
+ default: h(() => [(d(!0), a(n, null, f(e.tagNames, (t) => (d(), r(A, {
75
+ key: t,
76
+ class: "px-0"
77
+ }, {
78
+ default: h(() => [o("div", y, [
79
+ c(M, {
80
+ size: "small",
81
+ variant: "tonal",
82
+ class: "flex-shrink-0",
83
+ style: { "min-width": "120px" }
84
+ }, {
85
+ default: h(() => [s(m(t), 1)]),
86
+ _: 2
87
+ }, 1024),
88
+ c(N, {
89
+ "model-value": e.preferences.boost[t],
90
+ min: e.sliderConfigResolved.min,
91
+ max: e.globalSliderMax,
92
+ step: .01,
93
+ "hide-details": "",
94
+ class: "flex-grow-1",
95
+ "thumb-label": "",
96
+ "onUpdate:modelValue": (n) => e.updateBoost(t, n)
97
+ }, {
98
+ "thumb-label": h(({ modelValue: t }) => [s(m(e.formatMultiplier(t)), 1)]),
99
+ _: 2
100
+ }, 1032, [
101
+ "model-value",
102
+ "min",
103
+ "max",
104
+ "onUpdate:modelValue"
105
+ ]),
106
+ o("span", b, m(e.formatMultiplier(e.preferences.boost[t])), 1),
107
+ c(P, {
108
+ icon: "mdi-plus",
109
+ size: "small",
110
+ variant: "text",
111
+ density: "compact",
112
+ disabled: e.globalSliderMax >= e.sliderConfigResolved.absoluteMax,
113
+ onClick: (n) => e.expandSliderRange(t)
114
+ }, null, 8, ["disabled", "onClick"]),
115
+ c(P, {
116
+ icon: "mdi-delete",
117
+ size: "small",
118
+ variant: "text",
119
+ density: "compact",
120
+ onClick: (n) => e.removeTag(t)
121
+ }, null, 8, ["onClick"])
122
+ ])]),
123
+ _: 2
124
+ }, 1024))), 128))]),
125
+ _: 1
126
+ })) : (d(), r(k, {
127
+ key: 1,
128
+ type: "info",
129
+ variant: "tonal",
130
+ class: "mt-4"
131
+ }, {
132
+ default: h(() => t[9] || (t[9] = [s(" No tag preferences configured yet. Add tags above to get started. ")])),
133
+ _: 1
134
+ }))
135
+ ]),
136
+ e.saveError ? (d(), r(k, {
137
+ key: 0,
138
+ type: "error",
139
+ variant: "tonal",
140
+ class: "mt-4",
141
+ closable: "",
142
+ "onClick:close": t[1] || (t[1] = (t) => e.saveError = "")
143
+ }, {
144
+ default: h(() => [s(m(e.saveError), 1)]),
145
+ _: 1
146
+ })) : i("", !0),
147
+ e.saveSuccess ? (d(), r(k, {
148
+ key: 1,
149
+ type: "success",
150
+ variant: "tonal",
151
+ class: "mt-4",
152
+ closable: "",
153
+ "onClick:close": t[2] || (t[2] = (t) => e.saveSuccess = !1)
154
+ }, {
155
+ default: h(() => t[11] || (t[11] = [s(" Preferences saved successfully ")])),
156
+ _: 1
157
+ })) : i("", !0)
158
+ ])) : (d(), r(k, {
159
+ key: 0,
160
+ type: "info",
161
+ variant: "tonal",
162
+ class: "mb-4"
163
+ }, {
164
+ default: h(() => t[6] || (t[6] = [s(" Select a course to configure your learning preferences. ")])),
165
+ _: 1
166
+ }))]),
167
+ _: 1
168
+ }),
169
+ e.courseId && !e.loading ? (d(), r(z, { key: 1 }, {
170
+ default: h(() => [
171
+ c(R),
172
+ c(P, {
173
+ variant: "text",
174
+ disabled: !e.hasChanges,
175
+ onClick: e.resetToSaved
176
+ }, {
177
+ default: h(() => t[12] || (t[12] = [s(" Reset ")])),
178
+ _: 1
179
+ }, 8, ["disabled", "onClick"]),
180
+ c(P, {
181
+ color: "primary",
182
+ variant: "flat",
183
+ loading: e.saving,
184
+ disabled: !e.hasChanges,
185
+ onClick: e.savePreferences
186
+ }, {
187
+ default: h(() => t[13] || (t[13] = [s(" Save Preferences ")])),
188
+ _: 1
189
+ }, 8, [
190
+ "loading",
191
+ "disabled",
192
+ "onClick"
193
+ ])
194
+ ]),
195
+ _: 1
196
+ })) : i("", !0)
197
+ ]),
198
+ _: 1
199
+ }, 8, ["loading"]);
200
+ }
201
+ var S = /* @__PURE__ */ e(t, [["render", _sfc_render], ["__scopeId", "data-v-8fb14605"]]);
202
+ //#endregion
203
+ export { S as default };
204
+
205
+ //# sourceMappingURL=UserTagPreferences.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UserTagPreferences.mjs","names":[],"sources":["../../src/components/UserTagPreferences.vue"],"sourcesContent":["<template>\n <v-card :loading=\"loading\">\n <v-card-title>\n <v-icon start>mdi-tune</v-icon>\n Learning Preferences\n </v-card-title>\n\n <v-card-subtitle v-if=\"courseId\">\n Customize how content is presented in this course\n </v-card-subtitle>\n\n <v-card-text>\n <v-alert v-if=\"!courseId\" type=\"info\" variant=\"tonal\" class=\"mb-4\">\n Select a course to configure your learning preferences.\n </v-alert>\n\n <div v-else-if=\"!loading\">\n <!-- Tag Preferences Section -->\n <div class=\"mb-6\">\n <h3 class=\"text-subtitle-1 font-weight-bold mb-2\">\n <v-icon start size=\"small\">mdi-tune-variant</v-icon>\n Tag Preferences\n </h3>\n <p class=\"text-body-2 text-medium-emphasis mb-3\">\n Adjust how much you want to see cards with specific tags. 0 = exclude, 1 = neutral, higher = prefer more.\n </p>\n\n <!-- Tag autocomplete -->\n <v-autocomplete\n v-model=\"tagToAdd\"\n :items=\"availableTagsToAdd\"\n item-title=\"name\"\n item-value=\"name\"\n label=\"Add tag preference\"\n placeholder=\"Search tags...\"\n density=\"compact\"\n variant=\"outlined\"\n clearable\n hide-details\n class=\"mb-4\"\n @update:model-value=\"addTag\"\n >\n <template #item=\"{ props, item }\">\n <v-list-item v-bind=\"props\">\n <template #subtitle>\n {{ item.raw.snippet }}\n </template>\n </v-list-item>\n </template>\n </v-autocomplete>\n\n <!-- Tag slider list -->\n <v-list v-if=\"tagNames.length > 0\" class=\"mt-4\">\n <v-list-item\n v-for=\"tagName in tagNames\"\n :key=\"tagName\"\n class=\"px-0\"\n >\n <template #default>\n <div class=\"d-flex align-center ga-3 w-100\">\n <!-- Tag name -->\n <v-chip\n size=\"small\"\n variant=\"tonal\"\n class=\"flex-shrink-0\"\n style=\"min-width: 120px;\"\n >\n {{ tagName }}\n </v-chip>\n\n <!-- Slider -->\n <v-slider\n :model-value=\"preferences.boost[tagName]\"\n :min=\"sliderConfigResolved.min\"\n :max=\"globalSliderMax\"\n :step=\"0.01\"\n hide-details\n class=\"flex-grow-1\"\n thumb-label\n @update:model-value=\"(val: number) => updateBoost(tagName, val)\"\n >\n <template #thumb-label=\"{ modelValue }\">\n {{ formatMultiplier(modelValue) }}\n </template>\n </v-slider>\n\n <!-- Current value display -->\n <span class=\"text-body-2 flex-shrink-0\" style=\"min-width: 60px; text-align: right;\">\n {{ formatMultiplier(preferences.boost[tagName]) }}\n </span>\n\n <!-- Expand range button (always visible) -->\n <v-btn\n icon=\"mdi-plus\"\n size=\"small\"\n variant=\"text\"\n density=\"compact\"\n :disabled=\"globalSliderMax >= sliderConfigResolved.absoluteMax\"\n @click=\"expandSliderRange(tagName)\"\n />\n\n <!-- Delete button -->\n <v-btn\n icon=\"mdi-delete\"\n size=\"small\"\n variant=\"text\"\n density=\"compact\"\n @click=\"removeTag(tagName)\"\n />\n </div>\n </template>\n </v-list-item>\n </v-list>\n\n <v-alert v-else type=\"info\" variant=\"tonal\" class=\"mt-4\">\n No tag preferences configured yet. Add tags above to get started.\n </v-alert>\n </div>\n\n <!-- Status / Feedback -->\n <v-alert v-if=\"saveError\" type=\"error\" variant=\"tonal\" class=\"mt-4\" closable @click:close=\"saveError = ''\">\n {{ saveError }}\n </v-alert>\n\n <v-alert v-if=\"saveSuccess\" type=\"success\" variant=\"tonal\" class=\"mt-4\" closable @click:close=\"saveSuccess = false\">\n Preferences saved successfully\n </v-alert>\n </div>\n\n <div v-else class=\"d-flex justify-center py-8\">\n <v-progress-circular indeterminate color=\"primary\" />\n </div>\n </v-card-text>\n\n <v-card-actions v-if=\"courseId && !loading\">\n <v-spacer />\n <v-btn\n variant=\"text\"\n :disabled=\"!hasChanges\"\n @click=\"resetToSaved\"\n >\n Reset\n </v-btn>\n <v-btn\n color=\"primary\"\n variant=\"flat\"\n :loading=\"saving\"\n :disabled=\"!hasChanges\"\n @click=\"savePreferences\"\n >\n Save Preferences\n </v-btn>\n </v-card-actions>\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { getDataLayer, Tag, CourseDBInterface, UserDBInterface } from '@vue-skuilder/db';\nimport { getCurrentUser } from '../stores/useAuthStore';\n\n/**\n * User's tag preference state, matching the backend schema.\n */\ninterface UserTagPreferenceState {\n boost: Record<string, number>;\n updatedAt: string;\n}\n\n/**\n * Slider configuration for tag preferences.\n */\ninterface SliderConfig {\n min?: number; // default: 0\n startingMax?: number; // default: 2\n absoluteMax?: number; // default: 10\n}\n\nconst DEFAULT_SLIDER_CONFIG: Required<SliderConfig> = {\n min: 0,\n startingMax: 2,\n absoluteMax: 10,\n};\n\nconst STRATEGY_KEY = 'UserTagPreferenceFilter';\n\nexport default defineComponent({\n name: 'UserTagPreferences',\n\n props: {\n /**\n * Course ID to configure preferences for.\n * If not provided, component shows a prompt to select a course.\n */\n courseId: {\n type: String as PropType<string>,\n required: false,\n default: '',\n },\n\n /**\n * Slider configuration (min, startingMax, absoluteMax).\n * All fields optional, defaults: { min: 0, startingMax: 2, absoluteMax: 10 }\n */\n sliderConfig: {\n type: Object as PropType<SliderConfig>,\n required: false,\n default: () => ({}),\n },\n },\n\n emits: ['preferences-saved', 'preferences-changed'],\n\n data() {\n return {\n loading: true,\n saving: false,\n saveError: '',\n saveSuccess: false,\n\n // Current working state\n preferences: {\n boost: {} as Record<string, number>,\n },\n\n // Saved state for comparison\n savedPreferences: {\n boost: {} as Record<string, number>,\n },\n\n // Per-tag current max range (for dynamic expansion)\n tagMaxRanges: {} as Record<string, number>,\n\n // Available tags from course\n availableTags: [] as Tag[],\n\n // Autocomplete model\n tagToAdd: null as string | null,\n\n // DB references\n courseDB: null as CourseDBInterface | null,\n user: null as UserDBInterface | null,\n };\n },\n\n computed: {\n /**\n * Resolved slider config with defaults\n */\n sliderConfigResolved(): Required<SliderConfig> {\n return {\n min: this.sliderConfig.min ?? DEFAULT_SLIDER_CONFIG.min,\n startingMax: this.sliderConfig.startingMax ?? DEFAULT_SLIDER_CONFIG.startingMax,\n absoluteMax: this.sliderConfig.absoluteMax ?? DEFAULT_SLIDER_CONFIG.absoluteMax,\n };\n },\n\n /**\n * List of tag names that have preferences (sorted alphabetically)\n */\n tagNames(): string[] {\n return Object.keys(this.preferences.boost).sort();\n },\n\n /**\n * Global max for all sliders (highest tagMaxRanges value)\n * Ensures all sliders have the same visual scale\n */\n globalSliderMax(): number {\n const maxValues = Object.values(this.tagMaxRanges);\n if (maxValues.length === 0) {\n return this.sliderConfigResolved.startingMax;\n }\n return Math.max(...maxValues);\n },\n\n /**\n * Tags available to add (not already in preferences)\n */\n availableTagsToAdd(): Tag[] {\n const usedTags = new Set(Object.keys(this.preferences.boost));\n return this.availableTags.filter((t) => !usedTags.has(t.name));\n },\n\n /**\n * Check if current preferences differ from saved state\n */\n hasChanges(): boolean {\n const currentTags = Object.keys(this.preferences.boost).sort();\n const savedTags = Object.keys(this.savedPreferences.boost).sort();\n\n if (currentTags.length !== savedTags.length) {\n return true;\n }\n\n if (currentTags.join(',') !== savedTags.join(',')) {\n return true;\n }\n\n return currentTags.some(\n (tag) => this.preferences.boost[tag] !== this.savedPreferences.boost[tag]\n );\n },\n },\n\n watch: {\n courseId: {\n immediate: true,\n async handler(newCourseId: string) {\n if (newCourseId) {\n await this.loadPreferences();\n } else {\n this.loading = false;\n }\n },\n },\n },\n\n methods: {\n /**\n * Load preferences from strategy state and available tags from course\n */\n async loadPreferences() {\n this.loading = true;\n this.saveError = '';\n\n try {\n // Get user and course DB\n this.user = (await getCurrentUser()) ?? null;\n if (!this.user) {\n throw new Error('User not available');\n }\n\n this.courseDB = getDataLayer().getCourseDB(this.courseId);\n\n // Load available tags\n const tagStubs = await this.courseDB.getCourseTagStubs();\n this.availableTags = tagStubs.rows.map((r) => r.doc!).filter(Boolean);\n\n // Load saved preferences\n const state = await this.user.getStrategyState<UserTagPreferenceState>(\n this.courseId,\n STRATEGY_KEY\n );\n\n if (state) {\n this.preferences.boost = { ...state.boost };\n } else {\n // No preferences yet - start fresh\n this.preferences.boost = {};\n }\n\n // Store saved state for comparison\n this.savedPreferences.boost = { ...this.preferences.boost };\n\n // Initialize tag max ranges, auto-expanding if saved value exceeds startingMax\n this.tagMaxRanges = {};\n Object.keys(this.preferences.boost).forEach((tag) => {\n const savedValue = this.preferences.boost[tag];\n const startingMax = this.sliderConfigResolved.startingMax;\n\n // If saved value exceeds startingMax, expand range to accommodate it\n // (capped at absoluteMax)\n if (savedValue > startingMax) {\n this.tagMaxRanges[tag] = Math.min(\n Math.ceil(savedValue),\n this.sliderConfigResolved.absoluteMax\n );\n } else {\n this.tagMaxRanges[tag] = startingMax;\n }\n });\n } catch (e) {\n console.error('Failed to load preferences:', e);\n this.saveError = 'Failed to load preferences. Please try again.';\n } finally {\n this.loading = false;\n }\n },\n\n /**\n * Add a tag to preferences with default multiplier of 1.0\n */\n addTag(tagName: string | null) {\n if (tagName && !(tagName in this.preferences.boost)) {\n this.preferences.boost[tagName] = 1.0;\n this.tagMaxRanges[tagName] = this.sliderConfigResolved.startingMax;\n this.$emit('preferences-changed', this.preferences);\n }\n // Clear the autocomplete\n this.$nextTick(() => {\n this.tagToAdd = null;\n });\n },\n\n /**\n * Remove a tag from preferences\n */\n removeTag(tagName: string) {\n delete this.preferences.boost[tagName];\n delete this.tagMaxRanges[tagName];\n this.$emit('preferences-changed', this.preferences);\n },\n\n /**\n * Update boost multiplier for a tag\n */\n updateBoost(tagName: string, value: number) {\n this.preferences.boost[tagName] = value;\n this.$emit('preferences-changed', this.preferences);\n },\n\n /**\n * Expand the global slider range by 1 and move the triggering tag's slider to new max\n */\n expandSliderRange(tagName: string) {\n const currentGlobalMax = this.globalSliderMax;\n if (currentGlobalMax < this.sliderConfigResolved.absoluteMax) {\n const newGlobalMax = currentGlobalMax + 1;\n\n // Expand all tag ranges to the new global max\n // (ensures all sliders stay synchronized)\n Object.keys(this.tagMaxRanges).forEach((tag) => {\n this.tagMaxRanges[tag] = Math.max(this.tagMaxRanges[tag], newGlobalMax);\n });\n\n // Move the triggering tag's slider to new max\n this.preferences.boost[tagName] = newGlobalMax;\n this.$emit('preferences-changed', this.preferences);\n }\n },\n\n /**\n * Format multiplier for display\n */\n formatMultiplier(value: number): string {\n if (value === 0) {\n return '0x';\n }\n if (value < 0.1) {\n return value.toFixed(2) + 'x';\n }\n return value.toFixed(1) + 'x';\n },\n\n /**\n * Reset to last saved state\n */\n resetToSaved() {\n this.preferences.boost = { ...this.savedPreferences.boost };\n this.saveSuccess = false;\n this.saveError = '';\n\n // Reset tag max ranges, auto-expanding if saved value exceeds startingMax\n this.tagMaxRanges = {};\n Object.keys(this.preferences.boost).forEach((tag) => {\n const savedValue = this.preferences.boost[tag];\n const startingMax = this.sliderConfigResolved.startingMax;\n\n if (savedValue > startingMax) {\n this.tagMaxRanges[tag] = Math.min(\n Math.ceil(savedValue),\n this.sliderConfigResolved.absoluteMax\n );\n } else {\n this.tagMaxRanges[tag] = startingMax;\n }\n });\n },\n\n /**\n * Save preferences to strategy state\n */\n async savePreferences() {\n if (!this.user || !this.courseId) {\n this.saveError = 'Unable to save - user or course not available';\n return;\n }\n\n this.saving = true;\n this.saveError = '';\n this.saveSuccess = false;\n\n try {\n const state: UserTagPreferenceState = {\n boost: { ...this.preferences.boost },\n updatedAt: new Date().toISOString(),\n };\n\n await this.user.putStrategyState<UserTagPreferenceState>(\n this.courseId,\n STRATEGY_KEY,\n state\n );\n\n // Update saved state\n this.savedPreferences.boost = { ...this.preferences.boost };\n\n this.saveSuccess = true;\n this.$emit('preferences-saved', state);\n } catch (e) {\n console.error('Failed to save preferences:', e);\n this.saveError = 'Failed to save preferences. Please try again.';\n } finally {\n this.saving = false;\n }\n },\n },\n});\n</script>\n\n<style scoped>\n/* Additional styles if needed */\n</style>\n"],"mappings":"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"}
@@ -0,0 +1,139 @@
1
+ import "../_virtual/_rolldown/runtime.mjs";
2
+ import { getCurrentUser as e } from "../stores/useAuthStore.mjs";
3
+ import { defineComponent as t } from "vue";
4
+ import { getDataLayer as n } from "@vue-skuilder/db";
5
+ //#region src/components/UserTagPreferences.vue?vue&type=script&lang.ts
6
+ var r = {
7
+ min: 0,
8
+ startingMax: 2,
9
+ absoluteMax: 10
10
+ }, i = "UserTagPreferenceFilter", a = t({
11
+ name: "UserTagPreferences",
12
+ props: {
13
+ courseId: {
14
+ type: String,
15
+ required: !1,
16
+ default: ""
17
+ },
18
+ sliderConfig: {
19
+ type: Object,
20
+ required: !1,
21
+ default: () => ({})
22
+ }
23
+ },
24
+ emits: ["preferences-saved", "preferences-changed"],
25
+ data() {
26
+ return {
27
+ loading: !0,
28
+ saving: !1,
29
+ saveError: "",
30
+ saveSuccess: !1,
31
+ preferences: { boost: {} },
32
+ savedPreferences: { boost: {} },
33
+ tagMaxRanges: {},
34
+ availableTags: [],
35
+ tagToAdd: null,
36
+ courseDB: null,
37
+ user: null
38
+ };
39
+ },
40
+ computed: {
41
+ sliderConfigResolved() {
42
+ return {
43
+ min: this.sliderConfig.min ?? r.min,
44
+ startingMax: this.sliderConfig.startingMax ?? r.startingMax,
45
+ absoluteMax: this.sliderConfig.absoluteMax ?? r.absoluteMax
46
+ };
47
+ },
48
+ tagNames() {
49
+ return Object.keys(this.preferences.boost).sort();
50
+ },
51
+ globalSliderMax() {
52
+ let e = Object.values(this.tagMaxRanges);
53
+ return e.length === 0 ? this.sliderConfigResolved.startingMax : Math.max(...e);
54
+ },
55
+ availableTagsToAdd() {
56
+ let e = new Set(Object.keys(this.preferences.boost));
57
+ return this.availableTags.filter((t) => !e.has(t.name));
58
+ },
59
+ hasChanges() {
60
+ let e = Object.keys(this.preferences.boost).sort(), t = Object.keys(this.savedPreferences.boost).sort();
61
+ return e.length !== t.length || e.join(",") !== t.join(",") ? !0 : e.some((e) => this.preferences.boost[e] !== this.savedPreferences.boost[e]);
62
+ }
63
+ },
64
+ watch: { courseId: {
65
+ immediate: !0,
66
+ async handler(e) {
67
+ e ? await this.loadPreferences() : this.loading = !1;
68
+ }
69
+ } },
70
+ methods: {
71
+ async loadPreferences() {
72
+ this.loading = !0, this.saveError = "";
73
+ try {
74
+ if (this.user = await e() ?? null, !this.user) throw Error("User not available");
75
+ this.courseDB = n().getCourseDB(this.courseId), this.availableTags = (await this.courseDB.getCourseTagStubs()).rows.map((e) => e.doc).filter(Boolean);
76
+ let t = await this.user.getStrategyState(this.courseId, i);
77
+ t ? this.preferences.boost = { ...t.boost } : this.preferences.boost = {}, this.savedPreferences.boost = { ...this.preferences.boost }, this.tagMaxRanges = {}, Object.keys(this.preferences.boost).forEach((e) => {
78
+ let t = this.preferences.boost[e], n = this.sliderConfigResolved.startingMax;
79
+ t > n ? this.tagMaxRanges[e] = Math.min(Math.ceil(t), this.sliderConfigResolved.absoluteMax) : this.tagMaxRanges[e] = n;
80
+ });
81
+ } catch (e) {
82
+ console.error("Failed to load preferences:", e), this.saveError = "Failed to load preferences. Please try again.";
83
+ } finally {
84
+ this.loading = !1;
85
+ }
86
+ },
87
+ addTag(e) {
88
+ e && !(e in this.preferences.boost) && (this.preferences.boost[e] = 1, this.tagMaxRanges[e] = this.sliderConfigResolved.startingMax, this.$emit("preferences-changed", this.preferences)), this.$nextTick(() => {
89
+ this.tagToAdd = null;
90
+ });
91
+ },
92
+ removeTag(e) {
93
+ delete this.preferences.boost[e], delete this.tagMaxRanges[e], this.$emit("preferences-changed", this.preferences);
94
+ },
95
+ updateBoost(e, t) {
96
+ this.preferences.boost[e] = t, this.$emit("preferences-changed", this.preferences);
97
+ },
98
+ expandSliderRange(e) {
99
+ let t = this.globalSliderMax;
100
+ if (t < this.sliderConfigResolved.absoluteMax) {
101
+ let n = t + 1;
102
+ Object.keys(this.tagMaxRanges).forEach((e) => {
103
+ this.tagMaxRanges[e] = Math.max(this.tagMaxRanges[e], n);
104
+ }), this.preferences.boost[e] = n, this.$emit("preferences-changed", this.preferences);
105
+ }
106
+ },
107
+ formatMultiplier(e) {
108
+ return e === 0 ? "0x" : e < .1 ? e.toFixed(2) + "x" : e.toFixed(1) + "x";
109
+ },
110
+ resetToSaved() {
111
+ this.preferences.boost = { ...this.savedPreferences.boost }, this.saveSuccess = !1, this.saveError = "", this.tagMaxRanges = {}, Object.keys(this.preferences.boost).forEach((e) => {
112
+ let t = this.preferences.boost[e], n = this.sliderConfigResolved.startingMax;
113
+ t > n ? this.tagMaxRanges[e] = Math.min(Math.ceil(t), this.sliderConfigResolved.absoluteMax) : this.tagMaxRanges[e] = n;
114
+ });
115
+ },
116
+ async savePreferences() {
117
+ if (!this.user || !this.courseId) {
118
+ this.saveError = "Unable to save - user or course not available";
119
+ return;
120
+ }
121
+ this.saving = !0, this.saveError = "", this.saveSuccess = !1;
122
+ try {
123
+ let e = {
124
+ boost: { ...this.preferences.boost },
125
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
126
+ };
127
+ await this.user.putStrategyState(this.courseId, i, e), this.savedPreferences.boost = { ...this.preferences.boost }, this.saveSuccess = !0, this.$emit("preferences-saved", e);
128
+ } catch (e) {
129
+ console.error("Failed to save preferences:", e), this.saveError = "Failed to save preferences. Please try again.";
130
+ } finally {
131
+ this.saving = !1;
132
+ }
133
+ }
134
+ }
135
+ });
136
+ //#endregion
137
+ export { a as default };
138
+
139
+ //# sourceMappingURL=UserTagPreferences.vue_vue_type_script_lang.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UserTagPreferences.vue_vue_type_script_lang.mjs","names":[],"sources":["../../src/components/UserTagPreferences.vue"],"sourcesContent":["<template>\n <v-card :loading=\"loading\">\n <v-card-title>\n <v-icon start>mdi-tune</v-icon>\n Learning Preferences\n </v-card-title>\n\n <v-card-subtitle v-if=\"courseId\">\n Customize how content is presented in this course\n </v-card-subtitle>\n\n <v-card-text>\n <v-alert v-if=\"!courseId\" type=\"info\" variant=\"tonal\" class=\"mb-4\">\n Select a course to configure your learning preferences.\n </v-alert>\n\n <div v-else-if=\"!loading\">\n <!-- Tag Preferences Section -->\n <div class=\"mb-6\">\n <h3 class=\"text-subtitle-1 font-weight-bold mb-2\">\n <v-icon start size=\"small\">mdi-tune-variant</v-icon>\n Tag Preferences\n </h3>\n <p class=\"text-body-2 text-medium-emphasis mb-3\">\n Adjust how much you want to see cards with specific tags. 0 = exclude, 1 = neutral, higher = prefer more.\n </p>\n\n <!-- Tag autocomplete -->\n <v-autocomplete\n v-model=\"tagToAdd\"\n :items=\"availableTagsToAdd\"\n item-title=\"name\"\n item-value=\"name\"\n label=\"Add tag preference\"\n placeholder=\"Search tags...\"\n density=\"compact\"\n variant=\"outlined\"\n clearable\n hide-details\n class=\"mb-4\"\n @update:model-value=\"addTag\"\n >\n <template #item=\"{ props, item }\">\n <v-list-item v-bind=\"props\">\n <template #subtitle>\n {{ item.raw.snippet }}\n </template>\n </v-list-item>\n </template>\n </v-autocomplete>\n\n <!-- Tag slider list -->\n <v-list v-if=\"tagNames.length > 0\" class=\"mt-4\">\n <v-list-item\n v-for=\"tagName in tagNames\"\n :key=\"tagName\"\n class=\"px-0\"\n >\n <template #default>\n <div class=\"d-flex align-center ga-3 w-100\">\n <!-- Tag name -->\n <v-chip\n size=\"small\"\n variant=\"tonal\"\n class=\"flex-shrink-0\"\n style=\"min-width: 120px;\"\n >\n {{ tagName }}\n </v-chip>\n\n <!-- Slider -->\n <v-slider\n :model-value=\"preferences.boost[tagName]\"\n :min=\"sliderConfigResolved.min\"\n :max=\"globalSliderMax\"\n :step=\"0.01\"\n hide-details\n class=\"flex-grow-1\"\n thumb-label\n @update:model-value=\"(val: number) => updateBoost(tagName, val)\"\n >\n <template #thumb-label=\"{ modelValue }\">\n {{ formatMultiplier(modelValue) }}\n </template>\n </v-slider>\n\n <!-- Current value display -->\n <span class=\"text-body-2 flex-shrink-0\" style=\"min-width: 60px; text-align: right;\">\n {{ formatMultiplier(preferences.boost[tagName]) }}\n </span>\n\n <!-- Expand range button (always visible) -->\n <v-btn\n icon=\"mdi-plus\"\n size=\"small\"\n variant=\"text\"\n density=\"compact\"\n :disabled=\"globalSliderMax >= sliderConfigResolved.absoluteMax\"\n @click=\"expandSliderRange(tagName)\"\n />\n\n <!-- Delete button -->\n <v-btn\n icon=\"mdi-delete\"\n size=\"small\"\n variant=\"text\"\n density=\"compact\"\n @click=\"removeTag(tagName)\"\n />\n </div>\n </template>\n </v-list-item>\n </v-list>\n\n <v-alert v-else type=\"info\" variant=\"tonal\" class=\"mt-4\">\n No tag preferences configured yet. Add tags above to get started.\n </v-alert>\n </div>\n\n <!-- Status / Feedback -->\n <v-alert v-if=\"saveError\" type=\"error\" variant=\"tonal\" class=\"mt-4\" closable @click:close=\"saveError = ''\">\n {{ saveError }}\n </v-alert>\n\n <v-alert v-if=\"saveSuccess\" type=\"success\" variant=\"tonal\" class=\"mt-4\" closable @click:close=\"saveSuccess = false\">\n Preferences saved successfully\n </v-alert>\n </div>\n\n <div v-else class=\"d-flex justify-center py-8\">\n <v-progress-circular indeterminate color=\"primary\" />\n </div>\n </v-card-text>\n\n <v-card-actions v-if=\"courseId && !loading\">\n <v-spacer />\n <v-btn\n variant=\"text\"\n :disabled=\"!hasChanges\"\n @click=\"resetToSaved\"\n >\n Reset\n </v-btn>\n <v-btn\n color=\"primary\"\n variant=\"flat\"\n :loading=\"saving\"\n :disabled=\"!hasChanges\"\n @click=\"savePreferences\"\n >\n Save Preferences\n </v-btn>\n </v-card-actions>\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { getDataLayer, Tag, CourseDBInterface, UserDBInterface } from '@vue-skuilder/db';\nimport { getCurrentUser } from '../stores/useAuthStore';\n\n/**\n * User's tag preference state, matching the backend schema.\n */\ninterface UserTagPreferenceState {\n boost: Record<string, number>;\n updatedAt: string;\n}\n\n/**\n * Slider configuration for tag preferences.\n */\ninterface SliderConfig {\n min?: number; // default: 0\n startingMax?: number; // default: 2\n absoluteMax?: number; // default: 10\n}\n\nconst DEFAULT_SLIDER_CONFIG: Required<SliderConfig> = {\n min: 0,\n startingMax: 2,\n absoluteMax: 10,\n};\n\nconst STRATEGY_KEY = 'UserTagPreferenceFilter';\n\nexport default defineComponent({\n name: 'UserTagPreferences',\n\n props: {\n /**\n * Course ID to configure preferences for.\n * If not provided, component shows a prompt to select a course.\n */\n courseId: {\n type: String as PropType<string>,\n required: false,\n default: '',\n },\n\n /**\n * Slider configuration (min, startingMax, absoluteMax).\n * All fields optional, defaults: { min: 0, startingMax: 2, absoluteMax: 10 }\n */\n sliderConfig: {\n type: Object as PropType<SliderConfig>,\n required: false,\n default: () => ({}),\n },\n },\n\n emits: ['preferences-saved', 'preferences-changed'],\n\n data() {\n return {\n loading: true,\n saving: false,\n saveError: '',\n saveSuccess: false,\n\n // Current working state\n preferences: {\n boost: {} as Record<string, number>,\n },\n\n // Saved state for comparison\n savedPreferences: {\n boost: {} as Record<string, number>,\n },\n\n // Per-tag current max range (for dynamic expansion)\n tagMaxRanges: {} as Record<string, number>,\n\n // Available tags from course\n availableTags: [] as Tag[],\n\n // Autocomplete model\n tagToAdd: null as string | null,\n\n // DB references\n courseDB: null as CourseDBInterface | null,\n user: null as UserDBInterface | null,\n };\n },\n\n computed: {\n /**\n * Resolved slider config with defaults\n */\n sliderConfigResolved(): Required<SliderConfig> {\n return {\n min: this.sliderConfig.min ?? DEFAULT_SLIDER_CONFIG.min,\n startingMax: this.sliderConfig.startingMax ?? DEFAULT_SLIDER_CONFIG.startingMax,\n absoluteMax: this.sliderConfig.absoluteMax ?? DEFAULT_SLIDER_CONFIG.absoluteMax,\n };\n },\n\n /**\n * List of tag names that have preferences (sorted alphabetically)\n */\n tagNames(): string[] {\n return Object.keys(this.preferences.boost).sort();\n },\n\n /**\n * Global max for all sliders (highest tagMaxRanges value)\n * Ensures all sliders have the same visual scale\n */\n globalSliderMax(): number {\n const maxValues = Object.values(this.tagMaxRanges);\n if (maxValues.length === 0) {\n return this.sliderConfigResolved.startingMax;\n }\n return Math.max(...maxValues);\n },\n\n /**\n * Tags available to add (not already in preferences)\n */\n availableTagsToAdd(): Tag[] {\n const usedTags = new Set(Object.keys(this.preferences.boost));\n return this.availableTags.filter((t) => !usedTags.has(t.name));\n },\n\n /**\n * Check if current preferences differ from saved state\n */\n hasChanges(): boolean {\n const currentTags = Object.keys(this.preferences.boost).sort();\n const savedTags = Object.keys(this.savedPreferences.boost).sort();\n\n if (currentTags.length !== savedTags.length) {\n return true;\n }\n\n if (currentTags.join(',') !== savedTags.join(',')) {\n return true;\n }\n\n return currentTags.some(\n (tag) => this.preferences.boost[tag] !== this.savedPreferences.boost[tag]\n );\n },\n },\n\n watch: {\n courseId: {\n immediate: true,\n async handler(newCourseId: string) {\n if (newCourseId) {\n await this.loadPreferences();\n } else {\n this.loading = false;\n }\n },\n },\n },\n\n methods: {\n /**\n * Load preferences from strategy state and available tags from course\n */\n async loadPreferences() {\n this.loading = true;\n this.saveError = '';\n\n try {\n // Get user and course DB\n this.user = (await getCurrentUser()) ?? null;\n if (!this.user) {\n throw new Error('User not available');\n }\n\n this.courseDB = getDataLayer().getCourseDB(this.courseId);\n\n // Load available tags\n const tagStubs = await this.courseDB.getCourseTagStubs();\n this.availableTags = tagStubs.rows.map((r) => r.doc!).filter(Boolean);\n\n // Load saved preferences\n const state = await this.user.getStrategyState<UserTagPreferenceState>(\n this.courseId,\n STRATEGY_KEY\n );\n\n if (state) {\n this.preferences.boost = { ...state.boost };\n } else {\n // No preferences yet - start fresh\n this.preferences.boost = {};\n }\n\n // Store saved state for comparison\n this.savedPreferences.boost = { ...this.preferences.boost };\n\n // Initialize tag max ranges, auto-expanding if saved value exceeds startingMax\n this.tagMaxRanges = {};\n Object.keys(this.preferences.boost).forEach((tag) => {\n const savedValue = this.preferences.boost[tag];\n const startingMax = this.sliderConfigResolved.startingMax;\n\n // If saved value exceeds startingMax, expand range to accommodate it\n // (capped at absoluteMax)\n if (savedValue > startingMax) {\n this.tagMaxRanges[tag] = Math.min(\n Math.ceil(savedValue),\n this.sliderConfigResolved.absoluteMax\n );\n } else {\n this.tagMaxRanges[tag] = startingMax;\n }\n });\n } catch (e) {\n console.error('Failed to load preferences:', e);\n this.saveError = 'Failed to load preferences. Please try again.';\n } finally {\n this.loading = false;\n }\n },\n\n /**\n * Add a tag to preferences with default multiplier of 1.0\n */\n addTag(tagName: string | null) {\n if (tagName && !(tagName in this.preferences.boost)) {\n this.preferences.boost[tagName] = 1.0;\n this.tagMaxRanges[tagName] = this.sliderConfigResolved.startingMax;\n this.$emit('preferences-changed', this.preferences);\n }\n // Clear the autocomplete\n this.$nextTick(() => {\n this.tagToAdd = null;\n });\n },\n\n /**\n * Remove a tag from preferences\n */\n removeTag(tagName: string) {\n delete this.preferences.boost[tagName];\n delete this.tagMaxRanges[tagName];\n this.$emit('preferences-changed', this.preferences);\n },\n\n /**\n * Update boost multiplier for a tag\n */\n updateBoost(tagName: string, value: number) {\n this.preferences.boost[tagName] = value;\n this.$emit('preferences-changed', this.preferences);\n },\n\n /**\n * Expand the global slider range by 1 and move the triggering tag's slider to new max\n */\n expandSliderRange(tagName: string) {\n const currentGlobalMax = this.globalSliderMax;\n if (currentGlobalMax < this.sliderConfigResolved.absoluteMax) {\n const newGlobalMax = currentGlobalMax + 1;\n\n // Expand all tag ranges to the new global max\n // (ensures all sliders stay synchronized)\n Object.keys(this.tagMaxRanges).forEach((tag) => {\n this.tagMaxRanges[tag] = Math.max(this.tagMaxRanges[tag], newGlobalMax);\n });\n\n // Move the triggering tag's slider to new max\n this.preferences.boost[tagName] = newGlobalMax;\n this.$emit('preferences-changed', this.preferences);\n }\n },\n\n /**\n * Format multiplier for display\n */\n formatMultiplier(value: number): string {\n if (value === 0) {\n return '0x';\n }\n if (value < 0.1) {\n return value.toFixed(2) + 'x';\n }\n return value.toFixed(1) + 'x';\n },\n\n /**\n * Reset to last saved state\n */\n resetToSaved() {\n this.preferences.boost = { ...this.savedPreferences.boost };\n this.saveSuccess = false;\n this.saveError = '';\n\n // Reset tag max ranges, auto-expanding if saved value exceeds startingMax\n this.tagMaxRanges = {};\n Object.keys(this.preferences.boost).forEach((tag) => {\n const savedValue = this.preferences.boost[tag];\n const startingMax = this.sliderConfigResolved.startingMax;\n\n if (savedValue > startingMax) {\n this.tagMaxRanges[tag] = Math.min(\n Math.ceil(savedValue),\n this.sliderConfigResolved.absoluteMax\n );\n } else {\n this.tagMaxRanges[tag] = startingMax;\n }\n });\n },\n\n /**\n * Save preferences to strategy state\n */\n async savePreferences() {\n if (!this.user || !this.courseId) {\n this.saveError = 'Unable to save - user or course not available';\n return;\n }\n\n this.saving = true;\n this.saveError = '';\n this.saveSuccess = false;\n\n try {\n const state: UserTagPreferenceState = {\n boost: { ...this.preferences.boost },\n updatedAt: new Date().toISOString(),\n };\n\n await this.user.putStrategyState<UserTagPreferenceState>(\n this.courseId,\n STRATEGY_KEY,\n state\n );\n\n // Update saved state\n this.savedPreferences.boost = { ...this.preferences.boost };\n\n this.saveSuccess = true;\n this.$emit('preferences-saved', state);\n } catch (e) {\n console.error('Failed to save preferences:', e);\n this.saveError = 'Failed to save preferences. Please try again.';\n } finally {\n this.saving = false;\n }\n },\n },\n});\n</script>\n\n<style scoped>\n/* Additional styles if needed */\n</style>\n"],"mappings":"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"}