docus 5.4.4 → 5.5.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.
Files changed (32) hide show
  1. package/app/app.vue +16 -6
  2. package/app/components/app/AppHeader.vue +5 -0
  3. package/app/components/docs/DocsAsideRightBottom.vue +28 -1
  4. package/app/composables/useSeo.ts +207 -0
  5. package/app/pages/[[lang]]/[...slug].vue +17 -9
  6. package/app/plugins/i18n.ts +28 -11
  7. package/app/templates/landing.vue +4 -10
  8. package/app/types/index.d.ts +49 -0
  9. package/app/utils/navigation.ts +31 -0
  10. package/i18n/locales/en.json +27 -0
  11. package/i18n/locales/fr.json +28 -1
  12. package/modules/assistant/README.md +213 -0
  13. package/modules/assistant/index.ts +100 -0
  14. package/modules/assistant/runtime/components/AssistantChat.vue +21 -0
  15. package/modules/assistant/runtime/components/AssistantChatDisabled.vue +3 -0
  16. package/modules/assistant/runtime/components/AssistantFloatingInput.vue +110 -0
  17. package/modules/assistant/runtime/components/AssistantLoading.vue +164 -0
  18. package/modules/assistant/runtime/components/AssistantMatrix.vue +92 -0
  19. package/modules/assistant/runtime/components/AssistantPanel.vue +329 -0
  20. package/modules/assistant/runtime/components/AssistantPreStream.vue +46 -0
  21. package/modules/assistant/runtime/composables/useAssistant.ts +107 -0
  22. package/modules/assistant/runtime/composables/useHighlighter.ts +34 -0
  23. package/modules/assistant/runtime/server/api/search.ts +111 -0
  24. package/modules/assistant/runtime/types.ts +7 -0
  25. package/modules/config.ts +6 -4
  26. package/modules/css.ts +6 -2
  27. package/modules/markdown-rewrite.ts +130 -0
  28. package/nuxt.config.ts +22 -1
  29. package/nuxt.schema.ts +63 -0
  30. package/package.json +24 -15
  31. package/server/routes/sitemap.xml.ts +93 -0
  32. package/utils/meta.ts +9 -3
