@asd20/ui-next 2.3.7 → 2.4.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,24 @@
1
1
  # Changelog
2
2
 
3
+ # [2.4.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.3.8...ui-next-v2.4.0) (2026-05-01)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * fix search state routing ([64f10cc](https://github.com/academydistrict20/asd20-ui-next/commit/64f10cc37b2f3d6e2b27b54e4fe96b94054bfb6d))
9
+
10
+
11
+ ### Features
12
+
13
+ * improve search persistence and clearing ([6a27f9b](https://github.com/academydistrict20/asd20-ui-next/commit/6a27f9bfe62898ca00618b7fe896b061c44e5532))
14
+
15
+ ## [2.3.8](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.3.7...ui-next-v2.3.8) (2026-04-30)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * fix search results not persisting ([041aba0](https://github.com/academydistrict20/asd20-ui-next/commit/041aba02a5dc761ee3c5ca47ab18a364d369c0b5))
21
+
3
22
  ## [2.3.7](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.3.6...ui-next-v2.3.7) (2026-04-29)
4
23
 
5
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.3.7",
3
+ "version": "2.4.0",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -83,6 +83,27 @@ import Asd20TagGroup from '../../../components/organisms/Asd20TagGroup'
83
83
  import Asd20TextAvatar from '../../../components/atoms/Asd20TextAvatar'
84
84
  import Asd20ThumbnailAvatar from '../../../components/atoms/Asd20ThumbnailAvatar'
85
85
 
86
+ function resolveRouter(vm) {
87
+ const internal = vm && vm.$
88
+ const context = internal && internal.ctx
89
+ if (context && context.$router) {
90
+ return context.$router
91
+ }
92
+
93
+ const parentContext = internal && internal.parent && internal.parent.ctx
94
+ if (parentContext && parentContext.$router) {
95
+ return parentContext.$router
96
+ }
97
+
98
+ return (
99
+ internal &&
100
+ internal.appContext &&
101
+ internal.appContext.config &&
102
+ internal.appContext.config.globalProperties &&
103
+ internal.appContext.config.globalProperties.$router
104
+ )
105
+ }
106
+
86
107
  export default {
87
108
  name: 'Asd20StoryItem',
88
109
 
@@ -132,7 +153,8 @@ export default {
132
153
  return this.link || this.to
133
154
  },
134
155
  usesRouterNavigation() {
135
- if (!this.$router || !this.to) return false
156
+ const router = resolveRouter(this)
157
+ if (!router || !this.to) return false
136
158
  if (typeof this.to === 'string') return this.to.startsWith('/')
137
159
  return typeof this.to === 'object'
138
160
  },
@@ -143,9 +165,11 @@ export default {
143
165
 
144
166
  methods: {
145
167
  onClick(e) {
146
- if (this.usesRouterNavigation) {
168
+ const router = resolveRouter(this)
169
+
170
+ if (this.usesRouterNavigation && router && typeof router.push === 'function') {
147
171
  e.preventDefault()
148
- this.$router.push(this.to)
172
+ router.push(this.to)
149
173
  }
150
174
  },
151
175
  },
@@ -32,6 +32,27 @@
32
32
  import Asd20Icon from '../../../components/atoms/Asd20Icon'
33
33
  import Asd20Badge from '../../../components/atoms/Asd20Badge'
34
34
 
35
+ function resolveRouter(vm) {
36
+ const internal = vm && vm.$
37
+ const context = internal && internal.ctx
38
+ if (context && context.$router) {
39
+ return context.$router
40
+ }
41
+
42
+ const parentContext = internal && internal.parent && internal.parent.ctx
43
+ if (parentContext && parentContext.$router) {
44
+ return parentContext.$router
45
+ }
46
+
47
+ return (
48
+ internal &&
49
+ internal.appContext &&
50
+ internal.appContext.config &&
51
+ internal.appContext.config.globalProperties &&
52
+ internal.appContext.config.globalProperties.$router
53
+ )
54
+ }
55
+
35
56
  export default {
36
57
  name: 'Asd20Tab',
37
58
 
@@ -52,7 +73,7 @@ export default {
52
73
 
53
74
  computed: {
54
75
  component() {
55
- if (this.$router && this.to) return 'router-link'
76
+ if (resolveRouter(this) && this.to) return 'router-link'
56
77
  return 'button'
57
78
  },
58
79
  classes() {
@@ -374,21 +374,30 @@ export default {
374
374
  hasExistingResults() {
375
375
  const siteSearch = this.$refs.siteSearch
376
376
  if (!siteSearch) return false
377
+ const pages = Array.isArray(siteSearch.currentPages)
378
+ ? siteSearch.currentPages
379
+ : siteSearch.resolvedPages
380
+ const files = Array.isArray(siteSearch.currentFiles)
381
+ ? siteSearch.currentFiles
382
+ : siteSearch.resolvedFiles
383
+ const groups = Array.isArray(siteSearch.currentGroups)
384
+ ? siteSearch.currentGroups
385
+ : siteSearch.resolvedGroups
377
386
  if (
378
- Array.isArray(siteSearch.resolvedPages) &&
379
- siteSearch.resolvedPages.length
387
+ Array.isArray(pages) &&
388
+ pages.length
380
389
  ) {
381
390
  return true
382
391
  }
383
392
  if (
384
- Array.isArray(siteSearch.resolvedFiles) &&
385
- siteSearch.resolvedFiles.length
393
+ Array.isArray(files) &&
394
+ files.length
386
395
  ) {
387
396
  return true
388
397
  }
389
398
  if (
390
- Array.isArray(siteSearch.resolvedGroups) &&
391
- siteSearch.resolvedGroups.length
399
+ Array.isArray(groups) &&
400
+ groups.length
392
401
  ) {
393
402
  return true
394
403
  }
@@ -327,19 +327,19 @@
327
327
  v-if="
328
328
  hasSubmitted &&
329
329
  showContacts &&
330
- resolvedGroups.length === 0 &&
330
+ currentGroups.length === 0 &&
331
331
  !searchingFiles
332
332
  "
333
333
  title="No Contacts Found"
334
334
  description="Try using different keywords."
335
335
  />
336
336
  <div
337
- v-if="resolvedGroups.length > 0 && showContacts"
337
+ v-if="currentGroups.length > 0 && showContacts"
338
338
  class="asd20-site-search__suggested"
339
339
  >
340
340
  <h3>
341
341
  {{
342
- resolvedGroups.length > 1
342
+ currentGroups.length > 1
343
343
  ? 'Suggested Contacts:'
344
344
  : 'Suggested Contact:'
345
345
  }}
@@ -357,7 +357,7 @@
357
357
  />
358
358
  </div>
359
359
  <asd20-loader
360
- v-if="searchingFiles && resolvedGroups.length === 0"
360
+ v-if="searchingFiles && currentGroups.length === 0"
361
361
  size="lg"
362
362
  />
363
363
  </asd20-viewport>
@@ -381,7 +381,7 @@
381
381
  <hr/>AI can make mistakes. Verify important information."
382
382
  />
383
383
  <asd20-notification
384
- v-if="keywords && resolvedFiles.length === 0 && !searchingFiles"
384
+ v-if="keywords && currentFiles.length === 0 && !searchingFiles"
385
385
  title="No Files Found"
386
386
  description="Try using different keywords."
387
387
  />
@@ -707,6 +707,10 @@ export default {
707
707
  searchingPages: false,
708
708
  searchingFiles: false,
709
709
 
710
+ localSearchPages: null,
711
+ localSearchFiles: null,
712
+ localSearchGroups: null,
713
+
710
714
  keywordsFromAi: false,
711
715
  aiKeywordsDisplayAnswer: '',
712
716
  aiKeywordsDisplayPages: '',
@@ -725,6 +729,7 @@ export default {
725
729
  .toString(36)
726
730
  .slice(2, 9)}`,
727
731
  activeAiQuestionKey: '',
732
+ searchRunId: 0,
728
733
 
729
734
  scrollReflowTimer: null,
730
735
  bodyScrollLock: null,
@@ -738,6 +743,21 @@ export default {
738
743
  }),
739
744
 
740
745
  computed: {
746
+ currentPages() {
747
+ if (Array.isArray(this.localSearchPages)) return this.localSearchPages
748
+ return Array.isArray(this.resolvedPages) ? this.resolvedPages : []
749
+ },
750
+
751
+ currentFiles() {
752
+ if (Array.isArray(this.localSearchFiles)) return this.localSearchFiles
753
+ return Array.isArray(this.resolvedFiles) ? this.resolvedFiles : []
754
+ },
755
+
756
+ currentGroups() {
757
+ if (Array.isArray(this.localSearchGroups)) return this.localSearchGroups
758
+ return Array.isArray(this.resolvedGroups) ? this.resolvedGroups : []
759
+ },
760
+
741
761
  shouldUsePageFallbacks() {
742
762
  const hasSearchText =
743
763
  typeof this.keywords === 'string' && this.keywords.trim()
@@ -745,14 +765,14 @@ export default {
745
765
  this.hasSubmitted &&
746
766
  !!hasSearchText &&
747
767
  !this.searchingPages &&
748
- Array.isArray(this.resolvedPages) &&
749
- this.resolvedPages.length === 0
768
+ Array.isArray(this.currentPages) &&
769
+ this.currentPages.length === 0
750
770
  )
751
771
  },
752
772
 
753
773
  displayPages() {
754
- if (Array.isArray(this.resolvedPages) && this.resolvedPages.length) {
755
- return this.resolvedPages
774
+ if (Array.isArray(this.currentPages) && this.currentPages.length) {
775
+ return this.currentPages
756
776
  }
757
777
  if (!this.shouldUsePageFallbacks) return []
758
778
 
@@ -821,8 +841,8 @@ export default {
821
841
  },
822
842
 
823
843
  fileListItems() {
824
- return Array.isArray(this.resolvedFiles)
825
- ? mapFilesToListItems(this.resolvedFiles)
844
+ return Array.isArray(this.currentFiles)
845
+ ? mapFilesToListItems(this.currentFiles)
826
846
  : []
827
847
  },
828
848
 
@@ -843,13 +863,13 @@ export default {
843
863
  if (this.showContacts) {
844
864
  assistantTabs.push({
845
865
  label: 'Contacts',
846
- badge: this.resolvedGroups.length,
866
+ badge: this.currentGroups.length,
847
867
  active: this.currentTab === 'Contacts',
848
868
  })
849
869
  }
850
870
  assistantTabs.push({
851
871
  label: 'Files',
852
- badge: this.resolvedFiles.length,
872
+ badge: this.currentFiles.length,
853
873
  active: this.currentTab === 'Files',
854
874
  })
855
875
  return assistantTabs
@@ -866,13 +886,13 @@ export default {
866
886
  if (this.showContacts) {
867
887
  keywordTabs.push({
868
888
  label: 'Contacts',
869
- badge: this.resolvedGroups.length,
889
+ badge: this.currentGroups.length,
870
890
  active: this.currentTab === 'Contacts',
871
891
  })
872
892
  }
873
893
  keywordTabs.push({
874
894
  label: 'Files',
875
- badge: this.resolvedFiles.length,
895
+ badge: this.currentFiles.length,
876
896
  active: this.currentTab === 'Files',
877
897
  })
878
898
  return keywordTabs
@@ -952,7 +972,7 @@ export default {
952
972
  })
953
973
  },
954
974
  groupListItems() {
955
- return (this.resolvedGroups || []).map(g => {
975
+ return (this.currentGroups || []).map(g => {
956
976
  const org =
957
977
  (Array.isArray(this.organizationOptions) &&
958
978
  this.organizationOptions.find(
@@ -1125,17 +1145,19 @@ export default {
1125
1145
  return
1126
1146
  }
1127
1147
  if (newVal !== oldVal) {
1148
+ const runId = this.beginSearchRun()
1128
1149
  this.selectedGroup = null
1129
- this.searchPages()
1130
- this.searchFilesWithGroupOwners()
1150
+ this.searchPages({ runId })
1151
+ this.searchFilesWithGroupOwners({ runId })
1131
1152
  this.syncSearchStateToRoute()
1132
1153
  }
1133
1154
  }, 400),
1134
1155
 
1135
1156
  resolvedIncludeDistrictResults() {
1136
1157
  if (this.routeRestoreInProgress) return
1137
- this.searchPages()
1138
- this.searchFilesWithGroupOwners()
1158
+ const runId = this.beginSearchRun()
1159
+ this.searchPages({ runId })
1160
+ this.searchFilesWithGroupOwners({ runId })
1139
1161
  this.syncSearchStateToRoute()
1140
1162
  },
1141
1163
  // Clear AI answer if user clears the input
@@ -1244,9 +1266,44 @@ export default {
1244
1266
  },
1245
1267
 
1246
1268
  setSearchResults({ pages, files, groups } = {}) {
1247
- if (pages !== undefined) this.resolvedPages = pages
1248
- if (files !== undefined) this.resolvedFiles = files
1249
- if (groups !== undefined) this.resolvedGroups = groups
1269
+ if (pages !== undefined) {
1270
+ this.localSearchPages = Array.isArray(pages) ? pages : []
1271
+ }
1272
+ if (files !== undefined) {
1273
+ this.localSearchFiles = Array.isArray(files) ? files : []
1274
+ }
1275
+ if (groups !== undefined) {
1276
+ this.localSearchGroups = Array.isArray(groups) ? groups : []
1277
+ }
1278
+ },
1279
+
1280
+ readSearchResults(target) {
1281
+ if (target === 'pages') return this.currentPages
1282
+ if (target === 'files') return this.currentFiles
1283
+ if (target === 'groups') return this.currentGroups
1284
+ return []
1285
+ },
1286
+
1287
+ readResolvedSearchResults(target) {
1288
+ if (target === 'pages') {
1289
+ return Array.isArray(this.resolvedPages) ? this.resolvedPages : []
1290
+ }
1291
+ if (target === 'files') {
1292
+ return Array.isArray(this.resolvedFiles) ? this.resolvedFiles : []
1293
+ }
1294
+ if (target === 'groups') {
1295
+ return Array.isArray(this.resolvedGroups) ? this.resolvedGroups : []
1296
+ }
1297
+ return []
1298
+ },
1299
+
1300
+ beginSearchRun() {
1301
+ this.searchRunId += 1
1302
+ return this.searchRunId
1303
+ },
1304
+
1305
+ isCurrentSearchRun(runId) {
1306
+ return !runId || this.searchRunId === runId
1250
1307
  },
1251
1308
 
1252
1309
  async querySearchCollectionWithFallback({
@@ -1254,23 +1311,32 @@ export default {
1254
1311
  storeAction,
1255
1312
  payload,
1256
1313
  target,
1314
+ runId,
1257
1315
  }) {
1258
1316
  if (typeof handler === 'function') {
1259
1317
  const result = await handler(payload)
1318
+ if (!this.isCurrentSearchRun(runId)) {
1319
+ return this.readSearchResults(target)
1320
+ }
1260
1321
  if (Array.isArray(result)) {
1261
- this[target] = result
1322
+ this.setSearchResults({ [target]: result })
1262
1323
  return result
1263
1324
  }
1264
- return Array.isArray(this[target]) ? this[target] : []
1325
+ return this.readSearchResults(target)
1265
1326
  }
1266
1327
 
1267
1328
  const searchStore = this.getSearchStore()
1268
1329
  if (searchStore) {
1269
1330
  await searchStore.dispatch(storeAction, payload)
1270
- return Array.isArray(this[target]) ? this[target] : []
1331
+ const result = this.readResolvedSearchResults(target)
1332
+ if (!this.isCurrentSearchRun(runId)) {
1333
+ return this.readSearchResults(target)
1334
+ }
1335
+ this.setSearchResults({ [target]: result })
1336
+ return result
1271
1337
  }
1272
1338
 
1273
- return Array.isArray(this[target]) ? this[target] : []
1339
+ return this.readSearchResults(target)
1274
1340
  },
1275
1341
 
1276
1342
  async queryAiSiteWithFallback(payload) {
@@ -1511,9 +1577,9 @@ export default {
1511
1577
  : 'Pages',
1512
1578
  searchMode: this.searchMode || '',
1513
1579
  includeDistrictResults: !!this.resolvedIncludeDistrictResults,
1514
- pages: Array.isArray(this.resolvedPages) ? this.resolvedPages : [],
1515
- files: Array.isArray(this.resolvedFiles) ? this.resolvedFiles : [],
1516
- groups: Array.isArray(this.resolvedGroups) ? this.resolvedGroups : [],
1580
+ pages: Array.isArray(this.currentPages) ? this.currentPages : [],
1581
+ files: Array.isArray(this.currentFiles) ? this.currentFiles : [],
1582
+ groups: Array.isArray(this.currentGroups) ? this.currentGroups : [],
1517
1583
  aiAnswer: this.aiAnswer || null,
1518
1584
  aiSources: Array.isArray(this.aiSources) ? this.aiSources : [],
1519
1585
  aiErrorMessage: this.aiErrorMessage || '',
@@ -1791,7 +1857,34 @@ export default {
1791
1857
 
1792
1858
  appendSearchContextToUrl(rawUrl) {
1793
1859
  if (!rawUrl || typeof rawUrl !== 'string') return rawUrl
1794
- return rawUrl
1860
+ const state = this.getCurrentSearchStateForRoute()
1861
+ if (!state) return rawUrl
1862
+ if (typeof window === 'undefined' || typeof URL === 'undefined') {
1863
+ return rawUrl
1864
+ }
1865
+
1866
+ const trimmedUrl = rawUrl.trim()
1867
+ if (!trimmedUrl) return rawUrl
1868
+ if (/^(#|mailto:|tel:|javascript:)/i.test(trimmedUrl)) return rawUrl
1869
+
1870
+ try {
1871
+ const resolvedUrl = new URL(trimmedUrl, window.location.href)
1872
+ if (resolvedUrl.origin !== window.location.origin) return rawUrl
1873
+
1874
+ const stateQueryValues = this.buildRouteSearchQueryValues(state)
1875
+ Object.entries(stateQueryValues).forEach(([key, value]) => {
1876
+ if (value === undefined || value === null || value === '') {
1877
+ resolvedUrl.searchParams.delete(key)
1878
+ return
1879
+ }
1880
+
1881
+ resolvedUrl.searchParams.set(key, value)
1882
+ })
1883
+
1884
+ return `${resolvedUrl.pathname}${resolvedUrl.search}${resolvedUrl.hash}`
1885
+ } catch (error) {
1886
+ return rawUrl
1887
+ }
1795
1888
  },
1796
1889
 
1797
1890
  async restoreSearchStateFromRoute() {
@@ -1870,18 +1963,21 @@ export default {
1870
1963
  await this.searchAi(restoredState.text)
1871
1964
  } else {
1872
1965
  await this.clearSearchState()
1966
+ const runId = this.searchRunId
1873
1967
  this.keywordsFromAi = false
1874
1968
  this.skipKeywordWatcher = true
1875
1969
  this.keywords = this.limitWords(restoredState.text, 4)
1876
1970
  this.searchQuestion = restoredState.text
1877
1971
 
1878
1972
  await Promise.all([
1879
- this.searchPages({ keepAnswersTab: false }),
1880
- this.searchFilesWithGroupOwners(),
1973
+ this.searchPages({ keepAnswersTab: false, runId }),
1974
+ this.searchFilesWithGroupOwners({ runId }),
1881
1975
  ])
1882
1976
  }
1883
1977
  }
1884
1978
 
1979
+ if (this.lastRestoredSearchKey !== restoreKey) return
1980
+
1885
1981
  const hasManualTabOverride =
1886
1982
  this.hasRecentManualTabSelection() ||
1887
1983
  (this.lastManualTabChangeAt > restoreStartedAt &&
@@ -2372,7 +2468,18 @@ export default {
2372
2468
  async clearSearchState(options = {}) {
2373
2469
  const resetConversation = options.resetConversation !== false
2374
2470
  const resetTranscript = options.resetTranscript !== false
2471
+ const preserveSearchRun = options.preserveSearchRun === true
2472
+ const preserveAiLoading = options.preserveAiLoading === true
2473
+ if (!preserveSearchRun) {
2474
+ this.beginSearchRun()
2475
+ }
2375
2476
  this.selectedGroup = null
2477
+ this.searchingAi = preserveAiLoading ? this.searchingAi : false
2478
+ this.searchingPages = false
2479
+ this.searchingFiles = false
2480
+ this.activeAiQuestionKey = preserveAiLoading
2481
+ ? this.activeAiQuestionKey
2482
+ : ''
2376
2483
  this.aiAnswer = null
2377
2484
  this.aiSources = []
2378
2485
  this.aiKeywordsDisplayAnswer = ''
@@ -2422,38 +2529,50 @@ export default {
2422
2529
 
2423
2530
  async searchPages(options = {}) {
2424
2531
  const keepAnswersTab = options.keepAnswersTab || this.keywordsFromAi
2532
+ const runId = options.runId || this.searchRunId
2425
2533
  const searchInput = {
2426
2534
  keywords: this.keywords,
2427
2535
  question: this.searchQuestion || this.inputText,
2428
2536
  }
2429
2537
 
2430
2538
  this.searchingPages = true
2431
- await this.querySearchCollectionWithFallback({
2432
- handler: this.queryPagesHandler,
2433
- storeAction: 'search/queryPages',
2434
- payload: searchInput,
2435
- target: 'resolvedPages',
2436
- })
2437
- this.searchingPages = false
2539
+ try {
2540
+ await this.querySearchCollectionWithFallback({
2541
+ handler: this.queryPagesHandler,
2542
+ storeAction: 'search/queryPages',
2543
+ payload: searchInput,
2544
+ target: 'pages',
2545
+ runId,
2546
+ })
2547
+ if (!this.isCurrentSearchRun(runId)) {
2548
+ return this.currentPages
2549
+ }
2438
2550
 
2439
- if (this.keywords && this.keywords.trim() && !keepAnswersTab) {
2440
- this.currentTab = 'Pages'
2441
- this.scrollResultsToTop()
2442
- }
2551
+ if (this.keywords && this.keywords.trim() && !keepAnswersTab) {
2552
+ this.currentTab = 'Pages'
2553
+ this.scrollResultsToTop()
2554
+ }
2443
2555
 
2444
- this.keywordsFromAi = false
2556
+ this.keywordsFromAi = false
2445
2557
 
2446
- if (!keepAnswersTab) {
2447
- this.aiAnswer = null
2448
- this.aiSources = []
2449
- this.aiKeywordsDisplayAnswer = ''
2450
- this.aiKeywordsDisplayPages = ''
2451
- }
2558
+ if (!keepAnswersTab) {
2559
+ this.aiAnswer = null
2560
+ this.aiSources = []
2561
+ this.aiKeywordsDisplayAnswer = ''
2562
+ this.aiKeywordsDisplayPages = ''
2563
+ }
2452
2564
 
2453
- this.persistSearchCacheSnapshot()
2565
+ this.persistSearchCacheSnapshot()
2566
+ return this.currentPages
2567
+ } finally {
2568
+ if (this.isCurrentSearchRun(runId)) {
2569
+ this.searchingPages = false
2570
+ }
2571
+ }
2454
2572
  },
2455
2573
 
2456
2574
  async searchFiles(options = {}) {
2575
+ const runId = options.runId || this.searchRunId
2457
2576
  const searchInput = {
2458
2577
  keywords: this.keywords,
2459
2578
  question: this.searchQuestion || this.inputText,
@@ -2462,26 +2581,41 @@ export default {
2462
2581
  if (owners.length > 0) searchInput.owners = owners
2463
2582
 
2464
2583
  this.searchingFiles = true
2465
- await this.querySearchCollectionWithFallback({
2466
- handler: this.queryFilesHandler,
2467
- storeAction: 'search/queryFiles',
2468
- payload: searchInput,
2469
- target: 'resolvedFiles',
2470
- })
2471
- this.searchingFiles = false
2472
- this.persistSearchCacheSnapshot()
2584
+ try {
2585
+ await this.querySearchCollectionWithFallback({
2586
+ handler: this.queryFilesHandler,
2587
+ storeAction: 'search/queryFiles',
2588
+ payload: searchInput,
2589
+ target: 'files',
2590
+ runId,
2591
+ })
2592
+ if (!this.isCurrentSearchRun(runId)) {
2593
+ return this.currentFiles
2594
+ }
2595
+ this.persistSearchCacheSnapshot()
2596
+ return this.currentFiles
2597
+ } finally {
2598
+ if (this.isCurrentSearchRun(runId)) {
2599
+ this.searchingFiles = false
2600
+ }
2601
+ }
2473
2602
  },
2474
2603
 
2475
- async searchFilesWithGroupOwners() {
2476
- const groupsPromise = this.searchGroups()
2477
- await this.searchFiles()
2604
+ async searchFilesWithGroupOwners(options = {}) {
2605
+ const runId = options.runId || this.searchRunId
2606
+ const groupsPromise = this.searchGroups({ runId })
2607
+ await this.searchFiles({ runId })
2608
+ if (!this.isCurrentSearchRun(runId)) return []
2478
2609
  const ownerFilters = await groupsPromise
2610
+ if (!this.isCurrentSearchRun(runId)) return ownerFilters
2479
2611
  if (ownerFilters.length > 0) {
2480
- await this.searchFiles({ owners: ownerFilters })
2612
+ await this.searchFiles({ owners: ownerFilters, runId })
2481
2613
  }
2614
+ return ownerFilters
2482
2615
  },
2483
2616
 
2484
- async searchGroups() {
2617
+ async searchGroups(options = {}) {
2618
+ const runId = options.runId || this.searchRunId
2485
2619
  const searchInput = {
2486
2620
  keywords: this.keywords,
2487
2621
  question: this.searchQuestion || this.inputText,
@@ -2490,12 +2624,22 @@ export default {
2490
2624
  handler: this.queryGroupsHandler,
2491
2625
  storeAction: 'search/queryGroups',
2492
2626
  payload: searchInput,
2493
- target: 'resolvedGroups',
2627
+ target: 'groups',
2628
+ runId,
2494
2629
  })
2630
+ if (!this.isCurrentSearchRun(runId)) {
2631
+ return Array.from(
2632
+ new Set(
2633
+ this.currentGroups
2634
+ .map(group => (group && group.title ? group.title.trim() : ''))
2635
+ .filter(Boolean)
2636
+ )
2637
+ )
2638
+ }
2495
2639
  this.persistSearchCacheSnapshot()
2496
2640
  return Array.from(
2497
2641
  new Set(
2498
- (this.resolvedGroups || [])
2642
+ (this.currentGroups || [])
2499
2643
  .map(group => (group && group.title ? group.title.trim() : ''))
2500
2644
  .filter(Boolean)
2501
2645
  )
@@ -2530,11 +2674,13 @@ export default {
2530
2674
  this.searchAi(text)
2531
2675
  } else {
2532
2676
  await this.clearSearchState()
2677
+ const runId = this.searchRunId
2533
2678
  this.keywordsFromAi = false
2534
2679
  this.skipKeywordWatcher = true
2535
2680
  this.keywords = this.limitWords(text, 4)
2536
- await this.searchPages({ keepAnswersTab: false })
2537
- this.searchFilesWithGroupOwners()
2681
+ await this.searchPages({ keepAnswersTab: false, runId })
2682
+ this.searchFilesWithGroupOwners({ runId })
2683
+ if (!this.isCurrentSearchRun(runId)) return
2538
2684
  this.currentTab = 'Pages'
2539
2685
  this.syncSearchStateToRoute({ text, mode, tab: 'Pages' })
2540
2686
  this.$nextTick(() => this.refreshResultsScrollLayer())
@@ -2621,6 +2767,7 @@ export default {
2621
2767
 
2622
2768
  this.activeAiQuestionKey = normalizedQuestion
2623
2769
 
2770
+ const runId = this.beginSearchRun()
2624
2771
  this.searchMode = 'question'
2625
2772
  const isFollowUpQuestion = this.hasAssistantTurns
2626
2773
  this.currentTab = 'Answers'
@@ -2642,6 +2789,8 @@ export default {
2642
2789
  await this.clearSearchState({
2643
2790
  resetConversation: false,
2644
2791
  resetTranscript: false,
2792
+ preserveSearchRun: true,
2793
+ preserveAiLoading: true,
2645
2794
  })
2646
2795
  this.searchQuestion = normalizedQuestion
2647
2796
  let assistantTurnId = ''
@@ -2668,6 +2817,7 @@ export default {
2668
2817
  includeDistrictResults: this.resolvedIncludeDistrictResults,
2669
2818
  }
2670
2819
  )
2820
+ if (!this.isCurrentSearchRun(runId)) return
2671
2821
 
2672
2822
  const { cleanAnswer, keywords: aiKeywords } =
2673
2823
  this.extractKeywordsAndCleanAnswer(answer || '')
@@ -2732,11 +2882,12 @@ export default {
2732
2882
  this.keywordsFromAi = true
2733
2883
  this.skipKeywordWatcher = true
2734
2884
  this.keywords = keywordsToUse
2735
- await this.searchPages({ keepAnswersTab: true })
2885
+ await this.searchPages({ keepAnswersTab: true, runId })
2886
+ if (!this.isCurrentSearchRun(runId)) return
2736
2887
  relatedPagesForAnalytics = this.getAnalyticsPages()
2737
2888
  .slice(0, 10)
2738
2889
  .map(p => ({ title: p.title || '', url: p.url || '' }))
2739
- this.searchFilesWithGroupOwners()
2890
+ this.searchFilesWithGroupOwners({ runId })
2740
2891
  }
2741
2892
 
2742
2893
  this.currentTab = 'Answers'
@@ -2747,6 +2898,7 @@ export default {
2747
2898
  this.refreshResultsScrollLayer()
2748
2899
  })
2749
2900
  } catch (e) {
2901
+ if (!this.isCurrentSearchRun(runId)) return
2750
2902
  console.error('AI search failed', e)
2751
2903
  this.aiAnswer = null
2752
2904
  this.aiSources = []
@@ -2769,13 +2921,15 @@ export default {
2769
2921
  this.scrollTurnToTop(this.anchoredUserTurnId, { defer: true })
2770
2922
  }
2771
2923
  } finally {
2772
- this.searchingAi = false
2773
- this.activeAiQuestionKey = ''
2774
- this.persistSearchCacheSnapshot({
2775
- text: this.searchQuestion || normalizedQuestion,
2776
- mode: 'question',
2777
- tab: 'Answers',
2778
- })
2924
+ if (this.isCurrentSearchRun(runId)) {
2925
+ this.searchingAi = false
2926
+ this.activeAiQuestionKey = ''
2927
+ this.persistSearchCacheSnapshot({
2928
+ text: this.searchQuestion || normalizedQuestion,
2929
+ mode: 'question',
2930
+ tab: 'Answers',
2931
+ })
2932
+ }
2779
2933
 
2780
2934
  // Log AI search analytics
2781
2935
  const logPagesFromPages =
@@ -3178,8 +3332,8 @@ export default {
3178
3332
  },
3179
3333
 
3180
3334
  getAnalyticsPages() {
3181
- if (!Array.isArray(this.resolvedPages)) return []
3182
- return this.resolvedPages.filter(
3335
+ if (!Array.isArray(this.currentPages)) return []
3336
+ return this.currentPages.filter(
3183
3337
  page => !this.isNoResultsFallbackPage(page)
3184
3338
  )
3185
3339
  },