docus 5.11.0 → 5.12.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.
Files changed (30) hide show
  1. package/app/app.config.ts +3 -0
  2. package/app/app.vue +22 -27
  3. package/app/components/app/AppSearch.vue +54 -0
  4. package/app/components/docs/DocsAsideRight.vue +1 -2
  5. package/app/components/docs/DocsAsideRightBottom.vue +2 -2
  6. package/app/components/docs/DocsPageHeaderLinks.vue +4 -4
  7. package/app/composables/useDocusShortcuts.ts +24 -0
  8. package/app/composables/useUIConfig.ts +1 -1
  9. package/app/error.vue +1 -10
  10. package/app/pages/[[lang]]/[...slug].vue +6 -5
  11. package/app/types/index.d.ts +22 -0
  12. package/i18n/locales/pl.json +2 -2
  13. package/modules/assistant/index.ts +9 -3
  14. package/modules/assistant/runtime/components/AssistantChat.vue +5 -2
  15. package/modules/assistant/runtime/components/AssistantComark.ts +9 -0
  16. package/modules/assistant/runtime/components/AssistantFloatingInput.vue +9 -10
  17. package/modules/assistant/runtime/components/AssistantIndicator.vue +116 -0
  18. package/modules/assistant/runtime/components/AssistantPanel.vue +268 -258
  19. package/modules/assistant/runtime/components/AssistantPreStream.vue +1 -1
  20. package/modules/assistant/runtime/composables/useAssistant.ts +34 -38
  21. package/modules/assistant/runtime/server/api/search.ts +22 -42
  22. package/modules/css.ts +8 -0
  23. package/nuxt.schema.ts +28 -0
  24. package/package.json +46 -28
  25. package/server/mcp/tools/get-page.ts +11 -5
  26. package/server/mcp/tools/list-pages.ts +5 -4
  27. package/server/routes/sitemap.xml.ts +7 -4
  28. package/server/utils/content.ts +4 -0
  29. package/modules/assistant/runtime/components/AssistantLoading.vue +0 -164
  30. package/modules/assistant/runtime/components/AssistantMatrix.vue +0 -92
@@ -1,42 +1,47 @@
1
1
  <script setup lang="ts">
2
- import { defineAsyncComponent } from 'vue'
3
- import type { UIMessage } from 'ai'
2
+ import type { ToolUIPart, DynamicToolUIPart } from 'ai'
3
+ import { DefaultChatTransport, isToolUIPart, isReasoningUIPart, isTextUIPart, getToolName } from 'ai'
4
4
  import { Chat } from '@ai-sdk/vue'
5
- import { DefaultChatTransport } from 'ai'
6
- import { createReusableTemplate } from '@vueuse/core'
5
+ import { isPartStreaming, isToolStreaming } from '@nuxt/ui/utils/ai'
7
6
  import { useDocusI18n } from '../../../../app/composables/useDocusI18n'
7
+ import AssistantComark from './AssistantComark'
8
+ import AssistantIndicator from './AssistantIndicator.vue'
8
9
 
9
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
- const components: Record<string, any> = {
11
- pre: defineAsyncComponent(() => import('./AssistantPreStream.vue')),
12
- }
13
-
14
- const [DefineChatContent, ReuseChatContent] = createReusableTemplate<{ showExpandButton?: boolean }>()
15
-
16
- const { isOpen, isExpanded, isMobile, panelWidth, toggleExpanded, messages, pendingMessage, clearPending, faqQuestions } = useAssistant()
10
+ const { isOpen, isStudioExpanded, messages, faqQuestions } = useAssistant()
17
11
  const config = useRuntimeConfig()
18
12
  const toast = useToast()
19
13
  const { t } = useDocusI18n()
20
14
  const input = ref('')
21
15
 
16
+ const open = computed({
17
+ get: () => isOpen.value && !isStudioExpanded.value,
18
+ set: (value) => {
19
+ if (!isStudioExpanded.value) {
20
+ isOpen.value = value
21
+ }
22
+ },
23
+ })
24
+
22
25
  const displayTitle = computed(() => t('assistant.title'))
