@windward/integrations 0.16.0 → 0.18.0

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 (63) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/components/Integration/AiAgentIntegration/ChatWindow.vue +916 -0
  3. package/components/Integration/Driver/LoginSamlButton.vue +119 -0
  4. package/components/Integration/Driver/ManageSaml.vue +327 -0
  5. package/components/LLM/BloomTaxonomySelector.vue +120 -0
  6. package/components/LLM/ContentSelector.vue +66 -0
  7. package/components/LLM/GenerateContent/AssessmentQuestionGenerateButton.vue +285 -0
  8. package/components/LLM/GenerateContent/BlockQuestionGenerateButton.vue +514 -0
  9. package/components/LLM/GenerateContent/FakeTextStream.vue +67 -0
  10. package/components/SecretField.vue +62 -5
  11. package/config/integration.config.js +2 -0
  12. package/helpers/Driver/SamlSso.ts +12 -0
  13. package/i18n/en-US/components/ai_agent/chat.ts +20 -0
  14. package/i18n/en-US/components/ai_agent/index.ts +5 -0
  15. package/i18n/en-US/components/index.ts +4 -0
  16. package/i18n/en-US/components/integration/driver.ts +23 -0
  17. package/i18n/en-US/components/llm/blooms.ts +15 -0
  18. package/i18n/en-US/components/llm/content_selector.ts +3 -0
  19. package/i18n/en-US/components/llm/generate_content/fake_text_stream.ts +62 -0
  20. package/i18n/en-US/components/llm/generate_content/generate_questions.ts +81 -0
  21. package/i18n/en-US/components/llm/generate_content/index.ts +7 -0
  22. package/i18n/en-US/components/llm/index.ts +10 -0
  23. package/i18n/en-US/pages/login/index.ts +2 -0
  24. package/i18n/en-US/pages/login/saml.ts +7 -0
  25. package/i18n/en-US/shared/permission.ts +10 -0
  26. package/i18n/en-US/shared/settings.ts +2 -1
  27. package/i18n/es-ES/components/ai_agent/chat.ts +20 -0
  28. package/i18n/es-ES/components/ai_agent/index.ts +5 -0
  29. package/i18n/es-ES/components/index.ts +4 -0
  30. package/i18n/es-ES/components/integration/driver.ts +23 -0
  31. package/i18n/es-ES/components/llm/blooms.ts +15 -0
  32. package/i18n/es-ES/components/llm/content_selector.ts +3 -0
  33. package/i18n/es-ES/components/llm/generate_content/fake_text_stream.ts +62 -0
  34. package/i18n/es-ES/components/llm/generate_content/generate_questions.ts +85 -0
  35. package/i18n/es-ES/components/llm/generate_content/index.ts +7 -0
  36. package/i18n/es-ES/components/llm/index.ts +10 -0
  37. package/i18n/es-ES/pages/login/index.ts +2 -0
  38. package/i18n/es-ES/pages/login/saml.ts +7 -0
  39. package/i18n/es-ES/shared/permission.ts +10 -0
  40. package/i18n/es-ES/shared/settings.ts +2 -1
  41. package/i18n/sv-SE/components/ai_agent/chat.ts +19 -0
  42. package/i18n/sv-SE/components/ai_agent/index.ts +5 -0
  43. package/i18n/sv-SE/components/index.ts +4 -0
  44. package/i18n/sv-SE/components/integration/driver.ts +23 -0
  45. package/i18n/sv-SE/components/llm/blooms.ts +15 -0
  46. package/i18n/sv-SE/components/llm/content_selector.ts +3 -0
  47. package/i18n/sv-SE/components/llm/generate_content/fake_text_stream.ts +62 -0
  48. package/i18n/sv-SE/components/llm/generate_content/generate_questions.ts +82 -0
  49. package/i18n/sv-SE/components/llm/generate_content/index.ts +7 -0
  50. package/i18n/sv-SE/components/llm/index.ts +10 -0
  51. package/i18n/sv-SE/pages/login/index.ts +2 -0
  52. package/i18n/sv-SE/pages/login/saml.ts +7 -0
  53. package/i18n/sv-SE/shared/permission.ts +10 -0
  54. package/i18n/sv-SE/shared/settings.ts +1 -0
  55. package/jest.config.js +3 -0
  56. package/models/Activity.ts +8 -0
  57. package/models/AgentChat.ts +12 -0
  58. package/models/AgentChatMessage.ts +12 -0
  59. package/models/Auth/Saml.ts +21 -0
  60. package/package.json +2 -1
  61. package/plugin.js +51 -1
  62. package/test/__mocks__/componentsMock.js +81 -1
  63. package/test/setup.js +1 -0
