docus 5.4.2 → 5.5.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 (31) 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/pages/[[lang]]/[...slug].vue +9 -6
  5. package/app/plugins/i18n.ts +28 -11
  6. package/app/types/index.d.ts +49 -0
  7. package/content.config.ts +1 -1
  8. package/i18n/locales/en.json +27 -0
  9. package/i18n/locales/fr.json +28 -1
  10. package/modules/assistant/README.md +213 -0
  11. package/modules/assistant/index.ts +100 -0
  12. package/modules/assistant/runtime/components/AssistantChat.vue +21 -0
  13. package/modules/assistant/runtime/components/AssistantChatDisabled.vue +3 -0
  14. package/modules/assistant/runtime/components/AssistantFloatingInput.vue +105 -0
  15. package/modules/assistant/runtime/components/AssistantLoading.vue +164 -0
  16. package/modules/assistant/runtime/components/AssistantMatrix.vue +92 -0
  17. package/modules/assistant/runtime/components/AssistantPanel.vue +329 -0
  18. package/modules/assistant/runtime/components/AssistantPreStream.vue +46 -0
  19. package/modules/assistant/runtime/composables/useAssistant.ts +107 -0
  20. package/modules/assistant/runtime/composables/useHighlighter.ts +34 -0
  21. package/modules/assistant/runtime/server/api/search.ts +111 -0
  22. package/modules/assistant/runtime/types.ts +7 -0
  23. package/modules/config.ts +5 -3
  24. package/modules/css.ts +3 -1
  25. package/modules/markdown-rewrite.ts +130 -0
  26. package/nuxt.config.ts +13 -1
  27. package/nuxt.schema.ts +63 -0
  28. package/package.json +24 -15
  29. package/server/routes/sitemap.xml.ts +74 -0
  30. package/utils/meta.ts +9 -3
  31. package/server/routes/raw/[...slug].md.get.ts +0 -45
package/app/app.vue CHANGED
@@ -5,6 +5,7 @@ import * as nuxtUiLocales from '@nuxt/ui/locale'
5
5
  const { seo } = useAppConfig()
6
6
  const site = useSiteConfig()
7
7
  const { locale, locales, isEnabled, switchLocalePath } = useDocusI18n()
8
+ const { isEnabled: isAssistantEnabled, panelWidth: assistantPanelWidth, shouldPushContent } = useAssistant()
8
9
 
9
10
  const nuxtUiLocale = computed(() => nuxtUiLocales[locale.value as keyof typeof nuxtUiLocales] || nuxtUiLocales.en)
10
11
  const lang = computed(() => nuxtUiLocale.value.code)