@@ -0,0 +1,329 @@
1
+ <script setup lang="ts">
2
+ import { defineAsyncComponent } from 'vue'
3
+ import type { UIMessage } from 'ai'
4
+ import { Chat } from '@ai-sdk/vue'
5
+ import { DefaultChatTransport } from 'ai'
6
+ import { createReusableTemplate } from '@vueuse/core'
7
+ import { useDocusI18n } from '../../../../app/composables/useDocusI18n'
8
+
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()
17
+ const config = useRuntimeConfig()
18
+ const toast = useToast()
19
+ const { t } = useDocusI18n()
20
+ const input = ref('')
21
+
22
+ const displayTitle = computed(() => t('assistant.title'))
23
+ const displayPlaceholder = computed(() => t('assistant.placeholder'))
24
+
25
+ const chat = new Chat({
26
+ messages: messages.value,
27
+ transport: new DefaultChatTransport({
28
+ api: config.public.assistant.apiPath,
29
+ }),
30
+ onError: (error: Error) => {
31
+ const message = (() => {
32
+ try {
33
+ const parsed = JSON.parse(error.message)
34
+ return parsed?.message || error.message
35
+ }
36
+ catch {
37
+ return error.message
38
+ }
39
+ })()
40
+
41
+ toast.add({
42
+ description: message,
43
+ icon: 'i-lucide-alert-circle',
44
+ color: 'error',
45
+ duration: 0,
46
+ })
47
+ },
48
+ onFinish: () => {
49
+ messages.value = chat.messages
50
+ },
51
+ })
52
+
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 })
64
+
65
+ watch(messages, (newMessages: UIMessage[]) => {
66
+ if (newMessages.length === 0 && chat.messages.length > 0) {
67
+ chat.messages.length = 0
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 || [])
86
+ }
87
+
88
+ function handleSubmit(event?: Event) {
89
+ event?.preventDefault()
90
+
91
+ if (!input.value.trim()) {
92
+ return
93
+ }
94
+
95
+ chat.sendMessage({
96
+ text: input.value,
97
+ })
98
+
99
+ input.value = ''
100
+ }
101
+
102
+ function askQuestion(question: string) {
103
+ chat.sendMessage({
104
+ text: question,
105
+ })
106
+ }
107
+
108
+ function resetChat() {
109
+ chat.stop()
110
+ messages.value = []
111
+ chat.messages.length = 0
112
+ }
113
+
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
+ }
124
+ })
125
+ </script>
126
+
127
+ <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')"
149
+ >
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>
171
+
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"
188
+ />
189
+ <template
190
+ v-for="(part, index) in message.parts"
191
+ :key="`${message.id}-${part.type}-${index}${'state' in part ? `-${part.state}` : ''}`"
192
+ >
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>
203
+ </template>
204
+ </UChatMessages>
205
+
206
+ <div
207
+ v-else
208
+ class="p-4"
209
+ >
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>
226
+ </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>
255
+ </div>
256
+ </div>
257
+
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()"
288
+ />
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>
297
+ </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>
316
+
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" />
327
+ </template>
328
+ </USlideover>
329
+ </template>
@@ -0,0 +1,46 @@
1
+ <script setup lang="ts">
2
+ import { ShikiCachedRenderer } from 'shiki-stream/vue'
3
+ import { useColorMode } from '#imports'
4
+ import { useHighlighter } from '../composables/useHighlighter'
5
+
6
+ const colorMode = useColorMode()
7
+ const highlighter = await useHighlighter()
8
+ const props = defineProps<{
9
+ code: string
10
+ language: string
11
+ class?: string
12
+ meta?: string
13
+ }>()
14
+ const trimmedCode = computed(() => {
15
+ return props.code.trim().replace(/`+$/, '')
16
+ })
17
+ const lang = computed(() => {
18
+ switch (props.language) {
19
+ case 'vue':
20
+ return 'vue'
21
+ case 'javascript':
22
+ return 'js'
23
+ case 'typescript':
24
+ return 'ts'
25
+ case 'css':
26
+ return 'css'
27
+ default:
28
+ return props.language
29
+ }
30
+ })
31
+ const key = computed(() => {
32
+ return `${lang.value}-${colorMode.value}`
33
+ })
34
+ </script>
35
+
36
+ <template>
37
+ <ProsePre v-bind="props">
38
+ <ShikiCachedRenderer
39
+ :key="key"
40
+ :highlighter="highlighter"
41
+ :code="trimmedCode"
42
+ :lang="lang"
43
+ :theme="colorMode.value === 'dark' ? 'material-theme-palenight' : 'material-theme-lighter'"
44
+ />
45
+ </ProsePre>
46
+ </template>
@@ -0,0 +1,107 @@
1
+ import type { UIMessage } from 'ai'
2
+ import { useMediaQuery } from '@vueuse/core'
3
+ import type { FaqCategory, FaqQuestions, LocalizedFaqQuestions } from '../types'
4
+
5
+ function normalizeFaqQuestions(questions: FaqQuestions): FaqCategory[] {
6
+ if (!questions || (Array.isArray(questions) && questions.length === 0)) {
7
+ return []
8
+ }
9
+
10
+ if (typeof questions[0] === 'string') {
11
+ return [{
12
+ category: 'Questions',
13
+ items: questions as string[],
14
+ }]
15
+ }
16
+
17
+ return questions as FaqCategory[]
18
+ }
19
+
20
+ const PANEL_WIDTH_COMPACT = 360
21
+ const PANEL_WIDTH_EXPANDED = 520
22
+
23
+ export function useAssistant() {
24
+ const config = useRuntimeConfig()
25
+ const appConfig = useAppConfig()
26
+ const isEnabled = computed(() => config.public.assistant?.enabled ?? false)
27
+
28
+ const isOpen = useState('assistant-open', () => false)
29
+ const isExpanded = useState('assistant-expanded', () => false)
30
+ const messages = useState<UIMessage[]>('assistant-messages', () => [])
31
+ const pendingMessage = useState<string | undefined>('assistant-pending', () => undefined)
32
+
33
+ const isMobile = useMediaQuery('(max-width: 767px)')
34
+ const panelWidth = computed(() => isExpanded.value ? PANEL_WIDTH_EXPANDED : PANEL_WIDTH_COMPACT)
35
+ const shouldPushContent = computed(() => !isMobile.value && isOpen.value)
36
+
37
+ const faqQuestions = computed<FaqCategory[]>(() => {
38
+ const assistantConfig = appConfig.assistant
39
+ const faqConfig = assistantConfig?.faqQuestions
40
+ if (!faqConfig) return []
41
+
42
+ // Check if it's a localized object (has locale keys like 'en', 'fr')
43
+ if (!Array.isArray(faqConfig)) {
44
+ const localizedConfig = faqConfig as LocalizedFaqQuestions
45
+ const currentLocale = appConfig.docus?.locale || 'en'
46
+ const defaultLocale = config.public.i18n?.defaultLocale || 'en'
47
+
48
+ // Try current locale, then default locale, then first available
49
+ const questions = localizedConfig[currentLocale]
50
+ || localizedConfig[defaultLocale]
51
+ || Object.values(localizedConfig)[0]
52
+
53
+ return normalizeFaqQuestions(questions || [])
54
+ }
55
+
56
+ return normalizeFaqQuestions(faqConfig)
57
+ })
58
+
59
+ function open(initialMessage?: string, clearPrevious = false) {
60
+ if (clearPrevious) {
61
+ messages.value = []
62
+ }
63
+
64
+ if (initialMessage) {
65
+ pendingMessage.value = initialMessage
66
+ }
67
+ isOpen.value = true
68
+ }
69
+
70
+ function clearPending() {
71
+ pendingMessage.value = undefined
72
+ }
73
+
74
+ function close() {
75
+ isOpen.value = false
76
+ }
77
+
78
+ function toggle() {
79
+ isOpen.value = !isOpen.value
80
+ }
81
+
82
+ function clearMessages() {
83
+ messages.value = []
84
+ }
85
+
86
+ function toggleExpanded() {
87
+ isExpanded.value = !isExpanded.value
88
+ }
89
+
90
+ return {
91
+ isEnabled,
92
+ isOpen,
93
+ isExpanded,
94
+ isMobile,
95
+ panelWidth,
96
+ shouldPushContent,
97
+ messages,
98
+ pendingMessage,
99
+ faqQuestions,
100
+ open,
101
+ clearPending,
102
+ close,
103
+ toggle,
104
+ toggleExpanded,
105
+ clearMessages,
106
+ }
107
+ }
@@ -0,0 +1,34 @@
1
+ import { createHighlighterCore } from '@shikijs/core'
2
+ import type { HighlighterCore } from '@shikijs/core'
3
+ import { createJavaScriptRegexEngine } from '@shikijs/engine-javascript'
4
+
5
+ let highlighter: HighlighterCore | null = null
6
+ let promise: Promise<HighlighterCore> | null = null
7
+
8
+ export const useHighlighter = async () => {
9
+ if (!promise) {
10
+ promise = createHighlighterCore({
11
+ langs: [
12
+ import('@shikijs/langs/vue'),
13
+ import('@shikijs/langs/javascript'),
14
+ import('@shikijs/langs/typescript'),
15
+ import('@shikijs/langs/css'),
16
+ import('@shikijs/langs/html'),
17
+ import('@shikijs/langs/json'),
18
+ import('@shikijs/langs/yaml'),
19
+ import('@shikijs/langs/markdown'),
20
+ import('@shikijs/langs/bash'),
21
+ ],
22
+ themes: [
23
+ import('@shikijs/themes/material-theme-palenight'),
24
+ import('@shikijs/themes/material-theme-lighter'),
25
+ ],
26
+ engine: createJavaScriptRegexEngine(),
27
+ })
28
+ }
29
+ if (!highlighter) {
30
+ highlighter = await promise
31
+ }
32
+
33
+ return highlighter
34
+ }
@@ -0,0 +1,111 @@
1
+ import { streamText, convertToModelMessages, createUIMessageStream, createUIMessageStreamResponse } from 'ai'
2
+ import type { UIMessageStreamWriter, ToolCallPart, ToolSet } from 'ai'
3
+ import { createMCPClient } from '@ai-sdk/mcp'
4
+
5
+ const MAX_STEPS = 10
6
+
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ function stopWhenResponseComplete({ steps }: { steps: any[] }): boolean {
9
+ const lastStep = steps.at(-1)
10
+ if (!lastStep) return false
11
+
12
+ // Primary condition: stop when model gives a text response without tool calls
13
+ const hasText = Boolean(lastStep.text && lastStep.text.trim().length > 0)
14
+ const hasNoToolCalls = !lastStep.toolCalls || lastStep.toolCalls.length === 0
15
+
16
+ if (hasText && hasNoToolCalls) return true
17
+
18
+ return steps.length >= MAX_STEPS
19
+ }
20
+
21
+ function getSystemPrompt(siteName: string) {
22
+ return `You are the documentation assistant for ${siteName}. Help users navigate and understand the project documentation.
23
+
24
+ **Your identity:**
25
+ - You are an assistant helping users with ${siteName} documentation
26
+ - NEVER use first person ("I", "me", "my") - always refer to the project by name: "${siteName} provides...", "${siteName} supports...", "The project offers..."
27
+ - Be confident and knowledgeable about the project
28
+ - Speak as a helpful guide, not as the documentation itself
29
+
30
+ **Tool usage (CRITICAL):**
31
+ - You have tools: list-pages (discover pages) and get-page (read a page)
32
+ - If a page title clearly matches the question, read it directly without listing first
33
+ - ALWAYS respond with text after using tools - never end with just tool calls
34
+
35
+ **Guidelines:**
36
+ - If you can't find something, say "There is no documentation on that yet" or "${siteName} doesn't cover that topic yet"
37
+ - Be concise, helpful, and direct
38
+ - Guide users like a friendly expert would
39
+
40
+ **FORMATTING RULES (CRITICAL):**
41
+ - NEVER use markdown headings (#, ##, ###, etc.)
42
+ - Use **bold text** for emphasis and section labels
43
+ - Start responses with content directly, never with a heading
44
+ - Use bullet points for lists
45
+ - Keep code examples focused and minimal
46
+
47
+ **Response style:**
48
+ - Conversational but professional
49
+ - "Here's how you can do that:" instead of "The documentation shows:"
50
+ - "${siteName} supports TypeScript out of the box" instead of "I support TypeScript"
51
+ - Provide actionable guidance, not just information dumps`
52
+ }
53
+
54
+ export default defineEventHandler(async (event) => {
55
+ const { messages } = await readBody(event)
56
+ const config = useRuntimeConfig()
57
+ const siteConfig = getSiteConfig(event)
58
+
59
+ const siteName = siteConfig.name || 'Documentation'
60
+
61
+ const mcpServer = config.assistant.mcpServer
62
+ const isExternalUrl = mcpServer.startsWith('http://') || mcpServer.startsWith('https://')
63
+ const mcpUrl = isExternalUrl
64
+ ? mcpServer
65
+ : import.meta.dev
66
+ ? `http://localhost:3000${mcpServer}`
67
+ : `${getRequestURL(event).origin}${mcpServer}`
68
+
69
+ const httpClient = await createMCPClient({
70
+ transport: { type: 'http', url: mcpUrl },
71
+ })
72
+ const mcpTools = await httpClient.tools()
73
+
74
+ const stream = createUIMessageStream({
75
+ execute: async ({ writer }: { writer: UIMessageStreamWriter }) => {
76
+ const modelMessages = await convertToModelMessages(messages)
77
+ const result = streamText({
78
+ model: config.assistant.model,
79
+ maxOutputTokens: 4000,
80
+ maxRetries: 2,
81
+ stopWhen: stopWhenResponseComplete,
82
+ system: getSystemPrompt(siteName),
83
+ messages: modelMessages,
84
+ tools: mcpTools as ToolSet,
85
+ onStepFinish: ({ toolCalls }: { toolCalls: ToolCallPart[] }) => {
86
+ if (toolCalls.length === 0) return
87
+ writer.write({
88
+ id: toolCalls[0]?.toolCallId,
89
+ type: 'data-tool-calls',
90
+ data: {
91
+ tools: toolCalls.map((tc: ToolCallPart) => {
92
+ const args = 'args' in tc ? tc.args : 'input' in tc ? tc.input : {}
93
+ return {
94
+ toolName: tc.toolName,
95
+ toolCallId: tc.toolCallId,
96
+ args,
97
+ }
98
+ }),
99
+ },
100
+ })
101
+ },
102
+ })
103
+ writer.merge(result.toUIMessageStream())
104
+ },
105
+ onFinish: async () => {
106
+ await httpClient.close()
107
+ },
108
+ })
109
+
110
+ return createUIMessageStreamResponse({ stream })
111
+ })
@@ -0,0 +1,7 @@
1
+ export interface FaqCategory {
2
+ category: string
3
+ items: string[]
4
+ }
5
+
6
+ export type FaqQuestions = string[] | FaqCategory[]
7
+ export type LocalizedFaqQuestions = Record<string, FaqQuestions>
package/modules/config.ts CHANGED
@@ -1,10 +1,12 @@
1
- import { createResolver, defineNuxtModule } from '@nuxt/kit'
1
+ import { createResolver, defineNuxtModule, logger } from '@nuxt/kit'
2
2
  import { defu } from 'defu'
3
3
  import { existsSync } from 'node:fs'
4
4
  import { join } from 'node:path'
5
5
  import { inferSiteURL, getPackageJsonMetadata } from '../utils/meta'
6
6
  import { getGitBranch, getGitEnv, getLocalGitInfo } from '../utils/git'
7
7
 
8
+ const log = logger.withTag('Docus')
9
+
8
10
  export default defineNuxtModule({
9
11
  meta: {
10
12
  name: 'config',
@@ -14,7 +16,7 @@ export default defineNuxtModule({
14
16
  const url = inferSiteURL()
15
17
  const meta = await getPackageJsonMetadata(dir)
16
18
  const gitInfo = await getLocalGitInfo(dir) || getGitEnv()
17
- const siteName = nuxt.options?.site?.name || meta.name || gitInfo?.name || ''
19
+ const siteName = (typeof nuxt.options.site === 'object' && nuxt.options.site?.name) || meta.name || gitInfo?.name || ''
18
20
 
19
21
  nuxt.options.llms = defu(nuxt.options.llms, {
20
22
  domain: url,
@@ -68,11 +70,11 @@ export default defineNuxtModule({
68
70
  const hasContentFolder = existsSync(contentPath)
69
71
 
70
72
  if (!hasLocaleFile) {
71
- console.warn(`[Docus] Locale file not found: ${localeCode}.json - skipping locale "${localeCode}"`)
73
+ log.warn(`Locale file not found: ${localeCode}.json - skipping locale "${localeCode}"`)
72
74
  }
73
75
 
74
76
  if (!hasContentFolder) {
75
- console.warn(`[Docus] Content folder not found: content/${localeCode}/ - skipping locale "${localeCode}"`)
77
+ log.warn(`Content folder not found: content/${localeCode}/ - skipping locale "${localeCode}"`)
76
78
  }
77
79
 
78
80
  return hasLocaleFile && hasContentFolder
package/modules/css.ts CHANGED
@@ -14,6 +14,7 @@ export default defineNuxtModule({
14
14
  const uiPath = resolveModulePath('@nuxt/ui', { from: import.meta.url, conditions: ['style'] })
15
15
  const tailwindPath = resolveModulePath('tailwindcss', { from: import.meta.url, conditions: ['style'] })
16
16
  const layerDir = resolver.resolve('../app')
17
+ const assistantDir = resolver.resolve('../modules/assistant')
17
18
 
18
19
  const cssTemplate = addTemplate({
19
20
  filename: 'docus.css',
@@ -23,10 +24,13 @@ export default defineNuxtModule({
23
24
 
24
25
  @source "${contentDir.replace(/\\/g, '/')}/**/*";
25
26
  @source "${layerDir.replace(/\\/g, '/')}/**/*";
26
- @source "../../app.config.ts";`
27
+ @source "../../app.config.ts";
28
+ @source "${assistantDir.replace(/\\/g, '/')}/**/*";`
27
29
  },
28
30
  })
29
31
 
30
- nuxt.options.css.unshift(cssTemplate.dst)
32
+ if (Array.isArray(nuxt.options.css)) {
33
+ nuxt.options.css.unshift(cssTemplate.dst)
34
+ }
31
35
  },
32
36
  })