@asd20/ui 3.6.1 → 3.8.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.6.1",
3
+ "version": "3.8.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "sideEffects": [
@@ -9,7 +9,9 @@
9
9
  <div class="asd20-site-search__viewport">
10
10
  <div class="asd20-site-search__options">
11
11
  <!-- Ask a Question -->
12
- <div class="asd20-site-search__field asd20-site-search__field--question">
12
+ <div
13
+ class="asd20-site-search__field asd20-site-search__field--question"
14
+ >
13
15
  <!-- <label class="asd20-site-search__label">Ask a question</label> -->
14
16
  <div class="asd20-site-search__question-row">
15
17
  <asd20-search-field
@@ -17,19 +19,24 @@
17
19
  ref="question"
18
20
  v-model="questionText"
19
21
  @keyup.enter.stop.prevent="onAskQuestion"
20
- placeholder="Ask a question."
22
+ placeholder="Ask a question (preview)"
21
23
  />
22
- <button
23
- type="button"
24
+ <asd20-button
24
25
  class="asd20-site-search__ask-button"
25
- @click="onAskQuestion"
26
+ label="Ask"
27
+ size="md"
28
+ bordered
29
+ @click.native="onAskQuestion"
26
30
  :disabled="!questionText || searchingAi"
27
- >
28
- Ask
29
- </button>
31
+ />
32
+ <asd20-loader
33
+ v-if="searchingAi"
34
+ size="sm"
35
+ class="asd20-site-search__inline-loader"
36
+ />
30
37
  </div>
31
38
  </div>
32
- <hr/>
39
+ <hr />
33
40
 
34
41
  <!-- Search by Keyword -->
35
42
  <div class="asd20-site-search__field asd20-site-search__field--keyword">
@@ -38,7 +45,7 @@
38
45
  idTag="-sitewide"
39
46
  ref="search"
40
47
  v-model="keywords"
41
- placeholder="Search by keyword."
48
+ placeholder="Search by keyword"
42
49
  />
43
50
  </div>
44
51
 
@@ -56,39 +63,56 @@
56
63
 
57
64
  <asd20-viewport class="asd20-site-search__results" scrollable>
58
65
  <asd20-notification
59
- v-if="!keywords && !questionText && !searchingFiles && !searchingPages && !searchingAi"
66
+ v-if="
67
+ !keywords &&
68
+ !questionText &&
69
+ !searchingFiles &&
70
+ !searchingPages &&
71
+ !searchingAi
72
+ "
60
73
  title="Ask a Question, or Search by Keyword"
61
- description="Use the top box for plain-language questions, or search by keyword in the box below."
74
+ description="Use the top input for plain-language questions, then press 'Ask'; or search by keyword in the lower input box."
62
75
  />
76
+ <div class="disclaimer">
77
+ <p>
78
+ AI generated answers can have errors. Please contact our
79
+ <a href="www.asd20.org/help-desk">Help Desk</a> if you need
80
+ additional information.
81
+ </p>
82
+ </div>
63
83
 
64
- <!-- Pages tab -->
65
- <div v-show="currentTab === 'Pages'" scrollable>
66
- <!-- AI Answer Section -->
84
+ <!-- Answers tab -->
85
+ <div v-show="currentTab === 'Answers'" scrollable>
67
86
  <div v-if="aiAnswer" class="asd20-site-search__ai-result">
68
87
  <h3>Answer</h3>
69
88
  <div class="asd20-site-search__ai-answer" v-html="aiAnswer" />
70
89
 
71
- <div v-if="aiUiSources.length" class="asd20-site-search__ai-sources">
90
+ <div
91
+ v-if="aiUiSources.length"
92
+ class="asd20-site-search__ai-sources"
93
+ >
72
94
  <h4>Sources</h4>
73
95
  <ul>
74
96
  <li v-for="src in aiUiSources" :key="src.url || src.id">
75
- <strong>Content from: {{ src.hostLabel }}</strong><br />
97
+ <strong>Content from: {{ src.hostLabel }}</strong
98
+ ><br />
76
99
  <a :href="src.url" target="_blank" rel="noreferrer">
77
100
  {{ src.title }}
78
101
  </a>
79
- <p v-if="src.snippet" class="asd20-site-search__ai-snippet">
80
- {{ src.snippet }}
81
- </p>
82
102
  </li>
