@asd20/ui 3.6.1 → 3.7.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.7.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "sideEffects": [
@@ -17,16 +17,21 @@
17
17
  ref="question"
18
18
  v-model="questionText"
19
19
  @keyup.enter.stop.prevent="onAskQuestion"
20
- placeholder="Ask a question."
20
+ placeholder="Ask a question"
21
21
  />
22
- <button
23
- type="button"
22
+ <asd20-button
24
23
  class="asd20-site-search__ask-button"
25
- @click="onAskQuestion"
24
+ label="Ask"
25
+ size="md"
26
+ bordered
27
+ @click.native="onAskQuestion"
26
28
  :disabled="!questionText || searchingAi"
27
- >
28
- Ask
29
- </button>
29
+ />
30
+ <asd20-loader
31
+ v-if="searchingAi"
32
+ size="sm"
33
+ class="asd20-site-search__inline-loader"
34
+ />
30
35
  </div>
31
36
  </div>
32
37
  <hr/>
@@ -38,7 +43,7 @@
38
43
  idTag="-sitewide"
39
44
  ref="search"
40
45
  v-model="keywords"
41
- placeholder="Search by keyword."
46
+ placeholder="Search by keyword"
42
47
  />
43
48
  </div>
44
49
 
@@ -56,14 +61,19 @@
56
61
 
57
62
  <asd20-viewport class="asd20-site-search__results" scrollable>
58
63
  <asd20-notification
59
- v-if="!keywords && !questionText && !searchingFiles && !searchingPages && !searchingAi"
64
+ v-if="
65
+ !keywords &&
66
+ !questionText &&
67
+ !searchingFiles &&
68
+ !searchingPages &&
69
+ !searchingAi
70
+ "
60
71
  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."
72
+ description="Use the top input for plain-language questions, then press 'Ask'; or search by keyword in the lower input box."
62
73
  />
63
74
 
64
- <!-- Pages tab -->
65
- <div v-show="currentTab === 'Pages'" scrollable>
66
- <!-- AI Answer Section -->
75
+ <!-- Answers tab -->
76
+ <div v-show="currentTab === 'Answers'" scrollable>
67
77
  <div v-if="aiAnswer" class="asd20-site-search__ai-result">
68
78
  <h3>Answer</h3>
69
79
  <div class="asd20-site-search__ai-answer" v-html="aiAnswer" />
@@ -76,19 +86,21 @@
76
86
  <a :href="src.url" target="_blank" rel="noreferrer">
77
87
  {{ src.title }}
78
88
  </a>
79
- <p v-if="src.snippet" class="asd20-site-search__ai-snippet">
80
- {{ src.snippet }}
81
- </p>
82
89
  </li>
83
90
  </ul>
84
91
  </div>
85
92
 
86
93
  <asd20-loader v-if="searchingAi" size="lg" />
87
94
  </div>
95
+ <asd20-loader v-if="searchingAi && !aiAnswer" size="lg" />
96
+ </div>
97
+
98
+ <!-- Pages tab -->
99
+ <div v-show="currentTab === 'Pages'" scrollable>
88
100
 
89
101
  <!-- No results -->
90
102
  <asd20-notification
91
- v-if="keywords && _pages.length === 0 && !searchingPages && !aiAnswer"
103
+ v-if="keywords && _pages.length === 0 && !searchingPages"
92
104
  title="No Pages Found"
93
105
  description="Try using different keywords."
94
106
  />
@@ -164,6 +176,7 @@ import Asd20Loader from '../../molecules/Asd20Loader'
164
176
  import Asd20Modal from '../../molecules/Asd20Modal'
165
177
  import Asd20DepartmentContactCard from '../../molecules/Asd20DepartmentContactCard'
166
178
  import Asd20Checkbox from '../../atoms/Asd20Checkbox'
179
+ import Asd20Button from '../../atoms/Asd20Button'
167
180
 
168
181
  // Helpers
169
182
  import debounce from 'lodash/debounce'
@@ -224,6 +237,7 @@ export default {
224
237
  Asd20Modal,
225
238
  Asd20DepartmentContactCard,
226
239
  Asd20Checkbox,
240
+ Asd20Button,
227
241
  },
228
242
 
229
243
  props: {
@@ -233,7 +247,7 @@ export default {
233
247
  },
234
248
 
235
249
  data: () => ({
236
- currentTab: 'Pages',
250
+ currentTab: 'Answers',
237
251
  // keyword search (classic)
238
252
  keywords: '',
239
253
  // question input (AI)
@@ -279,6 +293,11 @@ export default {
279
293
  },
280
294
  tabs() {
281
295
  return [
296
+ {
297
+ label: 'Answers',
298
+ badge: this.aiAnswer ? 1 : 0,
299
+ active: this.currentTab === 'Answers',
300
+ },
282
301
  {
283
302
  label: 'Pages',
284
303
  badge: this._pages.length,
@@ -357,6 +376,9 @@ export default {
357
376
  this.searchingPages = true
358
377
  await this.$store.dispatch('search/queryPages', this.keywords)
359
378
  this.searchingPages = false
379
+ if (this.keywords && this.keywords.trim()) {
380
+ this.currentTab = 'Pages'
381
+ }
360
382
  }
361
383
  },
362
384
 
@@ -436,6 +458,7 @@ export default {
436
458
  if (this.searchingAi) return
437
459
 
438
460
  this.searchingAi = true
461
+ this.currentTab = 'Answers'
439
462
  try {
440
463
  const organizationId =
441
464
  this.organization && this.organization.id
@@ -456,9 +479,9 @@ export default {
456
479
  }
457
480
  )
458
481
 
459
- this.aiAnswer = answer
482
+ this.aiAnswer = this.sanitizeAnswer(answer)
460
483
  this.aiSources = sources || []
461
- this.currentTab = 'Pages'
484
+ this.currentTab = 'Answers'
462
485
  } catch (e) {
463
486
  console.error('AI search failed', e)
464
487
  this.aiAnswer = null
@@ -467,6 +490,71 @@ export default {
467
490
  this.searchingAi = false
468
491
  }
469
492
  },
493
+
494
+ sanitizeAnswer(rawHtml) {
495
+ if (!rawHtml) return ''
496
+ // If no DOM (e.g., SSR), return raw; client will re-run and sanitize.
497
+ if (typeof window === 'undefined' || !window.document) return rawHtml
498
+
499
+ const allowedTags = new Set([
500
+ 'P',
501
+ 'UL',
502
+ 'OL',
503
+ 'LI',
504
+ 'STRONG',
505
+ 'EM',
506
+ 'A',
507
+ ])
508
+ const allowedAttrs = {
509
+ A: ['href'],
510
+ }
511
+
512
+ const container = window.document.createElement('div')
513
+ container.innerHTML = rawHtml
514
+
515
+ const cleanNode = (node) => {
516
+ const children = Array.from(node.childNodes)
517
+ children.forEach((child) => {
518
+ if (child.nodeType === 1) {
519
+ const tag = child.tagName.toUpperCase()
520
+ if (!allowedTags.has(tag)) {
521
+ while (child.firstChild) {
522
+ node.insertBefore(child.firstChild, child)
523
+ }
524
+ node.removeChild(child)
525
+ return
526
+ }
527
+
528
+ const allowed = allowedAttrs[tag] || []
529
+ Array.from(child.attributes).forEach((attr) => {
530
+ const name = attr.name.toLowerCase()
531
+ if (!allowed.includes(attr.name)) {
532
+ child.removeAttribute(attr.name)
533
+ } else if (tag === 'A' && name === 'href') {
534
+ const href = attr.value || ''
535
+ const safe =
536
+ href.startsWith('#') ||
537
+ href.startsWith('mailto:') ||
538
+ /^https?:\/\//i.test(href)
539
+ if (!safe) {
540
+ child.removeAttribute('href')
541
+ } else {
542
+ child.setAttribute('rel', 'noreferrer noopener')
543
+ child.setAttribute('target', '_blank')
544
+ }
545
+ }
546
+ })
547
+
548
+ cleanNode(child)
549
+ } else if (child.nodeType === 8) {
550
+ node.removeChild(child)
551
+ }
552
+ })
553
+ }
554
+
555
+ cleanNode(container)
556
+ return container.innerHTML
557
+ },
470
558
  },
471
559
  }
472
560
  </script>
@@ -502,7 +590,7 @@ export default {
502
590
  }
503
591
 
504
592
  &__options {
505
- padding: space(0.5);
593
+ padding: space(0.125);
506
594
  display: flex;
507
595
  flex-direction: column;
508
596
  }
@@ -532,20 +620,10 @@ export default {
532
620
 
533
621
  &__ask-button {
534
622
  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;
623
+ }
544
624
 
545
- &:disabled {
546
- opacity: 0.5;
547
- cursor: default;
548
- }
625
+ &__inline-loader {
626
+ align-self: center;
549
627
  }
550
628
 
551
629
  &__results {
@@ -608,6 +686,9 @@ export default {
608
686
  flex-grow: 0;
609
687
  }
610
688
  }
689
+ hr {
690
+ padding: 0.2px;
691
+ }
611
692
  }
612
693
 
613
694
  @media (min-width: 768px) {