@asd20/ui 3.10.0 → 3.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui",
3
- "version": "3.10.0",
3
+ "version": "3.11.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "sideEffects": [
@@ -1,4 +1,4 @@
1
- <template>
1
+ \<template>
2
2
  <div
3
3
  ref="container"
4
4
  class="asd20-site-search"
@@ -8,47 +8,27 @@
8
8
  >
9
9
  <div class="asd20-site-search__viewport">
10
10
  <div class="asd20-site-search__options">
11
- <!-- Ask a Question -->
12
- <div
13
- class="asd20-site-search__field asd20-site-search__field--question"
14
- >
15
- <!-- <label class="asd20-site-search__label">Ask a question</label> -->
11
+ <!-- Unified Query Input -->
12
+ <div class="asd20-site-search__field asd20-site-search__field--question">
16
13
  <div class="asd20-site-search__question-row">
17
14
  <asd20-search-field
18
- idTag="-question"
19
- ref="question"
20
- v-model="questionText"
21
- @keyup.enter.native.stop.prevent="onAskQuestion"
22
- placeholder="Ask a question (preview)"
15
+ idTag="-unified"
16
+ ref="query"
17
+ v-model="inputText"
18
+ @keyup.enter.native.stop.prevent="onSubmitInput"
19
+ placeholder="Question (preview) or keywords..."
23
20
  />
24
21
  <asd20-button
25
22
  class="asd20-site-search__ask-button"
26
- label="Ask"
23
+ label="Go"
27
24
  size="md"
28
25
  bordered
29
- @click.native="onAskQuestion"
30
- :disabled="!questionText || searchingAi"
31
- />
32
- <asd20-loader
33
- v-if="searchingAi"
34
- size="sm"
35
- class="asd20-site-search__inline-loader"
26
+ reversed
27
+ @click.native="onSubmitInput"
28
+ :disabled="!inputText || searchingAi"
36
29
  />
37
30
  </div>
38
31
  </div>
39
- <hr />
40
-
41
- <!-- Search by Keyword -->
42
- <div class="asd20-site-search__field asd20-site-search__field--keyword">
43
- <!-- <label class="asd20-site-search__label">Search by keyword</label> -->
44
- <asd20-search-field
45
- idTag="-sitewide"
46
- ref="search"
47
- v-model="keywords"
48
- placeholder="Search by keyword"
49
- />
50
- </div>
51
-
52
32
  <!-- Only show school results -->
53
33
  <asd20-checkbox
54
34
  v-show="hasParentOrg"
@@ -61,17 +41,22 @@
61
41
 
62
42
  <asd20-tab-bar :tabs="tabs" @tabClick="onTabClick" />
63
43
 
64
- <asd20-viewport class="asd20-site-search__results" scrollable>
44
+ <asd20-viewport
45
+ ref="results"
46
+ class="asd20-site-search__results"
47
+ scrollable
48
+ >
65
49
  <asd20-notification
66
50
  v-if="
67
51
  !keywords &&
68
- !questionText &&
52
+ !inputText &&
69
53
  !searchingFiles &&
70
54
  !searchingPages &&
71
55
  !searchingAi
72
56
  "
73
- title="Ask a Question, or Search by Keyword"
74
- description="Use the top input for plain-language questions, then press 'Ask'; or search by keyword in the lower input box."
57
+ title="Ask a Question or Search by Keyword"
58
+ description="Asking a question will generate an AI answer. Search by for pages and files by entering keyword(s).
59
+ <hr/>Our AI functionality is currently in preview and may not always provide accurate answers."
75
60
  />
76
61
 
77
62
  <!-- Answers tab -->
@@ -94,14 +79,11 @@
94
79
  {{ aiKeywordsDisplay }}
95
80
  </div>
96
81
 
97
- <div
98
- v-if="aiGroupedSources.length"
99
- class="asd20-site-search__ai-sources"
100
- >
101
- <h4>Sources</h4>
102
- <ul class="asd20-site-search__ai-source-groups">
103
- <li v-for="group in aiGroupedSources" :key="group.hostLabel">
104
- <strong>Content from: {{ group.hostLabel }}</strong>
82
+ <!--
83
+ <div v-if="aiGroupedSources.length" class="asd20-site-search__ai-sources">
84
+ <h3>Sources</h3>
85
+ <div class="asd20-site-search__ai-source-groups">
86
+ <div v-for="group in aiGroupedSources" :key="group.hostLabel">
105
87
  <ul class="asd20-site-search__ai-source-list">
