@asd20/ui 3.8.0 → 3.10.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
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
idTag="-question"
|
|
19
19
|
ref="question"
|
|
20
20
|
v-model="questionText"
|
|
21
|
-
@keyup.enter.stop.prevent="onAskQuestion"
|
|
21
|
+
@keyup.enter.native.stop.prevent="onAskQuestion"
|
|
22
22
|
placeholder="Ask a question (preview)"
|
|
23
23
|
/>
|
|
24
24
|
<asd20-button
|
|
@@ -73,32 +73,42 @@
|
|
|
73
73
|
title="Ask a Question, or Search by Keyword"
|
|
74
74
|
description="Use the top input for plain-language questions, then press 'Ask'; or search by keyword in the lower input box."
|
|
75
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>
|
|
83
76
|
|
|
84
77
|
<!-- Answers tab -->
|
|
85
78
|
<div v-show="currentTab === 'Answers'" scrollable>
|
|
79
|
+
<div v-if="aiAnswer" class="disclaimer">
|
|
80
|
+
<p>
|
|
81
|
+
AI generated answers can have errors. Please contact our
|
|
82
|
+
<a href="www.asd20.org/help-desk">Help Desk</a> if you need
|
|
83
|
+
additional information.
|
|
84
|
+
</p>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
86
87
|
<div v-if="aiAnswer" class="asd20-site-search__ai-result">
|
|
87
88
|
<h3>Answer</h3>
|
|
88
89
|
<div class="asd20-site-search__ai-answer" v-html="aiAnswer" />
|
|
90
|
+
<div
|
|
91
|
+
v-if="aiKeywordsDisplay"
|
|
92
|
+
class="asd20-site-search__ai-keywords"
|
|
93
|
+
>
|
|
94
|
+
{{ aiKeywordsDisplay }}
|
|
95
|
+
</div>
|
|
89
96
|
|
|
90
97
|
<div
|
|
91
|
-
v-if="
|
|
98
|
+
v-if="aiGroupedSources.length"
|
|
92
99
|
class="asd20-site-search__ai-sources"
|
|
93
100
|
>
|
|
94
101
|
<h4>Sources</h4>
|
|
95
|
-
<ul>
|
|
96
|
-
<li v-for="
|
|
97
|
-
<strong>Content from: {{
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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>
|
|
105
|
+
<ul class="asd20-site-search__ai-source-list">
|
|
106
|
+
<li v-for="src in group.sources" :key="src.url || src.id">
|
|
107
|
+
<a :href="src.url" target="_blank" rel="noreferrer">
|
|
108
|
+
{{ src.title }}
|
|
109
|
+
</a>
|
|
110
|
+
</li>
|
|
111
|
+
</ul>
|
|
102
112
|
</li>
|
|
103
113
|
</ul>
|
|
104
114
|
</div>
|
|
@@ -275,6 +285,8 @@ export default {
|
|
|
275
285
|
selectedGroup: null,
|
|
276
286
|
searchingPages: false,
|
|
277
287
|
searchingFiles: false,
|
|
288
|
+
keywordsFromAi: false,
|
|
289
|
+
aiKeywordsDisplay: '',
|
|
278
290
|
}),
|
|
279
291
|
|
|
280
292
|
computed: {
|
|
@@ -345,9 +357,42 @@ export default {
|
|
|
345
357
|
aiUiSources() {
|
|
346
358
|
return (this.aiSources || []).map(src => ({
|
|
347
359
|
...src,
|
|
348
|
-
hostLabel:
|
|
360
|
+
hostLabel:
|
|
361
|
+
this.resolveOrgTitleFromUrl(src.url) || this.labelFromUrl(src.url),
|
|
349
362
|
}))
|
|
350
363
|
},
|
|
364
|
+
aiGroupedSources() {
|
|
365
|
+
const groups = new Map()
|
|
366
|
+
const filteredSources = this.aiUiSources.filter((src) =>
|
|
367
|
+
this.isSourceCurrentOrg(src.url, src.hostLabel)
|
|
368
|
+
)
|
|
369
|
+
filteredSources.forEach(src => {
|
|
370
|
+
const key = src.hostLabel || 'Academy District 20'
|
|
371
|
+
if (!groups.has(key)) {
|
|
372
|
+
groups.set(key, { hostLabel: key, sources: [] })
|
|
373
|
+
}
|
|
374
|
+
const group = groups.get(key)
|
|
375
|
+
const exists = group.sources.find(
|
|
376
|
+
s => (s.url && src.url && s.url === src.url) || (s.id && src.id && s.id === src.id)
|
|
377
|
+
)
|
|
378
|
+
if (!exists) {
|
|
379
|
+
group.sources.push(src)
|
|
380
|
+
}
|
|
381
|
+
})
|
|
382
|
+
const currentOrgTitle =
|
|
383
|
+
(this.organization && this.organization.title) || 'Academy District 20'
|
|
384
|
+
const currentLower = currentOrgTitle.toLowerCase()
|
|
385
|
+
|
|
386
|
+
return Array.from(groups.values()).sort((a, b) => {
|
|
387
|
+
const aIsCurrent =
|
|
388
|
+
(a.hostLabel || '').toLowerCase() === currentLower
|
|
389
|
+
const bIsCurrent =
|
|
390
|
+
(b.hostLabel || '').toLowerCase() === currentLower
|
|
391
|
+
if (aIsCurrent && !bIsCurrent) return -1
|
|
392
|
+
if (bIsCurrent && !aIsCurrent) return 1
|
|
393
|
+
return (a.hostLabel || '').localeCompare(b.hostLabel || '')
|
|
394
|
+
})
|
|
395
|
+
},
|
|
351
396
|
},
|
|
352
397
|
|
|
353
398
|
watch: {
|
|
@@ -388,14 +433,16 @@ export default {
|
|
|
388
433
|
},
|
|
389
434
|
|
|
390
435
|
methods: {
|
|
391
|
-
async searchPages() {
|
|
436
|
+
async searchPages(options = {}) {
|
|
392
437
|
if (this.$store && this.$store.state.search) {
|
|
438
|
+
const keepAnswersTab = options.keepAnswersTab || this.keywordsFromAi
|
|
393
439
|
this.searchingPages = true
|
|
394
440
|
await this.$store.dispatch('search/queryPages', this.keywords)
|
|
395
441
|
this.searchingPages = false
|
|
396
|
-
if (this.keywords && this.keywords.trim()) {
|
|
442
|
+
if (this.keywords && this.keywords.trim() && !keepAnswersTab) {
|
|
397
443
|
this.currentTab = 'Pages'
|
|
398
444
|
}
|
|
445
|
+
this.keywordsFromAi = false
|
|
399
446
|
}
|
|
400
447
|
},
|
|
401
448
|
|
|
@@ -447,6 +494,12 @@ export default {
|
|
|
447
494
|
return 'Rampart High School'
|
|
448
495
|
case 'pinecreek':
|
|
449
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'
|
|
450
503
|
// add more mappings as needed
|
|
451
504
|
default:
|
|
452
505
|
return subdomain.charAt(0).toUpperCase() + subdomain.slice(1)
|
|
@@ -459,6 +512,53 @@ export default {
|
|
|
459
512
|
}
|
|
460
513
|
},
|
|
461
514
|
|
|
515
|
+
// Try to resolve the full organization title from organizationOptions by URL
|
|
516
|
+
resolveOrgTitleFromUrl(url) {
|
|
517
|
+
if (!url || !Array.isArray(this.organizationOptions)) return null
|
|
518
|
+
try {
|
|
519
|
+
const host = new URL(url).hostname.toLowerCase()
|
|
520
|
+
const match = this.organizationOptions.find((org) => {
|
|
521
|
+
if (!org || !org.website) return false
|
|
522
|
+
try {
|
|
523
|
+
const orgHost = new URL(org.website).hostname.toLowerCase()
|
|
524
|
+
return host === orgHost || host.endsWith(orgHost)
|
|
525
|
+
} catch {
|
|
526
|
+
return false
|
|
527
|
+
}
|
|
528
|
+
})
|
|
529
|
+
return match && match.title ? match.title : null
|
|
530
|
+
} catch {
|
|
531
|
+
return null
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
|
|
535
|
+
isSourceCurrentOrg(url, hostLabel) {
|
|
536
|
+
const org = this.organization || {}
|
|
537
|
+
const orgTitle = (org.title || '').toLowerCase()
|
|
538
|
+
const orgWebsite = org.website || ''
|
|
539
|
+
|
|
540
|
+
// If org website is available, match by hostname
|
|
541
|
+
if (orgWebsite) {
|
|
542
|
+
try {
|
|
543
|
+
const orgHost = new URL(orgWebsite).hostname.toLowerCase()
|
|
544
|
+
const srcHost = url ? new URL(url).hostname.toLowerCase() : ''
|
|
545
|
+
if (orgHost && srcHost && (srcHost === orgHost || srcHost.endsWith(`.${orgHost}`))) {
|
|
546
|
+
return true
|
|
547
|
+
}
|
|
548
|
+
} catch {
|
|
549
|
+
// fall through to title comparison
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Fallback to title match
|
|
554
|
+
if (hostLabel && orgTitle) {
|
|
555
|
+
return hostLabel.toLowerCase() === orgTitle
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// If no organization info, include nothing
|
|
559
|
+
return false
|
|
560
|
+
},
|
|
561
|
+
|
|
462
562
|
// Called when user clicks Ask or presses Enter in question field
|
|
463
563
|
onAskQuestion() {
|
|
464
564
|
const q = (this.questionText || '').trim()
|
|
@@ -496,8 +596,30 @@ export default {
|
|
|
496
596
|
}
|
|
497
597
|
)
|
|
498
598
|
|
|
499
|
-
|
|
599
|
+
const { cleanAnswer, keywords: aiKeywords } =
|
|
600
|
+
this.extractKeywordsAndCleanAnswer(answer || '')
|
|
601
|
+
|
|
602
|
+
this.aiAnswer = this.sanitizeAnswer(cleanAnswer)
|
|
500
603
|
this.aiSources = sources || []
|
|
604
|
+
|
|
605
|
+
const safeKeywords = this.sanitizeKeywords(aiKeywords)
|
|
606
|
+
const fallbackKeywords = safeKeywords
|
|
607
|
+
? ''
|
|
608
|
+
: this.deriveFallbackKeywords(this.questionText)
|
|
609
|
+
const keywordsToUse = safeKeywords || fallbackKeywords
|
|
610
|
+
|
|
611
|
+
// Show the AI-provided keywords (or fallback) in brackets for visibility.
|
|
612
|
+
const displayKeywordsSource = aiKeywords || keywordsToUse || ''
|
|
613
|
+
this.aiKeywordsDisplay = displayKeywordsSource
|
|
614
|
+
? `[Keywords: ${displayKeywordsSource.replace(/<[^>]*>/g, '')}]`
|
|
615
|
+
: ''
|
|
616
|
+
|
|
617
|
+
if (keywordsToUse) {
|
|
618
|
+
this.keywordsFromAi = true
|
|
619
|
+
this.keywords = keywordsToUse
|
|
620
|
+
// Let the keywords watcher trigger the searches; keep Answers tab active.
|
|
621
|
+
}
|
|
622
|
+
|
|
501
623
|
this.currentTab = 'Answers'
|
|
502
624
|
} catch (e) {
|
|
503
625
|
console.error('AI search failed', e)
|
|
@@ -564,6 +686,122 @@ export default {
|
|
|
564
686
|
cleanNode(container)
|
|
565
687
|
return container.innerHTML
|
|
566
688
|
},
|
|
689
|
+
|
|
690
|
+
extractKeywordsAndCleanAnswer(answerText) {
|
|
691
|
+
if (!answerText) return { cleanAnswer: '', keywords: '' }
|
|
692
|
+
|
|
693
|
+
// Grab the last occurrence of a keywords marker to reduce prompt-injection tricks.
|
|
694
|
+
// Accepts `[Keywords: ...]` or `Keywords: ...` without brackets.
|
|
695
|
+
const regex = /\[?\s*keywords[:\-\s]*([^\]\n\r]+)\]?/gi
|
|
696
|
+
let match
|
|
697
|
+
let lastMatch = null
|
|
698
|
+
while ((match = regex.exec(answerText)) !== null) {
|
|
699
|
+
lastMatch = match
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const rawKeywords = lastMatch ? (lastMatch[1] || '').trim() : ''
|
|
703
|
+
|
|
704
|
+
// Keep the keywords block visible for now. To hide later, uncomment below:
|
|
705
|
+
const cleanAnswer = lastMatch
|
|
706
|
+
? `${answerText.slice(0, lastMatch.index)}${answerText
|
|
707
|
+
.slice(lastMatch.index + lastMatch[0].length)
|
|
708
|
+
.trim()}`
|
|
709
|
+
: answerText
|
|
710
|
+
|
|
711
|
+
return { cleanAnswer: cleanAnswer.trim(), keywords: rawKeywords }
|
|
712
|
+
},
|
|
713
|
+
|
|
714
|
+
sanitizeKeywords(rawKeywords) {
|
|
715
|
+
if (!rawKeywords || typeof rawKeywords !== 'string') return ''
|
|
716
|
+
|
|
717
|
+
// Remove brackets, leading "keywords" label, and weird punctuation; keep basic word chars, spaces, hyphens, commas
|
|
718
|
+
const cleaned = rawKeywords
|
|
719
|
+
.replace(/<[^>]*>/g, ' ')
|
|
720
|
+
.replace(/[\[\]\(\)\{\}]/g, ' ')
|
|
721
|
+
.replace(/\bkeywords\b[:\-\s]*/i, '')
|
|
722
|
+
.replace(/[^a-z0-9,\-\s]/gi, ' ')
|
|
723
|
+
.trim()
|
|
724
|
+
|
|
725
|
+
if (!cleaned) return ''
|
|
726
|
+
|
|
727
|
+
// Split on commas/semicolons/pipes first; fall back to spaces
|
|
728
|
+
const parts =
|
|
729
|
+
cleaned.split(/[,;|]/).filter(Boolean).map(p => p.trim()).filter(Boolean)
|
|
730
|
+
const tokens =
|
|
731
|
+
parts.length > 0
|
|
732
|
+
? parts
|
|
733
|
+
: cleaned.split(/\s+/).filter(Boolean)
|
|
734
|
+
|
|
735
|
+
const unique = []
|
|
736
|
+
for (const token of tokens) {
|
|
737
|
+
if (!token) continue
|
|
738
|
+
if (token.length <= 1) continue
|
|
739
|
+
const clipped = token.slice(0, 32)
|
|
740
|
+
if (!unique.includes(clipped)) {
|
|
741
|
+
unique.push(clipped)
|
|
742
|
+
}
|
|
743
|
+
if (unique.length >= 3) break
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const result = unique.join(' ').slice(0, 100).trim()
|
|
747
|
+
return result
|
|
748
|
+
},
|
|
749
|
+
|
|
750
|
+
deriveFallbackKeywords(questionText) {
|
|
751
|
+
if (!questionText) return ''
|
|
752
|
+
const STOP = new Set([
|
|
753
|
+
'a',
|
|
754
|
+
'an',
|
|
755
|
+
'the',
|
|
756
|
+
'and',
|
|
757
|
+
'or',
|
|
758
|
+
'of',
|
|
759
|
+
'for',
|
|
760
|
+
'to',
|
|
761
|
+
'in',
|
|
762
|
+
'on',
|
|
763
|
+
'with',
|
|
764
|
+
'at',
|
|
765
|
+
'by',
|
|
766
|
+
'from',
|
|
767
|
+
'about',
|
|
768
|
+
'as',
|
|
769
|
+
'is',
|
|
770
|
+
'are',
|
|
771
|
+
'was',
|
|
772
|
+
'were',
|
|
773
|
+
'be',
|
|
774
|
+
'been',
|
|
775
|
+
'being',
|
|
776
|
+
'this',
|
|
777
|
+
'that',
|
|
778
|
+
'these',
|
|
779
|
+
'those',
|
|
780
|
+
'it',
|
|
781
|
+
'its',
|
|
782
|
+
'your',
|
|
783
|
+
'you',
|
|
784
|
+
'we',
|
|
785
|
+
'us',
|
|
786
|
+
'our',
|
|
787
|
+
'i',
|
|
788
|
+
'me',
|
|
789
|
+
'my',
|
|
790
|
+
])
|
|
791
|
+
const tokens = questionText
|
|
792
|
+
.toLowerCase()
|
|
793
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
794
|
+
.split(/\s+/)
|
|
795
|
+
.filter(Boolean)
|
|
796
|
+
.filter((t) => !STOP.has(t))
|
|
797
|
+
|
|
798
|
+
const uniq = []
|
|
799
|
+
for (const t of tokens) {
|
|
800
|
+
if (!uniq.includes(t)) uniq.push(t)
|
|
801
|
+
if (uniq.length >= 3) break
|
|
802
|
+
}
|
|
803
|
+
return uniq.join(' ')
|
|
804
|
+
},
|
|
567
805
|
},
|
|
568
806
|
}
|
|
569
807
|
</script>
|
|
@@ -658,6 +896,22 @@ export default {
|
|
|
658
896
|
|
|
659
897
|
&__ai-answer {
|
|
660
898
|
margin-bottom: space(0.5);
|
|
899
|
+
|
|
900
|
+
::v-deep ol,
|
|
901
|
+
::v-deep ul {
|
|
902
|
+
display: block !important;
|
|
903
|
+
list-style-position: inside;
|
|
904
|
+
padding-left: 0;
|
|
905
|
+
margin-left: 0;
|
|
906
|
+
flex: 0 0 auto;
|
|
907
|
+
flex-wrap: nowrap;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
::v-deep li {
|
|
911
|
+
display: list-item;
|
|
912
|
+
flex: 0 0 auto;
|
|
913
|
+
width: auto;
|
|
914
|
+
}
|
|
661
915
|
}
|
|
662
916
|
|
|
663
917
|
&__ai-sources {
|
|
@@ -670,17 +924,39 @@ export default {
|
|
|
670
924
|
}
|
|
671
925
|
|
|
672
926
|
ul {
|
|
673
|
-
list-style:
|
|
927
|
+
list-style: disc;
|
|
928
|
+
list-style-position: inside;
|
|
674
929
|
padding-left: 0;
|
|
675
930
|
margin: 0;
|
|
676
931
|
}
|
|
677
932
|
|
|
678
933
|
li {
|
|
934
|
+
list-style: none;
|
|
679
935
|
font-size: 0.85rem;
|
|
680
936
|
margin-bottom: space(0.25);
|
|
681
937
|
}
|
|
682
938
|
}
|
|
683
939
|
|
|
940
|
+
&__ai-source-groups {
|
|
941
|
+
list-style: none;
|
|
942
|
+
padding-left: 0;
|
|
943
|
+
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
|
+
}
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
|
|
684
960
|
&__ai-snippet {
|
|
685
961
|
margin: 0;
|
|
686
962
|
opacity: 0.85;
|