@asd20/ui-next 2.3.8 → 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,17 @@
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
+
3
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)
4
16
 
5
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.3.8",
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 || '',
@@ -1897,18 +1963,21 @@ export default {
1897
1963
  await this.searchAi(restoredState.text)
1898
1964
  } else {
1899
1965
  await this.clearSearchState()
1966
+ const runId = this.searchRunId
1900
1967
  this.keywordsFromAi = false
1901
1968
  this.skipKeywordWatcher = true
1902
1969
  this.keywords = this.limitWords(restoredState.text, 4)
1903
1970
  this.searchQuestion = restoredState.text
1904
1971
 
1905
1972
  await Promise.all([
1906
- this.searchPages({ keepAnswersTab: false }),
1907
- this.searchFilesWithGroupOwners(),
1973
+ this.searchPages({ keepAnswersTab: false, runId }),
1974
+ this.searchFilesWithGroupOwners({ runId }),
1908
1975
  ])
1909
1976
  }
1910
1977
  }
1911
1978
 
1979
+ if (this.lastRestoredSearchKey !== restoreKey) return
1980
+
1912
1981
  const hasManualTabOverride =
1913
1982
  this.hasRecentManualTabSelection() ||
1914
1983
  (this.lastManualTabChangeAt > restoreStartedAt &&
@@ -2399,7 +2468,18 @@ export default {
2399
2468
  async clearSearchState(options = {}) {
2400
2469
  const resetConversation = options.resetConversation !== false
2401
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
+ }
2402
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
+ : ''
2403
2483
  this.aiAnswer = null
2404
2484
  this.aiSources = []
2405
2485
  this.aiKeywordsDisplayAnswer = ''
@@ -2449,38 +2529,50 @@ export default {
2449
2529
 
2450
2530
  async searchPages(options = {}) {
2451
2531
  const keepAnswersTab = options.keepAnswersTab || this.keywordsFromAi
2532
+ const runId = options.runId || this.searchRunId
2452
2533
  const searchInput = {
2453
2534
  keywords: this.keywords,
2454
2535
  question: this.searchQuestion || this.inputText,
2455
2536
  }
2456
2537
 
2457
2538
  this.searchingPages = true
2458
- await this.querySearchCollectionWithFallback({
2459
- handler: this.queryPagesHandler,
2460
- storeAction: 'search/queryPages',
2461
- payload: searchInput,
2462
- target: 'resolvedPages',
2463
- })
2464
- 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
+ }
2465
2550
 
2466
- if (this.keywords && this.keywords.trim() && !keepAnswersTab) {
2467
- this.currentTab = 'Pages'
2468
- this.scrollResultsToTop()
2469
- }
2551
+ if (this.keywords && this.keywords.trim() && !keepAnswersTab) {
2552
+ this.currentTab = 'Pages'
2553
+ this.scrollResultsToTop()
2554
+ }
2470
2555
 
2471
- this.keywordsFromAi = false
2556
+ this.keywordsFromAi = false
2472
2557
 
2473
- if (!keepAnswersTab) {
2474
- this.aiAnswer = null
2475
- this.aiSources = []
2476
- this.aiKeywordsDisplayAnswer = ''
2477
- this.aiKeywordsDisplayPages = ''
2478
- }
2558
+ if (!keepAnswersTab) {
2559
+ this.aiAnswer = null
2560
+ this.aiSources = []
2561
+ this.aiKeywordsDisplayAnswer = ''
2562
+ this.aiKeywordsDisplayPages = ''
2563
+ }
2479
2564
 
2480
- this.persistSearchCacheSnapshot()
2565
+ this.persistSearchCacheSnapshot()
2566
+ return this.currentPages
2567
+ } finally {
2568
+ if (this.isCurrentSearchRun(runId)) {
2569
+ this.searchingPages = false
2570
+ }
2571
+ }
2481
2572
  },
2482
2573
 
2483
2574
  async searchFiles(options = {}) {
2575
+ const runId = options.runId || this.searchRunId
2484
2576
  const searchInput = {
2485
2577
  keywords: this.keywords,
2486
2578
  question: this.searchQuestion || this.inputText,
@@ -2489,26 +2581,41 @@ export default {
2489
2581
  if (owners.length > 0) searchInput.owners = owners
2490
2582
 
2491
2583
  this.searchingFiles = true
2492
- await this.querySearchCollectionWithFallback({
2493
- handler: this.queryFilesHandler,
2494
- storeAction: 'search/queryFiles',
2495
- payload: searchInput,
2496
- target: 'resolvedFiles',
2497
- })
2498
- this.searchingFiles = false
2499
- 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
+ }
2500
2602
  },
2501
2603
 
2502
- async searchFilesWithGroupOwners() {
2503
- const groupsPromise = this.searchGroups()
2504
- 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 []
2505
2609
  const ownerFilters = await groupsPromise
2610
+ if (!this.isCurrentSearchRun(runId)) return ownerFilters
2506
2611
  if (ownerFilters.length > 0) {
2507
- await this.searchFiles({ owners: ownerFilters })
2612
+ await this.searchFiles({ owners: ownerFilters, runId })
2508
2613
  }
2614
+ return ownerFilters
2509
2615
  },
2510
2616
 
2511
- async searchGroups() {
2617
+ async searchGroups(options = {}) {
2618
+ const runId = options.runId || this.searchRunId
2512
2619
  const searchInput = {
2513
2620
  keywords: this.keywords,
2514
2621
  question: this.searchQuestion || this.inputText,
@@ -2517,12 +2624,22 @@ export default {
2517
2624
  handler: this.queryGroupsHandler,
2518
2625
  storeAction: 'search/queryGroups',
2519
2626
  payload: searchInput,
2520
- target: 'resolvedGroups',
2627
+ target: 'groups',
2628
+ runId,
2521
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
+ }
2522
2639
  this.persistSearchCacheSnapshot()
2523
2640
  return Array.from(
2524
2641
  new Set(
2525
- (this.resolvedGroups || [])
2642
+ (this.currentGroups || [])
2526
2643
  .map(group => (group && group.title ? group.title.trim() : ''))
2527
2644
  .filter(Boolean)
2528
2645
  )
@@ -2557,11 +2674,13 @@ export default {
2557
2674
  this.searchAi(text)
2558
2675
  } else {
2559
2676
  await this.clearSearchState()
2677
+ const runId = this.searchRunId
2560
2678
  this.keywordsFromAi = false
2561
2679
  this.skipKeywordWatcher = true
2562
2680
  this.keywords = this.limitWords(text, 4)
2563
- await this.searchPages({ keepAnswersTab: false })
2564
- this.searchFilesWithGroupOwners()
2681
+ await this.searchPages({ keepAnswersTab: false, runId })
2682
+ this.searchFilesWithGroupOwners({ runId })
2683
+ if (!this.isCurrentSearchRun(runId)) return
2565
2684
  this.currentTab = 'Pages'
2566
2685
  this.syncSearchStateToRoute({ text, mode, tab: 'Pages' })
2567
2686
  this.$nextTick(() => this.refreshResultsScrollLayer())
@@ -2648,6 +2767,7 @@ export default {
2648
2767
 
2649
2768
  this.activeAiQuestionKey = normalizedQuestion
2650
2769
 
2770
+ const runId = this.beginSearchRun()
2651
2771
  this.searchMode = 'question'
2652
2772
  const isFollowUpQuestion = this.hasAssistantTurns
2653
2773
  this.currentTab = 'Answers'
@@ -2669,6 +2789,8 @@ export default {
2669
2789
  await this.clearSearchState({
2670
2790
  resetConversation: false,
2671
2791
  resetTranscript: false,
2792
+ preserveSearchRun: true,
2793
+ preserveAiLoading: true,
2672
2794
  })
2673
2795
  this.searchQuestion = normalizedQuestion
2674
2796
  let assistantTurnId = ''
@@ -2695,6 +2817,7 @@ export default {
2695
2817
  includeDistrictResults: this.resolvedIncludeDistrictResults,
2696
2818
  }
2697
2819
  )
2820
+ if (!this.isCurrentSearchRun(runId)) return
2698
2821
 
2699
2822
  const { cleanAnswer, keywords: aiKeywords } =
2700
2823
  this.extractKeywordsAndCleanAnswer(answer || '')
@@ -2759,11 +2882,12 @@ export default {
2759
2882
  this.keywordsFromAi = true
2760
2883
  this.skipKeywordWatcher = true
2761
2884
  this.keywords = keywordsToUse
2762
- await this.searchPages({ keepAnswersTab: true })
2885
+ await this.searchPages({ keepAnswersTab: true, runId })
2886
+ if (!this.isCurrentSearchRun(runId)) return
2763
2887
  relatedPagesForAnalytics = this.getAnalyticsPages()
2764
2888
  .slice(0, 10)
2765
2889
  .map(p => ({ title: p.title || '', url: p.url || '' }))
2766
- this.searchFilesWithGroupOwners()
2890
+ this.searchFilesWithGroupOwners({ runId })
2767
2891
  }
2768
2892
 
2769
2893
  this.currentTab = 'Answers'
@@ -2774,6 +2898,7 @@ export default {
2774
2898
  this.refreshResultsScrollLayer()
2775
2899
  })
2776
2900
  } catch (e) {
2901
+ if (!this.isCurrentSearchRun(runId)) return
2777
2902
  console.error('AI search failed', e)
2778
2903
  this.aiAnswer = null
2779
2904
  this.aiSources = []
@@ -2796,13 +2921,15 @@ export default {
2796
2921
  this.scrollTurnToTop(this.anchoredUserTurnId, { defer: true })
2797
2922
  }
2798
2923
  } finally {
2799
- this.searchingAi = false
2800
- this.activeAiQuestionKey = ''
2801
- this.persistSearchCacheSnapshot({
2802
- text: this.searchQuestion || normalizedQuestion,
2803
- mode: 'question',
2804
- tab: 'Answers',
2805
- })
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
+ }
2806
2933
 
2807
2934
  // Log AI search analytics
2808
2935
  const logPagesFromPages =
@@ -3205,8 +3332,8 @@ export default {
3205
3332
  },
3206
3333
 
3207
3334
  getAnalyticsPages() {
3208
- if (!Array.isArray(this.resolvedPages)) return []
3209
- return this.resolvedPages.filter(
3335
+ if (!Array.isArray(this.currentPages)) return []
3336
+ return this.currentPages.filter(
3210
3337
  page => !this.isNoResultsFallbackPage(page)
3211
3338
  )
3212
3339
  },