@windward/integrations 0.24.0 → 0.25.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## Release [0.25.0] - 2026-03-30
4
+
5
+ * Merged in feature/LE-2279/review-course (pull request #137)
6
+ * Merge remote-tracking branch 'origin/release/0.25.0' into feature/LE-2279/review-course
7
+
8
+
3
9
  ## Release [0.24.0] - 2026-03-10
4
10
 
5
11
  * Merged in feature/LE-2240-organization-sales-transaction-p (pull request #136)
@@ -105,7 +105,12 @@
105
105
  </div>
106
106
 
107
107
  <!-- Messages Area -->
108
- <div ref="messagesScroll" class="messages" @scroll="onMessagesScroll">
108
+ <div
109
+ ref="messagesScroll"
110
+ class="messages"
111
+ @scroll="onMessagesScroll"
112
+ @click="onMessageLinkClick"
113
+ >
109
114
  <div
110
115
  v-if="loadingOlder"
111
116
  class="top-loader d-flex justify-center mb-2"
@@ -127,7 +132,7 @@
127
132
  <div v-else>
128
133
  <div
129
134
  v-for="(msg, i) in messages"
130
- :key="i"
135
+ :key="messageKey(msg, i)"
131
136
  class="message"
132
137
  :class="msg.role"
133
138
  >
@@ -268,11 +273,12 @@
268
273
  import _ from 'lodash'
269
274
  import { mapGetters } from 'vuex'
270
275
 
271
- import AgentChat from '../../../models/AgentChat'
272
- import AgentChatMessage from '../../../models/AgentChatMessage'
273
276
  import Organization from '~/models/Organization'
274
277
  import Course from '~/models/Course'
278
+ import Uuid from '~/helpers/Uuid'
275
279
  import TextViewer from '~/components/Text/TextViewer.vue'
280
+ import AgentChat from '../../../models/AgentChat'
281
+ import AgentChatMessage from '../../../models/AgentChatMessage'
276
282
 
277
283
  export default {
278
284
  name: 'ChatWindow',
@@ -312,7 +318,7 @@ export default {
312
318
  )
313
319
  const quotes = []
314
320
 
315
- for (const [_key, quote] of Object.entries(localizedQuotes)) {
321
+ for (const quote of Object.values(localizedQuotes)) {
316
322
  quotes.push(quote)
317
323
  }
318
324
 
@@ -365,6 +371,68 @@ export default {
365
371
  })
366
372
  },
367
373
  methods: {
374
+ // Intercept internal links rendered inside AI message content so we can
375
+ // navigate within the SPA and preserve block focus query params.
376
+ async onMessageLinkClick(event) {
377
+ const target = event.target
378
+ if (!(target instanceof Element)) return
379
+
380
+ // Only handle anchor clicks from the rendered message body.
381
+ const anchor = target.closest('a')
382
+ if (!anchor) return
383
+
384
+ if (
385
+ event.defaultPrevented ||
386
+ event.button !== 0 ||
387
+ event.metaKey ||
388
+ event.ctrlKey ||
389
+ event.shiftKey ||
390
+ event.altKey ||
391
+ anchor.target === '_blank' ||
392
+ anchor.hasAttribute('download')
393
+ ) {
394
+ return
395
+ }
396
+
397
+ const href = anchor.getAttribute('href')
398
+ if (!href) return
399
+
400
+ let url
401
+ try {
402
+ url = new URL(href, window.location.href)
403
+ } catch (e) {
404
+ return
405
+ }
406
+
407
+ if (url.origin !== window.location.origin) {
408
+ return
409
+ }
410
+
411
+ const route = this.routeFromUrl(url, href)
412
+ if (!route) {
413
+ return
414
+ }
415
+
416
+ event.preventDefault()
417
+
418
+ if (
419
+ route.contentId &&
420
+ route.courseId === this.$route.params.course &&
421
+ route.sectionId === this.$route.params.section
422
+ ) {
423
+ // Reuse the in-page content loader when the link stays within
424
+ // the current course section so focus state updates without a
425
+ // full route push.
426
+ await this.$ContentService.set(route.contentId, {}, route.query)
427
+ return
428
+ }
429
+
430
+ await this.$router.push({
431
+ path: route.path,
432
+ query: route.query,
433
+ hash: route.hash,
434
+ })
435
+ },
368
436
  clearError() {
369
437
  this.sendError = null
370
438
  this.lastFailedPrompt = ''
@@ -397,8 +465,8 @@ export default {
397
465
  // clear any previous error
398
466
  this.sendError = null
399
467
  this.lastFailedPrompt = ''
400
- // Show the user's message optimistically
401
468
  this.messages.push({
469
+ id: this.temporaryMessageId(),
402
470
  role: 'user',
403
471
  text: userText,
404
472
  created_at: Date.now(),
@@ -442,7 +510,8 @@ export default {
442
510
  )
443
511
  .save()
444
512
  }
445
- // success path
513
+ this.currentSessionId = reply.id
514
+ await this.reloadLatestMessages()
446
515
  this.awaitingReply = false
447
516
  this.showTypingDots = false
448
517
  this.showProcessingMessage = false
@@ -451,9 +520,6 @@ export default {
451
520
  if (this.longWaitTimeoutId) clearTimeout(this.longWaitTimeoutId)
452
521
  if (this.feedbackIntervalId)
453
522
  clearInterval(this.feedbackIntervalId)
454
- // set current session and refresh messages (latest page)
455
- this.currentSessionId = reply.id
456
- await this.reloadLatestMessages()
457
523
  this.$nextTick(this.scrollToBottom)
458
524
  // persist current session on each send
459
525
  this.persistCurrentSession(reply.id)
@@ -469,7 +535,6 @@ export default {
469
535
  if (this.feedbackIntervalId)
470
536
  clearInterval(this.feedbackIntervalId)
471
537
 
472
- // Remove the last optimistic user message to avoid duplicates on retry
473
538
  const last = this.messages[this.messages.length - 1]
474
539
  if (last && last.role === 'user' && last.text === userText) {
475
540
  this.messages.pop()
@@ -628,23 +693,34 @@ export default {
628
693
  return 0
629
694
  },
630
695
 
631
- getMessageTimestamp(msg) {
632
- // Determine timestamp
633
- const eventDate = Date.parse(msg.created_at)
634
- if (!isNaN(eventDate)) return eventDate
635
- return Date.now()
696
+ messageKey(msg, index) {
697
+ return this.getMessageId(msg) || `message-${index}`
698
+ },
699
+
700
+ temporaryMessageId() {
701
+ return `temp-${Date.now()}-${Math.random()
702
+ .toString(36)
703
+ .slice(2, 10)}`
704
+ },
705
+
706
+ getMessageId(msg) {
707
+ if (msg == null || msg.id == null) return ''
708
+ return String(msg.id)
636
709
  },
637
710
 
638
711
  normalizeMessages(list) {
639
712
  if (!Array.isArray(list)) return []
640
- return list
641
- .map((m) => {
642
- const role =
643
- m.role || (m.sender === 'user' ? 'user' : 'assistant')
644
- const createdAtTs = this.getMessageTimestamp(m)
645
- return { ...m, role, created_at: createdAtTs }
646
- })
647
- .sort((a, b) => a.created_at - b.created_at)
713
+ return list.map((m) => {
714
+ const role =
715
+ m.role || (m.sender === 'user' ? 'user' : 'assistant')
716
+
717
+ return {
718
+ ...m,
719
+ id: this.getMessageId(m),
720
+ role,
721
+ created_at: m.created_at ?? m.createdAt,
722
+ }
723
+ })
648
724
  },
649
725
 
650
726
  senderLabel(msg) {
@@ -724,12 +800,9 @@ export default {
724
800
  return Array.isArray(results) ? results : []
725
801
  },
726
802
  async reloadLatestMessages() {
727
- // Start from newest page (desc page 1)
728
803
  this.resetPaginationState()
729
804
  const data = await this.fetchMessagesPage(1)
730
- // We receive newest first; reverse to show ascending in UI
731
- const normalized = this.normalizeMessages(data)
732
- this.messages = normalized
805
+ this.messages = this.normalizeMessages(data)
733
806
  this.nextPage = 2
734
807
  // If fewer than perPage, no more older
735
808
  if (!data || data.length < this.perPage) this.noMoreOlder = true
@@ -767,6 +840,104 @@ export default {
767
840
  this.loadOlder()
768
841
  }
769
842
  },
843
+ buildCourseContentRoute(contentId, query = {}, hash = undefined) {
844
+ const courseId = _.get(this.$route, 'params.course', null)
845
+ const sectionId = _.get(this.$route, 'params.section', null)
846
+
847
+ if (!courseId || !sectionId || !Uuid.test(contentId)) {
848
+ return null
849
+ }
850
+
851
+ return {
852
+ path: `/course/${courseId}/section/${sectionId}/content/${contentId}`,
853
+ query,
854
+ hash,
855
+ courseId,
856
+ sectionId,
857
+ contentId,
858
+ }
859
+ },
860
+ routeFromRelativeHref(href, query = {}, hash = undefined) {
861
+ if (typeof href !== 'string') {
862
+ return null
863
+ }
864
+
865
+ const trimmedHref = href.trim()
866
+ if (!trimmedHref) {
867
+ return null
868
+ }
869
+
870
+ const contentId = trimmedHref
871
+ .replace(/^\.\//, '')
872
+ .split(/[?#]/, 1)[0]
873
+
874
+ if (
875
+ !contentId ||
876
+ trimmedHref.startsWith('/') ||
877
+ !Uuid.test(contentId)
878
+ ) {
879
+ return null
880
+ }
881
+
882
+ return this.buildCourseContentRoute(contentId, query, hash)
883
+ },
884
+ routeFromUrl(url, href = '') {
885
+ const query = {}
886
+ url.searchParams.forEach((value, key) => {
887
+ query[key] = value
888
+ })
889
+
890
+ const relativeRoute = this.routeFromRelativeHref(
891
+ href,
892
+ query,
893
+ url.hash || undefined
894
+ )
895
+ if (relativeRoute) {
896
+ return relativeRoute
897
+ }
898
+
899
+ const path = url.pathname
900
+ if (!path.startsWith('/course/')) {
901
+ return null
902
+ }
903
+
904
+ const segments = path.split('/').filter(Boolean)
905
+ const courseIndex = segments.indexOf('course')
906
+ const sectionIndex = segments.indexOf('section')
907
+ const contentIndex = segments.indexOf('content')
908
+
909
+ if (courseIndex === -1 || sectionIndex === -1) {
910
+ return null
911
+ }
912
+
913
+ if (contentIndex === -1) {
914
+ const currentSectionId = _.get(this.$route, 'params.section')
915
+ const possibleContentId = segments[sectionIndex + 1] || ''
916
+
917
+ if (
918
+ currentSectionId &&
919
+ possibleContentId !== currentSectionId &&
920
+ Uuid.test(possibleContentId)
921
+ ) {
922
+ return this.buildCourseContentRoute(
923
+ possibleContentId,
924
+ query,
925
+ url.hash || undefined
926
+ )
927
+ }
928
+
929
+ return null
930
+ }
931
+
932
+ return {
933
+ path,
934
+ query,
935
+ hash: url.hash || undefined,
936
+ courseId: segments[courseIndex + 1] || null,
937
+ sectionId: segments[sectionIndex + 1] || null,
938
+ contentId: segments[contentIndex + 1] || null,
939
+ }
940
+ },
770
941
  },
771
942
  }
772
943
  </script>
@@ -261,7 +261,8 @@ export default {
261
261
  )
262
262
 
263
263
  let response
264
- if (this.keepExisting) {
264
+ const allowKeepExisting = this.questionType !== 'true_false'
265
+ if (this.keepExisting && allowKeepExisting) {
265
266
  const existingInputs = this.buildExistingInputs()
266
267
  const hasExisting = this.hasAnyExistingInput(existingInputs)
267
268
 
@@ -1,5 +1,7 @@
1
1
  import transformBlock from './transform_block'
2
+ import transformActivity from './transform_activity'
2
3
 
3
4
  export default {
4
5
  transform_block: transformBlock,
6
+ transform_activity: transformActivity,
5
7
  }
@@ -0,0 +1,7 @@
1
+ export default {
2
+ change_block_type: 'Change block type',
3
+ change_block_type_insufficient_items:
4
+ 'Change block type is available when the activity has at least two items.',
5
+ change_block_type_error:
6
+ 'Unable to change block type. Try again or edit manually.',
7
+ }
@@ -14,4 +14,5 @@ export default {
14
14
  missing_tool: 'The tool id {0} is missing',
15
15
  unknown_error: 'Something went wrong! Could not launch this link!',
16
16
  link_disabled: 'Link disabled',
17
+ try_again: 'Try again',
17
18
  }
@@ -7,4 +7,6 @@ export default {
7
7
  launch_type_modal: 'Modal',
8
8
  link_select: 'Select a Link:',
9
9
  link_disabled: 'Link Disabled',
10
+ no_links: 'No links available.',
11
+ create_link: 'Go to external integrations page to create links.',
10
12
  }
@@ -1,5 +1,7 @@
1
1
  import transformBlock from './transform_block'
2
+ import transformActivity from './transform_activity'
2
3
 
3
4
  export default {
4
5
  transform_block: transformBlock,
6
+ transform_activity: transformActivity,
5
7
  }
@@ -0,0 +1,7 @@
1
+ export default {
2
+ change_block_type: 'Change block type',
3
+ change_block_type_insufficient_items:
4
+ 'Change block type is available when the activity has at least two items.',
5
+ change_block_type_error:
6
+ 'Unable to change block type. Try again or edit manually.',
7
+ }
@@ -14,4 +14,5 @@ export default {
14
14
  missing_tool: 'The tool id {0} is missing',
15
15
  unknown_error: 'Something went wrong! Could not launch this link!',
16
16
  link_disabled: 'Link disabled',
17
+ try_again: 'Try again',
17
18
  }
@@ -7,4 +7,6 @@ export default {
7
7
  launch_type_modal: 'Modal',
8
8
  link_select: 'Select a Link:',
9
9
  link_disabled: 'Link Disabled',
10
+ no_links: 'No links available.',
11
+ create_link: 'Go to external integrations page to create links.',
10
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windward/integrations",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Windward UI Plugin Integrations for 3rd Party Systems",
5
5
  "main": "plugin.js",
6
6
  "scripts": {