@@ -0,0 +1,285 @@
1
+ <template>
2
+ <div>
3
+ <v-row class="container-generate-ai mt-2">
4
+ <v-col cols="12">{{
5
+ $t('windward.integrations.components.llm.ai_assistance')
6
+ }}</v-col>
7
+ <v-col cols="12">
8
+ <ContentSelector
9
+ v-model="selectedContent"
10
+ :disabled="isLoading || disabled"
11
+ ></ContentSelector>
12
+ </v-col>
13
+ <v-col cols="12">
14
+ <BloomTaxonomySelector
15
+ v-model="selectedDifficulty"
16
+ :advanced-levels="hasAdvancedBlooms"
17
+ :disabled="isLoading || disabled"
18
+ ></BloomTaxonomySelector>
19
+ </v-col>
20
+ <v-col cols="12">
21
+ <v-btn
22
+ elevation="0"
23
+ color="secondary"
24
+ class="mb-1 w-100"
25
+ :loading="isLoading"
26
+ :disabled="isLoading || disabled"
27
+ @click="generateAIQuestion"
28
+ >
29
+ <v-icon v-if="!isLoading" class="pr-1">
30
+ mdi-magic-staff
31
+ </v-icon>
32
+ {{
33
+ $t(
34
+ 'windward.integrations.components.llm.generate_content.generate_questions.button_label'
35
+ )
36
+ }}
37
+ <template #loader>
38
+ <v-progress-circular
39
+ v-if="!showText || !showLoadingText"
40
+ indeterminate
41
+ size="23"
42
+ ></v-progress-circular>
43
+ <FakeTextStream
44
+ v-if="showText && showLoadingText"
45
+ ></FakeTextStream>
46
+ </template>
47
+ </v-btn>
48
+ </v-col>
49
+ </v-row>
50
+ </div>
51
+ </template>
52
+
53
+ <script>
54
+ import _ from 'lodash'
55
+ import ContentSelector from '../ContentSelector.vue'
56
+ import BloomTaxonomySelector from '../BloomTaxonomySelector.vue'
57
+ import FakeTextStream from './FakeTextStream'
58
+ import AssessmentQuestion from '~/models/AssessmentQuestion'
59
+ import Course from '~/models/Course'
60
+ import Assessment from '~/models/Assessment'
61
+ import Content from '~/models/Content'
62
+ import ContentBlock from '~/models/ContentBlock'
63
+
64
+ export default {
65
+ name: 'AssessmentQuestionGenerateButton',
66
+ components: {
67
+ ContentSelector,
68
+ BloomTaxonomySelector,
69
+ FakeTextStream,
70
+ },
71
+ props: {
72
+ // The assessment question
73
+ value: { type: [AssessmentQuestion, Object], required: true },
74
+ course: { type: [Course, Object], required: true },
75
+ content: { type: [Content, Object], required: true },
76
+ block: { type: [ContentBlock, Object], required: true },
77
+ disabled: { type: Boolean, required: false, default: false },
78
+ showText: { type: Boolean, required: false, default: true },
79
+ },
80
+ data() {
81
+ return {
82
+ isLoading: false,
83
+ loadingTimeout: null,
84
+ showLoadingText: false,
85
+ selectedContent: null,
86
+ selectedDifficulty: null,
87
+ }
88
+ },
89
+ computed: {
90
+ questionType() {
91
+ let type = _.get(this.value, 'question_type', '')
92
+
93
+ // Type wasn't readily available on the this.value. Determine based off the question type id
94
+ if (
95
+ type === '' &&
96
+ _.get(this.value, 'assessment_question_type_id', null) !== null
97
+ ) {
98
+ type = _.get(
99
+ this.$Assessment.getQuestionType(
100
+ _.get(this.value, 'assessment_question_type_id')
101
+ ),
102
+ 'name',
103
+ ''
104
+ )
105
+ }
106
+ return type
107
+ },
108
+
109
+ hasAdvancedBlooms() {
110
+ // Only add higher-level Bloom's taxonomy for supported question types
111
+ return [
112
+ 'multi_choice_single_answer',
113
+ 'multi_choice_multi_answer',
114
+ 'ordering',
115
+ ].includes(this.questionType)
116
+ },
117
+ },
118
+ methods: {
119
+ async generateAIQuestion() {
120
+ this.isLoading = true
121
+
122
+ // Wait 5 seconds before kicking on the text
123
+ this.loadingTimeout = setTimeout(() => {
124
+ this.showLoadingText = true
125
+ }, 5000)
126
+
127
+ try {
128
+ const course = new Course(this.course)
129
+ const content = new Content(
130
+ this.selectedContent || this.content
131
+ )
132
+
133
+ // ASSESSMENT QUESTION GENERATION
134
+ const assessment = new Assessment({ id: this.block.id })
135
+
136
+ const responseBuilder = AssessmentQuestion.custom(
137
+ course,
138
+ content,
139
+ assessment,
140
+ `suggest-questions`
141
+ ).where('question_type', this.questionType)
142
+
143
+ // Apply blooms level filter if it's not 'None'
144
+ if (this.selectedDifficulty !== 'None') {
145
+ responseBuilder.where(
146
+ 'blooms_level',
147
+ this.selectedDifficulty
148
+ )
149
+ }
150
+
151
+ const response = await responseBuilder.get()
152
+
153
+ if (response && response.length > 0) {
154
+ const generatedQuestion = response[0]
155
+
156
+ this.$emit('input', generatedQuestion)
157
+ } else {
158
+ throw new Error('Invalid response from question generation')
159
+ }
160
+ } catch (error) {
161
+ const errorMessage =
162
+ error.response?.data?.error?.message ||
163
+ error.message ||
164
+ 'assessment.error.technical'
165
+ const errorType = errorMessage.split('.').pop()
166
+ const basePath =
167
+ 'windward.integrations.components.llm.generate_content.generate_questions.errors'
168
+
169
+ let errorText = ''
170
+
171
+ // Check for character limit error by structured data first
172
+ const errorSubtype =
173
+ error.response?.data?.error?.details?.error_subtype
174
+ const errorDetails = error.response?.data?.error?.details
175
+
176
+ if (errorSubtype === 'CHARACTER_LIMIT_EXCEEDED') {
177
+ // Use structured data if available
178
+ const field = errorDetails?.field || 'content'
179
+ const limit = errorDetails?.limit
180
+ const actual = errorDetails?.actual
181
+
182
+ // Use parameterized message if we have the data
183
+ if (limit && actual) {
184
+ errorText = this.$t(
185
+ `${basePath}.character_limit_detailed`,
186
+ { field, limit, actual }
187
+ )
188
+ } else {
189
+ // Fallback to generic message
190
+ errorText = this.$t(`${basePath}.character_limit`)
191
+ }
192
+ }
193
+ // Keep backward compatibility - check for old message pattern
194
+ else if (
195
+ errorDetails?.message ===
196
+ 'An error occurred while generating content. Try generating again.'
197
+ ) {
198
+ errorText = this.$t(`${basePath}.character_limit`)
199
+ } else if (
200
+ errorType === 'insufficient_content' &&
201
+ error.response?.data?.error?.details
202
+ ) {
203
+ // Handle dynamic Bloom's taxonomy insufficient content errors
204
+ const details = error.response.data.error.details
205
+ const bloomsLevel = details.blooms_level
206
+ const minimumRequired = details.minimum_required
207
+
208
+ if (bloomsLevel && bloomsLevel !== 'None') {
209
+ // Message with Bloom's level
210
+ errorText =
211
+ this.$t(`${basePath}.insufficient_content_blooms`, {
212
+ bloomsLevel,
213
+ }) +
214
+ '\n\n' +
215
+ this.$t(
216
+ `${basePath}.insufficient_content_blooms_support`,
217
+ { minimumRequired }
218
+ )
219
+ } else if (minimumRequired) {
220
+ // Message without Bloom's level but with word count
221
+ errorText =
222
+ this.$t(
223
+ `${basePath}.insufficient_content_dynamic`
224
+ ) +
225
+ '\n\n' +
226
+ this.$t(
227
+ `${basePath}.insufficient_content_dynamic_support`,
228
+ { minimumRequired }
229
+ )
230
+ } else {
231
+ // Fallback to static translation
232
+ errorText =
233
+ this.$t(`${basePath}.${errorType}`) +
234
+ '\n\n' +
235
+ this.$t(`${basePath}.${errorType}_support`)
236
+ }
237
+ } else {
238
+ // Check if the error type is a valid i18n key
239
+ const errorKey = `${basePath}.${errorType}`
240
+ const supportKey = `${basePath}.${errorType}_support`
241
+
242
+ // Try to get the translation, fall back to default error if not found
243
+ const hasTranslation = this.$te(errorKey)
244
+
245
+ if (hasTranslation) {
246
+ errorText =
247
+ this.$t(errorKey) + '\n\n' + this.$t(supportKey)
248
+ } else {
249
+ // Fall back to default error message
250
+ errorText =
251
+ this.$t(`${basePath}.default`) +
252
+ '\n\n' +
253
+ this.$t(`${basePath}.default_support`)
254
+ }
255
+
256
+ if (errorType === 'technical') {
257
+ const errorCode =
258
+ error.response?.data?.error?.details?.error_type ||
259
+ 'UNKNOWN'
260
+ errorText = errorText.replace('[ERROR_CODE]', errorCode)
261
+ }
262
+ }
263
+
264
+ this.$dialog.error(errorText, {
265
+ duration: 5000,
266
+ keepOnHover: true,
267
+ singleton: true,
268
+ type: 'error',
269
+ })
270
+ } finally {
271
+ this.isLoading = false
272
+ this.showLoadingText = false
273
+ clearTimeout(this.loadingTimeout)
274
+ }
275
+ },
276
+ },
277
+ }
278
+ </script>
279
+
280
+ <style lang="scss" scoped>
281
+ .container-generate-ai {
282
+ outline: 1px solid var(--v-secondary-base);
283
+ border-radius: $border-radius-root;
284
+ }
285
+ </style>