@asd20/ui 3.9.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
|
@@ -87,13 +87,19 @@
|
|
|
87
87
|
<div v-if="aiAnswer" class="asd20-site-search__ai-result">
|
|
88
88
|
<h3>Answer</h3>
|
|
89
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>
|
|
90
96
|
|
|
91
97
|
<div
|
|
92
98
|
v-if="aiGroupedSources.length"
|
|
93
99
|
class="asd20-site-search__ai-sources"
|
|
94
100
|
>
|
|
95
101
|
<h4>Sources</h4>
|
|
96
|
-
<ul>
|
|
102
|
+
<ul class="asd20-site-search__ai-source-groups">
|
|
97
103
|
<li v-for="group in aiGroupedSources" :key="group.hostLabel">
|
|
98
104
|
<strong>Content from: {{ group.hostLabel }}</strong>
|
|
99
105
|
<ul class="asd20-site-search__ai-source-list">
|
|
@@ -279,6 +285,8 @@ export default {
|
|
|
279
285
|
selectedGroup: null,
|
|
280
286
|
searchingPages: false,
|
|
281
287
|
searchingFiles: false,
|
|
288
|
+
keywordsFromAi: false,
|
|
289
|
+
aiKeywordsDisplay: '',
|
|
282
290
|
}),
|
|
283
291
|
|
|
284
292
|
computed: {
|
|
@@ -355,7 +363,10 @@ export default {
|
|
|
355
363
|
},
|
|
356
364
|
aiGroupedSources() {
|
|
357
365
|
const groups = new Map()
|
|
358
|
-
this.aiUiSources.
|
|
366
|
+
const filteredSources = this.aiUiSources.filter((src) =>
|
|
367
|
+
this.isSourceCurrentOrg(src.url, src.hostLabel)
|
|
368
|
+
)
|
|
369
|
+
filteredSources.forEach(src => {
|
|
359
370
|
const key = src.hostLabel || 'Academy District 20'
|
|
360
371
|
if (!groups.has(key)) {
|
|
361
372
|
groups.set(key, { hostLabel: key, sources: [] })
|
|
@@ -422,14 +433,16 @@ export default {
|
|
|
422
433
|
},
|
|
423
434
|
|
|
424
435
|
methods: {
|
|
425
|
-
async searchPages() {
|
|
436
|
+
async searchPages(options = {}) {
|
|
426
437
|
if (this.$store && this.$store.state.search) {
|
|
438
|
+
const keepAnswersTab = options.keepAnswersTab || this.keywordsFromAi
|
|
427
439
|
this.searchingPages = true
|
|
428
440
|
await this.$store.dispatch('search/queryPages', this.keywords)
|
|
429
441
|
this.searchingPages = false
|
|
430
|
-
if (this.keywords && this.keywords.trim()) {
|
|
442
|
+
if (this.keywords && this.keywords.trim() && !keepAnswersTab) {
|
|
431
443
|
this.currentTab = 'Pages'
|
|
432
444
|
}
|
|
445
|
+
this.keywordsFromAi = false
|
|
433
446
|
}
|
|
434
447
|
},
|
|
435
448
|
|
|
@@ -519,6 +532,33 @@ export default {
|
|
|
519
532
|
}
|
|
520
533
|
},
|
|
521
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
|
+
|
|
522
562
|
// Called when user clicks Ask or presses Enter in question field
|
|
523
563
|
onAskQuestion() {
|
|
524
564
|
const q = (this.questionText || '').trim()
|
|
@@ -556,8 +596,30 @@ export default {
|
|
|
556
596
|
}
|
|
557
597
|
)
|
|
558
598
|
|
|
559
|
-
|
|
599
|
+
const { cleanAnswer, keywords: aiKeywords } =
|
|
600
|
+
this.extractKeywordsAndCleanAnswer(answer || '')
|
|
601
|
+
|
|
602
|
+
this.aiAnswer = this.sanitizeAnswer(cleanAnswer)
|
|
560
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
|
+
|
|
561
623
|
this.currentTab = 'Answers'
|
|
562
624
|
} catch (e) {
|
|
563
625
|
console.error('AI search failed', e)
|
|
@@ -624,6 +686,122 @@ export default {
|
|
|
624
686
|
cleanNode(container)
|
|
625
687
|
return container.innerHTML
|
|
626
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
|
+
},
|
|
627
805
|
},
|
|
628
806
|
}
|
|
629
807
|
</script>
|
|
@@ -746,30 +924,39 @@ export default {
|
|
|
746
924
|
}
|
|
747
925
|
|
|
748
926
|
ul {
|
|
749
|
-
list-style:
|
|
927
|
+
list-style: disc;
|
|
928
|
+
list-style-position: inside;
|
|
750
929
|
padding-left: 0;
|
|
751
930
|
margin: 0;
|
|
752
931
|
}
|
|
753
932
|
|
|
754
933
|
li {
|
|
934
|
+
list-style: none;
|
|
755
935
|
font-size: 0.85rem;
|
|
756
936
|
margin-bottom: space(0.25);
|
|
757
937
|
}
|
|
758
938
|
}
|
|
759
939
|
|
|
760
|
-
&__ai-source-
|
|
761
|
-
list-style:
|
|
762
|
-
list-style-position: inside;
|
|
940
|
+
&__ai-source-groups {
|
|
941
|
+
list-style: none;
|
|
763
942
|
padding-left: 0;
|
|
764
|
-
margin:
|
|
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);
|
|
765
949
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
950
|
+
li {
|
|
951
|
+
display: list-item;
|
|
952
|
+
margin: 0;
|
|
953
|
+
padding: 0;
|
|
954
|
+
}
|
|
770
955
|
}
|
|
771
956
|
}
|
|
772
957
|
|
|
958
|
+
|
|
959
|
+
|
|
773
960
|
&__ai-snippet {
|
|
774
961
|
margin: 0;
|
|
775
962
|
opacity: 0.85;
|