23
26
  const displayPlaceholder = computed(() => t('assistant.placeholder'))
24
27
 
28
+ let _skipSync = false
29
+
25
30
  const chat = new Chat({
26
31
  messages: messages.value,
27
32
  transport: new DefaultChatTransport({
28
33
  api: (config.app?.baseURL.replace(/\/$/, '') || '') + config.public.assistant.apiPath,
29
34
  }),
30
35
  onError: (error: Error) => {
31
- const message = (() => {
36
+ let message = error.message
37
+ if (typeof message === 'string' && message[0] === '{') {
32
38
  try {
33
- const parsed = JSON.parse(error.message)
34
- return parsed?.message || error.message
39
+ message = JSON.parse(message).message || message
35
40
  }
36
41
  catch {
37
- return error.message
42
+ // keep original on malformed JSON
38
43
  }
39
- })()
44
+ }
40
45
 
41
46
  toast.add({
42
47
  description: message,
@@ -46,284 +51,289 @@ const chat = new Chat({
46
51
  })
47
52
  },
48
53
  onFinish: () => {
49
- messages.value = chat.messages
54
+ _skipSync = true
55
+ messages.value = [...chat.messages]
56
+ nextTick(() => {
57
+ _skipSync = false
58
+ })
50
59
  },
51
60
  })
52
61
 
53
- watch(pendingMessage, (message: string | undefined) => {
54
- if (message) {
55
- if (messages.value.length === 0 && chat.messages.length > 0) {
56
- chat.messages.length = 0
57
- }
58
- chat.sendMessage({
59
- text: message,
60
- })
61
- clearPending()
62
- }
63
- }, { immediate: true })
62
+ watch(messages, (newMessages) => {
63
+ if (_skipSync) return
64
64
 
65
- watch(messages, (newMessages: UIMessage[]) => {
66
- if (newMessages.length === 0 && chat.messages.length > 0) {
67
- chat.messages.length = 0
65
+ chat.messages = newMessages
66
+ if (chat.lastMessage?.role === 'user' && chat.status !== 'streaming') {
67
+ chat.regenerate()
68
68
  }
69
- }, { deep: true })
70
-
71
- const lastMessage = computed(() => chat.messages.at(-1))
72
- const showThinking = computed(() =>
73
- chat.status === 'streaming'
74
- && lastMessage.value?.role === 'assistant'
75
- && !lastMessage.value?.parts?.some((p: { type: string }) => p.type === 'text'),
76
- )
77
-
78
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
- function getMessageToolCalls(message: any) {
80
- if (!message?.parts) return []
81
- return message.parts
82
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
- .filter((p: any) => p.type === 'data-tool-calls')
84
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
- .flatMap((p: any) => p.data?.tools || [])
69
+ })
70
+
71
+ const canClear = computed(() => messages.value.length > 0)
72
+
73
+ type ToolPart = ToolUIPart | DynamicToolUIPart
74
+ type ToolState = ToolPart['state']
75
+
76
+ function getToolMessage(state: ToolState, toolName: string, input: Record<string, string | undefined>) {
77
+ const searchVerb = state === 'output-available' ? 'Searched' : 'Searching'
78
+ const readVerb = state === 'output-available' ? 'Read' : 'Reading'
79
+
80
+ return {
81
+ 'list-pages': `${searchVerb} pages`,
82
+ 'get-page': `${readVerb} ${input.path || '...'}`,
83
+ }[toolName] || `${searchVerb} ${toolName}`
84
+ }
85
+
86
+ function getToolText(part: ToolPart) {
87
+ return getToolMessage(part.state, getToolName(part), (part.input || {}) as Record<string, string | undefined>)
86
88
  }
87
89
 
88
- function handleSubmit(event?: Event) {
89
- event?.preventDefault()
90
+ function getToolIcon(part: ToolPart): string {
91
+ const toolName = getToolName(part)
90
92
 
91
- if (!input.value.trim()) {
92
- return
93
+ return {
94
+ 'get-page': 'i-lucide-file-text',
95
+ }[toolName] || 'i-lucide-search'
96
+ }
97
+
98
+ function getToolOutput(part: ToolPart): string | undefined {
99
+ if (part.state !== 'output-available' || !part.output) return undefined
100
+
101
+ const output = part.output as Record<string, unknown>
102
+
103
+ if (getToolName(part) === 'list-pages') {
104
+ const content = (output.content ?? output) as Array<{ text?: string }> | string
105
+ if (typeof content === 'string') return content
106
+ return content
107
+ ?.map(c => c.text)
108
+ .filter(Boolean)
109
+ .join('\n') || undefined
110
+ }
111
+
112
+ if (getToolName(part) === 'get-page') {
113
+ const content = (output.content ?? output) as Array<{ text?: string }> | string
114
+ if (typeof content === 'string') {
115
+ return content.length > 500 ? `${content.slice(0, 500)}…` : content
116
+ }
117
+ const text = content?.map(c => c.text).filter(Boolean).join('\n') || ''
118
+ return text.length > 500 ? `${text.slice(0, 500)}…` : text || undefined
93
119
  }
94
120
 
95
- chat.sendMessage({
96
- text: input.value,
97
- })
121
+ return JSON.stringify(output, null, 2).slice(0, 500)
122
+ }
98
123
 
124
+ function onSubmit() {
125
+ if (!input.value.trim()) return
126
+
127
+ chat.sendMessage({ text: input.value })
99
128
  input.value = ''
100
129
  }
101
130
 
102
131
  function askQuestion(question: string) {
103
- chat.sendMessage({
104
- text: question,
105
- })
132
+ input.value = question
133
+ onSubmit()
106
134
  }
107
135
 
108
- function resetChat() {
109
- chat.stop()
136
+ function clearMessages() {
137
+ if (chat.status === 'streaming') {
138
+ chat.stop()
139
+ }
110
140
  messages.value = []
111
- chat.messages.length = 0
141
+ chat.messages = []
112
142
  }
113
143
 
114
- onMounted(() => {
115
- if (pendingMessage.value) {
116
- chat.sendMessage({
117
- text: pendingMessage.value,
118
- })
119
- clearPending()
120
- }
121
- else if (chat.lastMessage?.role === 'user') {
122
- chat.regenerate()
123
- }
144
+ defineShortcuts({
145
+ meta_i: {
146
+ handler: () => {
147
+ open.value = !open.value
148
+ },
149
+ usingInput: true,
150
+ },
124
151
  })
125
152
  </script>
126
153
 
127
154
  <template>
128
- <DefineChatContent v-slot="{ showExpandButton = true }">
129
- <div class="flex h-full flex-col">
130
- <div class="flex h-16 shrink-0 items-center justify-between border-b border-default px-4">
131
- <span class="font-medium text-highlighted">{{ displayTitle }}</span>
132
- <div class="flex items-center gap-1">
133
- <UTooltip
134
- v-if="showExpandButton"
135
- :text="isExpanded ? t('assistant.collapse') : t('assistant.expand')"
136
- >
137
- <UButton
138
- :icon="isExpanded ? 'i-lucide-minimize-2' : 'i-lucide-maximize-2'"
139
- color="neutral"
140
- variant="ghost"
141
- size="sm"
142
- class="text-muted hover:text-highlighted"
143
- @click="toggleExpanded"
144
- />
145
- </UTooltip>
146
- <UTooltip
147
- v-if="chat.messages.length > 0"
148
- :text="t('assistant.clearChat')"
155
+ <USidebar
156
+ v-model:open="open"
157
+ side="right"
158
+ :title="displayTitle"
159
+ rail
160
+ :style="{ '--sidebar-width': '24rem' }"
161
+ :ui="{ footer: 'p-0', actions: 'gap-0.5', container: '!left-auto' }"
162
+ >
163
+ <template #actions>
164
+ <UTooltip
165
+ v-if="canClear"
166
+ :text="t('assistant.clearChat')"
167
+ >
168
+ <UButton
169
+ icon="i-lucide-list-x"
170
+ color="neutral"
171
+ variant="ghost"
172
+ @click="clearMessages"
173
+ />
174
+ </UTooltip>
175
+ </template>
176
+
177
+ <template #close>
178
+ <UTooltip
179
+ :text="t('assistant.close')"
180
+ :kbds="['meta', 'i']"
181
+ >
182
+ <UButton
183
+ icon="i-lucide-panel-right-close"
184
+ color="neutral"
185
+ variant="ghost"
186
+ aria-label="Close"
187
+ @click="open = false"
188
+ />
189
+ </UTooltip>
190
+ </template>
191
+
192
+ <UTheme
193
+ :ui="{
194
+ prose: {
195
+ p: { base: 'my-2 text-sm/6' },
196
+ li: { base: 'my-0.5 text-sm/6' },
197
+ ul: { base: 'my-2' },
198
+ ol: { base: 'my-2' },
199
+ h1: { base: 'text-xl mb-4' },
200
+ h2: { base: 'text-lg mt-6 mb-3' },
201
+ h3: { base: 'text-base mt-4 mb-2' },
202
+ h4: { base: 'text-sm mt-3 mb-1.5' },
203
+ code: { base: 'text-xs' },
204
+ pre: { root: 'my-2', base: 'text-xs/5' },
205
+ table: { root: 'my-2' },
206
+ hr: { base: 'my-4' },
207
+ },
208
+ }"
209
+ >
210
+ <UChatMessages
211
+ v-if="chat.messages.length"
212
+ should-auto-scroll
213
+ :messages="chat.messages"
214
+ :status="chat.status"
215
+ compact
216
+ class="px-0 gap-2"
217
+ :user="{ ui: { container: 'max-w-full' } }"
218
+ >
219
+ <template #indicator>
220
+ <AssistantIndicator />
221
+ </template>
222
+
223
+ <template #content="{ message }">
224
+ <template
225
+ v-for="(part, index) in message.parts"
226
+ :key="`${message.id}-${part.type}-${index}`"
149
227
  >
150
- <UButton
151
- icon="i-lucide-trash-2"
152
- color="neutral"
153
- variant="ghost"
154
- size="sm"
155
- class="text-muted hover:text-highlighted"
156
- @click="resetChat"
157
- />
158
- </UTooltip>
159
- <UTooltip :text="t('assistant.close')">
160
- <UButton
161
- icon="i-lucide-x"
162
- color="neutral"
163
- variant="ghost"
164
- size="sm"
165
- class="text-muted hover:text-highlighted"
166
- @click="isOpen = false"
167
- />
168
- </UTooltip>
169
- </div>
170
- </div>
228
+ <UChatReasoning
229
+ v-if="isReasoningUIPart(part)"
230
+ :text="part.text"
231
+ :streaming="isPartStreaming(part)"
232
+ icon="i-lucide-brain"
233
+ >
234
+ <AssistantComark
235
+ :markdown="part.text"
236
+ :streaming="isPartStreaming(part)"
237
+ />
238
+ </UChatReasoning>
171
239
 
172
- <div class="min-h-0 flex-1 overflow-y-auto">
173
- <UChatMessages
174
- v-if="chat.messages.length > 0"
175
- :messages="chat.messages"
176
- compact
177
- :status="chat.status"
178
- :user="{ ui: { content: 'text-sm' } }"
179
- :ui="{ indicator: '*:bg-accented', root: 'h-auto!' }"
180
- class="px-4 py-4"
181
- >
182
- <template #content="{ message }">
183
- <div class="flex flex-col gap-2">
184
- <AssistantLoading
185
- v-if="message.role === 'assistant' && (getMessageToolCalls(message).length > 0 || (showThinking && message.id === lastMessage?.id))"
186
- :tool-calls="getMessageToolCalls(message)"
187
- :is-loading="showThinking && message.id === lastMessage?.id"
240
+ <template v-else-if="isTextUIPart(part) && part.text.length > 0">
241
+ <AssistantComark
242
+ v-if="message.role === 'assistant'"
243
+ :markdown="part.text"
244
+ :streaming="isPartStreaming(part)"
188
245
  />
189
- <template
190
- v-for="(part, index) in message.parts"
191
- :key="`${message.id}-${part.type}-${index}${'state' in part ? `-${part.state}` : ''}`"
246
+ <p
247
+ v-else-if="message.role === 'user'"
248
+ class="whitespace-pre-wrap text-sm/6"
192
249
  >
193
- <MDCCached
194
- v-if="part.type === 'text' && part.text"
195
- :value="part.text"
196
- :cache-key="`${message.id}-${index}`"
197
- :components="components"
198
- :parser-options="{ highlight: false }"
199
- class="*:first:mt-0 *:last:mb-0"
200
- />
201
- </template>
202
- </div>
250
+ {{ part.text }}
251
+ </p>
252
+ </template>
253
+
254
+ <UChatTool
255
+ v-else-if="isToolUIPart(part)"
256
+ :text="getToolText(part)"
257
+ :icon="getToolIcon(part)"
258
+ :streaming="isToolStreaming(part)"
259
+ chevron="leading"
260
+ >
261
+ <pre
262
+ v-if="getToolOutput(part)"
263
+ class="text-xs text-dimmed whitespace-pre-wrap"
264
+ v-text="getToolOutput(part)"
265
+ />
266
+ </UChatTool>
203
267
  </template>
204
- </UChatMessages>
268
+ </template>
269
+ </UChatMessages>
205
270
 
271
+ <div v-else>
206
272
  <div
207
- v-else
208
- class="p-4"
273
+ v-if="!faqQuestions?.length"
274
+ class="flex h-full flex-col items-center justify-center py-12 text-center"
209
275
  >
210
- <div
211
- v-if="!faqQuestions?.length"
212
- class="flex h-full flex-col items-center justify-center py-12 text-center"
213
- >
214
- <div class="mb-4 flex size-12 items-center justify-center rounded-full bg-primary/10">
215
- <UIcon
216
- name="i-lucide-message-circle-question"
217
- class="size-6 text-primary"
218
- />
219
- </div>
220
- <h3 class="mb-2 text-base font-medium text-highlighted">
221
- {{ t('assistant.askMeAnything') }}
222
- </h3>
223
- <p class="max-w-xs text-sm text-muted">
224
- {{ t('assistant.askMeAnythingDescription') }}
225
- </p>
276
+ <div class="mb-4 flex size-12 items-center justify-center rounded-full bg-primary/10">
277
+ <UIcon
278
+ name="i-lucide-message-circle-question"
279
+ class="size-6 text-primary"
280
+ />
226
281
  </div>
227
-
228
- <template v-else>
229
- <p class="mb-4 text-sm font-medium text-muted">
230
- {{ t('assistant.faq') }}
231
- </p>
232
-
233
- <div class="flex flex-col gap-5">
234
- <div
235
- v-for="category in faqQuestions"
236
- :key="category.category"
237
- class="flex flex-col gap-1.5"
238
- >
239
- <h4 class="text-xs font-medium uppercase tracking-wide text-dimmed">
240
- {{ category.category }}
241
- </h4>
242
- <div class="flex flex-col">
243
- <button
244
- v-for="question in category.items"
245
- :key="question"
246
- class="py-1.5 text-left text-sm text-muted transition-colors hover:text-highlighted"
247
- @click="askQuestion(question)"
248
- >
249
- {{ question }}
250
- </button>
251
- </div>
252
- </div>
253
- </div>
254
- </template>
282
+ <h3 class="mb-2 text-base font-medium text-highlighted">
283
+ {{ t('assistant.askMeAnything') }}
284
+ </h3>
285
+ <p class="max-w-xs text-sm text-muted">
286
+ {{ t('assistant.askMeAnythingDescription') }}
287
+ </p>
255
288
  </div>
256
- </div>
257
289
 
258
- <div class="w-full shrink-0 p-3">
259
- <UChatPrompt
260
- v-model="input"
261
- :rows="2"
262
- :placeholder="displayPlaceholder"
263
- maxlength="1000"
264
- :ui="{
265
- root: 'shadow-none!',
266
- body: '*:p-0! *:rounded-none! *:text-base!',
267
- }"
268
- @submit="handleSubmit"
269
- >
270
- <template #footer>
271
- <div class="flex items-center gap-1 text-xs text-muted">
272
- <span>{{ t('assistant.lineBreak') }}</span>
273
- <UKbd
274
- size="sm"
275
- value="shift"
276
- />
277
- <UKbd
278
- size="sm"
279
- value="enter"
280
- />
281
- </div>
282
- <UChatPromptSubmit
283
- class="ml-auto"
284
- size="xs"
285
- :status="chat.status"
286
- @stop="chat.stop()"
287
- @reload="chat.regenerate()"
290
+ <template v-else>
291
+ <div class="flex flex-col gap-6">
292
+ <UPageLinks
293
+ v-for="category in faqQuestions"
294
+ :key="category.category"
295
+ :title="category.category"
296
+ :links="category.items.map(item => ({ label: item, onClick: () => askQuestion(item) }))"
288
297
  />
289
- </template>
290
- </UChatPrompt>
291
- <div class="mt-1 flex text-xs text-dimmed items-center justify-between">
292
- <span>{{ t('assistant.chatCleared') }}</span>
293
- <span>
294
- {{ input.length }}/1000
295
- </span>
296
- </div>
298
+ </div>
299
+ </template>
297
300
  </div>
298
- </div>
299
- </DefineChatContent>
300
-
301
- <aside
302
- v-if="!isMobile"
303
- class="left-auto! fixed top-0 z-50 h-dvh overflow-hidden border-l border-default bg-default/95 backdrop-blur-xl transition-[right,width] duration-200 ease-linear will-change-[right,width]"
304
- :style="{
305
- width: `${panelWidth}px`,
306
- right: isOpen ? '0' : `-${panelWidth}px`,
307
- }"
308
- >
309
- <div
310
- class="h-full transition-[width] duration-200 ease-linear"
311
- :style="{ width: `${panelWidth}px` }"
312
- >
313
- <ReuseChatContent :show-expand-button="true" />
314
- </div>
315
- </aside>
301
+ </UTheme>
316
302
 
317
- <USlideover
318
- v-else
319
- v-model:open="isOpen"
320
- side="right"
321
- :ui="{
322
- content: 'ring-0 bg-default',
323
- }"
324
- >
325
- <template #content>
326
- <ReuseChatContent :show-expand-button="false" />
303
+ <template #footer>
304
+ <UChatPrompt
305
+ v-model="input"
306
+ :error="chat.error"
307
+ :placeholder="displayPlaceholder"
308
+ variant="naked"
309
+ size="sm"
310
+ autofocus
311
+ :ui="{ base: 'px-0' }"
312
+ class="px-4"
313
+ @submit="onSubmit"
314
+ >
315
+ <template #footer>
316
+ <div class="flex items-center gap-1.5 text-xs text-dimmed">
317
+ <span>{{ t('assistant.lineBreak') }}</span>
318
+ <UKbd
319
+ size="sm"
320
+ value="shift"
321
+ />
322
+ <UKbd
323
+ size="sm"
324
+ value="enter"
325
+ />
326
+ </div>
327
+
328
+ <UChatPromptSubmit
329
+ size="sm"
330
+ :status="chat.status"
331
+ :disabled="chat.status === 'ready' && !input.trim()"
332
+ @stop="chat.stop()"
333
+ @reload="chat.regenerate()"
334
+ />
335
+ </template>
336
+ </UChatPrompt>
327
337
  </template>
328
- </USlideover>
338
+ </USidebar>
329
339
  </template>
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { ShikiCachedRenderer } from 'shiki-stream/vue'
2
+ import { ShikiCachedRenderer } from '@shikijs/stream/vue'
3
3
  import { useColorMode } from '#imports'
4
4
  import { useHighlighter } from '../composables/useHighlighter'
5
5