@@ -47,7 +48,7 @@ const { data: navigation } = await useAsyncData(() => `navigation_${collectionNa
47
48
  transform: (data: ContentNavigationItem[]) => {
48
49
  const rootResult = data.find(item => item.path === '/docs')?.children || data || []
49
50
 
50
- return rootResult.find(item => item.path === `/${locale.value}`)?.children || rootResult
51
+ return rootResult.find((item: ContentNavigationItem) => item.path === `/${locale.value}`)?.children || rootResult
51
52
  },
52
53
  watch: [locale],
53
54
  })
@@ -63,17 +64,26 @@ provide('navigation', navigation)
63
64
  <UApp :locale="nuxtUiLocale">
64
65
  <NuxtLoadingIndicator color="var(--ui-primary)" />
65
66
 
66
- <AppHeader v-if="$route.meta.header !== false" />
67
- <NuxtLayout>
68
- <NuxtPage />
69
- </NuxtLayout>
70
- <AppFooter v-if="$route.meta.footer !== false" />
67
+ <div
68
+ class="transition-[margin-right] duration-200 ease-linear will-change-[margin-right]"
69
+ :style="{ marginRight: shouldPushContent ? `${assistantPanelWidth}px` : '0' }"
70
+ >
71
+ <AppHeader v-if="$route.meta.header !== false" />
72
+ <NuxtLayout>
73
+ <NuxtPage />
74
+ </NuxtLayout>
75
+ <AppFooter v-if="$route.meta.footer !== false" />
76
+ </div>
71
77
 
72
78
  <ClientOnly>
73
79
  <LazyUContentSearch
74
80
  :files="files"
75
81
  :navigation="navigation"
76
82
  />
83
+ <template v-if="isAssistantEnabled">
84
+ <LazyAssistantFloatingInput />
85
+ <LazyAssistantPanel />
86
+ </template>
77
87
  </ClientOnly>
78
88
  </UApp>
79
89
  </template>
@@ -4,6 +4,7 @@ import { useDocusI18n } from '../../composables/useDocusI18n'
4
4
  const appConfig = useAppConfig()
5
5
  const site = useSiteConfig()
6
6
 
7
+ const { isEnabled: isAssistantEnabled } = useAssistant()
7
8
  const { localePath, isEnabled, locales } = useDocusI18n()
8
9
 
9
10
  const links = computed(() => appConfig.github && appConfig.github.url
@@ -33,6 +34,10 @@ const links = computed(() => appConfig.github && appConfig.github.url
33
34
  <template #right>
34
35
  <AppHeaderCTA />
35
36
 
37
+ <template v-if="isAssistantEnabled">
38
+ <AssistantChat />
39
+ </template>
40
+
36
41
  <template v-if="isEnabled && locales.length > 1">
37
42
  <ClientOnly>
38
43
  <LanguageSelect />
@@ -1,18 +1,45 @@
1
1
  <script setup lang="ts">
2
+ const route = useRoute()
3
+
4
+ const pageUrl = route.path
2
5
  const appConfig = useAppConfig()
3
6
  const { t } = useDocusI18n()
7
+ const { isEnabled, open } = useAssistant()
8
+
9
+ const showExplainWithAi = computed(() => {
10
+ return isEnabled.value && appConfig.assistant?.explainWithAi !== false
11
+ })
12
+
13
+ const explainIcon = computed(() => appConfig.assistant?.icons?.explain || 'i-lucide-brain')
4
14
  </script>
5
15
 
6
16
  <template>
7
17
  <div
8
- v-if="appConfig.toc?.bottom?.links?.length"
18
+ v-if="appConfig.toc?.bottom?.links?.length || showExplainWithAi"
9
19
  class="hidden lg:block space-y-6"
10
20
  >
11
21
  <USeparator type="dashed" />
12
22
 
13
23
  <UPageLinks
24
+ v-if="appConfig.toc?.bottom?.links?.length"
14
25
  :title="appConfig.toc?.bottom?.title || t('docs.links')"
15
26
  :links="appConfig.toc?.bottom?.links"
16
27
  />
28
+
29
+ <USeparator
30
+ v-if="appConfig.toc?.bottom?.links?.length && showExplainWithAi"
31
+ type="dashed"
32
+ />
33
+
34
+ <UButton
35
+ v-if="showExplainWithAi"
36
+ :icon="explainIcon"
37
+ :label="t('assistant.explainWithAi')"
38
+ size="sm"
39
+ variant="link"
40
+ class="p-0 text-sm"
41
+ color="neutral"
42
+ @click="open(`Explain the page ${pageUrl}`, true)"
43
+ />
17
44
  </div>
18
45
  </template>
@@ -2,7 +2,6 @@
2
2
  import { kebabCase } from 'scule'
3
3
  import type { ContentNavigationItem, Collections, DocsCollectionItem } from '@nuxt/content'
4
4
  import { findPageHeadline } from '@nuxt/content/utils'
5
- import { addPrerenderPath } from '../../utils/prerender'
6
5
 
7
6
  definePageMeta({
8
7
  layout: 'docs',
@@ -12,6 +11,7 @@ const route = useRoute()
12
11
  const { locale, isEnabled, t } = useDocusI18n()
13
12
  const appConfig = useAppConfig()
14
13
  const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
14
+ const { shouldPushContent: shouldHideToc } = useAssistant()
15
15
 
16
16
  const collectionName = computed(() => isEnabled.value ? `docs_${locale.value}` : 'docs')
17
17
 
@@ -28,9 +28,6 @@ if (!page.value) {
28
28
  throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
29
29
  }
30
30
 
31
- // Add the page path to the prerender list
32
- addPrerenderPath(`/raw${route.path}.md`)
33
-
34
31
  const title = page.value.seo?.title || page.value.title
35
32
  const description = page.value.seo?.description || page.value.description
36
33
 
@@ -66,10 +63,16 @@ const editLink = computed(() => {
66
63
  `${page.value?.stem}.${page.value?.extension}`,
67
64
  ].filter(Boolean).join('/')
68
65
  })
66
+
67
+ // Add the page path to the prerender list
68
+ addPrerenderPath(`/raw${route.path}.md`)
69
69
  </script>
70
70
 
71
71
  <template>
72
- <UPage v-if="page">
72
+ <UPage
73
+ v-if="page"
74
+ :key="`page-${shouldHideToc}`"
75
+ >
73
76
  <UPageHeader
74
77
  :title="page.title"
75
78
  :description="page.description"
@@ -128,7 +131,7 @@ const editLink = computed(() => {
128
131
  </UPageBody>
129
132
 
130
133
  <template
131
- v-if="page?.body?.toc?.links?.length"
134
+ v-if="page?.body?.toc?.links?.length && !shouldHideToc"
132
135
  #right
133
136
  >
134
137
  <UContentToc
@@ -1,5 +1,10 @@
1
- import en from '../../i18n/locales/en.json'
2
1
  import type { RouteLocationNormalized } from 'vue-router'
2
+ import { consola } from 'consola'
3
+
4
+ const log = consola.withTag('Docus')
5
+
6
+ // Lazy import functions for locale files (bundled but not eagerly loaded)
7
+ const localeFiles = import.meta.glob<{ default: Record<string, unknown> }>('../../i18n/locales/*.json')
3
8
 
4
9
  export default defineNuxtPlugin(async () => {
5
10
  const nuxtApp = useNuxtApp()
@@ -8,19 +13,31 @@ export default defineNuxtPlugin(async () => {
8
13
 
9
14
  // If i18n is not enabled, fetch and provide the configured locale in app config
10
15
  if (!i18nConfig) {
11
- let locale = 'en'
12
- let resolvedMessages: Record<string, unknown> = en
13
-
14
16
  const appConfig = useAppConfig()
15
- const configuredLocale = appConfig.docus.locale
16
- if (configuredLocale !== 'en') {
17
- const localeMessages = await import(`../../i18n/locales/${configuredLocale}.json`)
18
- if (!localeMessages) {
19
- console.warn(`[Docus] Missing locale file for "${configuredLocale}". Falling back to "en".`)
17
+ const configuredLocale = appConfig.docus.locale || 'en'
18
+
19
+ let locale = configuredLocale
20
+ let resolvedMessages: Record<string, unknown>
21
+
22
+ // Try to load the requested locale file
23
+ const localeKey = `../../i18n/locales/${configuredLocale}.json`
24
+ const localeLoader = localeFiles[localeKey]
25
+
26
+ if (localeLoader) {
27
+ const localeModule = await localeLoader()
28
+ resolvedMessages = localeModule.default
29
+ }
30
+ else {
31
+ log.warn(`Missing locale file for "${configuredLocale}". Falling back to "en".`)
32
+ locale = 'en'
33
+ const fallbackKey = '../../i18n/locales/en.json'
34
+ const fallbackLoader = localeFiles[fallbackKey]
35
+ if (fallbackLoader) {
36
+ const fallbackModule = await fallbackLoader()
37
+ resolvedMessages = fallbackModule.default
20
38
  }
21
39
  else {
22
- locale = configuredLocale
23
- resolvedMessages = localeMessages
40
+ resolvedMessages = {} as Record<string, unknown>
24
41
  }
25
42
  }
26
43
 
@@ -1,3 +1,7 @@
1
+ import type { FaqQuestions, LocalizedFaqQuestions } from '../../modules/assistant/runtime/types'
2
+
3
+ export type { FaqCategory, FaqQuestions, LocalizedFaqQuestions } from '../../modules/assistant/runtime/types'
4
+
1
5
  declare module 'nuxt/schema' {
2
6
  interface AppConfig {
3
7
  docus: {
@@ -36,6 +40,51 @@ declare module 'nuxt/schema' {
36
40
  branch: string
37
41
  rootDir?: string
38
42
  } | false
43
+ assistant?: {
44
+ /**
45
+ * Show the floating input at the bottom of documentation pages.
46
+ * @default true
47
+ */
48
+ floatingInput?: boolean
49
+ /**
50
+ * Show the "Explain with AI" button in the documentation sidebar.
51
+ * @default true
52
+ */
53
+ explainWithAi?: boolean
54
+ /**
55
+ * FAQ questions to display in the chat slideover.
56
+ * Can be a simple array of strings, an array of categories, or a locale-based object.
57
+ * @example Simple format: ['How to install?', 'How to configure?']
58
+ * @example Category format: [{ category: 'Getting Started', items: ['How to install?'] }]
59
+ * @example Localized format: { en: ['How to install?'], fr: ['Comment installer ?'] }
60
+ */
61
+ faqQuestions?: FaqQuestions | LocalizedFaqQuestions
62
+ /**
63
+ * Keyboard shortcuts configuration.
64
+ */
65
+ shortcuts?: {
66
+ /**
67
+ * Shortcut to focus the floating input.
68
+ * @default 'meta_i'
69
+ */
70
+ focusInput?: string
71
+ }
72
+ /**
73
+ * Icons configuration.
74
+ */
75
+ icons?: {
76
+ /**
77
+ * Icon for the assistant trigger button and slideover header.
78
+ * @default 'i-lucide-sparkles'
79
+ */
80
+ trigger?: string
81
+ /**
82
+ * Icon for the "Explain with AI" button.
83
+ * @default 'i-lucide-brain'
84
+ */
85
+ explain?: string
86
+ }
87
+ }
39
88
  }
40
89
  }
41
90
 
package/content.config.ts CHANGED
@@ -21,7 +21,7 @@ let collections: Record<string, DefinedCollection>
21
21
  if (locales && Array.isArray(locales)) {
22
22
  collections = {}
23
23
  for (const locale of locales) {
24
- const code = typeof locale === 'string' ? locale : locale.code
24
+ const code = (typeof locale === 'string' ? locale : locale.code).replace('-', '_')
25
25
 
26
26
  collections[`landing_${code}`] = defineCollection({
27
27
  type: 'page',
@@ -18,5 +18,32 @@
18
18
  "toc": "On this page",
19
19
  "report": "Report an issue",
20
20
  "edit": "Edit this page"
21
+ },
22
+ "assistant": {
23
+ "title": "Ask AI",
24
+ "placeholder": "Ask a question...",
25
+ "tooltip": "Ask AI a question",
26
+ "tryAsking": "Try asking a question",
27
+ "askAnything": "Ask anything...",
28
+ "clearChat": "Clear chat",
29
+ "close": "Close",
30
+ "expand": "Expand",
31
+ "collapse": "Collapse",
32
+ "thinking": "Thinking...",
33
+ "askMeAnything": "Ask anything",
34
+ "askMeAnythingDescription": "Get help navigating the documentation, understanding concepts, and finding answers.",
35
+ "faq": "FAQ",
36
+ "chatCleared": "Chat is cleared on refresh",
37
+ "lineBreak": "Line break",
38
+ "explainWithAi": "Explain with AI",
39
+ "toolListPages": "Listed documentation pages",
40
+ "toolReadPage": "Read",
41
+ "loading": {
42
+ "searching": "Searching the documentation",
43
+ "reading": "Reading through the docs",
44
+ "analyzing": "Analyzing the content",
45
+ "finding": "Finding the best answer",
46
+ "finished": "Sources used"
47
+ }
21
48
  }
22
49
  }
@@ -18,5 +18,32 @@
18
18
  "toc": "Sur cette page",
19
19
  "report": "Signaler un problème",
20
20
  "edit": "Éditer cette page"
21
+ },
22
+ "assistant": {
23
+ "title": "Demander à l'IA",
24
+ "placeholder": "Posez une question...",
25
+ "tooltip": "Poser une question à l'IA",
26
+ "tryAsking": "Essayez de poser une question",
27
+ "askAnything": "Demandez n'importe quoi...",
28
+ "clearChat": "Effacer le chat",
29
+ "close": "Fermer",
30
+ "expand": "Agrandir",
31
+ "collapse": "Réduire",
32
+ "thinking": "Réflexion...",
33
+ "askMeAnything": "Posez une question",
34
+ "askMeAnythingDescription": "Obtenez de l'aide pour naviguer dans la documentation, comprendre des concepts et trouver des réponses.",
35
+ "faq": "FAQ",
36
+ "chatCleared": "Le chat est effacé au rechargement",
37
+ "lineBreak": "Retour à la ligne",
38
+ "explainWithAi": "Expliquer avec l'IA",
39
+ "toolListPages": "Pages de documentation listées",
40
+ "toolReadPage": "Lecture de",
41
+ "loading": {
42
+ "searching": "Recherche dans la documentation",
43
+ "reading": "Lecture des documents",
44
+ "analyzing": "Analyse du contenu",
45
+ "finding": "Recherche de la meilleure réponse",
46
+ "finished": "Sources utilisées"
47
+ }
21
48
  }
22
- }
49
+ }
@@ -0,0 +1,213 @@
1
+ # Assistant Module
2
+
3
+ A Nuxt module that provides an AI-powered chat interface using MCP (Model Context Protocol) tools.
4
+
5
+ ## Features
6
+
7
+ - AI chat slideover component with streaming responses
8
+ - Floating input component for quick questions
9
+ - MCP tools integration for documentation search
10
+ - Syntax highlighting for code blocks
11
+ - FAQ suggestions
12
+ - Persistent chat state
13
+ - Keyboard shortcuts support
14
+
15
+ ## Installation
16
+
17
+ 1. Copy the `modules/assistant` folder to your Nuxt project
18
+ 2. Install the required dependencies:
19
+
20
+ ```bash
21
+ pnpm add @ai-sdk/mcp @ai-sdk/vue @ai-sdk/gateway ai motion-v shiki shiki-stream
22
+ ```
23
+
24
+ 3. Add the module to your `nuxt.config.ts`:
25
+
26
+ ```ts
27
+ export default defineNuxtConfig({
28
+ modules: ['./modules/assistant'],
29
+
30
+ assistant: {
31
+ apiPath: '/__docus__/assistant',
32
+ mcpServer: '/mcp',
33
+ model: 'google/gemini-3-flash',
34
+ }
35
+ })
36
+ ```
37
+
38
+ 4. Set up your API key as an environment variable:
39
+
40
+ ```bash
41
+ AI_GATEWAY_API_KEY=your-gateway-key
42
+ ```
43
+
44
+ > **Note:** The module will only be enabled if `AI_GATEWAY_API_KEY` is detected. If no key is found, the module is disabled and a message is logged to the console.
45
+
46
+ ## Usage
47
+
48
+ Add the components to your app:
49
+
50
+ ```vue
51
+ <template>
52
+ <div>
53
+ <!-- Button to open the chat -->
54
+ <AssistantChat />
55
+
56
+ <!-- Chat panel (place once in your app/layout) -->
57
+ <AssistantPanel />
58
+ </div>
59
+ </template>
60
+ ```
61
+
62
+ ### FAQ Questions
63
+
64
+ Configure FAQ questions in your `app.config.ts`:
65
+
66
+ ```ts
67
+ export default defineAppConfig({
68
+ assistant: {
69
+ faqQuestions: [
70
+ {
71
+ category: 'Getting Started',
72
+ items: ['How do I install?', 'How do I configure?'],
73
+ },
74
+ {
75
+ category: 'Advanced',
76
+ items: ['How do I customize?'],
77
+ },
78
+ ],
79
+ },
80
+ })
81
+ ```
82
+
83
+ You can also use localized FAQ questions:
84
+
85
+ ```ts
86
+ export default defineAppConfig({
87
+ assistant: {
88
+ faqQuestions: {
89
+ en: ['How do I install?', 'How do I configure?'],
90
+ fr: ['Comment installer ?', 'Comment configurer ?'],
91
+ },
92
+ },
93
+ })
94
+ ```
95
+
96
+ ### Floating Input
97
+
98
+ Use `AssistantFloatingInput` for a floating input at the bottom of the page.
99
+
100
+ **Recommended:** Use `Teleport` to render the floating input at the body level, ensuring it stays fixed at the bottom regardless of your component hierarchy:
101
+
102
+ ```vue
103
+ <template>
104
+ <div>
105
+ <!-- Teleport to body for proper fixed positioning -->
106
+ <Teleport to="body">
107
+ <ClientOnly>
108
+ <LazyAssistantFloatingInput />
109
+ </ClientOnly>
110
+ </Teleport>
111
+
112
+ <!-- Chat panel (required to display responses) -->
113
+ <AssistantPanel />
114
+ </div>
115
+ </template>
116
+ ```
117
+
118
+ The floating input:
119
+ - Appears at the bottom center of the viewport
120
+ - Automatically hides when the chat slideover is open
121
+ - Expands on focus for better typing experience
122
+ - Supports keyboard shortcuts: `⌘I` to focus, `Escape` to blur
123
+
124
+ ### Programmatic Control
125
+
126
+ Use the `useAssistant` composable to control the chat:
127
+
128
+ ```vue
129
+ <script setup>
130
+ const { open, close, toggle, isOpen, messages, clearMessages } = useAssistant()
131
+
132
+ // Open chat with an initial message
133
+ open('How do I install the module?')
134
+
135
+ // Open and clear previous messages
136
+ open('New question', true)
137
+
138
+ // Toggle chat visibility
139
+ toggle()
140
+
141
+ // Clear all messages
142
+ clearMessages()
143
+ </script>
144
+ ```
145
+
146
+ ## Module Options
147
+
148
+ | Option | Type | Default | Description |
149
+ |--------|------|---------|-------------|
150
+ | `apiPath` | `string` | `/__docus__/assistant` | API endpoint path for the chat |
151
+ | `mcpServer` | `string` | `/mcp` | MCP server path or full URL (e.g., `https://docs.example.com/mcp` for external servers) |
152
+ | `model` | `string` | `google/gemini-3-flash` | AI model identifier for AI SDK Gateway |
153
+
154
+ ## Components
155
+
156
+ ### `<AssistantChat>`
157
+
158
+ Button to toggle the chat panel. The tooltip text is automatically translated using i18n (`assistant.tooltip`).
159
+
160
+ ### `<AssistantPanel>`
161
+
162
+ Main chat interface displayed as a side panel. Configuration is done via `app.config.ts` (see FAQ Questions section above).
163
+
164
+ ### `<AssistantFloatingInput>`
165
+
166
+ Floating input field positioned at the bottom of the viewport. No props required.
167
+
168
+ **Keyboard shortcuts:**
169
+ - `⌘I` / `Ctrl+I` - Focus the input
170
+ - `Escape` - Blur the input
171
+ - `Enter` - Submit the question
172
+
173
+ ## Composables
174
+
175
+ ### `useAssistant`
176
+
177
+ Main composable for controlling the chat state.
178
+
179
+ ```ts
180
+ const {
181
+ isOpen, // Ref<boolean> - Whether the chat is open
182
+ messages, // Ref<UIMessage[]> - Chat messages
183
+ pendingMessage, // Ref<string | undefined> - Pending message to send
184
+ faqQuestions, // ComputedRef<FaqCategory[]> - FAQ questions from config
185
+ open, // (message?: string, clearPrevious?: boolean) => void
186
+ close, // () => void
187
+ toggle, // () => void
188
+ clearMessages, // () => void
189
+ clearPending, // () => void
190
+ } = useAssistant()
191
+ ```
192
+
193
+ ### `useHighlighter`
194
+
195
+ Composable for syntax highlighting code blocks with Shiki.
196
+
197
+ ## Requirements
198
+
199
+ - Nuxt 4.x
200
+ - Nuxt UI 3.x (for `USlideover`, `UButton`, `UTextarea`, `UChatMessages`, etc.)
201
+ - An MCP server running (path configurable via `mcpServer`)
202
+ - `AI_GATEWAY_API_KEY` environment variable
203
+
204
+ ## Customization
205
+
206
+ ### System Prompt
207
+
208
+ To customize the AI's behavior, edit the system prompt in:
209
+ `runtime/server/api/search.ts`
210
+
211
+ ### Styling
212
+
213
+ The components use Nuxt UI and Tailwind CSS design tokens. Customize the appearance by modifying the component files or overriding the UI props.
@@ -0,0 +1,100 @@
1
+ import { addComponent, addImports, addServerHandler, createResolver, defineNuxtModule, logger } from '@nuxt/kit'
2
+
3
+ export interface AssistantModuleOptions {
4
+ /**
5
+ * API endpoint path for the assistant
6
+ * @default '/__docus__/assistant'
7
+ */
8
+ apiPath?: string
9
+ /**
10
+ * MCP server URL or path.
11
+ * - Use a path like '/mcp' to use the built-in Docus MCP server
12
+ * - Use a full URL like 'https://docs.example.com/mcp' for external MCP servers
13
+ * @default '/mcp'
14
+ */
15
+ mcpServer?: string
16
+ /**
17
+ * AI model to use via AI SDK Gateway
18
+ * @default 'google/gemini-3-flash'
19
+ */
20
+ model?: string
21
+ }
22
+
23
+ const log = logger.withTag('Docus')
24
+
25
+ export default defineNuxtModule<AssistantModuleOptions>({
26
+ meta: {
27
+ name: 'assistant',
28
+ configKey: 'assistant',
29
+ },
30
+ defaults: {
31
+ apiPath: '/__docus__/assistant',
32
+ mcpServer: '/mcp',
33
+ model: 'google/gemini-3-flash',
34
+ },
35
+ setup(options, nuxt) {
36
+ const hasApiKey = !!process.env.AI_GATEWAY_API_KEY
37
+
38
+ const { resolve } = createResolver(import.meta.url)
39
+
40
+ nuxt.options.runtimeConfig.public.assistant = {
41
+ enabled: hasApiKey,
42
+ apiPath: options.apiPath!,
43
+ }
44
+
45
+ addImports([
46
+ {
47
+ name: 'useAssistant',
48
+ from: resolve('./runtime/composables/useAssistant'),
49
+ },
50
+ ])
51
+
52
+ const components = [
53
+ 'AssistantChat',
54
+ 'AssistantPanel',
55
+ 'AssistantFloatingInput',
56
+ 'AssistantLoading',
57
+ 'AssistantMatrix',
58
+ ]
59
+
60
+ components.forEach(name =>
61
+ addComponent({
62
+ name,
63
+ filePath: hasApiKey
64
+ ? resolve(`./runtime/components/${name}.vue`)
65
+ : resolve('./runtime/components/AssistantChatDisabled.vue'),
66
+ }),
67
+ )
68
+
69
+ if (!hasApiKey) {
70
+ log.warn('[Docus] AI assistant disabled: AI_GATEWAY_API_KEY not found')
71
+ return
72
+ }
73
+
74
+ nuxt.options.runtimeConfig.assistant = {
75
+ mcpServer: options.mcpServer!,
76
+ model: options.model!,
77
+ }
78
+
79
+ const routePath = options.apiPath!.replace(/^\//, '')
80
+ addServerHandler({
81
+ route: `/${routePath}`,
82
+ handler: resolve('./runtime/server/api/search'),
83
+ })
84
+ },
85
+ })
86
+
87
+ declare module 'nuxt/schema' {
88
+ interface PublicRuntimeConfig {
89
+ assistant: {
90
+ enabled: boolean
91
+ apiPath: string
92
+ }
93
+ }
94
+ interface RuntimeConfig {
95
+ assistant: {
96
+ mcpServer: string
97
+ model: string
98
+ }
99
+ }
100
+ }
@@ -0,0 +1,21 @@
1
+ <script setup lang="ts">
2
+ import { useDocusI18n } from '../../../../app/composables/useDocusI18n'
3
+
4
+ const appConfig = useAppConfig()
5
+ const { toggle } = useAssistant()
6
+ const { t } = useDocusI18n()
7
+
8
+ const tooltipText = computed(() => t('assistant.tooltip'))
9
+ const triggerIcon = computed(() => appConfig.assistant?.icons?.trigger || 'i-lucide-sparkles')
10
+ </script>
11
+
12
+ <template>
13
+ <UTooltip :text="tooltipText">
14
+ <UButton
15
+ :icon="triggerIcon"
16
+ variant="ghost"
17
+ class="rounded-full"
18
+ @click="toggle"
19
+ />
20
+ </UTooltip>
21
+ </template>