@asd20/ui 3.9.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,4 +1,4 @@
|
|
|
1
|
-
|
|
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
|
-
<!--
|
|
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="-
|
|
19
|
-
ref="
|
|
20
|
-
v-model="
|
|
21
|
-
@keyup.enter.native.stop.prevent="
|
|
22
|
-
placeholder="
|
|
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="
|
|
23
|
+
label="Go"
|
|
27
24
|
size="md"
|
|
28
25
|
bordered
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
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
|
-
!
|
|
52
|
+
!inputText &&
|
|
69
53
|
!searchingFiles &&
|
|
70
54
|
!searchingPages &&
|
|
71
55
|
!searchingAi
|
|
72
56
|
"
|
|
73
|
-
title="Ask a Question
|
|
74
|
-
description="
|
|
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 -->
|
|
@@ -87,15 +72,18 @@
|
|
|
87
72
|
<div v-if="aiAnswer" class="asd20-site-search__ai-result">
|
|
88
73
|
<h3>Answer</h3>
|
|
89
74
|
<div class="asd20-site-search__ai-answer" v-html="aiAnswer" />
|
|
90
|
-
|
|
91
75
|
<div
|
|
92
|
-
v-if="
|
|
93
|
-
class="asd20-site-search__ai-
|
|
76
|
+
v-if="aiKeywordsDisplay"
|
|
77
|
+
class="asd20-site-search__ai-keywords"
|
|
94
78
|
>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
79
|
+
{{ aiKeywordsDisplay }}
|
|
80
|
+
</div>
|
|
81
|
+
|
|
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">
|
|
99
87
|
<ul class="asd20-site-search__ai-source-list">
|
|
100
88
|
<li v-for="src in group.sources" :key="src.url || src.id">
|
|
101
89
|
<a :href="src.url" target="_blank" rel="noreferrer">
|
|
@@ -103,9 +91,10 @@
|
|
|
103
91
|
</a>
|
|
104
92
|
</li>
|
|
105
93
|
</ul>
|
|
106
|
-
</
|
|
107
|
-
</
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
108
96
|
</div>
|
|
97
|
+
-->
|
|
109
98
|
|
|
110
99
|
<asd20-loader v-if="searchingAi" size="lg" />
|
|
111
100
|
</div>
|
|
@@ -134,6 +123,13 @@
|
|
|
134
123
|
/>
|
|
135
124
|
</asd20-list>
|
|
136
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>
|
|
137
133
|
</div>
|
|
138
134
|
|
|
139
135
|
<!-- Files tab -->
|
|
@@ -204,6 +200,14 @@ import mapFilesToListItems from '../../../helpers/mapFilesToListItems'
|
|
|
204
200
|
// Mixins
|
|
205
201
|
import globalPropMixinFactory from '../../../mixins/globalPropMixinFactory.js'
|
|
206
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
|
+
|
|
207
211
|
export default {
|
|
208
212
|
name: 'Asd20SiteSearch',
|
|
209
213
|
|
|
@@ -269,16 +273,18 @@ export default {
|
|
|
269
273
|
|
|
270
274
|
data: () => ({
|
|
271
275
|
currentTab: 'Answers',
|
|
272
|
-
//
|
|
276
|
+
// unified input
|
|
277
|
+
inputText: '',
|
|
273
278
|
keywords: '',
|
|
274
|
-
// question input (AI)
|
|
275
|
-
questionText: '',
|
|
276
279
|
aiAnswer: null,
|
|
277
280
|
searchingAi: false,
|
|
278
281
|
aiSources: [],
|
|
279
282
|
selectedGroup: null,
|
|
280
283
|
searchingPages: false,
|
|
281
284
|
searchingFiles: false,
|
|
285
|
+
keywordsFromAi: false,
|
|
286
|
+
aiKeywordsDisplay: '',
|
|
287
|
+
skipKeywordWatcher: false,
|
|
282
288
|
}),
|
|
283
289
|
|
|
284
290
|
computed: {
|
|
@@ -349,13 +355,17 @@ export default {
|
|
|
349
355
|
aiUiSources() {
|
|
350
356
|
return (this.aiSources || []).map(src => ({
|
|
351
357
|
...src,
|
|
358
|
+
title: this.decodeHtml(src.title),
|
|
352
359
|
hostLabel:
|
|
353
360
|
this.resolveOrgTitleFromUrl(src.url) || this.labelFromUrl(src.url),
|
|
354
361
|
}))
|
|
355
362
|
},
|
|
356
363
|
aiGroupedSources() {
|
|
357
364
|
const groups = new Map()
|
|
358
|
-
this.aiUiSources.
|
|
365
|
+
const filteredSources = this.aiUiSources.filter((src) =>
|
|
366
|
+
this.isSourceCurrentOrg(src.url, src.hostLabel)
|
|
367
|
+
)
|
|
368
|
+
filteredSources.forEach(src => {
|
|
359
369
|
const key = src.hostLabel || 'Academy District 20'
|
|
360
370
|
if (!groups.has(key)) {
|
|
361
371
|
groups.set(key, { hostLabel: key, sources: [] })
|
|
@@ -388,47 +398,60 @@ export default {
|
|
|
388
398
|
active(newVal) {
|
|
389
399
|
if (newVal) {
|
|
390
400
|
// Focus keyword search by default for power users
|
|
391
|
-
if (this.$refs.
|
|
392
|
-
const input = this.$refs.
|
|
401
|
+
if (this.$refs.query && this.$refs.query.$el) {
|
|
402
|
+
const input = this.$refs.query.$el.querySelector('input')
|
|
393
403
|
if (input) input.focus()
|
|
394
404
|
}
|
|
395
|
-
} else if (this.$refs.
|
|
396
|
-
const input = this.$refs.
|
|
405
|
+
} else if (this.$refs.query && this.$refs.query.$el) {
|
|
406
|
+
const input = this.$refs.query.$el.querySelector('input')
|
|
397
407
|
if (input) input.blur()
|
|
398
408
|
}
|
|
399
409
|
},
|
|
400
410
|
|
|
401
|
-
//
|
|
411
|
+
// keyword updates trigger searches unless explicitly skipped
|
|
402
412
|
keywords: debounce(function (newVal, oldVal) {
|
|
413
|
+
if (this.skipKeywordWatcher) {
|
|
414
|
+
this.skipKeywordWatcher = false
|
|
415
|
+
return
|
|
416
|
+
}
|
|
403
417
|
if (newVal !== oldVal) {
|
|
404
418
|
this.searchPages()
|
|
405
419
|
this.searchFiles()
|
|
406
420
|
this.searchGroups()
|
|
407
421
|
}
|
|
408
|
-
},
|
|
422
|
+
}, 400),
|
|
409
423
|
|
|
410
424
|
_includeDistrictResults() {
|
|
411
425
|
this.searchPages()
|
|
412
426
|
this.searchFiles()
|
|
413
427
|
},
|
|
414
428
|
|
|
415
|
-
// Clear AI answer if user clears the
|
|
416
|
-
|
|
429
|
+
// Clear AI answer if user clears the input
|
|
430
|
+
inputText(newVal) {
|
|
417
431
|
if (!newVal) {
|
|
418
432
|
this.aiAnswer = null
|
|
419
433
|
this.aiSources = []
|
|
434
|
+
this.aiKeywordsDisplay = ''
|
|
420
435
|
}
|
|
421
436
|
},
|
|
422
437
|
},
|
|
423
438
|
|
|
424
439
|
methods: {
|
|
425
|
-
async searchPages() {
|
|
440
|
+
async searchPages(options = {}) {
|
|
426
441
|
if (this.$store && this.$store.state.search) {
|
|
442
|
+
const keepAnswersTab = options.keepAnswersTab || this.keywordsFromAi
|
|
427
443
|
this.searchingPages = true
|
|
428
444
|
await this.$store.dispatch('search/queryPages', this.keywords)
|
|
429
445
|
this.searchingPages = false
|
|
430
|
-
if (this.keywords && this.keywords.trim()) {
|
|
446
|
+
if (this.keywords && this.keywords.trim() && !keepAnswersTab) {
|
|
431
447
|
this.currentTab = 'Pages'
|
|
448
|
+
this.scrollResultsToTop()
|
|
449
|
+
}
|
|
450
|
+
this.keywordsFromAi = false
|
|
451
|
+
if (!keepAnswersTab) {
|
|
452
|
+
this.aiAnswer = null
|
|
453
|
+
this.aiSources = []
|
|
454
|
+
this.aiKeywordsDisplay = ''
|
|
432
455
|
}
|
|
433
456
|
}
|
|
434
457
|
},
|
|
@@ -449,6 +472,9 @@ export default {
|
|
|
449
472
|
|
|
450
473
|
onTabClick(tab) {
|
|
451
474
|
this.currentTab = tab.label
|
|
475
|
+
if (tab.label === 'Pages') {
|
|
476
|
+
this.scrollResultsToTop()
|
|
477
|
+
}
|
|
452
478
|
},
|
|
453
479
|
|
|
454
480
|
onTab(event) {
|
|
@@ -456,6 +482,16 @@ export default {
|
|
|
456
482
|
this.dismiss()
|
|
457
483
|
},
|
|
458
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
|
+
|
|
459
495
|
onBackgroundClick(e) {
|
|
460
496
|
if (e.target === this.$refs.container) this.dismiss()
|
|
461
497
|
},
|
|
@@ -470,27 +506,16 @@ export default {
|
|
|
470
506
|
labelFromUrl(url) {
|
|
471
507
|
if (!url) return 'Academy District 20'
|
|
472
508
|
try {
|
|
473
|
-
const host = new URL(url).hostname
|
|
509
|
+
const host = new URL(url).hostname.toLowerCase()
|
|
474
510
|
|
|
475
511
|
if (host === 'www.asd20.org') return 'Academy District 20'
|
|
476
512
|
|
|
477
513
|
if (host.endsWith('.asd20.org')) {
|
|
478
514
|
const subdomain = host.replace('.asd20.org', '')
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
return 'Pine Creek High School'
|
|
484
|
-
case 'dcchigh':
|
|
485
|
-
return 'Discovery Canyon Campus High School'
|
|
486
|
-
case 'liberty':
|
|
487
|
-
return 'Liberty High School'
|
|
488
|
-
case 'd20online':
|
|
489
|
-
return 'Academy Online High School'
|
|
490
|
-
// add more mappings as needed
|
|
491
|
-
default:
|
|
492
|
-
return subdomain.charAt(0).toUpperCase() + subdomain.slice(1)
|
|
493
|
-
}
|
|
515
|
+
return (
|
|
516
|
+
SUBDOMAIN_LABELS[subdomain] ||
|
|
517
|
+
subdomain.replace(/(^|[-_.])([a-z])/g, (_, __, c) => c.toUpperCase())
|
|
518
|
+
)
|
|
494
519
|
}
|
|
495
520
|
|
|
496
521
|
return host
|
|
@@ -519,15 +544,55 @@ export default {
|
|
|
519
544
|
}
|
|
520
545
|
},
|
|
521
546
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const
|
|
525
|
-
|
|
547
|
+
isSourceCurrentOrg(url, hostLabel) {
|
|
548
|
+
const org = this.organization || {}
|
|
549
|
+
const orgTitle = (org.title || '').toLowerCase()
|
|
550
|
+
const orgWebsite = org.website || ''
|
|
551
|
+
|
|
552
|
+
// If org website is available, match by hostname
|
|
553
|
+
if (orgWebsite) {
|
|
554
|
+
try {
|
|
555
|
+
const orgHost = new URL(orgWebsite).hostname.toLowerCase()
|
|
556
|
+
const srcHost = url ? new URL(url).hostname.toLowerCase() : ''
|
|
557
|
+
if (orgHost && srcHost && (srcHost === orgHost || srcHost.endsWith(`.${orgHost}`))) {
|
|
558
|
+
return true
|
|
559
|
+
}
|
|
560
|
+
} catch {
|
|
561
|
+
// fall through to title comparison
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Fallback to title match
|
|
566
|
+
if (hostLabel && orgTitle) {
|
|
567
|
+
return hostLabel.toLowerCase() === orgTitle
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// If no organization info, include nothing
|
|
571
|
+
return false
|
|
572
|
+
},
|
|
573
|
+
|
|
574
|
+
onSubmitInput() {
|
|
575
|
+
const text = (this.inputText || '').trim()
|
|
576
|
+
if (!text) {
|
|
526
577
|
this.aiAnswer = null
|
|
527
578
|
this.aiSources = []
|
|
579
|
+
this.aiKeywordsDisplay = ''
|
|
528
580
|
return
|
|
529
581
|
}
|
|
530
|
-
|
|
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
|
+
}
|
|
531
596
|
},
|
|
532
597
|
|
|
533
598
|
async searchAi(question) {
|
|
@@ -556,8 +621,33 @@ export default {
|
|
|
556
621
|
}
|
|
557
622
|
)
|
|
558
623
|
|
|
559
|
-
|
|
624
|
+
const { cleanAnswer, keywords: aiKeywords } =
|
|
625
|
+
this.extractKeywordsAndCleanAnswer(answer || '')
|
|
626
|
+
|
|
627
|
+
this.aiAnswer = this.sanitizeAnswer(cleanAnswer)
|
|
560
628
|
this.aiSources = sources || []
|
|
629
|
+
|
|
630
|
+
const safeKeywords = this.sanitizeKeywords(aiKeywords)
|
|
631
|
+
const fallbackKeywords = safeKeywords
|
|
632
|
+
? ''
|
|
633
|
+
: this.deriveFallbackKeywords(this.inputText)
|
|
634
|
+
const keywordsToUse = safeKeywords || fallbackKeywords
|
|
635
|
+
|
|
636
|
+
// Show the AI-provided keywords (or fallback) in brackets for visibility.
|
|
637
|
+
const displayKeywordsSource = aiKeywords || keywordsToUse || ''
|
|
638
|
+
this.aiKeywordsDisplay = displayKeywordsSource
|
|
639
|
+
? `[Keywords: ${displayKeywordsSource.replace(/<[^>]*>/g, '')}]`
|
|
640
|
+
: ''
|
|
641
|
+
|
|
642
|
+
if (keywordsToUse) {
|
|
643
|
+
this.keywordsFromAi = true
|
|
644
|
+
this.skipKeywordWatcher = true
|
|
645
|
+
this.keywords = keywordsToUse
|
|
646
|
+
this.searchPages({ keepAnswersTab: true })
|
|
647
|
+
this.searchFiles()
|
|
648
|
+
this.searchGroups()
|
|
649
|
+
}
|
|
650
|
+
|
|
561
651
|
this.currentTab = 'Answers'
|
|
562
652
|
} catch (e) {
|
|
563
653
|
console.error('AI search failed', e)
|
|
@@ -624,6 +714,162 @@ export default {
|
|
|
624
714
|
cleanNode(container)
|
|
625
715
|
return container.innerHTML
|
|
626
716
|
},
|
|
717
|
+
|
|
718
|
+
extractKeywordsAndCleanAnswer(answerText) {
|
|
719
|
+
if (!answerText) return { cleanAnswer: '', keywords: '' }
|
|
720
|
+
|
|
721
|
+
// Grab the last occurrence of a keywords marker to reduce prompt-injection tricks.
|
|
722
|
+
// Accepts `[Keywords: ...]` or `Keywords: ...` without brackets.
|
|
723
|
+
const regex = /\[?\s*keywords[:\-\s]*([^\]\n\r]+)\]?/gi
|
|
724
|
+
let match
|
|
725
|
+
let lastMatch = null
|
|
726
|
+
while ((match = regex.exec(answerText)) !== null) {
|
|
727
|
+
lastMatch = match
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const rawKeywords = lastMatch ? (lastMatch[1] || '').trim() : ''
|
|
731
|
+
|
|
732
|
+
// Keep the keywords block visible for now. To hide later, uncomment below:
|
|
733
|
+
const cleanAnswer = lastMatch
|
|
734
|
+
? `${answerText.slice(0, lastMatch.index)}${answerText
|
|
735
|
+
.slice(lastMatch.index + lastMatch[0].length)
|
|
736
|
+
.trim()}`
|
|
737
|
+
: answerText
|
|
738
|
+
|
|
739
|
+
return { cleanAnswer: cleanAnswer.trim(), keywords: rawKeywords }
|
|
740
|
+
},
|
|
741
|
+
|
|
742
|
+
sanitizeKeywords(rawKeywords) {
|
|
743
|
+
if (!rawKeywords || typeof rawKeywords !== 'string') return ''
|
|
744
|
+
|
|
745
|
+
// Remove brackets, leading "keywords" label, and weird punctuation; keep basic word chars, spaces, hyphens, commas
|
|
746
|
+
const cleaned = rawKeywords
|
|
747
|
+
.replace(/<[^>]*>/g, ' ')
|
|
748
|
+
.replace(/[\[\]\(\)\{\}]/g, ' ')
|
|
749
|
+
.replace(/\bkeywords\b[:\-\s]*/i, '')
|
|
750
|
+
.replace(/[^a-z0-9,\-\s]/gi, ' ')
|
|
751
|
+
.trim()
|
|
752
|
+
|
|
753
|
+
if (!cleaned) return ''
|
|
754
|
+
|
|
755
|
+
// Split on commas/semicolons/pipes first; fall back to spaces
|
|
756
|
+
const parts =
|
|
757
|
+
cleaned.split(/[,;|]/).filter(Boolean).map(p => p.trim()).filter(Boolean)
|
|
758
|
+
const tokens =
|
|
759
|
+
parts.length > 0
|
|
760
|
+
? parts
|
|
761
|
+
: cleaned.split(/\s+/).filter(Boolean)
|
|
762
|
+
|
|
763
|
+
const unique = []
|
|
764
|
+
for (const token of tokens) {
|
|
765
|
+
if (!token) continue
|
|
766
|
+
if (token.length <= 1) continue
|
|
767
|
+
const clipped = token.slice(0, 32)
|
|
768
|
+
if (!unique.includes(clipped)) {
|
|
769
|
+
unique.push(clipped)
|
|
770
|
+
}
|
|
771
|
+
if (unique.length >= 3) break
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const result = unique.join(' ').slice(0, 100).trim()
|
|
775
|
+
return result
|
|
776
|
+
},
|
|
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
|
+
|
|
818
|
+
deriveFallbackKeywords(questionText) {
|
|
819
|
+
if (!questionText) return ''
|
|
820
|
+
const STOP = new Set([
|
|
821
|
+
'a',
|
|
822
|
+
'an',
|
|
823
|
+
'the',
|
|
824
|
+
'and',
|
|
825
|
+
'or',
|
|
826
|
+
'of',
|
|
827
|
+
'for',
|
|
828
|
+
'to',
|
|
829
|
+
'in',
|
|
830
|
+
'on',
|
|
831
|
+
'with',
|
|
832
|
+
'at',
|
|
833
|
+
'by',
|
|
834
|
+
'from',
|
|
835
|
+
'about',
|
|
836
|
+
'as',
|
|
837
|
+
'is',
|
|
838
|
+
'are',
|
|
839
|
+
'was',
|
|
840
|
+
'were',
|
|
841
|
+
'be',
|
|
842
|
+
'been',
|
|
843
|
+
'being',
|
|
844
|
+
'this',
|
|
845
|
+
'that',
|
|
846
|
+
'these',
|
|
847
|
+
'those',
|
|
848
|
+
'it',
|
|
849
|
+
'its',
|
|
850
|
+
'your',
|
|
851
|
+
'you',
|
|
852
|
+
'we',
|
|
853
|
+
'us',
|
|
854
|
+
'our',
|
|
855
|
+
'i',
|
|
856
|
+
'me',
|
|
857
|
+
'my',
|
|
858
|
+
])
|
|
859
|
+
const tokens = questionText
|
|
860
|
+
.toLowerCase()
|
|
861
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
862
|
+
.split(/\s+/)
|
|
863
|
+
.filter(Boolean)
|
|
864
|
+
.filter((t) => !STOP.has(t))
|
|
865
|
+
|
|
866
|
+
const uniq = []
|
|
867
|
+
for (const t of tokens) {
|
|
868
|
+
if (!uniq.includes(t)) uniq.push(t)
|
|
869
|
+
if (uniq.length >= 3) break
|
|
870
|
+
}
|
|
871
|
+
return uniq.join(' ')
|
|
872
|
+
},
|
|
627
873
|
},
|
|
628
874
|
}
|
|
629
875
|
</script>
|
|
@@ -722,8 +968,8 @@ export default {
|
|
|
722
968
|
::v-deep ol,
|
|
723
969
|
::v-deep ul {
|
|
724
970
|
display: block !important;
|
|
725
|
-
list-style-position:
|
|
726
|
-
padding-left:
|
|
971
|
+
list-style-position: outside;
|
|
972
|
+
padding-left: 1.5rem;
|
|
727
973
|
margin-left: 0;
|
|
728
974
|
flex: 0 0 auto;
|
|
729
975
|
flex-wrap: nowrap;
|
|
@@ -747,26 +993,28 @@ export default {
|
|
|
747
993
|
|
|
748
994
|
ul {
|
|
749
995
|
list-style: none;
|
|
750
|
-
padding-left: 0;
|
|
751
996
|
margin: 0;
|
|
752
997
|
}
|
|
998
|
+
}
|
|
753
999
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
1000
|
+
&__ai-source-groups {
|
|
1001
|
+
list-style: none;
|
|
1002
|
+
padding-left: 0;
|
|
1003
|
+
margin: 0;
|
|
758
1004
|
}
|
|
759
1005
|
|
|
760
1006
|
&__ai-source-list {
|
|
1007
|
+
display: block;
|
|
761
1008
|
list-style: disc;
|
|
762
|
-
list-style-position:
|
|
763
|
-
padding-left: 0;
|
|
764
|
-
margin:
|
|
1009
|
+
list-style-position: outside;
|
|
1010
|
+
padding-left: space(0.5);
|
|
1011
|
+
margin: 0 0 0.35rem space(0.5);
|
|
765
1012
|
|
|
766
1013
|
li {
|
|
1014
|
+
list-style: disc;
|
|
1015
|
+
list-style-position: outside;
|
|
767
1016
|
display: list-item;
|
|
768
1017
|
margin: 0;
|
|
769
|
-
padding: 0;
|
|
770
1018
|
}
|
|
771
1019
|
}
|
|
772
1020
|
|