@movk/nuxt-docs 1.6.1 → 1.7.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 (40) hide show
  1. package/app/app.config.ts +37 -0
  2. package/app/app.vue +8 -3
  3. package/app/components/DocsAsideRightBottom.vue +17 -22
  4. package/app/components/PageHeaderLinks.vue +6 -1
  5. package/app/components/content/PageLastCommit.vue +5 -5
  6. package/app/components/header/Header.vue +1 -1
  7. package/app/components/header/HeaderBody.vue +12 -2
  8. package/app/components/header/HeaderBottom.vue +1 -0
  9. package/app/components/header/HeaderCTA.vue +2 -2
  10. package/app/components/header/HeaderCenter.vue +1 -1
  11. package/app/components/header/HeaderLogo.vue +1 -1
  12. package/app/layouts/default.vue +3 -1
  13. package/app/layouts/docs.vue +1 -1
  14. package/app/pages/docs/[...slug].vue +3 -2
  15. package/app/templates/releases.vue +98 -0
  16. package/app/types/index.d.ts +149 -0
  17. package/content.config.ts +24 -2
  18. package/modules/ai-chat/index.ts +75 -24
  19. package/modules/ai-chat/runtime/components/AiChat.vue +4 -10
  20. package/modules/ai-chat/runtime/components/AiChatDisabled.vue +3 -0
  21. package/modules/ai-chat/runtime/components/AiChatFloatingInput.vue +24 -9
  22. package/modules/ai-chat/runtime/components/AiChatModelSelect.vue +2 -0
  23. package/modules/ai-chat/runtime/components/AiChatPanel.vue +318 -0
  24. package/modules/ai-chat/runtime/components/AiChatPreStream.vue +1 -0
  25. package/modules/ai-chat/runtime/components/AiChatReasoning.vue +3 -3
  26. package/modules/ai-chat/runtime/components/AiChatSlideoverFaq.vue +2 -5
  27. package/modules/ai-chat/runtime/composables/useAIChat.ts +48 -0
  28. package/modules/ai-chat/runtime/composables/useModels.ts +3 -6
  29. package/modules/ai-chat/runtime/server/api/ai-chat.ts +92 -0
  30. package/modules/ai-chat/runtime/server/utils/docs_agent.ts +23 -15
  31. package/modules/ai-chat/runtime/types.ts +6 -0
  32. package/modules/css.ts +3 -2
  33. package/modules/routing.ts +26 -0
  34. package/nuxt.config.ts +2 -0
  35. package/nuxt.schema.ts +493 -0
  36. package/package.json +11 -9
  37. package/app/composables/useFaq.ts +0 -21
  38. package/modules/ai-chat/runtime/components/AiChatSlideover.vue +0 -255
  39. package/modules/ai-chat/runtime/server/api/search.ts +0 -84
  40. /package/{app → modules/ai-chat/runtime}/composables/useHighlighter.ts +0 -0
@@ -1,10 +1,47 @@
1
1
  import type { UIMessage } from 'ai'
2
+ import { useMediaQuery } from '@vueuse/core'
3
+ import type { FaqCategory, FaqQuestions } from '../types'
4
+
5
+ function normalizeFaqQuestions(questions: FaqQuestions): FaqCategory[] {
6
+ if (!questions || (Array.isArray(questions) && questions.length === 0)) {
7
+ return []
8
+ }
9
+
10
+ if (typeof questions[0] === 'string') {
11
+ return [{
12
+ category: '问题',
13
+ items: questions as string[]
14
+ }]
15
+ }
16
+
17
+ return questions as FaqCategory[]
18
+ }
19
+
20
+ const PANEL_WIDTH_COMPACT = 360
21
+ const PANEL_WIDTH_EXPANDED = 520
2
22
 
