docus 5.11.0 → 5.12.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.
- package/app/app.config.ts +3 -0
- 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 +4 -4
- 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 -5
- package/app/types/index.d.ts +22 -0
- package/i18n/locales/pl.json +2 -2
- package/modules/assistant/index.ts +6 -3
- package/modules/assistant/runtime/components/AssistantChat.vue +5 -2
- package/modules/assistant/runtime/components/AssistantComark.ts +10 -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/css.ts +8 -0
- package/nuxt.schema.ts +28 -0
- package/package.json +29 -28
- package/server/mcp/tools/get-page.ts +11 -5
- package/server/mcp/tools/list-pages.ts +5 -4
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { UIMessage } from 'ai'
|
|
2
|
-
import { useAppConfig, useRuntimeConfig
|
|
3
|
-
import {
|
|
4
|
-
import { computed } from 'vue'
|
|
2
|
+
import { useAppConfig, useRuntimeConfig } from '#imports'
|
|
3
|
+
import { createSharedComposable, useLocalStorage } from '@vueuse/core'
|
|
4
|
+
import { computed, ref, watch } from 'vue'
|
|
5
5
|
import type { FaqCategory, FaqQuestions, LocalizedFaqQuestions } from '../types'
|
|
6
6
|
|
|
7
7
|
function normalizeFaqQuestions(questions: FaqQuestions): FaqCategory[] {
|
|
@@ -19,10 +19,7 @@ function normalizeFaqQuestions(questions: FaqQuestions): FaqCategory[] {
|
|
|
19
19
|
return questions as FaqCategory[]
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const
|
|
23
|
-
const PANEL_WIDTH_EXPANDED = 520
|
|
24
|
-
|
|
25
|
-
export function useAssistant() {
|
|
22
|
+
export const useAssistant = createSharedComposable(() => {
|
|
26
23
|
const config = useRuntimeConfig()
|
|
27
24
|
const appConfig = useAppConfig()
|
|
28
25
|
const assistantRuntimeConfig = config.public.assistant as { enabled?: boolean } | undefined
|
|
@@ -30,26 +27,37 @@ export function useAssistant() {
|
|
|
30
27
|
const docusRuntimeConfig = appConfig.docus as { locale?: string } | undefined
|
|
31
28
|
const isEnabled = computed(() => assistantRuntimeConfig?.enabled ?? false)
|
|
32
29
|
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const
|
|
30
|
+
const storageOpen = useLocalStorage('assistant-open', false)
|
|
31
|
+
const messages = useLocalStorage<UIMessage[]>('assistant-messages', [])
|
|
32
|
+
|
|
33
|
+
const isOpen = ref(false)
|
|
34
|
+
const isStudioExpanded = ref(false)
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
onNuxtReady(() => {
|
|
37
|
+
nextTick(() => {
|
|
38
|
+
isOpen.value = storageOpen.value
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
isStudioExpanded.value = document.body.hasAttribute('data-expand-sidebar')
|
|
42
|
+
const observer = new MutationObserver(() => {
|
|
43
|
+
isStudioExpanded.value = document.body.hasAttribute('data-expand-sidebar')
|
|
44
|
+
})
|
|
45
|
+
observer.observe(document.body, { attributes: true, attributeFilter: ['data-expand-sidebar'] })
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
watch(isOpen, (value) => {
|
|
49
|
+
storageOpen.value = value
|
|
50
|
+
})
|
|
41
51
|
|
|
42
52
|
const faqQuestions = computed<FaqCategory[]>(() => {
|
|
43
53
|
const faqConfig = assistantConfig?.faqQuestions
|
|
44
54
|
if (!faqConfig) return []
|
|
45
55
|
|
|
46
|
-
// Check if it's a localized object (has locale keys like 'en', 'fr')
|
|
47
56
|
if (!Array.isArray(faqConfig)) {
|
|
48
57
|
const localizedConfig = faqConfig as LocalizedFaqQuestions
|
|
49
58
|
const currentLocale = docusRuntimeConfig?.locale || 'en'
|
|
50
59
|
const defaultLocale = config.public.i18n?.defaultLocale || 'en'
|
|
51
60
|
|
|
52
|
-
// Try current locale, then default locale, then first available
|
|
53
61
|
const questions = localizedConfig[currentLocale]
|
|
54
62
|
|| localizedConfig[defaultLocale]
|
|
55
63
|
|| Object.values(localizedConfig)[0]
|
|
@@ -61,51 +69,39 @@ export function useAssistant() {
|
|
|
61
69
|
})
|
|
62
70
|
|
|
63
71
|
function open(initialMessage?: string, clearPrevious = false) {
|
|
72
|
+
if (isStudioExpanded.value) return
|
|
73
|
+
|
|
64
74
|
if (clearPrevious) {
|
|
65
75
|
messages.value = []
|
|
66
76
|
}
|
|
67
77
|
|
|
68
78
|
if (initialMessage) {
|
|
69
|
-
|
|
79
|
+
messages.value = [...messages.value, {
|
|
80
|
+
id: String(Date.now()),
|
|
81
|
+
role: 'user' as const,
|
|
82
|
+
parts: [{ type: 'text' as const, text: initialMessage }],
|
|
83
|
+
}]
|
|
70
84
|
}
|
|
71
85
|
isOpen.value = true
|
|
72
86
|
}
|
|
73
87
|
|
|
74
|
-
function clearPending() {
|
|
75
|
-
pendingMessage.value = undefined
|
|
76
|
-
}
|
|
77
|
-
|
|
78
88
|
function close() {
|
|
79
89
|
isOpen.value = false
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
function toggle() {
|
|
93
|
+
if (isStudioExpanded.value) return
|
|
83
94
|
isOpen.value = !isOpen.value
|
|
84
95
|
}
|
|
85
96
|
|
|
86
|
-
function clearMessages() {
|
|
87
|
-
messages.value = []
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function toggleExpanded() {
|
|
91
|
-
isExpanded.value = !isExpanded.value
|
|
92
|
-
}
|
|
93
|
-
|
|
94
97
|
return {
|
|
95
98
|
isEnabled,
|
|
96
99
|
isOpen,
|
|
97
|
-
|
|
98
|
-
isMobile,
|
|
99
|
-
panelWidth,
|
|
100
|
-
shouldPushContent,
|
|
100
|
+
isStudioExpanded,
|
|
101
101
|
messages,
|
|
102
|
-
pendingMessage,
|
|
103
102
|
faqQuestions,
|
|
104
103
|
open,
|
|
105
|
-
clearPending,
|
|
106
104
|
close,
|
|
107
105
|
toggle,
|
|
108
|
-
toggleExpanded,
|
|
109
|
-
clearMessages,
|
|
110
106
|
}
|
|
111
|
-
}
|
|
107
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { streamText, convertToModelMessages
|
|
2
|
-
import type {
|
|
1
|
+
import { streamText, convertToModelMessages } from 'ai'
|
|
2
|
+
import type { ToolSet } from 'ai'
|
|
3
3
|
import { createMCPClient } from '@ai-sdk/mcp'
|
|
4
4
|
import type { H3Event } from 'h3'
|
|
5
5
|
|
|
@@ -23,11 +23,10 @@ function createLocalFetch(event: H3Event): typeof fetch {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
-
function stopWhenResponseComplete({ steps }: { steps:
|
|
26
|
+
function stopWhenResponseComplete({ steps }: { steps: { text?: string, toolCalls?: unknown[] }[] }): boolean {
|
|
27
27
|
const lastStep = steps.at(-1)
|
|
28
28
|
if (!lastStep) return false
|
|
29
29
|
|
|
30
|
-
// Primary condition: stop when model gives a text response without tool calls
|
|
31
30
|
const hasText = Boolean(lastStep.text && lastStep.text.trim().length > 0)
|
|
32
31
|
const hasNoToolCalls = !lastStep.toolCalls || lastStep.toolCalls.length === 0
|
|
33
32
|
|
|
@@ -57,7 +56,7 @@ function getSystemPrompt(siteName: string) {
|
|
|
57
56
|
|
|
58
57
|
**Links and exploration:**
|
|
59
58
|
- Tool results include a \`url\` for each page — prefer markdown links \`[label](url)\` so users can open the doc in one click
|
|
60
|
-
- When it helps, add extra links (related pages,
|
|
59
|
+
- When it helps, add extra links (related pages, "read more", side topics) — make the answer easy to dig into, not a wall of text
|
|
61
60
|
- Stick to URLs from tool results (\`url\` / \`path\`) so links stay valid
|
|
62
61
|
|
|
63
62
|
**FORMATTING RULES (CRITICAL):**
|
|
@@ -85,6 +84,9 @@ export default defineEventHandler(async (event) => {
|
|
|
85
84
|
const isExternalUrl = mcpServer.startsWith('http://') || mcpServer.startsWith('https://')
|
|
86
85
|
const baseURL = config.app?.baseURL?.replace(/\/$/, '') || ''
|
|
87
86
|
|
|
87
|
+
const abortController = new AbortController()
|
|
88
|
+
event.node.req.on('close', () => abortController.abort())
|
|
89
|
+
|
|
88
90
|
let transport: Parameters<typeof createMCPClient>[0]['transport']
|
|
89
91
|
if (isExternalUrl) {
|
|
90
92
|
transport = {
|
|
@@ -109,41 +111,19 @@ export default defineEventHandler(async (event) => {
|
|
|
109
111
|
const httpClient = await createMCPClient({ transport })
|
|
110
112
|
const mcpTools = await httpClient.tools()
|
|
111
113
|
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
type: 'data-tool-calls',
|
|
128
|
-
data: {
|
|
129
|
-
tools: toolCalls.map((tc: ToolCallPart) => {
|
|
130
|
-
const args = 'args' in tc ? tc.args : 'input' in tc ? tc.input : {}
|
|
131
|
-
return {
|
|
132
|
-
toolName: tc.toolName,
|
|
133
|
-
toolCallId: tc.toolCallId,
|
|
134
|
-
args,
|
|
135
|
-
}
|
|
136
|
-
}),
|
|
137
|
-
},
|
|
138
|
-
})
|
|
139
|
-
},
|
|
140
|
-
})
|
|
141
|
-
writer.merge(result.toUIMessageStream())
|
|
142
|
-
},
|
|
143
|
-
onFinish: async () => {
|
|
144
|
-
await httpClient.close()
|
|
145
|
-
},
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
return createUIMessageStreamResponse({ stream })
|
|
114
|
+
const closeMcp = () => event.waitUntil(httpClient.close())
|
|
115
|
+
|
|
116
|
+
return streamText({
|
|
117
|
+
model: config.assistant.model,
|
|
118
|
+
maxOutputTokens: 4000,
|
|
119
|
+
maxRetries: 2,
|
|
120
|
+
abortSignal: abortController.signal,
|
|
121
|
+
stopWhen: stopWhenResponseComplete,
|
|
122
|
+
system: getSystemPrompt(siteName),
|
|
123
|
+
messages: await convertToModelMessages(messages),
|
|
124
|
+
tools: mcpTools as ToolSet,
|
|
125
|
+
onFinish: closeMcp,
|
|
126
|
+
onAbort: closeMcp,
|
|
127
|
+
onError: closeMcp,
|
|
128
|
+
}).toUIMessageStreamResponse()
|
|
149
129
|
})
|
package/modules/css.ts
CHANGED
|
@@ -43,6 +43,14 @@ export default defineNuxtModule({
|
|
|
43
43
|
@source "../../app.config.ts";
|
|
44
44
|
@source "${assistantDir.replace(/\\/g, '/')}/**/*";
|
|
45
45
|
|
|
46
|
+
html.dark .shiki span {
|
|
47
|
+
color: var(--shiki-dark) !important;
|
|
48
|
+
background-color: var(--shiki-dark-bg) !important;
|
|
49
|
+
font-style: var(--shiki-dark-font-style) !important;
|
|
50
|
+
font-weight: var(--shiki-dark-font-weight) !important;
|
|
51
|
+
text-decoration: var(--shiki-dark-text-decoration) !important;
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
:root {
|
|
47
55
|
--ui-container: 90rem;
|
|
48
56
|
}
|
package/nuxt.schema.ts
CHANGED
|
@@ -297,6 +297,34 @@ export default defineNuxtSchema({
|
|
|
297
297
|
default: '',
|
|
298
298
|
required: ['', 'light', 'dark'],
|
|
299
299
|
}),
|
|
300
|
+
shortcuts: group({
|
|
301
|
+
title: 'Shortcuts',
|
|
302
|
+
description: 'Keyboard shortcuts configuration.',
|
|
303
|
+
icon: 'i-lucide-keyboard',
|
|
304
|
+
fields: {
|
|
305
|
+
toggleColorMode: field({
|
|
306
|
+
type: 'string',
|
|
307
|
+
title: 'Toggle Color Mode',
|
|
308
|
+
description: 'Shortcut to toggle light and dark mode (e.g., d, meta_d). Leave empty to disable.',
|
|
309
|
+
icon: 'i-lucide-keyboard',
|
|
310
|
+
default: 'd',
|
|
311
|
+
}),
|
|
312
|
+
},
|
|
313
|
+
}),
|
|
314
|
+
},
|
|
315
|
+
}),
|
|
316
|
+
search: group({
|
|
317
|
+
title: 'Search',
|
|
318
|
+
description: 'Search configuration.',
|
|
319
|
+
icon: 'i-lucide-search',
|
|
320
|
+
fields: {
|
|
321
|
+
fts: field({
|
|
322
|
+
type: 'boolean',
|
|
323
|
+
title: 'Full-Text Search',
|
|
324
|
+
description: 'Use SQLite FTS5 full-text search instead of Fuse.js. Requires @nuxt/content v3.14+.',
|
|
325
|
+
icon: 'i-lucide-database',
|
|
326
|
+
default: false,
|
|
327
|
+
}),
|
|
300
328
|
},
|
|
301
329
|
}),
|
|
302
330
|
assistant: group({
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docus",
|
|
3
3
|
"description": "Nuxt layer for Docus documentation theme",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.12.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./nuxt.config.ts",
|
|
7
7
|
"repository": {
|
|
@@ -23,41 +23,42 @@
|
|
|
23
23
|
"README.md"
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@ai-sdk/gateway": "^3.0.
|
|
27
|
-
"@ai-sdk/mcp": "^1.0.
|
|
28
|
-
"@ai-sdk/vue": "3.0.
|
|
29
|
-
"@
|
|
30
|
-
"@iconify-json/
|
|
31
|
-
"@iconify-json/
|
|
32
|
-
"@
|
|
26
|
+
"@ai-sdk/gateway": "^3.0.120",
|
|
27
|
+
"@ai-sdk/mcp": "^1.0.43",
|
|
28
|
+
"@ai-sdk/vue": "^3.0.191",
|
|
29
|
+
"@comark/vue": "^0.4.0",
|
|
30
|
+
"@iconify-json/lucide": "^1.2.111",
|
|
31
|
+
"@iconify-json/simple-icons": "^1.2.85",
|
|
32
|
+
"@iconify-json/vscode-icons": "^1.2.53",
|
|
33
|
+
"@nuxt/content": "^3.14.0",
|
|
33
34
|
"@nuxt/image": "^2.0.0",
|
|
34
|
-
"@nuxt/kit": "^4.4.
|
|
35
|
-
"@nuxt/ui": "^4.
|
|
36
|
-
"@nuxtjs/i18n": "^10.
|
|
37
|
-
"@nuxtjs/mcp-toolkit": "^0.
|
|
38
|
-
"@nuxtjs/mdc": "^0.
|
|
39
|
-
"@nuxtjs/robots": "^6.0.
|
|
40
|
-
"@shikijs/core": "^4.0
|
|
41
|
-
"@shikijs/engine-javascript": "^4.0
|
|
42
|
-
"@shikijs/langs": "^4.0
|
|
43
|
-
"@shikijs/
|
|
44
|
-
"@
|
|
45
|
-
"@
|
|
46
|
-
"
|
|
35
|
+
"@nuxt/kit": "^4.4.7",
|
|
36
|
+
"@nuxt/ui": "^4.8.1",
|
|
37
|
+
"@nuxtjs/i18n": "^10.4.0",
|
|
38
|
+
"@nuxtjs/mcp-toolkit": "^0.17.2",
|
|
39
|
+
"@nuxtjs/mdc": "^0.22.0",
|
|
40
|
+
"@nuxtjs/robots": "^6.0.9",
|
|
41
|
+
"@shikijs/core": "^4.2.0",
|
|
42
|
+
"@shikijs/engine-javascript": "^4.2.0",
|
|
43
|
+
"@shikijs/langs": "^4.2.0",
|
|
44
|
+
"@shikijs/stream": "^4.2.0",
|
|
45
|
+
"@shikijs/themes": "^4.2.0",
|
|
46
|
+
"@takumi-rs/core": "^1.6.0",
|
|
47
|
+
"@vueuse/core": "^14.3.0",
|
|
48
|
+
"ai": "^6.0.191",
|
|
47
49
|
"defu": "^6.1.7",
|
|
48
50
|
"exsolve": "^1.0.8",
|
|
49
51
|
"git-url-parse": "^16.1.0",
|
|
50
52
|
"motion-v": "^2.2.1",
|
|
51
53
|
"nuxt-llms": "^0.2.0",
|
|
52
|
-
"nuxt-og-image": "^6.
|
|
54
|
+
"nuxt-og-image": "^6.5.1",
|
|
53
55
|
"pathe": "^2.0.3",
|
|
54
|
-
"pkg-types": "^2.3.
|
|
56
|
+
"pkg-types": "^2.3.1",
|
|
55
57
|
"scule": "^1.3.0",
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"zod": "^4.3.6",
|
|
58
|
+
"tailwindcss": "^4.3.0",
|
|
59
|
+
"ufo": "^1.6.4",
|
|
60
|
+
"yaml": "^2.9.0",
|
|
61
|
+
"zod": "^4.4.3",
|
|
61
62
|
"zod-to-json-schema": "^3.25.2"
|
|
62
63
|
},
|
|
63
64
|
"peerDependencies": {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { joinURL } from 'ufo'
|
|
3
|
-
import { queryCollection } from '@nuxt/content/server'
|
|
4
1
|
import type { Collections } from '@nuxt/content'
|
|
5
|
-
import {
|
|
2
|
+
import { queryCollection } from '@nuxt/content/server'
|
|
3
|
+
import { joinURL } from 'ufo'
|
|
4
|
+
import { z } from 'zod'
|
|
6
5
|
import { inferSiteURL } from '../../../utils/meta'
|
|
6
|
+
import { getAvailableLocales, getCollectionFromPath, isNavigationPath } from '../../utils/content'
|
|
7
7
|
|
|
8
8
|
export default defineMcpTool({
|
|
9
9
|
description: `Retrieves the full content and details of a specific documentation page.
|
|
@@ -25,7 +25,9 @@ WORKFLOW: This tool returns the complete page content including title, descripti
|
|
|
25
25
|
openWorldHint: false,
|
|
26
26
|
},
|
|
27
27
|
inputSchema: {
|
|
28
|
-
path: z.string().describe(
|
|
28
|
+
path: z.string().describe(
|
|
29
|
+
'The page path from list-pages or provided by the user (e.g., /en/getting-started/installation)',
|
|
30
|
+
),
|
|
29
31
|
},
|
|
30
32
|
inputExamples: [
|
|
31
33
|
{ path: '/en/getting-started/installation' },
|
|
@@ -33,6 +35,10 @@ WORKFLOW: This tool returns the complete page content including title, descripti
|
|
|
33
35
|
],
|
|
34
36
|
cache: '1h',
|
|
35
37
|
handler: async ({ path }) => {
|
|
38
|
+
if (isNavigationPath(path)) {
|
|
39
|
+
throw createError({ statusCode: 404, message: 'Page not found' })
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
const event = useEvent()
|
|
37
43
|
const config = useRuntimeConfig(event)
|
|
38
44
|
const publicConfig = config.public
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { joinURL } from 'ufo'
|
|
3
|
-
import { queryCollection } from '@nuxt/content/server'
|
|
4
1
|
import type { Collections } from '@nuxt/content'
|
|
5
|
-
import {
|
|
2
|
+
import { queryCollection } from '@nuxt/content/server'
|
|
3
|
+
import { joinURL } from 'ufo'
|
|
4
|
+
import { z } from 'zod'
|
|
6
5
|
import { inferSiteURL } from '../../../utils/meta'
|
|
6
|
+
import { getAvailableLocales, getCollectionsToQuery } from '../../utils/content'
|
|
7
7
|
|
|
8
8
|
export default defineMcpTool({
|
|
9
9
|
description: `Lists all available documentation pages with their categories and basic information.
|
|
@@ -52,6 +52,7 @@ OUTPUT: Returns a structured list with:
|
|
|
52
52
|
const allPages = await Promise.all(
|
|
53
53
|
collections.map(async (collectionName) => {
|
|
54
54
|
const pages = await queryCollection(event, collectionName as keyof Collections)
|
|
55
|
+
.where('path', 'NOT LIKE', '%.navigation')
|
|
55
56
|
.select('title', 'path', 'description')
|
|
56
57
|
.all()
|
|
57
58
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { queryCollection } from '@nuxt/content/server'
|
|
2
|
-
import { getAvailableLocales, getCollectionsToQuery } from '../utils/content'
|
|
3
2
|
import { inferSiteURL } from '../../utils/meta'
|
|
3
|
+
import { getAvailableLocales, getCollectionsToQuery, isNavigationPath } from '../utils/content'
|
|
4
4
|
|
|
5
5
|
interface SitemapUrl {
|
|
6
6
|
loc: string
|
|
@@ -27,17 +27,20 @@ export default defineEventHandler(async (event) => {
|
|
|
27
27
|
|
|
28
28
|
for (const collection of collections) {
|
|
29
29
|
try {
|
|
30
|
-
const pages = await (queryCollection as unknown as (
|
|
30
|
+
const pages = await (queryCollection as unknown as (
|
|
31
|
+
event: unknown,
|
|
32
|
+
collection: string,
|
|
33
|
+
) => { all: () => Promise<Array<Record<string, unknown> & { path?: string }>> })(event, collection).all()
|
|
31
34
|
|
|
32
35
|
for (const page of pages) {
|
|
33
|
-
const meta = page as Record<string, unknown>
|
|
36
|
+
const meta = page.meta as Record<string, unknown>
|
|
34
37
|
const pagePath = page.path || '/'
|
|
35
38
|
|
|
36
39
|
// Skip pages with sitemap: false in frontmatter
|
|
37
40
|
if (meta.sitemap === false) continue
|
|
38
41
|
|
|
39
42
|
// Skip .navigation files (used for navigation configuration)
|
|
40
|
-
if (
|
|
43
|
+
if (isNavigationPath(pagePath)) continue
|
|
41
44
|
|
|
42
45
|
const urlEntry: SitemapUrl = {
|
|
43
46
|
loc: pagePath,
|
package/server/utils/content.ts
CHANGED
|
@@ -25,6 +25,10 @@ export function getCollectionsToQuery(locale: string | undefined, availableLocal
|
|
|
25
25
|
: ['docs']
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export function isNavigationPath(path: string): boolean {
|
|
29
|
+
return path.endsWith('.navigation') || path.includes('/.navigation/')
|
|
30
|
+
}
|
|
31
|
+
|
|
28
32
|
export function getCollectionFromPath(path: string, availableLocales: string[]): string {
|
|
29
33
|
const pathSegments = path.split('/').filter(Boolean)
|
|
30
34
|
const firstSegment = pathSegments[0]
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { motion } from 'motion-v'
|
|
3
|
-
import { useDocusI18n } from '../../../../app/composables/useDocusI18n'
|
|
4
|
-
|
|
5
|
-
interface ToolCall {
|
|
6
|
-
toolCallId: string
|
|
7
|
-
toolName: string
|
|
8
|
-
args: Record<string, unknown>
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const props = defineProps<{
|
|
12
|
-
text?: string
|
|
13
|
-
toolCalls?: ToolCall[]
|
|
14
|
-
isLoading?: boolean
|
|
15
|
-
}>()
|
|
16
|
-
|
|
17
|
-
const { t } = useDocusI18n()
|
|
18
|
-
|
|
19
|
-
const messages = computed(() => [
|
|
20
|
-
t('assistant.loading.searching'),
|
|
21
|
-
t('assistant.loading.reading'),
|
|
22
|
-
t('assistant.loading.analyzing'),
|
|
23
|
-
t('assistant.loading.finding'),
|
|
24
|
-
])
|
|
25
|
-
|
|
26
|
-
const finishedMessage = computed(() => t('assistant.loading.finished'))
|
|
27
|
-
|
|
28
|
-
const currentIndex = ref(0)
|
|
29
|
-
const targetText = computed(() => {
|
|
30
|
-
if (!props.isLoading) {
|
|
31
|
-
return finishedMessage.value
|
|
32
|
-
}
|
|
33
|
-
return props.text || messages.value[currentIndex.value]
|
|
34
|
-
})
|
|
35
|
-
const displayedText = ref(targetText.value)
|
|
36
|
-
|
|
37
|
-
const chars = 'abcdefghijklmnopqrstuvwxyz'
|
|
38
|
-
|
|
39
|
-
function scrambleText(from: string, to: string) {
|
|
40
|
-
const maxLength = Math.max(from.length, to.length)
|
|
41
|
-
let frame = 0
|
|
42
|
-
const totalFrames = 15
|
|
43
|
-
|
|
44
|
-
const animate = () => {
|
|
45
|
-
frame++
|
|
46
|
-
let result = ''
|
|
47
|
-
|
|
48
|
-
for (let i = 0; i < maxLength; i++) {
|
|
49
|
-
const progress = frame / totalFrames
|
|
50
|
-
const charProgress = progress * maxLength
|
|
51
|
-
|
|
52
|
-
if (i < charProgress - 2) {
|
|
53
|
-
result += to[i] || ''
|
|
54
|
-
}
|
|
55
|
-
else if (i < charProgress) {
|
|
56
|
-
result += chars[Math.floor(Math.random() * chars.length)]
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
result += from[i] || ''
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
displayedText.value = result
|
|
64
|
-
|
|
65
|
-
if (frame < totalFrames) {
|
|
66
|
-
requestAnimationFrame(animate)
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
displayedText.value = to
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
requestAnimationFrame(animate)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
let textInterval: ReturnType<typeof setInterval> | null = null
|
|
77
|
-
|
|
78
|
-
watch(targetText, (newText, oldText) => {
|
|
79
|
-
if (newText !== oldText && newText && oldText) {
|
|
80
|
-
scrambleText(oldText, newText)
|
|
81
|
-
}
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
// Stop text rotation when loading finishes
|
|
85
|
-
watch(() => props.isLoading, (isLoading) => {
|
|
86
|
-
if (!isLoading && textInterval) {
|
|
87
|
-
clearInterval(textInterval)
|
|
88
|
-
textInterval = null
|
|
89
|
-
}
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
93
|
-
function getToolLabel(toolName: string, args: any) {
|
|
94
|
-
const path = args?.path || ''
|
|
95
|
-
|
|
96
|
-
if (toolName === 'list-pages') {
|
|
97
|
-
return t('assistant.toolListPages')
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (toolName === 'get-page') {
|
|
101
|
-
return `${t('assistant.toolReadPage')} ${path || '...'}`
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return toolName
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
onMounted(() => {
|
|
108
|
-
// Text rotation only when loading
|
|
109
|
-
if (!props.text && props.isLoading) {
|
|
110
|
-
textInterval = setInterval(() => {
|
|
111
|
-
currentIndex.value = (currentIndex.value + 1) % messages.value.length
|
|
112
|
-
}, 3500)
|
|
113
|
-
}
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
onUnmounted(() => {
|
|
117
|
-
if (textInterval) clearInterval(textInterval)
|
|
118
|
-
})
|
|
119
|
-
</script>
|
|
120
|
-
|
|
121
|
-
<template>
|
|
122
|
-
<div class="flex flex-col gap-2">
|
|
123
|
-
<!-- Main loader with matrix and text -->
|
|
124
|
-
<div class="flex items-center text-xs text-muted overflow-hidden">
|
|
125
|
-
<motion.div
|
|
126
|
-
v-if="isLoading"
|
|
127
|
-
:initial="{ opacity: 1, width: 'auto' }"
|
|
128
|
-
:exit="{ opacity: 0, width: 0 }"
|
|
129
|
-
:transition="{ duration: 0.2 }"
|
|
130
|
-
class="shrink-0 mr-2"
|
|
131
|
-
>
|
|
132
|
-
<AssistantMatrix />
|
|
133
|
-
</motion.div>
|
|
134
|
-
<motion.span
|
|
135
|
-
:animate="{ x: 0 }"
|
|
136
|
-
:transition="{ duration: 0.2 }"
|
|
137
|
-
class="font-mono tracking-tight"
|
|
138
|
-
>
|
|
139
|
-
{{ displayedText }}
|
|
140
|
-
</motion.span>
|
|
141
|
-
</div>
|
|
142
|
-
|
|
143
|
-
<!-- Tool calls displayed below -->
|
|
144
|
-
<div
|
|
145
|
-
v-if="toolCalls?.length"
|
|
146
|
-
class="flex flex-col gap-1"
|
|
147
|
-
:class="isLoading ? 'pl-[22px]' : 'pl-0'"
|
|
148
|
-
>
|
|
149
|
-
<motion.div
|
|
150
|
-
v-for="tool in toolCalls"
|
|
151
|
-
:key="`${tool.toolCallId}-${JSON.stringify(tool.args)}`"
|
|
152
|
-
:initial="{ opacity: 0, x: -4 }"
|
|
153
|
-
:animate="{ opacity: 1, x: 0 }"
|
|
154
|
-
:transition="{ duration: 0.15 }"
|
|
155
|
-
class="flex items-center gap-1.5"
|
|
156
|
-
>
|
|
157
|
-
<span class="size-1 rounded-full bg-current opacity-40" />
|
|
158
|
-
<span class="text-[11px] text-dimmed truncate max-w-[200px]">
|
|
159
|
-
{{ getToolLabel(tool.toolName, tool.args) }}
|
|
160
|
-
</span>
|
|
161
|
-
</motion.div>
|
|
162
|
-
</div>
|
|
163
|
-
</div>
|
|
164
|
-
</template>
|