83
103
  </ul>
84
104
  </div>
85
105
 
86
106
  <asd20-loader v-if="searchingAi" size="lg" />
87
107
  </div>
108
+ <asd20-loader v-if="searchingAi && !aiAnswer" size="lg" />
109
+ </div>
88
110
 
111
+ <!-- Pages tab -->
112
+ <div v-show="currentTab === 'Pages'" scrollable>
89
113
  <!-- No results -->
90
114
  <asd20-notification
91
- v-if="keywords && _pages.length === 0 && !searchingPages && !aiAnswer"
115
+ v-if="keywords && _pages.length === 0 && !searchingPages"
92
116
  title="No Pages Found"
93
117
  description="Try using different keywords."
94
118
  />
@@ -122,7 +146,9 @@
122
146
 
123
147
  <div class="asd20-site-search__suggested" v-if="_groups.length > 0">
124
148
  <h3>
125
- {{ _groups.length > 1 ? 'Suggested Contacts:' : 'Suggested Contact:' }}
149
+ {{
150
+ _groups.length > 1 ? 'Suggested Contacts:' : 'Suggested Contact:'
151
+ }}
126
152
  </h3>
127
153
  <asd20-list-item
128
154
  v-for="g in _groups"
@@ -164,6 +190,7 @@ import Asd20Loader from '../../molecules/Asd20Loader'
164
190
  import Asd20Modal from '../../molecules/Asd20Modal'
165
191
  import Asd20DepartmentContactCard from '../../molecules/Asd20DepartmentContactCard'
166
192
  import Asd20Checkbox from '../../atoms/Asd20Checkbox'
193
+ import Asd20Button from '../../atoms/Asd20Button'
167
194
 
168
195
  // Helpers
169
196
  import debounce from 'lodash/debounce'
@@ -224,16 +251,20 @@ export default {
224
251
  Asd20Modal,
225
252
  Asd20DepartmentContactCard,
226
253
  Asd20Checkbox,
254
+ Asd20Button,
227
255
  },
228
256
 
229
257
  props: {
230
258
  active: { type: Boolean, default: false },
231
- organization: { type: Object, default: () => ({ title: 'Academy District 20' }) },
259
+ organization: {
260
+ type: Object,
261
+ default: () => ({ title: 'Academy District 20' }),
262
+ },
232
263
  organizationOptions: { type: Array, default: () => [] },
233
264
  },
234
265
 
