@docsector/docsector-reader 4.3.3 → 4.4.1
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/.env.example +18 -0
- package/README.md +135 -4
- package/bin/docsector.js +36 -1
- package/docsector.config.js +44 -0
- package/package.json +3 -2
- package/public/robots.txt +4 -0
- package/src/ai-assistant/config.js +91 -0
- package/src/ai-assistant/indexing.js +50 -0
- package/src/ai-assistant/layout.js +56 -0
- package/src/ai-assistant/messages.js +41 -0
- package/src/ai-assistant/panel.js +22 -0
- package/src/ai-assistant/server.js +348 -0
- package/src/ai-assistant/session.js +91 -0
- package/src/ai-assistant/stream.js +125 -0
- package/src/components/DAssistantPanel.vue +714 -0
- package/src/components/DPage.vue +128 -4
- package/src/components/DPageRichContent.vue +105 -0
- package/src/components/DPageTokens.vue +27 -16
- package/src/components/api-block-model.js +77 -1
- package/src/components/inline-code-copy.js +58 -0
- package/src/components/page-section-tokens.js +6 -4
- package/src/components/quasar-api-extends.json +235 -0
- package/src/composables/useAssistant.js +201 -0
- package/src/i18n/helpers.js +2 -0
- package/src/i18n/languages/en-US.hjson +22 -0
- package/src/i18n/languages/pt-BR.hjson +22 -0
- package/src/layouts/DefaultLayout.vue +22 -0
- package/src/markdown-agent.js +32 -0
- package/src/pages/manual/basic/ai-assistant.overview.en-US.md +69 -0
- package/src/pages/manual/basic/ai-assistant.overview.pt-BR.md +69 -0
- package/src/pages/manual.index.js +29 -0
- package/src/quasar.factory.js +166 -33
- package/src/sitemap.js +103 -0
- package/src/store/Layout.js +9 -1
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
{
|
|
2
|
+
"props": {
|
|
3
|
+
"readonly": {
|
|
4
|
+
"type": "Boolean",
|
|
5
|
+
"desc": "Put component in readonly mode",
|
|
6
|
+
"category": "state"
|
|
7
|
+
},
|
|
8
|
+
"disable": {
|
|
9
|
+
"type": "Boolean",
|
|
10
|
+
"desc": "Put component in disabled mode",
|
|
11
|
+
"category": "state"
|
|
12
|
+
},
|
|
13
|
+
"color": {
|
|
14
|
+
"type": "String",
|
|
15
|
+
"tsType": "NamedColor",
|
|
16
|
+
"desc": "Color name for component from the Quasar Color Palette",
|
|
17
|
+
"examples": ["'primary'", "'teal'", "'teal-10'"],
|
|
18
|
+
"category": "style"
|
|
19
|
+
},
|
|
20
|
+
"text-color": {
|
|
21
|
+
"type": "String",
|
|
22
|
+
"tsType": "NamedColor",
|
|
23
|
+
"desc": "Overrides text color (if needed); Color name from the Quasar Color Palette",
|
|
24
|
+
"examples": ["'primary'", "'teal'", "'teal-10'"],
|
|
25
|
+
"category": "style"
|
|
26
|
+
},
|
|
27
|
+
"dense": {
|
|
28
|
+
"type": "Boolean",
|
|
29
|
+
"desc": "Dense mode; occupies less space",
|
|
30
|
+
"category": "style"
|
|
31
|
+
},
|
|
32
|
+
"size": {
|
|
33
|
+
"type": "String",
|
|
34
|
+
"desc": "Size in CSS units, including unit name",
|
|
35
|
+
"examples": ["'16px'", "'2rem'"],
|
|
36
|
+
"category": "style"
|
|
37
|
+
},
|
|
38
|
+
"dark": {
|
|
39
|
+
"type": ["Boolean", "null"],
|
|
40
|
+
"default": "null",
|
|
41
|
+
"desc": "Notify the component that the background is a dark color",
|
|
42
|
+
"category": "style"
|
|
43
|
+
},
|
|
44
|
+
"icon": {
|
|
45
|
+
"type": "String",
|
|
46
|
+
"desc": "Icon name following Quasar convention; Make sure you have the icon library installed unless you are using 'img:' prefix; If 'none' (String) is used as value then no icon is rendered (but screen real estate will still be used for it)",
|
|
47
|
+
"examples": [
|
|
48
|
+
"'map'",
|
|
49
|
+
"'ion-add'",
|
|
50
|
+
"'img:https://cdn.quasar.dev/logo-v2/svg/logo.svg'",
|
|
51
|
+
"'img:path/to/some_image.png'"
|
|
52
|
+
],
|
|
53
|
+
"category": "content"
|
|
54
|
+
},
|
|
55
|
+
"flat": {
|
|
56
|
+
"type": "Boolean",
|
|
57
|
+
"desc": "Applies a 'flat' design (no default shadow)",
|
|
58
|
+
"category": "style"
|
|
59
|
+
},
|
|
60
|
+
"bordered": {
|
|
61
|
+
"type": "Boolean",
|
|
62
|
+
"desc": "Applies a default border to the component",
|
|
63
|
+
"category": "style"
|
|
64
|
+
},
|
|
65
|
+
"square": {
|
|
66
|
+
"type": "Boolean",
|
|
67
|
+
"desc": "Removes border-radius so borders are squared",
|
|
68
|
+
"category": "style"
|
|
69
|
+
},
|
|
70
|
+
"rounded": {
|
|
71
|
+
"type": "Boolean",
|
|
72
|
+
"desc": "Applies a small standard border-radius for a squared shape of the component",
|
|
73
|
+
"category": "style"
|
|
74
|
+
},
|
|
75
|
+
"tabindex": {
|
|
76
|
+
"type": ["Number", "String"],
|
|
77
|
+
"desc": "Tabindex HTML attribute value",
|
|
78
|
+
"examples": ["100", "'0'"],
|
|
79
|
+
"category": "general"
|
|
80
|
+
},
|
|
81
|
+
"transition": {
|
|
82
|
+
"type": "String",
|
|
83
|
+
"desc": "One of Quasar's embedded transitions",
|
|
84
|
+
"examples": ["'fade'", "'slide-down'"],
|
|
85
|
+
"category": "transition"
|
|
86
|
+
},
|
|
87
|
+
"animation-speed": {
|
|
88
|
+
"type": ["String", "Number"],
|
|
89
|
+
"desc": "Animation speed (in milliseconds, without unit)",
|
|
90
|
+
"examples": ["500", "'1200'"],
|
|
91
|
+
"category": "style"
|
|
92
|
+
},
|
|
93
|
+
"model-value": {
|
|
94
|
+
"desc": "Model of the component; Either use this property (along with a listener for 'update:model-value' event) OR use v-model directive",
|
|
95
|
+
"required": true,
|
|
96
|
+
"syncable": true,
|
|
97
|
+
"category": "model"
|
|
98
|
+
},
|
|
99
|
+
"html": {
|
|
100
|
+
"type": "Boolean",
|
|
101
|
+
"desc": "Force use of textContent instead of innerHTML to render text; Use it when the text might be unsafe (from user input)",
|
|
102
|
+
"category": "behavior"
|
|
103
|
+
},
|
|
104
|
+
"tag": {
|
|
105
|
+
"type": "String",
|
|
106
|
+
"desc": "HTML tag to use",
|
|
107
|
+
"category": "content"
|
|
108
|
+
},
|
|
109
|
+
"scroll-target": {
|
|
110
|
+
"type": ["Element", "String"],
|
|
111
|
+
"desc": "CSS selector or DOM element to be used as a custom scroll container instead of the auto detected one",
|
|
112
|
+
"examples": [
|
|
113
|
+
".scroll-target-class",
|
|
114
|
+
"#scroll-target-id",
|
|
115
|
+
"$refs.scrollTarget",
|
|
116
|
+
"document.body"
|
|
117
|
+
],
|
|
118
|
+
"category": "behavior"
|
|
119
|
+
},
|
|
120
|
+
"ripple": {
|
|
121
|
+
"type": ["Boolean", "Object"],
|
|
122
|
+
"desc": "Configure material ripple (disable it by setting it to 'false' or supply a config object)",
|
|
123
|
+
"default": "true",
|
|
124
|
+
"examples": [
|
|
125
|
+
"false",
|
|
126
|
+
"{ early: true, center: true, color: 'teal', keyCodes: [] }"
|
|
127
|
+
],
|
|
128
|
+
"category": "style"
|
|
129
|
+
},
|
|
130
|
+
"evt": {
|
|
131
|
+
"type": "Event",
|
|
132
|
+
"desc": "JS event object"
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
"slots": {
|
|
136
|
+
"default": {
|
|
137
|
+
"desc": "Default slot in the devland unslotted content of the component"
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
"events": {
|
|
141
|
+
"update:model-value": {
|
|
142
|
+
"desc": "Emitted when the component needs to change the model; Is also used by v-model",
|
|
143
|
+
"params": {
|
|
144
|
+
"value": {
|
|
145
|
+
"type": "Any",
|
|
146
|
+
"desc": "New model value",
|
|
147
|
+
"required": true
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
"show": {
|
|
152
|
+
"desc": "Emitted after component has triggered show()",
|
|
153
|
+
"params": {
|
|
154
|
+
"evt": {
|
|
155
|
+
"extends": "evt",
|
|
156
|
+
"required": true
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
"before-show": {
|
|
161
|
+
"desc": "Emitted when component triggers show() but before it finishes doing it",
|
|
162
|
+
"params": {
|
|
163
|
+
"evt": {
|
|
164
|
+
"extends": "evt",
|
|
165
|
+
"required": true
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
"after-show": {
|
|
170
|
+
"desc": "Emitted when component show animation is finished"
|
|
171
|
+
},
|
|
172
|
+
"hide": {
|
|
173
|
+
"desc": "Emitted after component has triggered hide()",
|
|
174
|
+
"params": {
|
|
175
|
+
"evt": {
|
|
176
|
+
"extends": "evt",
|
|
177
|
+
"required": true
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
"before-hide": {
|
|
182
|
+
"desc": "Emitted when component triggers hide() but before it finishes doing it",
|
|
183
|
+
"params": {
|
|
184
|
+
"evt": {
|
|
185
|
+
"extends": "evt",
|
|
186
|
+
"required": true
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
"after-hide": {
|
|
191
|
+
"desc": "Emitted when component hide animation is finished"
|
|
192
|
+
},
|
|
193
|
+
"click": {
|
|
194
|
+
"desc": "Emitted when user clicks/taps on the component",
|
|
195
|
+
"params": {
|
|
196
|
+
"evt": {
|
|
197
|
+
"extends": "evt",
|
|
198
|
+
"required": true
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
"methods": {
|
|
204
|
+
"show": {
|
|
205
|
+
"desc": "Triggers component to show",
|
|
206
|
+
"params": {
|
|
207
|
+
"evt": {
|
|
208
|
+
"extends": "evt",
|
|
209
|
+
"required": false
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
"returns": null
|
|
213
|
+
},
|
|
214
|
+
"hide": {
|
|
215
|
+
"desc": "Triggers component to hide",
|
|
216
|
+
"params": {
|
|
217
|
+
"evt": {
|
|
218
|
+
"extends": "evt",
|
|
219
|
+
"required": false
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
"returns": null
|
|
223
|
+
},
|
|
224
|
+
"toggle": {
|
|
225
|
+
"desc": "Triggers component to toggle between show/hide",
|
|
226
|
+
"params": {
|
|
227
|
+
"evt": {
|
|
228
|
+
"extends": "evt",
|
|
229
|
+
"required": false
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
"returns": null
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { computed, ref, watch } from 'vue'
|
|
2
|
+
|
|
3
|
+
import docsectorConfig from 'docsector.config.js'
|
|
4
|
+
import { normalizeAiAssistantConfig } from '../ai-assistant/config'
|
|
5
|
+
import { createAssistantRequestPayload } from '../ai-assistant/messages'
|
|
6
|
+
import { clearAssistantSession, loadAssistantSession, saveAssistantSession } from '../ai-assistant/session'
|
|
7
|
+
import { extractAssistantStreamDelta, normalizeAssistantSourceChunks, parseServerSentEvents } from '../ai-assistant/stream'
|
|
8
|
+
|
|
9
|
+
const assistantConfig = normalizeAiAssistantConfig(docsectorConfig)
|
|
10
|
+
const assistantMessages = ref([])
|
|
11
|
+
const assistantSources = ref([])
|
|
12
|
+
let assistantSessionReady = false
|
|
13
|
+
let assistantSessionWatcherReady = false
|
|
14
|
+
|
|
15
|
+
function createMessage (role, content = '') {
|
|
16
|
+
return {
|
|
17
|
+
id: `${role}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
18
|
+
role,
|
|
19
|
+
content
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getCompletionText (payload) {
|
|
24
|
+
return payload?.choices?.[0]?.message?.content
|
|
25
|
+
|| payload?.response
|
|
26
|
+
|| payload?.content
|
|
27
|
+
|| ''
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function persistAssistantSession () {
|
|
31
|
+
saveAssistantSession({
|
|
32
|
+
messages: assistantMessages.value,
|
|
33
|
+
sources: assistantSources.value
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ensureAssistantSession () {
|
|
38
|
+
if (!assistantSessionReady) {
|
|
39
|
+
const session = loadAssistantSession()
|
|
40
|
+
assistantMessages.value = session.messages
|
|
41
|
+
assistantSources.value = session.sources
|
|
42
|
+
assistantSessionReady = true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!assistantSessionWatcherReady) {
|
|
46
|
+
watch([assistantMessages, assistantSources], persistAssistantSession, { deep: true })
|
|
47
|
+
assistantSessionWatcherReady = true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default function useAssistant ({ route, locale, getContext } = {}) {
|
|
52
|
+
ensureAssistantSession()
|
|
53
|
+
|
|
54
|
+
const messages = assistantMessages
|
|
55
|
+
const sources = assistantSources
|
|
56
|
+
const loading = ref(false)
|
|
57
|
+
const error = ref('')
|
|
58
|
+
const abortController = ref(null)
|
|
59
|
+
|
|
60
|
+
const hasMessages = computed(() => messages.value.length > 0)
|
|
61
|
+
|
|
62
|
+
const clear = () => {
|
|
63
|
+
messages.value = []
|
|
64
|
+
sources.value = []
|
|
65
|
+
error.value = ''
|
|
66
|
+
clearAssistantSession()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const stop = () => {
|
|
70
|
+
abortController.value?.abort()
|
|
71
|
+
abortController.value = null
|
|
72
|
+
loading.value = false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const appendAssistantContent = (message, content) => {
|
|
76
|
+
if (!content) return
|
|
77
|
+
message.content += content
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const consumeStream = async (response, assistantMessage) => {
|
|
81
|
+
const reader = response.body.getReader()
|
|
82
|
+
const decoder = new TextDecoder()
|
|
83
|
+
let buffer = ''
|
|
84
|
+
let reading = true
|
|
85
|
+
|
|
86
|
+
while (reading) {
|
|
87
|
+
const { done, value } = await reader.read()
|
|
88
|
+
if (done) {
|
|
89
|
+
reading = false
|
|
90
|
+
break
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
buffer += decoder.decode(value, { stream: true })
|
|
94
|
+
const parts = buffer.split(/\r?\n\r?\n/)
|
|
95
|
+
buffer = parts.pop() || ''
|
|
96
|
+
|
|
97
|
+
for (const part of parts) {
|
|
98
|
+
const events = parseServerSentEvents(`${part}\n\n`)
|
|
99
|
+
for (const event of events) {
|
|
100
|
+
const delta = extractAssistantStreamDelta(event)
|
|
101
|
+
if (delta.done) return
|
|
102
|
+
appendAssistantContent(assistantMessage, delta.content)
|
|
103
|
+
if (delta.chunks.length > 0) {
|
|
104
|
+
sources.value = delta.chunks
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (buffer.trim()) {
|
|
111
|
+
const events = parseServerSentEvents(`${buffer}\n\n`)
|
|
112
|
+
for (const event of events) {
|
|
113
|
+
const delta = extractAssistantStreamDelta(event)
|
|
114
|
+
appendAssistantContent(assistantMessage, delta.content)
|
|
115
|
+
if (delta.chunks.length > 0) {
|
|
116
|
+
sources.value = delta.chunks
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const send = async (content) => {
|
|
123
|
+
const prompt = String(content || '').trim()
|
|
124
|
+
if (!prompt || loading.value) return
|
|
125
|
+
|
|
126
|
+
error.value = ''
|
|
127
|
+
sources.value = []
|
|
128
|
+
|
|
129
|
+
const userMessage = createMessage('user', prompt)
|
|
130
|
+
const assistantMessage = createMessage('assistant')
|
|
131
|
+
messages.value.push(userMessage, assistantMessage)
|
|
132
|
+
// Use the reactive proxy from the array so streamed mutations trigger
|
|
133
|
+
// live re-renders (raw object mutations bypass Vue reactivity).
|
|
134
|
+
const liveAssistantMessage = messages.value[messages.value.length - 1]
|
|
135
|
+
|
|
136
|
+
abortController.value = new AbortController()
|
|
137
|
+
loading.value = true
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const payload = createAssistantRequestPayload({
|
|
141
|
+
messages: messages.value
|
|
142
|
+
.filter(message => message.content.trim())
|
|
143
|
+
.map(({ role, content }) => ({ role, content })),
|
|
144
|
+
route: route?.value || route,
|
|
145
|
+
locale: locale?.value || locale,
|
|
146
|
+
context: typeof getContext === 'function' ? getContext() : {}
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const response = await fetch(assistantConfig.endpoint, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: { 'Content-Type': 'application/json' },
|
|
152
|
+
body: JSON.stringify(payload),
|
|
153
|
+
signal: abortController.value.signal
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
let message = `Assistant request failed with ${response.status}`
|
|
158
|
+
try {
|
|
159
|
+
const payload = await response.json()
|
|
160
|
+
message = payload?.error?.message || message
|
|
161
|
+
} catch {
|
|
162
|
+
// keep status message
|
|
163
|
+
}
|
|
164
|
+
throw new Error(message)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const contentType = response.headers.get('content-type') || ''
|
|
168
|
+
if (contentType.includes('text/event-stream') && response.body) {
|
|
169
|
+
await consumeStream(response, liveAssistantMessage)
|
|
170
|
+
} else {
|
|
171
|
+
const payload = await response.json()
|
|
172
|
+
liveAssistantMessage.content = getCompletionText(payload)
|
|
173
|
+
sources.value = normalizeAssistantSourceChunks(payload?.chunks)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!liveAssistantMessage.content.trim()) {
|
|
177
|
+
liveAssistantMessage.content = 'No answer was returned for this request.'
|
|
178
|
+
}
|
|
179
|
+
} catch (err) {
|
|
180
|
+
if (err?.name !== 'AbortError') {
|
|
181
|
+
error.value = err?.message || 'Assistant request failed.'
|
|
182
|
+
liveAssistantMessage.content = ''
|
|
183
|
+
}
|
|
184
|
+
} finally {
|
|
185
|
+
loading.value = false
|
|
186
|
+
abortController.value = null
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
config: assistantConfig,
|
|
192
|
+
messages,
|
|
193
|
+
sources,
|
|
194
|
+
loading,
|
|
195
|
+
error,
|
|
196
|
+
hasMessages,
|
|
197
|
+
send,
|
|
198
|
+
stop,
|
|
199
|
+
clear
|
|
200
|
+
}
|
|
201
|
+
}
|
package/src/i18n/helpers.js
CHANGED
|
@@ -28,6 +28,7 @@ const engineDefaults = {
|
|
|
28
28
|
newVersion: 'New in',
|
|
29
29
|
copyPage: 'Copy page',
|
|
30
30
|
copyPageCaption: 'Copy page as Markdown for LLMs',
|
|
31
|
+
copyInlineCode: 'Click to copy inline code',
|
|
31
32
|
copied: 'Copied!',
|
|
32
33
|
viewAsMarkdown: 'View as Markdown',
|
|
33
34
|
viewAsMarkdownCaption: 'View this page as plain text',
|
|
@@ -77,6 +78,7 @@ const engineDefaults = {
|
|
|
77
78
|
newVersion: 'Novo em',
|
|
78
79
|
copyPage: 'Copiar página',
|
|
79
80
|
copyPageCaption: 'Copiar página como Markdown para LLMs',
|
|
81
|
+
copyInlineCode: 'Clique para copiar o código inline',
|
|
80
82
|
copied: 'Copiado!',
|
|
81
83
|
viewAsMarkdown: 'Ver como Markdown',
|
|
82
84
|
viewAsMarkdownCaption: 'Ver esta página como texto simples',
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
newVersion: 'New in',
|
|
36
36
|
copyPage: 'Copy page',
|
|
37
37
|
copyPageCaption: 'Copy page as Markdown for LLMs',
|
|
38
|
+
copyInlineCode: 'Click to copy inline code',
|
|
38
39
|
copied: 'Copied!',
|
|
39
40
|
viewAsMarkdown: 'View as Markdown',
|
|
40
41
|
viewAsMarkdownCaption: 'View this page as plain text',
|
|
@@ -105,6 +106,27 @@
|
|
|
105
106
|
changelog: 'Changelog'
|
|
106
107
|
},
|
|
107
108
|
|
|
109
|
+
assistant: {
|
|
110
|
+
title: 'Docsector Assistant',
|
|
111
|
+
subtitle: "I'm here to help you with the docs.",
|
|
112
|
+
open: 'Open assistant',
|
|
113
|
+
close: 'Close assistant',
|
|
114
|
+
clear: 'Clear conversation',
|
|
115
|
+
placeholder: 'Ask, search, or explain...',
|
|
116
|
+
send: 'Send',
|
|
117
|
+
stop: 'Stop',
|
|
118
|
+
context: 'Based on your context',
|
|
119
|
+
sources: 'Sources',
|
|
120
|
+
sourcesCount: '{count} sources',
|
|
121
|
+
thinking: 'Searching the docs…',
|
|
122
|
+
resize: 'Resize assistant',
|
|
123
|
+
greeting: {
|
|
124
|
+
morning: 'Good morning',
|
|
125
|
+
afternoon: 'Good afternoon',
|
|
126
|
+
evening: 'Good evening'
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
|
|
108
130
|
// @
|
|
109
131
|
system: {
|
|
110
132
|
documentation: 'Documentation',
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
newVersion: 'Novo em',
|
|
35
35
|
copyPage: 'Copiar página',
|
|
36
36
|
copyPageCaption: 'Copiar página como Markdown para LLMs',
|
|
37
|
+
copyInlineCode: 'Clique para copiar o código inline',
|
|
37
38
|
copied: 'Copiado!',
|
|
38
39
|
viewAsMarkdown: 'Ver como Markdown',
|
|
39
40
|
viewAsMarkdownCaption: 'Ver esta página como texto simples',
|
|
@@ -104,6 +105,27 @@
|
|
|
104
105
|
changelog: 'Changelog'
|
|
105
106
|
},
|
|
106
107
|
|
|
108
|
+
assistant: {
|
|
109
|
+
title: 'Assistente Docsector',
|
|
110
|
+
subtitle: 'Estou aqui para ajudar com a documentação.',
|
|
111
|
+
open: 'Abrir assistente',
|
|
112
|
+
close: 'Fechar assistente',
|
|
113
|
+
clear: 'Limpar conversa',
|
|
114
|
+
placeholder: 'Pergunte, pesquise ou explique...',
|
|
115
|
+
send: 'Enviar',
|
|
116
|
+
stop: 'Parar',
|
|
117
|
+
context: 'Com base no seu contexto',
|
|
118
|
+
sources: 'Fontes',
|
|
119
|
+
sourcesCount: '{count} fontes',
|
|
120
|
+
thinking: 'Consultando a documentação…',
|
|
121
|
+
resize: 'Redimensionar assistente',
|
|
122
|
+
greeting: {
|
|
123
|
+
morning: 'Bom dia',
|
|
124
|
+
afternoon: 'Boa tarde',
|
|
125
|
+
evening: 'Boa noite'
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
|
|
107
129
|
// @
|
|
108
130
|
system: {
|
|
109
131
|
documentation: 'Documentação',
|
|
@@ -15,6 +15,17 @@
|
|
|
15
15
|
<q-icon class="q-mb-xs q-mr-sm" :name="headerTitleIcon" />
|
|
16
16
|
{{ headerTitleText }}
|
|
17
17
|
</q-toolbar-title>
|
|
18
|
+
<q-btn
|
|
19
|
+
v-if="assistantEnabled"
|
|
20
|
+
class="filled d-header__assistant-toggle"
|
|
21
|
+
:class="assistantOpen ? 'active' : null"
|
|
22
|
+
square
|
|
23
|
+
icon="auto_awesome"
|
|
24
|
+
:aria-label="t('assistant.open')"
|
|
25
|
+
@click="toggleAssistant"
|
|
26
|
+
>
|
|
27
|
+
<q-tooltip>{{ t('assistant.open') }}</q-tooltip>
|
|
28
|
+
</q-btn>
|
|
18
29
|
<q-btn class="filled" square icon="settings" aria-label="Configuration" @click="openSettingsDialog" />
|
|
19
30
|
</q-toolbar>
|
|
20
31
|
|
|
@@ -59,12 +70,15 @@ import { useMeta, colors } from 'quasar'
|
|
|
59
70
|
|
|
60
71
|
import DMenu from '../components/DMenu.vue'
|
|
61
72
|
import docsectorConfig from 'docsector.config.js'
|
|
73
|
+
import { normalizeAiAssistantConfig } from '../ai-assistant/config'
|
|
62
74
|
import { allBooks, booksByVersion } from 'virtual:docsector-books'
|
|
63
75
|
import { pageTitleI18nPath } from '../i18n/path'
|
|
64
76
|
|
|
65
77
|
defineOptions({ name: 'LayoutDefault' })
|
|
66
78
|
|
|
67
79
|
const branding = docsectorConfig.branding || {}
|
|
80
|
+
const assistantConfig = normalizeAiAssistantConfig(docsectorConfig)
|
|
81
|
+
const assistantEnabled = assistantConfig.enabled === true
|
|
68
82
|
|
|
69
83
|
const route = useRoute()
|
|
70
84
|
const router = useRouter()
|
|
@@ -75,6 +89,8 @@ const layout = ref({
|
|
|
75
89
|
menu: false
|
|
76
90
|
})
|
|
77
91
|
|
|
92
|
+
const assistantOpen = computed(() => store.state.layout.assistant)
|
|
93
|
+
|
|
78
94
|
const headerTitleIcon = computed(() => {
|
|
79
95
|
return route.matched[0].meta.icon ?? route.meta.icon
|
|
80
96
|
})
|
|
@@ -271,6 +287,10 @@ function openSettingsDialog () {
|
|
|
271
287
|
store.commit('settings/dialog', true)
|
|
272
288
|
}
|
|
273
289
|
|
|
290
|
+
function toggleAssistant () {
|
|
291
|
+
store.commit('layout/setAssistant', !store.state.layout.assistant)
|
|
292
|
+
}
|
|
293
|
+
|
|
274
294
|
function getFirstRoutePathByBook (bookId) {
|
|
275
295
|
const routes = router.options?.routes || []
|
|
276
296
|
const versionId = activeVersionId.value
|
|
@@ -356,6 +376,8 @@ store.commit('page/resetAnchors')
|
|
|
356
376
|
color: var(--d-book-tab-active-color, #ffffff)
|
|
357
377
|
.q-btn
|
|
358
378
|
border-radius: 0
|
|
379
|
+
.d-header__assistant-toggle.active
|
|
380
|
+
background: rgba(255, 255, 255, 0.16)
|
|
359
381
|
.q-btn:before
|
|
360
382
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2), 0 0 2px rgba(0, 0, 0, 0.14), 0 0 1px -2px rgba(0, 0, 0, 0.12)
|
|
361
383
|
border-radius: 0
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const MARKDOWN_AGENT_USER_AGENT_SOURCE = [
|
|
2
|
+
'GPTBot',
|
|
3
|
+
'ChatGPT-User',
|
|
4
|
+
'OAI-SearchBot',
|
|
5
|
+
'ClaudeBot',
|
|
6
|
+
'Claude-User',
|
|
7
|
+
'Claude-SearchBot',
|
|
8
|
+
'anthropic-ai',
|
|
9
|
+
'Google-Extended',
|
|
10
|
+
'Gemini-Deep-Research',
|
|
11
|
+
'PerplexityBot',
|
|
12
|
+
'Perplexity-User',
|
|
13
|
+
'Bytespider',
|
|
14
|
+
'CCBot',
|
|
15
|
+
'Meta-ExternalAgent',
|
|
16
|
+
'FacebookBot',
|
|
17
|
+
'Amazonbot',
|
|
18
|
+
'Applebot-Extended',
|
|
19
|
+
'cohere-ai',
|
|
20
|
+
'DuckAssistBot',
|
|
21
|
+
'GrokBot',
|
|
22
|
+
'AI2Bot',
|
|
23
|
+
'YouBot',
|
|
24
|
+
'PetalBot',
|
|
25
|
+
'Cloudflare-AI-Search'
|
|
26
|
+
].join('|')
|
|
27
|
+
|
|
28
|
+
export const MARKDOWN_AGENT_USER_AGENT_PATTERN = new RegExp(MARKDOWN_AGENT_USER_AGENT_SOURCE, 'i')
|
|
29
|
+
|
|
30
|
+
export function matchesMarkdownAgentUserAgent (userAgent = '') {
|
|
31
|
+
return MARKDOWN_AGENT_USER_AGENT_PATTERN.test(String(userAgent || ''))
|
|
32
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
## Overview
|
|
2
|
+
|
|
3
|
+
The AI Assistant adds an optional documentation chat panel to Docsector pages. It is designed for semantic RAG workflows on Cloudflare while keeping the browser integration simple: users open a drawer from the global header, and the Cloudflare Pages Function talks to the configured AI provider.
|
|
4
|
+
|
|
5
|
+
The first provider is Cloudflare AI Search. It can crawl Docsector's generated Markdown sitemap, retrieve relevant chunks with hybrid search, and stream an answer with source chunks that the panel can show as citations.
|
|
6
|
+
|
|
7
|
+
## What It Adds
|
|
8
|
+
|
|
9
|
+
- A right-side assistant drawer on desktop.
|
|
10
|
+
- A fullscreen assistant dialog on mobile.
|
|
11
|
+
- Suggested prompts, current-page context, streaming responses, and source links.
|
|
12
|
+
- Build-time AI Search artifacts when `siteUrl` is configured.
|
|
13
|
+
- A same-origin internal endpoint so credentials stay server-side.
|
|
14
|
+
|
|
15
|
+
## Enable It
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
export default {
|
|
19
|
+
siteUrl: 'https://docs.example.com',
|
|
20
|
+
|
|
21
|
+
aiAssistant: {
|
|
22
|
+
enabled: true,
|
|
23
|
+
provider: 'aiSearch',
|
|
24
|
+
endpoint: '/assistant',
|
|
25
|
+
aiSearch: {
|
|
26
|
+
binding: 'AI_SEARCH',
|
|
27
|
+
instanceNameEnv: 'AI_SEARCH_INSTANCE_NAME',
|
|
28
|
+
accountIdEnv: 'CLOUDFLARE_ACCOUNT_ID',
|
|
29
|
+
apiTokenEnv: 'CLOUDFLARE_API_TOKEN',
|
|
30
|
+
retrievalType: 'hybrid',
|
|
31
|
+
maxResults: 6,
|
|
32
|
+
stream: true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Set `AI_SEARCH_INSTANCE_NAME` in Cloudflare Pages environment variables for deployed environments, or in `.dev.vars` when running `wrangler pages dev` locally.
|
|
39
|
+
|
|
40
|
+
## Cloudflare AI Search
|
|
41
|
+
|
|
42
|
+
Create an AI Search instance and configure a Website data source. Docsector always publishes `/sitemap.xml` during build and advertises it from `robots.txt`, so Cloudflare's crawler can discover the site automatically.
|
|
43
|
+
|
|
44
|
+
For cleaner retrieval, point the specific sitemap setting to:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
https://docs.example.com/ai-search-sitemap.xml
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The AI Search sitemap points to Markdown URLs, which are cleaner for retrieval than rendered SPA HTML. The manifest at `/.well-known/ai-search/manifest.json` lists titles, routes, locales, books, versions, and subpages for the same source set.
|
|
51
|
+
|
|
52
|
+
## Runtime Endpoint
|
|
53
|
+
|
|
54
|
+
The generated Pages Function accepts chat messages, current route metadata, locale, and optional selected page text. It forwards the request to AI Search by binding when available, or by REST using encrypted Cloudflare environment variables. The endpoint is an internal API for the drawer, not a page users navigate to.
|
|
55
|
+
|
|
56
|
+
The browser never needs a Cloudflare API token.
|
|
57
|
+
|
|
58
|
+
## Validate
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx docsector build
|
|
62
|
+
cat dist/spa/sitemap.xml
|
|
63
|
+
cat dist/spa/robots.txt
|
|
64
|
+
cat dist/spa/ai-search-sitemap.xml
|
|
65
|
+
cat dist/spa/.well-known/ai-search/manifest.json
|
|
66
|
+
npx wrangler pages dev dist/spa
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Remote AI Search and Workers AI bindings can incur Cloudflare usage during local development.
|