106
88
  <li v-for="src in group.sources" :key="src.url || src.id">
107
89
  <a :href="src.url" target="_blank" rel="noreferrer">
@@ -109,9 +91,10 @@
109
91
  </a>
110
92
  </li>
111
93
  </ul>
112
- </li>
113
- </ul>
94
+ </div>
95
+ </div>
114
96
  </div>
97
+ -->
115
98
 
116
99
  <asd20-loader v-if="searchingAi" size="lg" />
117
100
  </div>
@@ -140,6 +123,13 @@
140
123
  />
141
124
  </asd20-list>
142
125
  <asd20-loader v-if="searchingPages" size="lg" />
126
+
127
+ <div
128
+ v-if="aiKeywordsDisplay"
129
+ class="asd20-site-search__ai-keywords"
130
+ >
131
+ {{ aiKeywordsDisplay }}
132
+ </div>
143
133
  </div>
144
134
 
145
135
  <!-- Files tab -->
@@ -210,6 +200,14 @@ import mapFilesToListItems from '../../../helpers/mapFilesToListItems'
210
200
  // Mixins
211
201
  import globalPropMixinFactory from '../../../mixins/globalPropMixinFactory.js'
212
202
 
203
+ const SUBDOMAIN_LABELS = {
204
+ rampart: 'Rampart High School',
205
+ pinecreek: 'Pine Creek High School',
206
+ dcchigh: 'Discovery Canyon Campus High School',
207
+ liberty: 'Liberty High School',
208
+ d20online: 'Academy Online High School',
209
+ }
210
+
213
211
  export default {
214
212
  name: 'Asd20SiteSearch',
215
213
 
@@ -275,10 +273,9 @@ export default {
275
273
 
276
274
  data: () => ({
277
275
  currentTab: 'Answers',
278
- // keyword search (classic)
276
+ // unified input
277
+ inputText: '',
279
278
  keywords: '',
280
- // question input (AI)
281
- questionText: '',
282
279
  aiAnswer: null,
283
280
  searchingAi: false,
284
281
  aiSources: [],
@@ -287,6 +284,7 @@ export default {
287
284
  searchingFiles: false,
288
285
  keywordsFromAi: false,
289
286
  aiKeywordsDisplay: '',
287
+ skipKeywordWatcher: false,
290
288
  }),
291
289
 
292
290
  computed: {
@@ -357,6 +355,7 @@ export default {
357
355
  aiUiSources() {
358
356
  return (this.aiSources || []).map(src => ({
359
357
  ...src,
358
+ title: this.decodeHtml(src.title),
360
359
  hostLabel:
361
360
  this.resolveOrgTitleFromUrl(src.url) || this.labelFromUrl(src.url),
362
361
  }))
@@ -399,35 +398,40 @@ export default {
399
398
  active(newVal) {
400
399
  if (newVal) {
401
400
  // Focus keyword search by default for power users
402
- if (this.$refs.search && this.$refs.search.$el) {
403
- const input = this.$refs.search.$el.querySelector('input')
401
+ if (this.$refs.query && this.$refs.query.$el) {
402
+ const input = this.$refs.query.$el.querySelector('input')
404
403
  if (input) input.focus()
405
404
  }
406
- } else if (this.$refs.search && this.$refs.search.$el) {
407
- const input = this.$refs.search.$el.querySelector('input')
405
+ } else if (this.$refs.query && this.$refs.query.$el) {
406
+ const input = this.$refs.query.$el.querySelector('input')
408
407
  if (input) input.blur()
409
408
  }
410
409
  },
411
410
 
412
- // classic keyword search unchanged behavior
411
+ // keyword updates trigger searches unless explicitly skipped
413
412
  keywords: debounce(function (newVal, oldVal) {
413
+ if (this.skipKeywordWatcher) {
414
+ this.skipKeywordWatcher = false
415
+ return
416
+ }
414
417
  if (newVal !== oldVal) {
415
418
  this.searchPages()
416
419
  this.searchFiles()
417
420
  this.searchGroups()
418
421
  }
419
- }, 500),
422
+ }, 400),
420
423
 
421
424
  _includeDistrictResults() {
422
425
  this.searchPages()
423
426
  this.searchFiles()
424
427
  },
425
428
 
426
- // Clear AI answer if user clears the question
427
- questionText(newVal) {
429
+ // Clear AI answer if user clears the input
430
+ inputText(newVal) {
428
431
  if (!newVal) {
429
432
  this.aiAnswer = null
430
433
  this.aiSources = []
434
+ this.aiKeywordsDisplay = ''
431
435
  }
432
436
  },
433
437
  },
@@ -441,8 +445,14 @@ export default {
441
445
  this.searchingPages = false
442
446
  if (this.keywords && this.keywords.trim() && !keepAnswersTab) {
443
447
  this.currentTab = 'Pages'
448
+ this.scrollResultsToTop()
444
449
  }
445
450
  this.keywordsFromAi = false
451
+ if (!keepAnswersTab) {
452
+ this.aiAnswer = null
453
+ this.aiSources = []
454
+ this.aiKeywordsDisplay = ''
455
+ }
446
456
  }
447
457
  },
448
458
 
@@ -462,6 +472,9 @@ export default {
462
472
 
463
473
  onTabClick(tab) {
464
474
  this.currentTab = tab.label
475
+ if (tab.label === 'Pages') {
476
+ this.scrollResultsToTop()
477
+ }
465
478
  },
466
479
 
467
480
  onTab(event) {
@@ -469,6 +482,16 @@ export default {
469
482
  this.dismiss()
470
483
  },
471
484
 
485
+ scrollResultsToTop() {
486
+ this.$nextTick(() => {
487
+ const ref = this.$refs.results
488
+ const el = ref ? ref.$el || ref : null
489
+ if (el && typeof el.scrollTop === 'number') {
490
+ el.scrollTop = 0
491
+ }
492
+ })
493
+ },
494
+
472
495
  onBackgroundClick(e) {
473
496
  if (e.target === this.$refs.container) this.dismiss()
474
497
  },
@@ -483,27 +506,16 @@ export default {
483
506
  labelFromUrl(url) {
484
507
  if (!url) return 'Academy District 20'
485
508
  try {
486
- const host = new URL(url).hostname
509
+ const host = new URL(url).hostname.toLowerCase()
487
510
 
488
511
  if (host === 'www.asd20.org') return 'Academy District 20'
489
512
 
490
513
  if (host.endsWith('.asd20.org')) {
491
514
  const subdomain = host.replace('.asd20.org', '')
492
- switch (subdomain) {
493
- case 'rampart':
494
- return 'Rampart High School'
495
- case 'pinecreek':
496
- return 'Pine Creek High School'
497
- case 'dcchigh':
498
- return 'Discovery Canyon Campus High School'
499
- case 'liberty':
500
- return 'Liberty High School'
501
- case 'd20online':
502
- return 'Academy Online High School'
503
- // add more mappings as needed
504
- default:
505
- return subdomain.charAt(0).toUpperCase() + subdomain.slice(1)
506
- }
515
+ return (
516
+ SUBDOMAIN_LABELS[subdomain] ||
517
+ subdomain.replace(/(^|[-_.])([a-z])/g, (_, __, c) => c.toUpperCase())
518
+ )
507
519
  }
508
520
 
509
521
  return host
@@ -559,15 +571,28 @@ export default {
559
571
  return false
560
572
  },
561
573
 
562
- // Called when user clicks Ask or presses Enter in question field
563
- onAskQuestion() {
564
- const q = (this.questionText || '').trim()
565
- if (!q) {
574
+ onSubmitInput() {
575
+ const text = (this.inputText || '').trim()
576
+ if (!text) {
566
577
  this.aiAnswer = null
567
578
  this.aiSources = []
579
+ this.aiKeywordsDisplay = ''
568
580
  return
569
581
  }
570
- this.searchAi(q)
582
+
583
+ const mode = this.decideMode(text)
584
+
585
+ if (mode === 'question') {
586
+ this.searchAi(text)
587
+ } else {
588
+ this.keywordsFromAi = false
589
+ this.skipKeywordWatcher = true
590
+ this.keywords = text
591
+ this.searchPages({ keepAnswersTab: false })
592
+ this.searchFiles()
593
+ this.searchGroups()
594
+ this.currentTab = 'Pages'
595
+ }
571
596
  },
572
597
 
573
598
  async searchAi(question) {
@@ -605,7 +630,7 @@ export default {
605
630
  const safeKeywords = this.sanitizeKeywords(aiKeywords)
606
631
  const fallbackKeywords = safeKeywords
607
632
  ? ''
608
- : this.deriveFallbackKeywords(this.questionText)
633
+ : this.deriveFallbackKeywords(this.inputText)
609
634
  const keywordsToUse = safeKeywords || fallbackKeywords
610
635
 
611
636
  // Show the AI-provided keywords (or fallback) in brackets for visibility.
@@ -616,8 +641,11 @@ export default {
616
641
 
617
642
  if (keywordsToUse) {
618
643
  this.keywordsFromAi = true
644
+ this.skipKeywordWatcher = true
619
645
  this.keywords = keywordsToUse
620
- // Let the keywords watcher trigger the searches; keep Answers tab active.
646
+ this.searchPages({ keepAnswersTab: true })
647
+ this.searchFiles()
648
+ this.searchGroups()
621
649
  }
622
650
 
623
651
  this.currentTab = 'Answers'
@@ -747,6 +775,46 @@ export default {
747
775
  return result
748
776
  },
749
777
 
778
+ decodeHtml(value) {
779
+ if (!value || typeof value !== 'string') return value || ''
780
+ if (typeof window === 'undefined' || !window.document) return value
781
+ const parser = new DOMParser()
782
+ const decoded = parser.parseFromString(value, 'text/html').documentElement
783
+ .textContent
784
+ return decoded || value
785
+ },
786
+
787
+ decideMode(text) {
788
+ const lower = (text || '').toLowerCase()
789
+ if (!lower) return 'keyword'
790
+ if (lower.includes('?')) return 'question'
791
+
792
+ const questionStarters = [
793
+ 'who',
794
+ 'what',
795
+ 'when',
796
+ 'where',
797
+ 'why',
798
+ 'how',
799
+ 'should',
800
+ 'can',
801
+ 'could',
802
+ 'would',
803
+ 'is',
804
+ 'are',
805
+ 'do',
806
+ 'does',
807
+ 'will',
808
+ 'may',
809
+ ]
810
+ const words = lower.split(/\s+/).filter(Boolean)
811
+ if (words.length >= 7) return 'question'
812
+ if (words.length > 0 && questionStarters.includes(words[0])) {
813
+ return 'question'
814
+ }
815
+ return 'keyword'
816
+ },
817
+
750
818
  deriveFallbackKeywords(questionText) {
751
819
  if (!questionText) return ''
752
820
  const STOP = new Set([
@@ -900,8 +968,8 @@ export default {
900
968
  ::v-deep ol,
901
969
  ::v-deep ul {
902
970
  display: block !important;
903
- list-style-position: inside;
904
- padding-left: 0;
971
+ list-style-position: outside;
972
+ padding-left: 1.5rem;
905
973
  margin-left: 0;
906
974
  flex: 0 0 auto;
907
975
  flex-wrap: nowrap;
@@ -924,16 +992,8 @@ export default {
924
992
  }
925
993
 
926
994
  ul {
927
- list-style: disc;
928
- list-style-position: inside;
929
- padding-left: 0;
930
- margin: 0;
931
- }
932
-
933
- li {
934
995
  list-style: none;
935
- font-size: 0.85rem;
936
- margin-bottom: space(0.25);
996
+ margin: 0;
937
997
  }
938
998
  }
939
999
 
@@ -941,21 +1001,22 @@ export default {
941
1001
  list-style: none;
942
1002
  padding-left: 0;
943
1003
  margin: 0;
944
- &__ai-source-list {
945
- list-style: disc;
946
- list-style-position: inside;
947
- padding-left: space(0.75);
948
- margin: space(0.25) 0 0.35rem space(0.5);
949
-
950
- li {
951
- display: list-item;
952
- margin: 0;
953
- padding: 0;
954
- }
955
- }
956
1004
  }
957
1005
 
1006
+ &__ai-source-list {
1007
+ display: block;
1008
+ list-style: disc;
1009
+ list-style-position: outside;
1010
+ padding-left: space(0.5);
1011
+ margin: 0 0 0.35rem space(0.5);
958
1012
 
1013
+ li {
1014
+ list-style: disc;
1015
+ list-style-position: outside;
1016
+ display: list-item;
1017
+ margin: 0;
1018
+ }
1019
+ }
959
1020
 
960
1021
  &__ai-snippet {
961
1022
  margin: 0;