@asd20/ui-next 2.29.0 → 2.29.1

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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.29.1](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.29.0...ui-next-v2.29.1) (2026-10-05)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * carry ai search verified supporting pages into related pages and pages tab ([46253e0](https://github.com/academydistrict20/asd20-ui-next/commit/46253e05ef064c99dbe3151cd093f6c938e2e9f6))
9
+
3
10
  # [2.29.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.28.1...ui-next-v2.29.0) (2026-10-02)
4
11
 
5
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.29.0",
3
+ "version": "2.29.1",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -2701,6 +2701,12 @@ export default {
2701
2701
  question: this.searchQuestion || this.inputText,
2702
2702
  }
2703
2703
  if (options.purpose) searchInput.purpose = options.purpose
2704
+ if (
2705
+ options.purpose === 'ai-related-pages' &&
2706
+ options.sourcePages?.length
2707
+ ) {
2708
+ searchInput.sourcePages = options.sourcePages
2709
+ }
2704
2710
 
2705
2711
  this.searchingPages = true
2706
2712
  try {
@@ -3105,6 +3111,16 @@ export default {
3105
3111
  ? `[Keywords: ${keywordsToUse.replace(/<[^>]*>/g, '')}]`
3106
3112
  : ''
3107
3113
 
3114
+ const sourcePages = Array.isArray(sources)
3115
+ ? sources
3116
+ .filter(
3117
+ source =>
3118
+ typeof source?.url === 'string' &&
3119
+ typeof source?.chunkId === 'string'
3120
+ )
3121
+ .slice(0, 8)
3122
+ .map(({ url, chunkId }) => ({ url, chunkId }))
3123
+ : []
3108
3124
  const assistantTurnKeywords = safeKeywords || keywordsToUse || ''
3109
3125
  const assistantTurn = {
3110
3126
  id: `a-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
@@ -3115,7 +3131,8 @@ export default {
3115
3131
  feedbackValue: null,
3116
3132
  feedbackSubmitting: false,
3117
3133
  relatedPages: [],
3118
- relatedPagesStatus: keywordsToUse ? 'pending' : 'complete',
3134
+ relatedPagesStatus:
3135
+ keywordsToUse || sourcePages.length ? 'pending' : 'complete',
3119
3136
  }
3120
3137
  assistantTurnId = assistantTurn.id
3121
3138
  this.aiTranscript = [...this.aiTranscript, assistantTurn]
@@ -3124,13 +3141,14 @@ export default {
3124
3141
  this.scrollTurnToTop(this.anchoredUserTurnId, { defer: true })
3125
3142
  }
3126
3143
 
3127
- if (keywordsToUse) {
3144
+ if (keywordsToUse || sourcePages.length) {
3128
3145
  this.keywordsFromAi = true
3129
3146
  this.skipKeywordWatcher = true
3130
3147
  this.keywords = keywordsToUse
3131
3148
  await this.searchPages({
3132
3149
  keepAnswersTab: true,
3133
3150
  purpose: 'ai-related-pages',
3151
+ sourcePages,
3134
3152
  runId,
3135
3153
  })
3136
3154
  if (!this.isCurrentSearchRun(runId)) return
@@ -3,6 +3,7 @@ import axios from 'axios'
3
3
  import { hasSearchProxyRuntime, resolveSearchProxyUrl } from './searchProxyUrl'
4
4
  import { resolveRuntimeConfigValue } from './runtimeConfig'
5
5
  import { buildTypoTolerantSearchQuery } from './searchQuery'
6
+ import { mergeSupportingPages } from './supportingPages'
6
7
 
7
8
  export const NO_PAGE_RESULTS_FALLBACKS = [
8
9
  {
@@ -100,6 +101,8 @@ function searchPayload(state) {
100
101
  */
101
102
  export default async function queryPages({
102
103
  keywords = '',
104
+ purpose = '',
105
+ sourcePages = [],
103
106
  top = 25,
104
107
  requireKeywords = false,
105
108
  organizations = [],
@@ -111,7 +114,11 @@ export default async function queryPages({
111
114
  apiVersion = '',
112
115
  searchVersion = '',
113
116
  }) {
114
- if (!keywords && requireKeywords)
117
+ const supportingSources =
118
+ purpose === 'ai-related-pages' && Array.isArray(sourcePages)
119
+ ? sourcePages.slice(0, 8)
120
+ : []
121
+ if (!keywords && requireKeywords && !supportingSources.length)
115
122
  return {
116
123
  pages: [],
117
124
  facets: [],
@@ -150,16 +157,17 @@ export default async function queryPages({
150
157
  const resolvedRequestUrl =
151
158
  typeof requestUrl === 'string' && requestUrl.trim()
152
159
  ? requestUrl.trim()
153
- : !resolvedSearchEndpoint &&
154
- hasSearchProxyRuntime(runtimeConfig)
155
- ? resolveSearchProxyUrl('pages', runtimeConfig)
156
- : ''
160
+ : !resolvedSearchEndpoint && hasSearchProxyRuntime(runtimeConfig)
161
+ ? resolveSearchProxyUrl('pages', runtimeConfig)
162
+ : ''
157
163
 
158
164
  if (
159
165
  !resolvedRequestUrl &&
160
166
  (!resolvedSearchEndpoint || !resolvedSearchKey || !resolvedApiVersion)
161
167
  ) {
162
- throw new Error('Azure Search or proxy configuration is required for queryPages.')
168
+ throw new Error(
169
+ 'Azure Search or proxy configuration is required for queryPages.'
170
+ )
163
171
  }
164
172
 
165
173
  const { data } = resolvedRequestUrl
@@ -170,6 +178,9 @@ export default async function queryPages({
170
178
  keywords,
171
179
  organizations,
172
180
  top,
181
+ ...(supportingSources.length
182
+ ? { sourcePages: supportingSources }
183
+ : {}),
173
184
  },
174
185
  })
175
186
  : await axios({
@@ -194,10 +205,15 @@ export default async function queryPages({
194
205
  ),
195
206
  })
196
207
 
197
- const pages = Array.isArray(data.value) ? data.value : []
208
+ const pages = mergeSupportingPages(
209
+ Array.isArray(data.value) ? data.value : [],
210
+ Array.isArray(data.supportingPages) ? data.supportingPages : [],
211
+ supportingSources,
212
+ organizations
213
+ )
198
214
  const resolvedPages = pages.length
199
215
  ? pages
200
- : NO_PAGE_RESULTS_FALLBACKS.map((page) => ({
216
+ : NO_PAGE_RESULTS_FALLBACKS.map(page => ({
201
217
  ...page,
202
218
  categories: Array.isArray(page.categories) ? [...page.categories] : [],
203
219
  }))
@@ -0,0 +1,53 @@
1
+ export function normalizeSupportingPageUrl(value) {
2
+ if (typeof value !== 'string' || value.length > 2048) return ''
3
+ try {
4
+ const url = new URL(value.trim())
5
+ if (
6
+ !['http:', 'https:'].includes(url.protocol) ||
7
+ url.username ||
8
+ url.password
9
+ )
10
+ return ''
11
+ url.hash = ''
12
+ url.pathname = url.pathname.replace(/\/+$/, '') || '/'
13
+ return url.toString().replace(/\/$/, '')
14
+ } catch (_error) {
15
+ return ''
16
+ }
17
+ }
18
+
19
+ export function mergeSupportingPages(
20
+ pages = [],
21
+ supportingPages = [],
22
+ sourcePages = [],
23
+ organizations = []
24
+ ) {
25
+ const sourceUrls = sourcePages
26
+ .map(source => normalizeSupportingPageUrl(source?.url))
27
+ .filter(Boolean)
28
+ const supported = supportingPages
29
+ .filter(
30
+ page =>
31
+ page.type === 'page' &&
32
+ sourceUrls.includes(normalizeSupportingPageUrl(page.url)) &&
33
+ (!organizations.length ||
34
+ organizations.includes(page.ownerOrganizationId))
35
+ )
36
+ .sort(
37
+ (a, b) =>
38
+ sourceUrls.indexOf(normalizeSupportingPageUrl(a.url)) -
39
+ sourceUrls.indexOf(normalizeSupportingPageUrl(b.url))
40
+ )
41
+ if (!supported.length) return pages
42
+ const seen = new Set()
43
+ return [
44
+ ...supported.map(page => ({ ...page, isAnswerSupportingPage: true })),
45
+ ...pages,
46
+ ].filter(page => {
47
+ const key = normalizeSupportingPageUrl(page.url) || page.id
48
+ if (!key) return true
49
+ if (seen.has(key)) return false
50
+ seen.add(key)
51
+ return true
52
+ })
53
+ }