docus 5.4.4 → 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.
- package/app/app.vue +16 -6
- package/app/components/app/AppHeader.vue +5 -0
- package/app/components/docs/DocsAsideRightBottom.vue +28 -1
- package/app/pages/[[lang]]/[...slug].vue +9 -2
- package/app/plugins/i18n.ts +28 -11
- package/app/types/index.d.ts +49 -0
- package/i18n/locales/en.json +27 -0
- package/i18n/locales/fr.json +28 -1
- package/modules/assistant/README.md +213 -0
- package/modules/assistant/index.ts +100 -0
- package/modules/assistant/runtime/components/AssistantChat.vue +21 -0
- package/modules/assistant/runtime/components/AssistantChatDisabled.vue +3 -0
- package/modules/assistant/runtime/components/AssistantFloatingInput.vue +105 -0
- package/modules/assistant/runtime/components/AssistantLoading.vue +164 -0
- package/modules/assistant/runtime/components/AssistantMatrix.vue +92 -0
- package/modules/assistant/runtime/components/AssistantPanel.vue +329 -0
- package/modules/assistant/runtime/components/AssistantPreStream.vue +46 -0
- package/modules/assistant/runtime/composables/useAssistant.ts +107 -0
- package/modules/assistant/runtime/composables/useHighlighter.ts +34 -0
- package/modules/assistant/runtime/server/api/search.ts +111 -0
- package/modules/assistant/runtime/types.ts +7 -0
- package/modules/config.ts +5 -3
- package/modules/css.ts +3 -1
- package/modules/markdown-rewrite.ts +130 -0
- package/nuxt.config.ts +13 -1
- package/nuxt.schema.ts +63 -0
- package/package.json +18 -9
- package/server/routes/sitemap.xml.ts +74 -0
- package/utils/meta.ts +9 -3
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
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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>
|
|
@@ -11,6 +11,7 @@ const route = useRoute()
|
|
|
11
11
|
const { locale, isEnabled, t } = useDocusI18n()
|
|
12
12
|
const appConfig = useAppConfig()
|
|
13
13
|
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
|
|
14
|
+
const { shouldPushContent: shouldHideToc } = useAssistant()
|
|
14
15
|
|
|
15
16
|
const collectionName = computed(() => isEnabled.value ? `docs_${locale.value}` : 'docs')
|
|
16
17
|
|
|
@@ -62,10 +63,16 @@ const editLink = computed(() => {
|
|
|
62
63
|
`${page.value?.stem}.${page.value?.extension}`,
|
|
63
64
|
].filter(Boolean).join('/')
|
|
64
65
|
})
|
|
66
|
+
|
|
67
|
+
// Add the page path to the prerender list
|
|
68
|
+
addPrerenderPath(`/raw${route.path}.md`)
|
|
65
69
|
</script>
|
|
66
70
|
|
|
67
71
|
<template>
|
|
68
|
-
<UPage
|
|
72
|
+
<UPage
|
|
73
|
+
v-if="page"
|
|
74
|
+
:key="`page-${shouldHideToc}`"
|
|
75
|
+
>
|
|
69
76
|
<UPageHeader
|
|
70
77
|
:title="page.title"
|
|
71
78
|
:description="page.description"
|
|
@@ -124,7 +131,7 @@ const editLink = computed(() => {
|
|
|
124
131
|
</UPageBody>
|
|
125
132
|
|
|
126
133
|
<template
|
|
127
|
-
v-if="page?.body?.toc?.links?.length"
|
|
134
|
+
v-if="page?.body?.toc?.links?.length && !shouldHideToc"
|
|
128
135
|
#right
|
|
129
136
|
>
|
|
130
137
|
<UContentToc
|
package/app/plugins/i18n.ts
CHANGED
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
resolvedMessages = localeMessages
|
|
40
|
+
resolvedMessages = {} as Record<string, unknown>
|
|
24
41
|
}
|
|
25
42
|
}
|
|
26
43
|
|
package/app/types/index.d.ts
CHANGED
|
@@ -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/i18n/locales/en.json
CHANGED
|
@@ -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
|
}
|
package/i18n/locales/fr.json
CHANGED
|
@@ -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>
|