@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,119 @@
1
+ <template>
2
+ <div v-if="samlEnabled">
3
+ <v-btn
4
+ color="primary"
5
+ elevation="0"
6
+ large
7
+ block
8
+ @click="loginWithSaml"
9
+ :loading="ssoLoading || loading"
10
+ :disabled="loading"
11
+ >
12
+ <v-icon class="pr-2">{{ samlButtonIcon }}</v-icon>
13
+ {{ samlButtonLabel }}
14
+ </v-btn>
15
+ </div>
16
+ </template>
17
+
18
+ <script>
19
+ import Saml from '../../../models/Auth/Saml'
20
+
21
+ export default {
22
+ name: 'LoginSamlButton',
23
+ props: {
24
+ loading: {
25
+ type: Boolean,
26
+ default: false,
27
+ },
28
+ valid: {
29
+ type: Boolean,
30
+ default: true,
31
+ },
32
+ },
33
+ data() {
34
+ return {
35
+ ssoLoading: false,
36
+ samlEnabled: false,
37
+ samlButtonLabel: 'Sign in with SSO',
38
+ samlButtonIcon: 'mdi-login',
39
+ organizationId: null,
40
+ }
41
+ },
42
+ mounted() {
43
+ this.checkSamlStatus()
44
+ },
45
+ methods: {
46
+ async checkSamlStatus() {
47
+ try {
48
+ // Get organization from store or context
49
+ const organization = this.$store.getters['organization/get']
50
+
51
+ // If no organization in store, try to use the hostname
52
+ let orgIdentifier = null
53
+
54
+ if (organization && organization.id) {
55
+ // Prefer using the organization ID if available
56
+ orgIdentifier = organization.id
57
+ } else {
58
+ // Fallback to using the hostname (full base_url)
59
+ orgIdentifier = window.location.hostname
60
+ }
61
+
62
+ if (!orgIdentifier) {
63
+ return
64
+ }
65
+
66
+ // Check if SAML is enabled for this organization
67
+ const response = await Saml.custom('saml/status').get()
68
+
69
+ // The API returns an array, so we need to get the first element
70
+ const samlData = Array.isArray(response)
71
+ ? response[0]
72
+ : response
73
+
74
+ if (samlData && samlData.enabled && samlData.configured) {
75
+ this.samlEnabled = true
76
+ this.organizationId = samlData.organization_id
77
+
78
+ // Use button settings from the integration configuration
79
+ this.samlButtonLabel =
80
+ samlData.button_label || 'Sign in with SSO'
81
+ this.samlButtonIcon = samlData.button_icon || 'mdi-login'
82
+ }
83
+ } catch (error) {
84
+ // SAML not configured or error checking status
85
+ // Silently fail - SSO button won't show
86
+ }
87
+ },
88
+ async loginWithSaml() {
89
+ this.ssoLoading = true
90
+ try {
91
+ // Get the SSO URL from the backend
92
+ const response = await Saml.custom('users/saml-login').get()
93
+
94
+ // The API returns an array, so we need to get the first element
95
+ const loginData = Array.isArray(response)
96
+ ? response[0]
97
+ : response
98
+
99
+ if (loginData && loginData.sso_url) {
100
+ // Redirect to the IdP for authentication
101
+ window.location.href = loginData.sso_url
102
+ } else {
103
+ this.$dialog.error('Unable to initiate SSO login')
104
+ }
105
+ } catch (error) {
106
+ // Only log error in development mode
107
+ if (process.env.NODE_ENV === 'development') {
108
+ console.error('SSO login failed:', error)
109
+ }
110
+ this.$dialog.error(
111
+ 'SSO login failed. Please try again or use standard login.'
112
+ )
113
+ } finally {
114
+ this.ssoLoading = false
115
+ }
116
+ },
117
+ },
118
+ }
119
+ </script>
@@ -0,0 +1,327 @@
1
+ <template>
2
+ <div>
3
+ <div v-if="!render" class="integration-loading">
4
+ <v-progress-circular size="128" indeterminate />
5
+ </div>
6
+ <div v-if="render">
7
+ <v-row justify="center" align="center" class="mt-5">
8
+ <v-col cols="12">
9
+ <v-switch
10
+ v-model="integration.enabled"
11
+ :label="
12
+ $t(
13
+ 'windward.integrations.components.integration.driver.enabled'
14
+ )
15
+ "
16
+ />
17
+
18
+ <v-text-field
19
+ id="saml-entity-id"
20
+ v-model="integration.metadata.config.idp_entity_id"
21
+ :label="
22
+ $t(
23
+ 'windward.integrations.components.integration.driver.saml_sso.idp_entity_id'
24
+ )
25
+ "
26
+ :hint="
27
+ $t(
28
+ 'windward.integrations.components.integration.driver.saml_sso.idp_entity_id_hint'
29
+ )
30
+ "
31
+ :rules="$Validation.getRule('exists')"
32
+ ></v-text-field>
33
+
34
+ <v-text-field
35
+ id="saml-sso-url"
36
+ v-model="integration.metadata.config.idp_sso_url"
37
+ :label="
38
+ $t(
39
+ 'windward.integrations.components.integration.driver.saml_sso.idp_sso_url'
40
+ )
41
+ "
42
+ :hint="
43
+ $t(
44
+ 'windward.integrations.components.integration.driver.saml_sso.idp_sso_url_hint'
45
+ )
46
+ "
47
+ :rules="$Validation.getRule('url')"
48
+ ></v-text-field>
49
+
50
+ <v-text-field
51
+ id="saml-slo-url"
52
+ v-model="integration.metadata.config.idp_slo_url"
53
+ autocomplete="off"
54
+ :label="
55
+ $t(
56
+ 'windward.integrations.components.integration.driver.saml_sso.idp_slo_url'
57
+ )
58
+ "
59
+ :hint="
60
+ $t(
61
+ 'windward.integrations.components.integration.driver.saml_sso.idp_slo_url_hint'
62
+ )
63
+ "
64
+ :rules="$Validation.getRule('url')"
65
+ ></v-text-field>
66
+
67
+ <SecretField
68
+ id="saml-certificate"
69
+ v-model="integration.metadata.config.idp_x509_cert"
70
+ tag="v-textarea"
71
+ autocomplete="off"
72
+ :label="
73
+ $t(
74
+ 'windward.integrations.components.integration.driver.saml_sso.idp_x509_cert'
75
+ )
76
+ "
77
+ :hint="
78
+ $t(
79
+ 'windward.integrations.components.integration.driver.saml_sso.idp_x509_cert_hint'
80
+ )
81
+ "
82
+ :append-icon="
83
+ showCertificate ? 'mdi-eye' : 'mdi-eye-off'
84
+ "
85
+ rows="4"
86
+ :rules="$Validation.getRule('exists')"
87
+ :readonly="!showCertificate"
88
+ :hidden="!showCertificate"
89
+ :type="showCertificate ? 'text' : 'password'"
90
+ @click:append="showCertificate = !showCertificate"
91
+ ></SecretField>
92
+
93
+ <v-alert
94
+ v-if="integration.metadata.config.idp_x509_cert"
95
+ type="success"
96
+ text
97
+ dense
98
+ class="mt-2"
99
+ >
100
+ <v-icon small left>mdi-lock</v-icon>
101
+ {{
102
+ $t(
103
+ 'windward.integrations.components.integration.driver.saml_sso.cert_stored'
104
+ )
105
+ }}
106
+ </v-alert>
107
+
108
+ <v-divider class="my-4" />
109
+
110
+ <h3 class="mb-3">
111
+ {{
112
+ $t(
113
+ 'windward.integrations.components.integration.driver.saml_sso.button_settings'
114
+ )
115
+ }}
116
+ </h3>
117
+
118
+ <v-text-field
119
+ id="saml-button-label"
120
+ v-model="integration.metadata.config.button_label"
121
+ :label="
122
+ $t(
123
+ 'windward.integrations.components.integration.driver.saml_sso.button_label'
124
+ )
125
+ "
126
+ :hint="
127
+ $t(
128
+ 'windward.integrations.components.integration.driver.saml_sso.button_label_hint'
129
+ )
130
+ "
131
+ :placeholder="
132
+ $t(
133
+ 'windward.integrations.components.integration.driver.saml_sso.button_label_default'
134
+ )
135
+ "
136
+ ></v-text-field>
137
+
138
+ <v-text-field
139
+ id="saml-button-icon"
140
+ v-model="integration.metadata.config.button_icon"
141
+ :label="
142
+ $t(
143
+ 'windward.integrations.components.integration.driver.saml_sso.button_icon'
144
+ )
145
+ "
146
+ :hint="
147
+ $t(
148
+ 'windward.integrations.components.integration.driver.saml_sso.button_icon_hint'
149
+ )
150
+ "
151
+ placeholder="mdi-login"
152
+ >
153
+ <template #prepend>
154
+ <v-icon>{{
155
+ integration.metadata.config.button_icon ||
156
+ 'mdi-login'
157
+ }}</v-icon>
158
+ </template>
159
+ </v-text-field>
160
+
161
+ <v-divider class="my-4" />
162
+
163
+ <v-alert type="info" outlined>
164
+ <div class="font-weight-bold mb-2">
165
+ {{
166
+ $t(
167
+ 'windward.integrations.components.integration.driver.saml_sso.sp_details'
168
+ )
169
+ }}
170
+ </div>
171
+ <div class="text--secondary">
172
+ {{
173
+ $t(
174
+ 'windward.integrations.components.integration.driver.saml_sso.sp_details_description'
175
+ )
176
+ }}
177
+ </div>
178
+ <div class="mt-3">
179
+ <div>
180
+ <strong
181
+ >{{
182
+ $t(
183
+ 'windward.integrations.components.integration.driver.saml_sso.sp_entity_id_label'
184
+ )
185
+ }}:</strong
186
+ >
187
+ {{ spEntityId }}
188
+ </div>
189
+ <div>
190
+ <strong
191
+ >{{
192
+ $t(
193
+ 'windward.integrations.components.integration.driver.saml_sso.acs_url_label'
194
+ )
195
+ }}:</strong
196
+ >
197
+ {{ acsUrl }}
198
+ </div>
199
+ <div>
200
+ <strong
201
+ >{{
202
+ $t(
203
+ 'windward.integrations.components.integration.driver.saml_sso.slo_url_label'
204
+ )
205
+ }}:</strong
206
+ >
207
+ {{ sloUrl }}
208
+ </div>
209
+ </div>
210
+ </v-alert>
211
+
212
+ <TestConnection
213
+ :disabled="
214
+ !integration.metadata.config.idp_entity_id ||
215
+ !integration.metadata.config.idp_sso_url ||
216
+ !integration.metadata.config.idp_slo_url ||
217
+ !integration.metadata.config.idp_x509_cert
218
+ "
219
+ :loading="testConnectionLoading"
220
+ :errors="errorMessage"
221
+ @click="onTestConnection"
222
+ ></TestConnection>
223
+ </v-col>
224
+ </v-row>
225
+ </div>
226
+ </div>
227
+ </template>
228
+
229
+ <script>
230
+ import _ from 'lodash'
231
+ import TestConnection from '../TestConnection.vue'
232
+ import ManageBaseVue from './ManageBase.vue'
233
+ import SecretField from '../../SecretField.vue'
234
+ import Uuid from '~/helpers/Uuid'
235
+
236
+ export default {
237
+ name: 'ManageSamlDriver',
238
+ components: { TestConnection, SecretField },
239
+ extends: ManageBaseVue,
240
+ data() {
241
+ return {
242
+ // formValid: true|false If this form is "complete" and passed validation on THIS component. Defined and watched in ManageBase.vue
243
+ // render: true|false If we should show the form aka when validation has passed. Defined and managed in ManageBase.vue
244
+ // integration: { metadata: {...} } The integration object to write to. Defined and loaded in ManageBase.vue
245
+ showCertificate: false,
246
+ errorMessage: '',
247
+ testConnectionLoading: false,
248
+ }
249
+ },
250
+ computed: {
251
+ spEntityId() {
252
+ return `${process.env.BASE_URL}/saml/metadata`
253
+ },
254
+ acsUrl() {
255
+ return `${process.env.BASE_URL}/saml/acs`
256
+ },
257
+ sloUrl() {
258
+ return `${process.env.BASE_URL}/saml/slo`
259
+ },
260
+ },
261
+ methods: {
262
+ /**
263
+ * Lifecycle event called from ManageBase.vue when async fetch() completes.
264
+ * Once called this.integration will be available containing the integration model (or a new one)
265
+ */
266
+ onIntegrationLoaded() {
267
+ // Initialize SAML config if not exists
268
+ if (!this.integration.metadata.config) {
269
+ this.integration.metadata.config = {
270
+ idp_entity_id: '',
271
+ idp_sso_url: '',
272
+ idp_slo_url: '',
273
+ idp_x509_cert: '',
274
+ button_label: '',
275
+ button_icon: 'mdi-login',
276
+ }
277
+ }
278
+
279
+ // Set defaults for button settings if not present
280
+ if (!this.integration.metadata.config.button_label) {
281
+ this.integration.metadata.config.button_label = ''
282
+ }
283
+ if (!this.integration.metadata.config.button_icon) {
284
+ this.integration.metadata.config.button_icon = 'mdi-login'
285
+ }
286
+
287
+ // Show the certificate by default if this is a new integration without a cert
288
+ this.showCertificate = _.isEmpty(
289
+ _.get(this.integration, 'metadata.config.idp_x509_cert', null)
290
+ )
291
+ },
292
+ async onTestConnection() {
293
+ this.testConnectionLoading = true
294
+ let response = { result: false }
295
+ try {
296
+ response = await this.testConnection(this.integration.metadata)
297
+
298
+ if (response.result) {
299
+ this.errorMessage = ''
300
+ this.$dialog.success(
301
+ this.$t(
302
+ 'windward.integrations.shared.error.connect_success'
303
+ )
304
+ )
305
+ } else {
306
+ this.errorMessage = response.message
307
+ this.$dialog.error(
308
+ this.$t(
309
+ 'windward.integrations.shared.error.connect_fail'
310
+ )
311
+ )
312
+ }
313
+ } catch (e) {
314
+ console.error(e)
315
+ this.$dialog.error(
316
+ this.$t('windward.integrations.shared.error.unknown')
317
+ )
318
+ }
319
+
320
+ // We will indirectly validate the form via connection tests
321
+ // That way we can 100% confirm that the integration is valid
322
+ this.formValid = response.result
323
+ this.testConnectionLoading = false
324
+ },
325
+ },
326
+ }
327
+ </script>
@@ -0,0 +1,120 @@
1
+ <template>
2
+ <v-select
3
+ v-model="selectedDifficulty"
4
+ :items="taxonomyLevels"
5
+ item-text="text"
6
+ outlined
7
+ :hide-details="hideDetails"
8
+ dense
9
+ :disabled="disabled"
10
+ :label="
11
+ $t('windward.integrations.components.llm.blooms.blooms_taxonomy')
12
+ "
13
+ @input="$emit('input', selectedDifficulty)"
14
+ ></v-select>
15
+ </template>
16
+
17
+ <script>
18
+ import _ from 'lodash'
19
+
20
+ export default {
21
+ name: 'LLMBloomTaxonomySelector',
22
+ props: {
23
+ // The assessment question
24
+ value: {
25
+ type: [String, null],
26
+ required: false,
27
+ default: 'None',
28
+ },
29
+ levels: {
30
+ type: Array,
31
+ required: false,
32
+ default: () => {
33
+ return []
34
+ },
35
+ },
36
+ advancedLevels: { type: Boolean, required: false, default: false },
37
+ hideDetails: { type: Boolean, required: false, default: true },
38
+ disabled: { type: Boolean, required: false, default: false },
39
+ },
40
+ data() {
41
+ return {
42
+ selectedDifficulty: 'None',
43
+ }
44
+ },
45
+ computed: {
46
+ taxonomyLevels() {
47
+ let levels = []
48
+
49
+ // Basic Bloom's taxonomy levels available to all question types
50
+ const basicBloomTaxonomy = [
51
+ {
52
+ value: 'None',
53
+ text: this.$t(
54
+ 'windward.integrations.components.llm.blooms.none'
55
+ ),
56
+ },
57
+ {
58
+ value: 'Remember',
59
+ text: this.$t(
60
+ 'windward.integrations.components.llm.blooms.remember'
61
+ ),
62
+ },
63
+ {
64
+ value: 'Understand',
65
+ text: this.$t(
66
+ 'windward.integrations.components.llm.blooms.understand'
67
+ ),
68
+ },
69
+ {
70
+ value: 'Apply',
71
+ text: this.$t(
72
+ 'windward.integrations.components.llm.blooms.apply'
73
+ ),
74
+ },
75
+ ]
76
+
77
+ const advancedBlooms = [
78
+ {
79
+ value: 'Analyze',
80
+ text: this.$t(
81
+ 'windward.integrations.components.llm.blooms.analyze'
82
+ ),
83
+ },
84
+ {
85
+ value: 'Evaluate',
86
+ text: this.$t(
87
+ 'windward.integrations.components.llm.blooms.evaluate'
88
+ ),
89
+ },
90
+ ]
91
+
92
+ // Manually defined levels
93
+ if (this.levels.length > 0) {
94
+ // All blooms for filtering
95
+ levels = levels.concat(basicBloomTaxonomy, advancedBlooms)
96
+
97
+ levels = levels.filter((v) => {
98
+ return this.levels.includes(v.value)
99
+ })
100
+ } else {
101
+ // Show all levels and omit advanced if not enabled
102
+ levels = levels.concat(basicBloomTaxonomy)
103
+
104
+ if (this.advancedLevels) {
105
+ levels = levels.concat(advancedBlooms)
106
+ }
107
+ }
108
+ return levels
109
+ },
110
+ },
111
+ mounted() {
112
+ if (_.isEmpty(this.value)) {
113
+ // Emit the default of 'None' as the initial value if none is set
114
+ this.$emit('input', this.selectedDifficulty)
115
+ } else {
116
+ this.selectedDifficulty = _.cloneDeep(this.value)
117
+ }
118
+ },
119
+ }
120
+ </script>
@@ -0,0 +1,66 @@
1
+ <template>
2
+ <v-select
3
+ v-model="selectedContent"
4
+ :items="flattenedContent"
5
+ outlined
6
+ hide-details
7
+ dense
8
+ :disabled="disabled"
9
+ :label="
10
+ $t(
11
+ 'windward.integrations.components.llm.content_selector.selected_pages'
12
+ )
13
+ "
14
+ item-text="content.name"
15
+ return-object
16
+ @input="$emit('input', selectedContent)"
17
+ ></v-select>
18
+ </template>
19
+
20
+ <script>
21
+ import _ from 'lodash'
22
+ import { mapGetters } from 'vuex'
23
+
24
+ export default {
25
+ name: 'LLMContentSelector',
26
+ props: {
27
+ value: {
28
+ type: [Object, null],
29
+ required: false,
30
+ default: null,
31
+ },
32
+ disabled: { type: Boolean, required: false, default: false },
33
+ },
34
+ data() {
35
+ return {
36
+ selectedContent: null,
37
+ }
38
+ },
39
+ computed: {
40
+ ...mapGetters({
41
+ content: 'content/get',
42
+ contentTree: 'content/getTree',
43
+ }),
44
+ flattenedContent() {
45
+ const flatTree = this.$ContentService.getFlatTree()
46
+
47
+ const homepage = this.$ContentService.getHomepage()
48
+ if (!_.isEmpty(homepage)) {
49
+ flatTree.unshift(homepage)
50
+ }
51
+
52
+ return flatTree
53
+ },
54
+ },
55
+ mounted() {
56
+ if (_.isEmpty(this.value)) {
57
+ this.selectedContent = _.cloneDeep(this.content)
58
+
59
+ // Emit the current page as selected if none was set
60
+ this.$emit('input', this.selectedContent)
61
+ } else {
62
+ this.selectedContent = _.cloneDeep(this.value)
63
+ }
64
+ },
65
+ }
66
+ </script>