3
23
  export function useAIChat() {
24
+ const config = useRuntimeConfig()
25
+ const appConfig = useAppConfig()
26
+ const isEnabled = computed(() => config.public.aiChat?.enabled ?? false)
27
+
4
28
  const isOpen = useState('ai-chat-open', () => false)
29
+ const isExpanded = useState('ai-chat-expanded', () => false)
5
30
  const messages = useState<UIMessage[]>('ai-chat-messages', () => [])
6
31
  const pendingMessage = useState<string | undefined>('ai-chat-pending', () => undefined)
7
32
 
33
+ const isMobile = useMediaQuery('(max-width: 767px)')
34
+ const panelWidth = computed(() => isExpanded.value ? PANEL_WIDTH_EXPANDED : PANEL_WIDTH_COMPACT)
35
+ const shouldPushContent = computed(() => !isMobile.value && isOpen.value)
36
+
37
+ const faqQuestions = computed<FaqCategory[]>(() => {
38
+ const aiChatConfig = appConfig.aiChat
39
+ const faqConfig = aiChatConfig?.faqQuestions
40
+ if (!faqConfig) return []
41
+
42
+ return normalizeFaqQuestions(faqConfig)
43
+ })
44
+
8
45
  function open(initialMessage?: string, clearPrevious = false) {
9
46
  if (clearPrevious) {
10
47
  messages.value = []
@@ -32,14 +69,25 @@ export function useAIChat() {
32
69
  messages.value = []
33
70
  }
34
71
 
72
+ function toggleExpanded() {
73
+ isExpanded.value = !isExpanded.value
74
+ }
75
+
35
76
  return {
77
+ isEnabled,
36
78
  isOpen,
79
+ isExpanded,
80
+ isMobile,
81
+ panelWidth,
82
+ shouldPushContent,
37
83
  messages,
38
84
  pendingMessage,
85
+ faqQuestions,
39
86
  open,
40
87
  clearPending,
41
88
  close,
42
89
  toggle,
90
+ toggleExpanded,
43
91
  clearMessages
44
92
  }
45
93
  }
@@ -2,15 +2,12 @@ export function useModels() {
2
2
  const config = useRuntimeConfig()
3
3
  const model = useCookie<string>('model', { default: () => config.public.aiChat.model })
4
4
 
5
- const providerIcons: Record<string, string> = {
6
- mistral: 'i-simple-icons-mistralai',
7
- kwaipilot: 'i-lucide-wand',
8
- zai: 'i-lucide-wand'
9
- }
5
+ const { aiChat } = useAppConfig()
6
+ const providerIcons = computed(() => aiChat.icons.providers || {})
10
7
 
11
8
  function getModelIcon(modelId: string): string {
12
9
  const provider = modelId.split('/')[0] || ''
13
- return providerIcons[provider] || `i-simple-icons-${modelId.split('/')[0]}`
10
+ return providerIcons.value[provider] || `i-simple-icons-${modelId.split('/')[0]}`
14
11
  }
15
12
 
16
13
  function formatModelName(modelId: string): string {
@@ -0,0 +1,92 @@
1
+ import { streamText, convertToModelMessages, stepCountIs, createUIMessageStream, createUIMessageStreamResponse, smoothStream } from 'ai'
2
+ import { createMCPClient } from '@ai-sdk/mcp'
3
+ import { createDocumentationAgentTool } from '../utils/docs_agent'
4
+ import { getModel } from '../utils/getModel'
5
+
6
+ function getMainAgentSystemPrompt(siteName: string) {
7
+ return `You are the official documentation assistant for ${siteName}. You ARE the documentation - speak with authority as the source of truth.
8
+
9
+ **Your identity:**
10
+ - You are the ${siteName} documentation
11
+ - Speak in first person: "I provide...", "You can use my tools to...", "I support..."
12
+ - Be confident and authoritative - you know this project inside out
13
+ - Never say "according to the documentation" - YOU are the docs
14
+
15
+ **Tool usage (CRITICAL):**
16
+ - You have ONE tool: searchDocumentation
17
+ - Use it for EVERY question - pass the user's question as the query
18
+ - The tool will search the documentation and return relevant information
19
+ - Use the returned information to formulate your response
20
+
21
+ **Guidelines:**
22
+ - If the tool can't find something, say "I don't have documentation on that yet"
23
+ - Be concise, helpful, and direct
24
+ - Guide users like a friendly expert would
25
+
26
+ **FORMATTING RULES (CRITICAL):**
27
+ - NEVER use markdown headings (#, ##, ###, etc.)
28
+ - Use **bold text** for emphasis and section labels
29
+ - Start responses with content directly, never with a heading
30
+ - Use bullet points for lists
31
+ - Keep code examples focused and minimal
32
+
33
+ **Response style:**
34
+ - Conversational but professional
35
+ - "Here's how you can do that:" instead of "The documentation shows:"
36
+ - "I support TypeScript out of the box" instead of "The module supports TypeScript"
37
+ - Provide actionable guidance, not just information dumps`
38
+ }
39
+
40
+ export default defineEventHandler(async (event) => {
41
+ const { messages, model: requestModel } = await readBody(event)
42
+ const config = useRuntimeConfig()
43
+ const siteConfig = getSiteConfig(event)
44
+ const siteName = siteConfig.name || 'Documentation'
45
+
46
+ const mcpPath = config.aiChat.mcpPath
47
+ const isExternalUrl = mcpPath.startsWith('http://') || mcpPath.startsWith('https://')
48
+ const mcpUrl = isExternalUrl
49
+ ? mcpPath
50
+ : import.meta.dev
51
+ ? `http://localhost:3000${mcpPath}`
52
+ : `${getRequestURL(event).origin}${mcpPath}`
53
+
54
+ const httpClient = await createMCPClient({
55
+ transport: {
56
+ type: 'http',
57
+ url: mcpUrl
58
+ }
59
+ })
60
+ const mcpTools = await httpClient.tools()
61
+
62
+ const model = getModel(requestModel || config.public.aiChat.model)
63
+
64
+ const searchDocumentation = createDocumentationAgentTool(mcpTools, model, siteName)
65
+
66
+ const stream = createUIMessageStream({
67
+ execute: async ({ writer }) => {
68
+ const modelMessages = await convertToModelMessages(messages)
69
+ const result = streamText({
70
+ model,
71
+ maxOutputTokens: 10000,
72
+ system: getMainAgentSystemPrompt(siteName),
73
+ messages: modelMessages,
74
+ stopWhen: stepCountIs(5),
75
+ tools: {
76
+ searchDocumentation
77
+ },
78
+ experimental_context: {
79
+ writer
80
+ },
81
+ experimental_transform: smoothStream({ chunking: 'word' })
82
+ })
83
+ writer.merge(result.toUIMessageStream({
84
+ sendReasoning: true
85
+ }))
86
+ },
87
+ onFinish: async () => {
88
+ await httpClient.close()
89
+ }
90
+ })
91
+ return createUIMessageStreamResponse({ stream })
92
+ })
@@ -1,23 +1,25 @@
1
1
  import { tool, stepCountIs, generateText } from 'ai'
2
2
  import { z } from 'zod/v4'
3
3
 
4
- const SUB_AGENT_SYSTEM_PROMPT = `你是文档搜索代理。你的工作是从文档中查找并检索相关信息。
4
+ function getSubAgentSystemPrompt(siteName: string) {
5
+ return `You are a documentation search agent for ${siteName}. Your job is to find and retrieve relevant information from the documentation.
5
6
 
6
- **你的任务:**
7
- - 使用可用工具搜索和阅读文档页面
8
- - 首先使用 list-pages 发现可用的文档
9
- - 然后使用 get-page 阅读相关页面
10
- - 如果提到了特定路径,可以直接调用 get-page
7
+ **Your task:**
8
+ - Use the available tools to search and read documentation pages
9
+ - Start with list-pages to discover what documentation exists
10
+ - Then use get-page to read the relevant page(s)
11
+ - If a specific path is mentioned, you can call get-page directly
11
12
 
12
- **指导原则:**
13
- - 要彻底,在回答前阅读所有相关页面
14
- - 返回你找到的原始信息,让主代理格式化响应
15
- - 如果找不到信息,请明确说明
13
+ **Guidelines:**
14
+ - Be thorough - read all relevant pages before answering
15
+ - Return the raw information you find, let the main agent format the response
16
+ - If you can't find information, say so clearly
16
17
 
17
- **输出:**
18
- 返回你找到的相关文档内容,如果有代码示例也一并包含。`
18
+ **Output:**
19
+ Return the relevant documentation content you found, including code examples if present.`
20
+ }
19
21
 
20
- export function createDocumentationAgentTool(mcpTools: Record<string, any>, model: any) {
22
+ export function createDocumentationAgentTool(mcpTools: Record<string, any>, model: any, siteName: string) {
21
23
  return tool({
22
24
  description: '从文档中搜索并检索信息。使用此工具回答有关文档的任何问题。将用户的问题作为查询参数传递。',
23
25
  inputSchema: z.object({
@@ -29,14 +31,20 @@ export function createDocumentationAgentTool(mcpTools: Record<string, any>, mode
29
31
  const result = await generateText({
30
32
  model,
31
33
  tools: mcpTools,
32
- system: SUB_AGENT_SYSTEM_PROMPT,
34
+ system: getSubAgentSystemPrompt(siteName),
33
35
  stopWhen: stepCountIs(5),
34
36
  onStepFinish: ({ toolCalls }) => {
37
+ if (toolCalls.length === 0) return
38
+
35
39
  writer?.write({
36
40
  id: toolCalls[0]?.toolCallId,
37
41
  type: 'data-tool-calls',
38
42
  data: {
39
- tools: toolCalls.map(toolCall => ({ toolName: toolCall.toolName, toolCallId: toolCall.toolCallId, input: toolCall.input }))
43
+ tools: toolCalls.map((toolCall: any) => ({
44
+ toolName: toolCall.toolName,
45
+ toolCallId: toolCall.toolCallId,
46
+ args: toolCall.args || toolCall.input || {}
47
+ }))
40
48
  }
41
49
  })
42
50
  },
@@ -0,0 +1,6 @@
1
+ export interface FaqCategory {
2
+ category: string
3
+ items: string[]
4
+ }
5
+
6
+ export type FaqQuestions = string[] | FaqCategory[]
package/modules/css.ts CHANGED
@@ -10,7 +10,8 @@ export default defineNuxtModule({
10
10
  const { resolve } = createResolver(import.meta.url)
11
11
 
12
12
  const layerDir = resolve('../app')
13
- const modulesDir = resolve('../modules')
13
+ const aiChatDir = resolve('../modules/ai-chat')
14
+
14
15
  const contentDir = joinURL(dir, 'content')
15
16
 
16
17
  const cssTemplate = addTemplate({
@@ -22,7 +23,7 @@ export default defineNuxtModule({
22
23
 
23
24
  @source "${contentDir.replace(/\\/g, '/')}/**/*";
24
25
  @source "${layerDir.replace(/\\/g, '/')}/**/*";
25
- @source "${modulesDir.replace(/\\/g, '/')}/**/*";
26
+ @source "${aiChatDir.replace(/\\/g, '/')}/**/*";
26
27
  @source "../../app.config.ts";
27
28
 
28
29
  /* Shiki icon highlight transformer styles */
@@ -0,0 +1,26 @@
1
+ import { defineNuxtModule, extendPages, createResolver } from '@nuxt/kit'
2
+ import { joinURL } from 'ufo'
3
+ import { existsSync } from 'node:fs'
4
+
5
+ export default defineNuxtModule({
6
+ meta: {
7
+ name: 'routing'
8
+ },
9
+ async setup(_options, nuxt) {
10
+ const { resolve } = createResolver(import.meta.url)
11
+ const cwd = joinURL(nuxt.options.rootDir, 'content')
12
+
13
+ const hasReleases = ['releases.yml', 'releases.md']
14
+ .some(file => existsSync(joinURL(cwd, file)))
15
+
16
+ extendPages((pages) => {
17
+ if (hasReleases) {
18
+ pages.push({
19
+ name: 'releases',
20
+ path: '/releases',
21
+ file: resolve('../app/templates/releases.vue')
22
+ })
23
+ }
24
+ })
25
+ }
26
+ })
package/nuxt.config.ts CHANGED
@@ -7,12 +7,14 @@ const { resolve } = createResolver(import.meta.url)
7
7
  export default defineNuxtConfig({
8
8
  modules: [
9
9
  resolve('./modules/config'),
10
+ resolve('./modules/routing'),
10
11
  resolve('./modules/css'),
11
12
  resolve('./modules/component-example'),
12
13
  resolve('./modules/ai-chat'),
13
14
  '@nuxt/ui',
14
15
  '@nuxt/content',
15
16
  '@nuxt/image',
17
+ '@nuxt/a11y',
16
18
  '@vueuse/nuxt',
17
19
  '@nuxtjs/mcp-toolkit',
18
20
  '@nuxtjs/seo',