docus 5.10.1 → 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.
- package/app/app.config.ts +4 -1
- package/app/app.vue +22 -27
- package/app/components/app/AppSearch.vue +54 -0
- package/app/components/docs/DocsAsideRight.vue +1 -2
- package/app/components/docs/DocsAsideRightBottom.vue +2 -2
- package/app/components/docs/DocsPageHeaderLinks.vue +11 -6
- package/app/composables/useDocusShortcuts.ts +24 -0
- package/app/composables/useUIConfig.ts +1 -1
- package/app/error.vue +1 -10
- package/app/pages/[[lang]]/[...slug].vue +6 -4
- package/app/plugins/i18n.ts +1 -1
- package/app/types/index.d.ts +22 -0
- package/i18n/locales/pl.json +2 -2
- package/modules/assistant/index.ts +13 -5
- package/modules/assistant/runtime/components/AssistantChat.vue +5 -2
- package/modules/assistant/runtime/components/AssistantComark.ts +9 -0
- package/modules/assistant/runtime/components/AssistantFloatingInput.vue +9 -10
- package/modules/assistant/runtime/components/AssistantIndicator.vue +116 -0
- package/modules/assistant/runtime/components/AssistantPanel.vue +268 -258
- package/modules/assistant/runtime/components/AssistantPreStream.vue +1 -1
- package/modules/assistant/runtime/composables/useAssistant.ts +34 -38
- package/modules/assistant/runtime/server/api/search.ts +22 -42
- package/modules/config.ts +12 -1
- package/modules/css.ts +35 -6
- package/modules/markdown-rewrite.ts +1 -1
- package/modules/skills/index.ts +4 -2
- package/nuxt.schema.ts +28 -0
- package/package.json +47 -28
- package/server/mcp/tools/get-page.ts +17 -8
- package/server/mcp/tools/list-pages.ts +10 -6
- package/server/routes/sitemap.xml.ts +7 -4
- package/server/utils/content.ts +4 -0
- package/modules/assistant/runtime/components/AssistantLoading.vue +0 -164
- package/modules/assistant/runtime/components/AssistantMatrix.vue +0 -92
package/app/app.config.ts
CHANGED
|
@@ -3,6 +3,9 @@ export default defineAppConfig({
|
|
|
3
3
|
locale: 'en',
|
|
4
4
|
colorMode: '',
|
|
5
5
|
},
|
|
6
|
+
search: {
|
|
7
|
+
fts: false,
|
|
8
|
+
},
|
|
6
9
|
ui: {
|
|
7
10
|
colors: {
|
|
8
11
|
primary: 'emerald',
|
|
@@ -17,7 +20,7 @@ export default defineAppConfig({
|
|
|
17
20
|
},
|
|
18
21
|
contentToc: {
|
|
19
22
|
defaultVariants: {
|
|
20
|
-
|
|
23
|
+
highlightVariant: 'circuit',
|
|
21
24
|
},
|
|
22
25
|
},
|
|
23
26
|
contentNavigation: {
|
package/app/app.vue
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
import type { ContentNavigationItem, PageCollections } from '@nuxt/content'
|
|
3
3
|
import * as nuxtUiLocales from '@nuxt/ui/locale'
|
|
4
4
|
import { transformNavigation } from './utils/navigation'
|
|
5
|
-
import {
|
|
5
|
+
import { useDocusShortcuts } from './composables/useDocusShortcuts'
|
|
6
6
|
import { useSubNavigation } from './composables/useSubNavigation'
|
|
7
7
|
|
|
8
8
|
const appConfig = useAppConfig()
|
|
9
9
|
const { seo } = appConfig
|
|
10
|
-
|
|
10
|
+
useDocusShortcuts()
|
|
11
11
|
const site = useSiteConfig()
|
|
12
12
|
const { locale, locales, isEnabled, switchLocalePath } = useDocusI18n()
|
|
13
|
-
const { isEnabled: isAssistantEnabled
|
|
13
|
+
const { isEnabled: isAssistantEnabled } = useAssistant()
|
|
14
14
|
|
|
15
15
|
const nuxtUiLocale = computed(() => nuxtUiLocales[locale.value as keyof typeof nuxtUiLocales] || nuxtUiLocales.en)
|
|
16
16
|
const lang = computed(() => nuxtUiLocale.value.code)
|
|
@@ -53,10 +53,6 @@ const { data: navigation } = await useAsyncData(() => `navigation_${collectionNa
|
|
|
53
53
|
transform: (data: ContentNavigationItem[]) => transformNavigation(data, isEnabled.value, locale.value),
|
|
54
54
|
watch: [locale],
|
|
55
55
|
})
|
|
56
|
-
const { data: files } = useLazyAsyncData(`search_${collectionName.value}`, () => queryCollectionSearchSections(collectionName.value as keyof PageCollections), {
|
|
57
|
-
server: false,
|
|
58
|
-
watch: [locale],
|
|
59
|
-
})
|
|
60
56
|
|
|
61
57
|
provide('navigation', navigation)
|
|
62
58
|
|
|
@@ -67,28 +63,27 @@ const { subNavigationMode } = useSubNavigation(navigation)
|
|
|
67
63
|
<UApp :locale="nuxtUiLocale">
|
|
68
64
|
<NuxtLoadingIndicator color="var(--ui-primary)" />
|
|
69
65
|
|
|
70
|
-
<div
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
66
|
+
<div class="flex">
|
|
67
|
+
<div
|
|
68
|
+
class="flex-1 min-w-0"
|
|
69
|
+
:class="{ 'docus-sub-header': subNavigationMode === 'header' }"
|
|
70
|
+
>
|
|
71
|
+
<AppHeader v-if="$route.meta.header !== false" />
|
|
72
|
+
<NuxtLayout>
|
|
73
|
+
<NuxtPage />
|
|
74
|
+
</NuxtLayout>
|
|
75
|
+
<AppFooter v-if="$route.meta.footer !== false" />
|
|
80
76
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
<
|
|
77
|
+
<ClientOnly>
|
|
78
|
+
<AppSearch :navigation="navigation" />
|
|
79
|
+
<LazyAssistantFloatingInput v-if="isAssistantEnabled" />
|
|
80
|
+
</ClientOnly>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<ClientOnly v-if="isAssistantEnabled">
|
|
88
84
|
<LazyAssistantPanel />
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
</ClientOnly>
|
|
85
|
+
</ClientOnly>
|
|
86
|
+
</div>
|
|
92
87
|
</UApp>
|
|
93
88
|
</template>
|
|
94
89
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ContentNavigationItem, PageCollections } from '@nuxt/content'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
navigation?: ContentNavigationItem[]
|
|
6
|
+
}>()
|
|
7
|
+
|
|
8
|
+
const appConfig = useAppConfig()
|
|
9
|
+
const { forced: forcedColorMode } = useDocusColorMode()
|
|
10
|
+
const { locale, isEnabled } = useDocusI18n()
|
|
11
|
+
|
|
12
|
+
const collectionName = computed(() => (isEnabled.value ? `docs_${locale.value}` : 'docs') as keyof PageCollections)
|
|
13
|
+
const useFts = appConfig.search.fts
|
|
14
|
+
|
|
15
|
+
const { data: files } = useFts
|
|
16
|
+
? { data: ref(null) }
|
|
17
|
+
: useLazyAsyncData(`search_${collectionName.value}`, () => queryCollectionSearchSections(collectionName.value), {
|
|
18
|
+
server: false,
|
|
19
|
+
watch: [locale],
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const { search, status: searchStatus, init } = useFts
|
|
23
|
+
? useSearchCollection(collectionName, { immediate: false, ignoredTags: ['style'] })
|
|
24
|
+
: { search: undefined, status: ref(undefined), init: () => {} }
|
|
25
|
+
|
|
26
|
+
if (useFts) {
|
|
27
|
+
const { open } = useContentSearch()
|
|
28
|
+
watch(open, (value) => {
|
|
29
|
+
if (value && searchStatus.value === 'idle') {
|
|
30
|
+
init()
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const links = computed(() => useFts
|
|
36
|
+
? props.navigation?.filter(item => item.children?.length).map(item => ({
|
|
37
|
+
label: item.title,
|
|
38
|
+
icon: item.icon,
|
|
39
|
+
to: item.children![0]!.path,
|
|
40
|
+
}))
|
|
41
|
+
: undefined,
|
|
42
|
+
)
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<LazyUContentSearch
|
|
47
|
+
:files="files"
|
|
48
|
+
:search="search"
|
|
49
|
+
:search-status="searchStatus"
|
|
50
|
+
:links="links"
|
|
51
|
+
:navigation="navigation"
|
|
52
|
+
:color-mode="!forcedColorMode"
|
|
53
|
+
/>
|
|
54
|
+
</template>
|
|
@@ -8,7 +8,6 @@ const props = defineProps<{
|
|
|
8
8
|
|
|
9
9
|
const links = computed(() => props.page?.body?.toc?.links || [])
|
|
10
10
|
|
|
11
|
-
const { shouldPushContent: shouldHideToc } = useAssistant()
|
|
12
11
|
const { subNavigationMode } = useSubNavigation()
|
|
13
12
|
const appConfig = useAppConfig()
|
|
14
13
|
const { t } = useDocusI18n()
|
|
@@ -19,7 +18,7 @@ const contentTocVariants = useUIConfig('contentToc')
|
|
|
19
18
|
<template>
|
|
20
19
|
<div>
|
|
21
20
|
<UContentToc
|
|
22
|
-
v-if="links.length
|
|
21
|
+
v-if="links.length"
|
|
23
22
|
:highlight="contentTocVariants.highlight ?? true"
|
|
24
23
|
:highlight-color="contentTocVariants.highlightColor"
|
|
25
24
|
:highlight-variant="contentTocVariants.highlightVariant"
|
|
@@ -4,10 +4,10 @@ const route = useRoute()
|
|
|
4
4
|
const pageUrl = route.path
|
|
5
5
|
const appConfig = useAppConfig()
|
|
6
6
|
const { t } = useDocusI18n()
|
|
7
|
-
const { isEnabled, open } = useAssistant()
|
|
7
|
+
const { isEnabled, isStudioExpanded, open } = useAssistant()
|
|
8
8
|
|
|
9
9
|
const showExplainWithAi = computed(() => {
|
|
10
|
-
return isEnabled.value && appConfig.assistant?.explainWithAi !== false
|
|
10
|
+
return isEnabled.value && appConfig.assistant?.explainWithAi !== false && !isStudioExpanded.value
|
|
11
11
|
})
|
|
12
12
|
|
|
13
13
|
const explainIcon = computed(() => appConfig.assistant?.icons?.explain || 'i-lucide-brain')
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { useClipboard } from '@vueuse/core'
|
|
3
|
+
import { joinURL, withTrailingSlash } from 'ufo'
|
|
3
4
|
import { useRuntimeConfig } from '#imports'
|
|
4
5
|
|
|
5
6
|
const route = useRoute()
|
|
6
7
|
const toast = useToast()
|
|
7
|
-
const
|
|
8
|
+
const runtimeConfig = useRuntimeConfig()
|
|
9
|
+
const appBaseURL = runtimeConfig.app?.baseURL || '/'
|
|
10
|
+
const mcpRoute = (runtimeConfig.public.mcp as { route?: string } | undefined)?.route || '/mcp'
|
|
8
11
|
|
|
9
12
|
const { copy, copied } = useClipboard()
|
|
10
13
|
const { t } = useDocusI18n()
|
|
11
14
|
|
|
12
|
-
const markdownLink = computed(() => `${window?.location?.origin}${appBaseURL}raw${route.path}.md`)
|
|
13
|
-
const
|
|
15
|
+
const markdownLink = computed(() => `${window?.location?.origin}${withTrailingSlash(appBaseURL)}raw${route.path}.md`)
|
|
16
|
+
const mcpServerUrl = computed(() => `${window?.location?.origin}${joinURL(appBaseURL, mcpRoute)}`)
|
|
17
|
+
const mcpDeeplink = computed(() => `${window?.location?.origin}${joinURL(appBaseURL, mcpRoute, 'deeplink')}`)
|
|
18
|
+
const items = computed(() => [
|
|
14
19
|
[{
|
|
15
20
|
label: t('docs.copy.link'),
|
|
16
21
|
icon: 'i-lucide-link',
|
|
@@ -41,7 +46,7 @@ const items = [
|
|
|
41
46
|
label: 'Copy MCP Server URL',
|
|
42
47
|
icon: 'i-lucide-link',
|
|
43
48
|
onSelect() {
|
|
44
|
-
copy(
|
|
49
|
+
copy(mcpServerUrl.value)
|
|
45
50
|
toast.add({
|
|
46
51
|
title: 'Copied to clipboard',
|
|
47
52
|
icon: 'i-lucide-check-circle',
|
|
@@ -52,10 +57,10 @@ const items = [
|
|
|
52
57
|
label: 'Add MCP Server',
|
|
53
58
|
icon: 'i-simple-icons:cursor',
|
|
54
59
|
target: '_blank',
|
|
55
|
-
to:
|
|
60
|
+
to: mcpDeeplink.value,
|
|
56
61
|
},
|
|
57
62
|
],
|
|
58
|
-
]
|
|
63
|
+
])
|
|
59
64
|
|
|
60
65
|
async function copyPage() {
|
|
61
66
|
const page = await $fetch<string>(`/raw${route.path}.md`)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useDocusColorMode } from './useDocusColorMode'
|
|
2
|
+
|
|
3
|
+
export function useDocusShortcuts() {
|
|
4
|
+
const appConfig = useAppConfig()
|
|
5
|
+
const { forced: forcedColorMode } = useDocusColorMode()
|
|
6
|
+
const colorMode = useColorMode()
|
|
7
|
+
const toggleColorModeShortcut = computed(() => appConfig.docus?.shortcuts?.toggleColorMode ?? 'd')
|
|
8
|
+
|
|
9
|
+
const shortcuts = computed(() => {
|
|
10
|
+
const key = toggleColorModeShortcut.value
|
|
11
|
+
if (!key) return {}
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
[key]: {
|
|
15
|
+
handler: () => {
|
|
16
|
+
if (forcedColorMode) return
|
|
17
|
+
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
defineShortcuts(shortcuts)
|
|
24
|
+
}
|
|
@@ -24,7 +24,7 @@ interface UIConfigMap {
|
|
|
24
24
|
export function useUIConfig<K extends keyof UIConfigMap>(componentName: K): ComputedRef<UIConfigMap[K]> {
|
|
25
25
|
const appConfig = useAppConfig()
|
|
26
26
|
return computed(() => {
|
|
27
|
-
const ui = appConfig.ui
|
|
27
|
+
const ui = appConfig.ui
|
|
28
28
|
return (ui?.[componentName]?.defaultVariants || {}) as UIConfigMap[K]
|
|
29
29
|
})
|
|
30
30
|
}
|
package/app/error.vue
CHANGED
|
@@ -3,13 +3,11 @@ import type { NuxtError } from '#app'
|
|
|
3
3
|
import type { ContentNavigationItem, PageCollections } from '@nuxt/content'
|
|
4
4
|
import * as nuxtUiLocales from '@nuxt/ui/locale'
|
|
5
5
|
import { transformNavigation } from './utils/navigation'
|
|
6
|
-
import { useDocusColorMode } from './composables/useDocusColorMode'
|
|
7
6
|
|
|
8
7
|
const props = defineProps<{
|
|
9
8
|
error: NuxtError
|
|
10
9
|
}>()
|
|
11
10
|
|
|
12
|
-
const { forced: forcedColorMode } = useDocusColorMode()
|
|
13
11
|
const { locale, locales, isEnabled, t, switchLocalePath } = useDocusI18n()
|
|
14
12
|
|
|
15
13
|
const nuxtUiLocale = computed(() => nuxtUiLocales[locale.value as keyof typeof nuxtUiLocales] || nuxtUiLocales.en)
|
|
@@ -53,9 +51,6 @@ const { data: navigation } = await useAsyncData(`navigation_${collectionName.val
|
|
|
53
51
|
transform: (data: ContentNavigationItem[]) => transformNavigation(data, isEnabled.value, locale.value),
|
|
54
52
|
watch: [locale],
|
|
55
53
|
})
|
|
56
|
-
const { data: files } = useLazyAsyncData(`search_${collectionName.value}`, () => queryCollectionSearchSections(collectionName.value as keyof PageCollections), {
|
|
57
|
-
server: false,
|
|
58
|
-
})
|
|
59
54
|
|
|
60
55
|
provide('navigation', navigation)
|
|
61
56
|
</script>
|
|
@@ -69,11 +64,7 @@ provide('navigation', navigation)
|
|
|
69
64
|
<AppFooter />
|
|
70
65
|
|
|
71
66
|
<ClientOnly>
|
|
72
|
-
<
|
|
73
|
-
:files="files"
|
|
74
|
-
:navigation="navigation"
|
|
75
|
-
:color-mode="!forcedColorMode"
|
|
76
|
-
/>
|
|
67
|
+
<AppSearch :navigation="navigation" />
|
|
77
68
|
</ClientOnly>
|
|
78
69
|
</UApp>
|
|
79
70
|
</template>
|
|
@@ -9,10 +9,9 @@ definePageMeta({
|
|
|
9
9
|
|
|
10
10
|
const route = useRoute()
|
|
11
11
|
const { locale, isEnabled, t } = useDocusI18n()
|
|
12
|
+
const { isOpen } = useAssistant()
|
|
12
13
|
const appConfig = useAppConfig()
|
|
13
14
|
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
|
|
14
|
-
const { shouldPushContent: shouldHideToc } = useAssistant()
|
|
15
|
-
|
|
16
15
|
const collectionName = computed(() => isEnabled.value ? `docs_${locale.value}` : 'docs')
|
|
17
16
|
|
|
18
17
|
const [{ data: page }, { data: surround }] = await Promise.all([
|
|
@@ -75,7 +74,7 @@ addPrerenderPath(`/raw${route.path}.md`)
|
|
|
75
74
|
<template>
|
|
76
75
|
<UPage
|
|
77
76
|
v-if="page"
|
|
78
|
-
:
|
|
77
|
+
:ui="isOpen ? { center: 'lg:col-span-10' } : undefined"
|
|
79
78
|
>
|
|
80
79
|
<UPageHeader
|
|
81
80
|
:title="page.title"
|
|
@@ -135,7 +134,10 @@ addPrerenderPath(`/raw${route.path}.md`)
|
|
|
135
134
|
<UContentSurround :surround="surround" />
|
|
136
135
|
</UPageBody>
|
|
137
136
|
|
|
138
|
-
<template
|
|
137
|
+
<template
|
|
138
|
+
v-if="!isOpen"
|
|
139
|
+
#right
|
|
140
|
+
>
|
|
139
141
|
<DocsAsideRight
|
|
140
142
|
:page="page"
|
|
141
143
|
/>
|
package/app/plugins/i18n.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { RouteLocationNormalized } from 'vue-router'
|
|
2
2
|
import { consola } from 'consola'
|
|
3
3
|
|
|
4
|
-
const log = consola.withTag('
|
|
4
|
+
const log = consola.withTag('docus')
|
|
5
5
|
|
|
6
6
|
// Lazy import functions for locale files (bundled but not eagerly loaded)
|
|
7
7
|
const localeFiles = import.meta.glob<{ default: Record<string, unknown> }>('../../i18n/locales/*.json')
|
package/app/types/index.d.ts
CHANGED
|
@@ -6,6 +6,20 @@ declare module 'nuxt/schema' {
|
|
|
6
6
|
interface AppConfig {
|
|
7
7
|
docus: {
|
|
8
8
|
locale: string
|
|
9
|
+
/**
|
|
10
|
+
* Force a specific color mode. Leave empty for system preference with toggle.
|
|
11
|
+
*/
|
|
12
|
+
colorMode?: '' | 'light' | 'dark'
|
|
13
|
+
/**
|
|
14
|
+
* Keyboard shortcuts configuration.
|
|
15
|
+
*/
|
|
16
|
+
shortcuts?: {
|
|
17
|
+
/**
|
|
18
|
+
* Shortcut to toggle light and dark mode.
|
|
19
|
+
* @default 'd'
|
|
20
|
+
*/
|
|
21
|
+
toggleColorMode?: string
|
|
22
|
+
}
|
|
9
23
|
}
|
|
10
24
|
seo: {
|
|
11
25
|
titleTemplate: string
|
|
@@ -48,6 +62,14 @@ declare module 'nuxt/schema' {
|
|
|
48
62
|
branch: string
|
|
49
63
|
rootDir?: string
|
|
50
64
|
} | false
|
|
65
|
+
search: {
|
|
66
|
+
/**
|
|
67
|
+
* Use SQLite FTS5 full-text search instead of Fuse.js.
|
|
68
|
+
* Requires @nuxt/content v3.14+.
|
|
69
|
+
* @default false
|
|
70
|
+
*/
|
|
71
|
+
fts: boolean
|
|
72
|
+
}
|
|
51
73
|
assistant?: {
|
|
52
74
|
/**
|
|
53
75
|
* Show the floating input at the bottom of documentation pages.
|
package/i18n/locales/pl.json
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"copyWordmarkFailed": "Nie udało się skopiować wordmarku"
|
|
35
35
|
},
|
|
36
36
|
"assistant": {
|
|
37
|
-
"title": "Zapytaj
|
|
37
|
+
"title": "Zapytaj AI",
|
|
38
38
|
"placeholder": "Zadaj pytanie...",
|
|
39
39
|
"tooltip": "Zadaj AI pytanie",
|
|
40
40
|
"tryAsking": "Spróbuj zadać pytanie",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"faq": "Często zadawane pytania",
|
|
50
50
|
"chatCleared": "Czat został wyczyszczony po odświeżeniu",
|
|
51
51
|
"lineBreak": "Podział wiersza",
|
|
52
|
-
"explainWithAi": "Wyjaśnij
|
|
52
|
+
"explainWithAi": "Wyjaśnij z AI",
|
|
53
53
|
"toolListPages": "Wymienione strony dokumentacji",
|
|
54
54
|
"toolReadPage": "Czytaj",
|
|
55
55
|
"loading": {
|
|
@@ -21,7 +21,7 @@ export interface AssistantModuleOptions {
|
|
|
21
21
|
model?: string
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const log = logger.withTag('
|
|
24
|
+
const log = logger.withTag('docus')
|
|
25
25
|
|
|
26
26
|
const defaults: Required<AssistantModuleOptions> = {
|
|
27
27
|
apiPath: '/__docus__/assistant',
|
|
@@ -33,6 +33,9 @@ export default defineNuxtModule<AssistantModuleOptions>({
|
|
|
33
33
|
meta: {
|
|
34
34
|
name: 'assistant',
|
|
35
35
|
},
|
|
36
|
+
moduleDependencies: {
|
|
37
|
+
'@comark/nuxt': {},
|
|
38
|
+
},
|
|
36
39
|
setup(_options, nuxt) {
|
|
37
40
|
const legacyOptions = nuxt.options.assistant
|
|
38
41
|
if (legacyOptions && Object.keys(legacyOptions).length > 0) {
|
|
@@ -63,8 +66,6 @@ export default defineNuxtModule<AssistantModuleOptions>({
|
|
|
63
66
|
'AssistantChat',
|
|
64
67
|
'AssistantPanel',
|
|
65
68
|
'AssistantFloatingInput',
|
|
66
|
-
'AssistantLoading',
|
|
67
|
-
'AssistantMatrix',
|
|
68
69
|
]
|
|
69
70
|
|
|
70
71
|
components.forEach(name =>
|
|
@@ -77,7 +78,9 @@ export default defineNuxtModule<AssistantModuleOptions>({
|
|
|
77
78
|
)
|
|
78
79
|
|
|
79
80
|
if (!hasAiGatewayAuth) {
|
|
80
|
-
|
|
81
|
+
nuxt.hook('modules:done', () => {
|
|
82
|
+
log.warn('AI assistant disabled: neither `AI_GATEWAY_API_KEY` nor `VERCEL_OIDC_TOKEN` found')
|
|
83
|
+
})
|
|
81
84
|
return
|
|
82
85
|
}
|
|
83
86
|
|
|
@@ -86,7 +89,12 @@ export default defineNuxtModule<AssistantModuleOptions>({
|
|
|
86
89
|
model: options.model,
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
|
|
92
|
+
addComponent({
|
|
93
|
+
name: 'AssistantComark',
|
|
94
|
+
filePath: resolve('./runtime/components/AssistantComark'),
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const routePath = options.apiPath!.replace(/^\//, '')
|
|
90
98
|
addServerHandler({
|
|
91
99
|
route: `/${routePath}`,
|
|
92
100
|
handler: resolve('./runtime/server/api/search'),
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { useDocusI18n } from '../../../../app/composables/useDocusI18n'
|
|
3
3
|
|
|
4
4
|
const appConfig = useAppConfig()
|
|
5
|
-
const { toggle } = useAssistant()
|
|
5
|
+
const { toggle, isStudioExpanded } = useAssistant()
|
|
6
6
|
const { t } = useDocusI18n()
|
|
7
7
|
|
|
8
8
|
const tooltipText = computed(() => t('assistant.tooltip'))
|
|
@@ -10,7 +10,10 @@ const triggerIcon = computed(() => appConfig.assistant?.icons?.trigger || 'i-cus
|
|
|
10
10
|
</script>
|
|
11
11
|
|
|
12
12
|
<template>
|
|
13
|
-
<UTooltip
|
|
13
|
+
<UTooltip
|
|
14
|
+
v-if="!isStudioExpanded"
|
|
15
|
+
:text="tooltipText"
|
|
16
|
+
>
|
|
14
17
|
<UButton
|
|
15
18
|
:icon="triggerIcon"
|
|
16
19
|
color="neutral"
|
|
@@ -4,7 +4,7 @@ import { useDocusI18n } from '../../../../app/composables/useDocusI18n'
|
|
|
4
4
|
|
|
5
5
|
const route = useRoute()
|
|
6
6
|
const appConfig = useAppConfig()
|
|
7
|
-
const { open, isOpen } = useAssistant()
|
|
7
|
+
const { open, isOpen, isStudioExpanded } = useAssistant()
|
|
8
8
|
const { t } = useDocusI18n()
|
|
9
9
|
const input = ref('')
|
|
10
10
|
const isVisible = ref(true)
|
|
@@ -42,31 +42,29 @@ const shortcuts = computed(() => ({
|
|
|
42
42
|
inputRef.value?.inputRef?.focus()
|
|
43
43
|
},
|
|
44
44
|
},
|
|
45
|
-
escape: {
|
|
46
|
-
usingInput: true,
|
|
47
|
-
handler: () => {
|
|
48
|
-
inputRef.value?.inputRef?.blur()
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
45
|
}))
|
|
52
46
|
|
|
53
47
|
defineShortcuts(shortcuts)
|
|
48
|
+
|
|
49
|
+
function onEscape() {
|
|
50
|
+
inputRef.value?.inputRef?.blur()
|
|
51
|
+
}
|
|
54
52
|
</script>
|
|
55
53
|
|
|
56
54
|
<template>
|
|
57
55
|
<AnimatePresence>
|
|
58
56
|
<motion.div
|
|
59
|
-
v-if="isFloatingInputEnabled && isDocsRoute && isVisible && !isOpen"
|
|
57
|
+
v-if="isFloatingInputEnabled && isDocsRoute && isVisible && !isOpen && !isStudioExpanded"
|
|
60
58
|
key="floating-input"
|
|
61
59
|
:initial="{ y: 20, opacity: 0 }"
|
|
62
60
|
:animate="{ y: 0, opacity: 1 }"
|
|
63
61
|
:exit="{ y: 100, opacity: 0 }"
|
|
64
62
|
:transition="{ duration: 0.2, ease: 'easeOut' }"
|
|
65
|
-
class="pointer-events-none fixed inset-x-0 z-10 bottom-[max(1.5rem,env(safe-area-inset-bottom))] px-4 sm:px-
|
|
63
|
+
class="pointer-events-none fixed inset-x-0 z-10 bottom-[max(1.5rem,env(safe-area-inset-bottom))] px-4 sm:px-24"
|
|
66
64
|
style="will-change: transform"
|
|
67
65
|
>
|
|
68
66
|
<form
|
|
69
|
-
class="pointer-events-none flex w-full justify-center"
|
|
67
|
+
class="pointer-events-none flex w-full min-w-3xs justify-center"
|
|
70
68
|
@submit.prevent="handleSubmit"
|
|
71
69
|
>
|
|
72
70
|
<div class="pointer-events-auto w-full max-w-96">
|
|
@@ -82,6 +80,7 @@ defineShortcuts(shortcuts)
|
|
|
82
80
|
trailing: 'pe-2',
|
|
83
81
|
}"
|
|
84
82
|
@keydown.enter.exact.prevent="handleSubmit"
|
|
83
|
+
@keydown.escape="onEscape"
|
|
85
84
|
>
|
|
86
85
|
<template #trailing>
|
|
87
86
|
<div class="flex items-center gap-2">
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const size = 4
|
|
3
|
+
const dotSize = 2
|
|
4
|
+
const gap = 2
|
|
5
|
+
const totalDots = size * size
|
|
6
|
+
|
|
7
|
+
const patterns = [
|
|
8
|
+
[[0], [1], [2], [3], [7], [11], [15], [14], [13], [12], [8], [4], [5], [6], [10], [9]],
|
|
9
|
+
[[0, 4, 8, 12], [1, 5, 9, 13], [2, 6, 10, 14], [3, 7, 11, 15]],
|
|
10
|
+
[[5, 6, 9, 10], [1, 4, 7, 8, 11, 14], [0, 3, 12, 15], [1, 4, 7, 8, 11, 14], [5, 6, 9, 10]],
|
|
11
|
+
[[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11], [12, 13, 14, 15]],
|
|
12
|
+
[[0], [3], [15], [12]],
|
|
13
|
+
[[5, 6, 9, 10], [1, 2, 4, 7, 8, 11, 13, 14], [0, 3, 12, 15]],
|
|
14
|
+
[[0], [1], [2], [3], [7], [6], [5], [4], [8], [9], [10], [11], [15], [14], [13], [12]],
|
|
15
|
+
[[0], [1, 4], [2, 5, 8], [3, 6, 9, 12], [7, 10, 13], [11, 14], [15]],
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const activeDots = ref<Set<number>>(new Set())
|
|
19
|
+
let patternIndex = 0
|
|
20
|
+
let stepIndex = 0
|
|
21
|
+
|
|
22
|
+
function nextStep() {
|
|
23
|
+
const pattern = patterns[patternIndex]
|
|
24
|
+
if (!pattern) return
|
|
25
|
+
|
|
26
|
+
activeDots.value = new Set(pattern[stepIndex])
|
|
27
|
+
stepIndex++
|
|
28
|
+
|
|
29
|
+
if (stepIndex >= pattern.length) {
|
|
30
|
+
stepIndex = 0
|
|
31
|
+
patternIndex = (patternIndex + 1) % patterns.length
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const statusMessages = ['Thinking...', 'Searching...', 'Reading...', 'Analyzing...']
|
|
36
|
+
const currentIndex = ref(0)
|
|
37
|
+
const displayedText = ref(statusMessages[0]!)
|
|
38
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz'
|
|
39
|
+
|
|
40
|
+
function scramble(from: string, to: string) {
|
|
41
|
+
const maxLength = Math.max(from.length, to.length)
|
|
42
|
+
let frame = 0
|
|
43
|
+
const totalFrames = 15
|
|
44
|
+
|
|
45
|
+
const step = () => {
|
|
46
|
+
frame++
|
|
47
|
+
let result = ''
|
|
48
|
+
const progress = (frame / totalFrames) * maxLength
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < maxLength; i++) {
|
|
51
|
+
if (i < progress - 2) {
|
|
52
|
+
result += to[i] || ''
|
|
53
|
+
}
|
|
54
|
+
else if (i < progress) {
|
|
55
|
+
result += chars[Math.floor(Math.random() * chars.length)]
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
result += from[i] || ''
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
displayedText.value = result
|
|
63
|
+
|
|
64
|
+
if (frame < totalFrames) {
|
|
65
|
+
requestAnimationFrame(step)
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
displayedText.value = to
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
requestAnimationFrame(step)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let matrixInterval: ReturnType<typeof setInterval> | undefined
|
|
76
|
+
let textInterval: ReturnType<typeof setInterval> | undefined
|
|
77
|
+
|
|
78
|
+
onMounted(() => {
|
|
79
|
+
nextStep()
|
|
80
|
+
matrixInterval = setInterval(nextStep, 120)
|
|
81
|
+
textInterval = setInterval(() => {
|
|
82
|
+
const prev = displayedText.value
|
|
83
|
+
currentIndex.value = (currentIndex.value + 1) % statusMessages.length
|
|
84
|
+
scramble(prev, statusMessages[currentIndex.value]!)
|
|
85
|
+
}, 3500)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
onUnmounted(() => {
|
|
89
|
+
clearInterval(matrixInterval)
|
|
90
|
+
clearInterval(textInterval)
|
|
91
|
+
})
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<template>
|
|
95
|
+
<div class="flex items-center text-xs text-muted overflow-hidden">
|
|
96
|
+
<div
|
|
97
|
+
class="shrink-0 mr-2 grid"
|
|
98
|
+
:style="{
|
|
99
|
+
gridTemplateColumns: `repeat(${size}, 1fr)`,
|
|
100
|
+
gap: `${gap}px`,
|
|
101
|
+
width: `${size * dotSize + (size - 1) * gap}px`,
|
|
102
|
+
height: `${size * dotSize + (size - 1) * gap}px`,
|
|
103
|
+
}"
|
|
104
|
+
>
|
|
105
|
+
<span
|
|
106
|
+
v-for="i in totalDots"
|
|
107
|
+
:key="i"
|
|
108
|
+
class="rounded-[0.5px] bg-current transition-opacity duration-100"
|
|
109
|
+
:class="activeDots.has(i - 1) ? 'opacity-100' : 'opacity-20'"
|
|
110
|
+
:style="{ width: `${dotSize}px`, height: `${dotSize}px` }"
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<UChatShimmer :text="displayedText" class="font-mono tracking-tight" />
|
|
115
|
+
</div>
|
|
116
|
+
</template>
|