235
266
  data: () => ({
236
- currentTab: 'Pages',
267
+ currentTab: 'Answers',
237
268
  // keyword search (classic)
238
269
  keywords: '',
239
270
  // question input (AI)
@@ -279,6 +310,11 @@ export default {
279
310
  },
280
311
  tabs() {
281
312
  return [
313
+ {
314
+ label: 'Answers',
315
+ badge: this.aiAnswer ? 1 : 0,
316
+ active: this.currentTab === 'Answers',
317
+ },
282
318
  {
283
319
  label: 'Pages',
284
320
  badge: this._pages.length,
@@ -357,6 +393,9 @@ export default {
357
393
  this.searchingPages = true
358
394
  await this.$store.dispatch('search/queryPages', this.keywords)
359
395
  this.searchingPages = false
396
+ if (this.keywords && this.keywords.trim()) {
397
+ this.currentTab = 'Pages'
398
+ }
360
399
  }
361
400
  },
362
401
 
@@ -436,6 +475,7 @@ export default {
436
475
  if (this.searchingAi) return
437
476
 
438
477
  this.searchingAi = true
478
+ this.currentTab = 'Answers'
439
479
  try {
440
480
  const organizationId =
441
481
  this.organization && this.organization.id
@@ -456,9 +496,9 @@ export default {
456
496
  }
457
497
  )
458
498
 
459
- this.aiAnswer = answer
499
+ this.aiAnswer = this.sanitizeAnswer(answer)
460
500
  this.aiSources = sources || []
461
- this.currentTab = 'Pages'
501
+ this.currentTab = 'Answers'
462
502
  } catch (e) {
463
503
  console.error('AI search failed', e)
464
504
  this.aiAnswer = null
@@ -467,6 +507,63 @@ export default {
467
507
  this.searchingAi = false
468
508
  }
469
509
  },
510
+
511
+ sanitizeAnswer(rawHtml) {
512
+ if (!rawHtml) return ''
513
+ // If no DOM (e.g., SSR), return raw; client will re-run and sanitize.
514
+ if (typeof window === 'undefined' || !window.document) return rawHtml
515
+
516
+ const allowedTags = new Set(['P', 'UL', 'OL', 'LI', 'STRONG', 'EM', 'A'])
517
+ const allowedAttrs = {
518
+ A: ['href'],
519
+ }
520
+
521
+ const container = window.document.createElement('div')
522
+ container.innerHTML = rawHtml
523
+
524
+ const cleanNode = node => {
525
+ const children = Array.from(node.childNodes)
526
+ children.forEach(child => {
527
+ if (child.nodeType === 1) {
528
+ const tag = child.tagName.toUpperCase()
529
+ if (!allowedTags.has(tag)) {
530
+ while (child.firstChild) {
531
+ node.insertBefore(child.firstChild, child)
532
+ }
533
+ node.removeChild(child)
534
+ return
535
+ }
536
+
537
+ const allowed = allowedAttrs[tag] || []
538
+ Array.from(child.attributes).forEach(attr => {
539
+ const name = attr.name.toLowerCase()
540
+ if (!allowed.includes(attr.name)) {
541
+ child.removeAttribute(attr.name)
542
+ } else if (tag === 'A' && name === 'href') {
543
+ const href = attr.value || ''
544
+ const safe =
545
+ href.startsWith('#') ||
546
+ href.startsWith('mailto:') ||
547
+ /^https?:\/\//i.test(href)
548
+ if (!safe) {
549
+ child.removeAttribute('href')
550
+ } else {
551
+ child.setAttribute('rel', 'noreferrer noopener')
552
+ child.setAttribute('target', '_blank')
553
+ }
554
+ }
555
+ })
556
+
557
+ cleanNode(child)
558
+ } else if (child.nodeType === 8) {
559
+ node.removeChild(child)
560
+ }
561
+ })
562
+ }
563
+
564
+ cleanNode(container)
565
+ return container.innerHTML
566
+ },
470
567
  },
471
568
  }
472
569
  </script>
@@ -502,7 +599,7 @@ export default {
502
599
  }
503
600
 
504
601
  &__options {
505
- padding: space(0.5);
602
+ padding: space(0.125);
506
603
  display: flex;
507
604
  flex-direction: column;
508
605
  }
@@ -532,24 +629,25 @@ export default {
532
629
 
533
630
  &__ask-button {
534
631
  flex: 0 0 auto;
535
- padding: 0 space(0.75);
536
- border-radius: 999px;
537
- border: 1px solid var(--color__primary, #22506a);
538
- background: var(--color__primary, #22506a);
539
- color: #fff;
540
- font-weight: 600;
541
- font-size: 0.9rem;
542
- cursor: pointer;
543
- white-space: nowrap;
632
+ }
544
633
 
545
- &:disabled {
546
- opacity: 0.5;
547
- cursor: default;
548
- }
634
+ &__inline-loader {
635
+ align-self: center;
549
636
  }
550
637
 
551
638
  &__results {
552
639
  flex-grow: 1;
640
+ .disclaimer {
641
+ padding: space(0.5);
642
+ font-size: 0.85rem;
643
+ background: var(--website-page__alternate-background-t25);
644
+ border-bottom: 1px solid var(--color__tertiary);
645
+
646
+ a {
647
+ color: var(--color__primary);
648
+ text-decoration: underline;
649
+ }
650
+ }
553
651
  }
554
652
 
555
653
  &__ai-result {
@@ -608,6 +706,9 @@ export default {
608
706
  flex-grow: 0;
609
707
  }
610
708
  }
709
+ hr {
710
+ padding: 0.2px;
711
+ }
611
712
  }
612
713
 
613
714
  @media (min-width: 768px) {
@@ -94,7 +94,7 @@
94
94
  ul,
95
95
  ol {
96
96
  padding: 0;
97
- list-style-position: outside;
97
+ list-style-position: inside;
98
98
  margin: space(0.25) 0 space(1) 0;
99
99
  display: flex;
100
100
  flex-wrap: wrap;