@movk/nuxt-docs 1.6.2 → 1.7.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.
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/content/StarsBg.vue +119 -0
  7. package/app/components/header/Header.vue +1 -1
  8. package/app/components/header/HeaderBody.vue +12 -2
  9. package/app/components/header/HeaderBottom.vue +1 -0
  10. package/app/components/header/HeaderCTA.vue +2 -2
  11. package/app/components/header/HeaderCenter.vue +1 -1
  12. package/app/components/header/HeaderLogo.vue +1 -1
  13. package/app/layouts/default.vue +3 -1
  14. package/app/layouts/docs.vue +1 -1
  15. package/app/pages/docs/[...slug].vue +3 -2
  16. package/app/templates/releases.vue +98 -0
  17. package/app/types/index.d.ts +149 -0
  18. package/content.config.ts +24 -2
  19. package/modules/ai-chat/index.ts +53 -21
  20. package/modules/ai-chat/runtime/components/AiChat.vue +4 -10
  21. package/modules/ai-chat/runtime/components/AiChatDisabled.vue +3 -0
  22. package/modules/ai-chat/runtime/components/AiChatFloatingInput.vue +24 -9
  23. package/modules/ai-chat/runtime/components/AiChatModelSelect.vue +2 -0
  24. package/modules/ai-chat/runtime/components/AiChatPanel.vue +318 -0
  25. package/modules/ai-chat/runtime/components/AiChatPreStream.vue +1 -0
  26. package/modules/ai-chat/runtime/components/AiChatReasoning.vue +3 -3
  27. package/modules/ai-chat/runtime/components/AiChatSlideoverFaq.vue +2 -5
  28. package/modules/ai-chat/runtime/composables/useAIChat.ts +48 -0
  29. package/modules/ai-chat/runtime/composables/useModels.ts +3 -6
  30. package/modules/ai-chat/runtime/server/api/ai-chat.ts +40 -32
  31. package/modules/ai-chat/runtime/server/utils/docs_agent.ts +23 -15
  32. package/modules/ai-chat/runtime/types.ts +6 -0
  33. package/modules/css.ts +3 -2
  34. package/modules/routing.ts +26 -0
  35. package/nuxt.config.ts +2 -0
  36. package/nuxt.schema.ts +493 -0
  37. package/package.json +11 -9
  38. package/app/composables/useFaq.ts +0 -21
  39. package/modules/ai-chat/runtime/components/AiChatSlideover.vue +0 -255
  40. /package/{app → modules/ai-chat/runtime}/composables/useHighlighter.ts +0 -0
package/content.config.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { existsSync } from 'node:fs'
1
2
  import { defineCollection, defineContentConfig } from '@nuxt/content'
2
3
  import { useNuxt } from '@nuxt/kit'
3
4
  import { asSeoCollection } from '@nuxtjs/seo/content'
@@ -7,6 +8,8 @@ import { z } from 'zod/v4'
7
8
  const { options } = useNuxt()
8
9
  const cwd = joinURL(options.rootDir, 'content')
9
10
 
11
+ const hasReleasesMd = existsSync(joinURL(cwd, 'releases.md'))
12
+
10
13
  const Avatar = z.object({
11
14
  src: z.string(),
12
15
  alt: z.string().optional()
@@ -27,6 +30,12 @@ const Button = z.object({
27
30
  class: z.string().optional()
28
31
  })
29
32
 
33
+ const PageHero = z.object({
34
+ title: z.string(),
35
+ description: z.string(),
36
+ links: z.array(Button).optional()
37
+ })
38
+
30
39
  export default defineContentConfig({
31
40
  collections: {
32
41
  landing: defineCollection(asSeoCollection({
@@ -38,10 +47,10 @@ export default defineContentConfig({
38
47
  })),
39
48
  docs: defineCollection(asSeoCollection({
40
49
  type: 'page',
41
- source: [{
50
+ source: {
42
51
  cwd,
43
52
  include: 'docs/**/*'
44
- }],
53
+ },
45
54
  schema: z.object({
46
55
  links: z.array(Button),
47
56
  category: z.string().optional(),
@@ -49,6 +58,19 @@ export default defineContentConfig({
49
58
  title: z.string().optional()
50
59
  })
51
60
  })
61
+ })),
62
+ releases: defineCollection(asSeoCollection({
63
+ type: 'page',
64
+ source: {
65
+ cwd,
66
+ include: hasReleasesMd ? 'releases.md' : 'releases.yml'
67
+ },
68
+ schema: z.object({
69
+ title: z.string(),
70
+ description: z.string(),
71
+ releases: z.string(),
72
+ hero: PageHero
73
+ })
52
74
  }))
53
75
  }
54
76
  })
@@ -1,13 +1,16 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
- import { addComponentsDir, addImportsDir, addServerHandler, createResolver, defineNuxtModule } from '@nuxt/kit'
3
+ import {
4
+ addComponent,
5
+ addComponentsDir,
6
+ addImports,
7
+ addServerHandler,
8
+ createResolver,
9
+ defineNuxtModule,
10
+ logger
11
+ } from '@nuxt/kit'
4
12
 
5
13
  export interface AiChatModuleOptions {
6
- /**
7
- * 是否启用 AI 聊天功能
8
- * @default import.meta.env.AI_GATEWAY_API_KEY || import.meta.env.OPENROUTER_API_KEY
9
- */
10
- enable?: boolean
11
14
  /**
12
15
  * 聊天 API 端点路径
13
16
  * @default '/api/ai-chat'
@@ -29,43 +32,72 @@ export interface AiChatModuleOptions {
29
32
  models?: string[]
30
33
  }
31
34
 
35
+ const log = logger.withTag('docus:ai-assistant')
36
+
32
37
  export default defineNuxtModule<AiChatModuleOptions>({
33
38
  meta: {
34
39
  name: 'ai-chat',
35
40
  configKey: 'aiChat'
36
41
  },
37
42
  defaults: {
38
- enable: !!(
39
- import.meta.env.AI_GATEWAY_API_KEY
40
- || import.meta.env.OPENROUTER_API_KEY
41
- ),
42
43
  apiPath: '/api/ai-chat',
43
44
  mcpPath: '/mcp',
44
45
  model: '',
45
46
  models: []
46
47
  },
47
48
  setup(options, nuxt) {
49
+ const hasApiKey = !!(process.env.AI_GATEWAY_API_KEY || process.env.OPENROUTER_API_KEY)
50
+
48
51
  const { resolve } = createResolver(import.meta.url)
49
52
 
50
53
  nuxt.options.runtimeConfig.public.aiChat = {
51
- enable: options.enable!,
54
+ enabled: hasApiKey,
52
55
  apiPath: options.apiPath!,
53
56
  model: options.model!,
54
57
  models: options.models!
55
58
  }
56
- nuxt.options.runtimeConfig.aiChat = {
57
- mcpPath: options.mcpPath!
59
+
60
+ addImports([
61
+ {
62
+ name: 'useAIChat',
63
+ from: resolve('./runtime/composables/useAIChat')
64
+ }
65
+ ])
66
+
67
+ if (hasApiKey) {
68
+ addComponentsDir({
69
+ path: resolve('./runtime/components'),
70
+ ignore: ['AiChatDisabled']
71
+ })
72
+ } else {
73
+ addComponent({
74
+ name: 'AiChatDisabled',
75
+ filePath: resolve('./runtime/components/AiChatDisabled.vue')
76
+ })
58
77
  }
59
78
 
60
- if (!options.enable) {
61
- console.info('[ai-chat] Module disabled: no AI_GATEWAY_API_KEY or OPENROUTER_API_KEY found')
79
+ if (!hasApiKey) {
80
+ log.warn('[ai-chat] Module disabled: no AI_GATEWAY_API_KEY or OPENROUTER_API_KEY found')
81
+ return
62
82
  }
63
83
 
64
- addComponentsDir({
65
- path: resolve('runtime/components')
66
- })
84
+ nuxt.options.runtimeConfig.aiChat = {
85
+ mcpPath: options.mcpPath!
86
+ }
87
+
88
+ addImports([
89
+ {
90
+ name: 'useHighlighter',
91
+ from: resolve('./runtime/composables/useHighlighter')
92
+ }
93
+ ])
67
94
 
68
- addImportsDir(resolve('runtime/composables'))
95
+ addImports([
96
+ {
97
+ name: 'useModels',
98
+ from: resolve('./runtime/composables/useModels')
99
+ }
100
+ ])
69
101
 
70
102
  /**
71
103
  * 检查用户项目中是否存在自定义 handler
@@ -87,7 +119,7 @@ export default defineNuxtModule<AiChatModuleOptions>({
87
119
  handler: resolve('./runtime/server/api/ai-chat')
88
120
  })
89
121
  } else {
90
- console.info(`[ai-chat] Using custom handler, skipping default handler registration`)
122
+ log.info(`[ai-chat] Using custom handler, skipping default handler registration`)
91
123
  }
92
124
  }
93
125
  })
@@ -95,7 +127,7 @@ export default defineNuxtModule<AiChatModuleOptions>({
95
127
  declare module 'nuxt/schema' {
96
128
  interface PublicRuntimeConfig {
97
129
  aiChat: {
98
- enable: boolean
130
+ enabled: boolean
99
131
  apiPath: string
100
132
  model: string
101
133
  models: string[]
@@ -1,21 +1,15 @@
1
1
  <script setup lang="ts">
2
- const { tooltipText = '向 AI 提问' } = defineProps<{
3
- /**
4
- * 鼠标悬停时的提示文本
5
- * @defaultValue 向 AI 提问
6
- */
7
- tooltipText?: string
8
- }>()
9
-
2
+ const { aiChat } = useAppConfig()
10
3
  const { toggle } = useAIChat()
11
4
  </script>
12
5
 
13
6
  <template>
14
- <UTooltip :text="tooltipText">
7
+ <UTooltip :text="aiChat.texts.trigger">
15
8
  <UButton
16
- icon="i-lucide-sparkles"
9
+ :icon="aiChat.icons.trigger"
17
10
  variant="ghost"
18
11
  class="rounded-full"
12
+ aria-label="AI Chat Trigger"
19
13
  @click="toggle"
20
14
  />
21
15
  </UTooltip>
@@ -0,0 +1,3 @@
1
+ <template>
2
+ <div />
3
+ </template>
@@ -2,11 +2,24 @@
2
2
  import { sleep } from '@movk/core'
3
3
  import { AnimatePresence, motion } from 'motion-v'
4
4
 
5
+ const route = useRoute()
6
+ const { aiChat } = useAppConfig()
5
7
  const { open, isOpen } = useAIChat()
8
+
6
9
  const input = ref('')
7
10
  const isVisible = ref(true)
8
11
  const inputRef = ref<{ inputRef: HTMLInputElement } | null>(null)
9
12
 
13
+ const isDocsRoute = computed(() => route.meta.layout === 'docs')
14
+ const isFloatingInputEnabled = computed(() => aiChat.floatingInput !== false)
15
+ const focusInputShortcut = computed(() => aiChat.shortcuts.focusInput)
16
+
17
+ const shortcutDisplayKeys = computed(() => {
18
+ const shortcut = focusInputShortcut.value
19
+ const parts = shortcut.split('_')
20
+ return parts.map(part => part === 'meta' ? 'meta' : part.toUpperCase())
21
+ })
22
+
10
23
  function handleSubmit() {
11
24
  if (!input.value.trim()) return
12
25
 
@@ -20,10 +33,11 @@ function handleSubmit() {
20
33
  })
21
34
  }
22
35
 
23
- defineShortcuts({
24
- meta_i: {
36
+ const shortcuts = computed(() => ({
37
+ [focusInputShortcut.value]: {
25
38
  usingInput: true,
26
39
  handler: () => {
40
+ if (!isDocsRoute.value || !isFloatingInputEnabled.value) return
27
41
  inputRef.value?.inputRef?.focus()
28
42
  }
29
43
  },
@@ -33,13 +47,15 @@ defineShortcuts({
33
47
  inputRef.value?.inputRef?.blur()
34
48
  }
35
49
  }
36
- })
50
+ }))
51
+
52
+ defineShortcuts(shortcuts)
37
53
  </script>
38
54
 
39
55
  <template>
40
56
  <AnimatePresence>
41
57
  <motion.div
42
- v-if="isVisible && !isOpen"
58
+ v-if="isFloatingInputEnabled && isDocsRoute && isVisible && !isOpen"
43
59
  key="floating-input"
44
60
  :initial="{ y: 20, opacity: 0 }"
45
61
  :animate="{ y: 0, opacity: 1 }"
@@ -48,11 +64,11 @@ defineShortcuts({
48
64
  class="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 px-4"
49
65
  style="will-change: transform"
50
66
  >
51
- <UForm @submit="handleSubmit">
67
+ <UForm @submit.prevent="handleSubmit">
52
68
  <UInput
53
69
  ref="inputRef"
54
70
  v-model="input"
55
- placeholder="输入你的问题..."
71
+ :placeholder="aiChat.texts.placeholder"
56
72
  size="lg"
57
73
  :ui="{
58
74
  root: 'w-72 py-0.5 focus-within:w-96 transition-all duration-300 ease-out',
@@ -64,17 +80,16 @@ defineShortcuts({
64
80
  <template #trailing>
65
81
  <div class="flex items-center gap-2">
66
82
  <div class="hidden sm:!flex items-center gap-1">
67
- <UKbd value="meta" />
68
- <UKbd value="I" />
83
+ <UKbd v-for="key in shortcutDisplayKeys" :key="key" :value="key" />
69
84
  </div>
70
85
 
71
86
  <UButton
87
+ aria-label="Send Message"
72
88
  type="submit"
73
89
  icon="i-lucide-arrow-up"
74
90
  color="primary"
75
91
  size="xs"
76
92
  :disabled="!input.trim()"
77
- class="rounded-lg"
78
93
  />
79
94
  </div>
80
95
  </template>
@@ -1,4 +1,6 @@
1
1
  <script setup lang="ts">
2
+ import { useModels } from '../composables/useModels'
3
+
2
4
  const { model, models, formatModelName, getModelIcon } = useModels()
3
5
 
4
6
  const items = computed(() => models.map(model => ({
@@ -0,0 +1,318 @@
1
+ <script setup lang="ts">
2
+ import type { DefineComponent } from 'vue'
3
+ import type { UIMessage } from 'ai'
4
+ import { Chat } from '@ai-sdk/vue'
5
+ import { DefaultChatTransport } from 'ai'
6
+ import { createReusableTemplate } from '@vueuse/core'
7
+ import AiChatPreStream from './AiChatPreStream.vue'
8
+ import { useModels } from '../composables/useModels'
9
+
10
+ const components = {
11
+ pre: AiChatPreStream as unknown as DefineComponent
12
+ }
13
+
14
+ const [DefineChatContent, ReuseChatContent] = createReusableTemplate<{ showExpandButton?: boolean }>()
15
+
16
+ const { isOpen, isExpanded, isMobile, panelWidth, toggleExpanded, messages, pendingMessage, clearPending, faqQuestions } = useAIChat()
17
+ const config = useRuntimeConfig()
18
+ const { aiChat } = useAppConfig()
19
+ const toast = useToast()
20
+ const { model } = useModels()
21
+
22
+ const input = ref('')
23
+
24
+ const chat = new Chat({
25
+ messages: messages.value,
26
+ transport: new DefaultChatTransport({
27
+ api: config.public.aiChat.apiPath,
28
+ body: () => ({ model: model.value })
29
+ }),
30
+ onError: (error: Error) => {
31
+ const message = (() => {
32
+ try {
33
+ const parsed = JSON.parse(error.message)
34
+ return parsed?.message || error.message
35
+ } catch {
36
+ return error.message
37
+ }
38
+ })()
39
+
40
+ toast.add({
41
+ description: message,
42
+ icon: 'i-lucide-circle-alert',
43
+ color: 'error',
44
+ duration: 0
45
+ })
46
+ },
47
+ onFinish: () => {
48
+ messages.value = chat.messages
49
+ }
50
+ })
51
+
52
+ watch(pendingMessage, (message: string | undefined) => {
53
+ if (message) {
54
+ if (messages.value.length === 0 && chat.messages.length > 0) {
55
+ chat.messages.length = 0
56
+ }
57
+ chat.sendMessage({
58
+ text: message
59
+ })
60
+ clearPending()
61
+ }
62
+ }, { immediate: true })
63
+
64
+ watch(messages, (newMessages: UIMessage[]) => {
65
+ if (newMessages.length === 0 && chat.messages.length > 0) {
66
+ chat.messages.length = 0
67
+ }
68
+ }, { deep: true })
69
+
70
+ const lastMessage = computed(() => chat.messages.at(-1))
71
+
72
+ const { tools } = useToolCall()
73
+ function getToolLabel(toolName: string, args?: any): string {
74
+ const label = tools[toolName]
75
+
76
+ if (!label) {
77
+ return toolName
78
+ }
79
+
80
+ return typeof label === 'function' ? label(args) : label
81
+ }
82
+
83
+ function handleSubmit(event?: Event) {
84
+ event?.preventDefault()
85
+
86
+ if (!input.value.trim()) {
87
+ return
88
+ }
89
+
90
+ chat.sendMessage({
91
+ text: input.value
92
+ })
93
+
94
+ input.value = ''
95
+ }
96
+
97
+ function askQuestion(question: string) {
98
+ chat.sendMessage({
99
+ text: question
100
+ })
101
+ }
102
+
103
+ function resetChat() {
104
+ chat.stop()
105
+ messages.value = []
106
+ chat.messages.length = 0
107
+ }
108
+
109
+ onMounted(() => {
110
+ if (pendingMessage.value) {
111
+ chat.sendMessage({
112
+ text: pendingMessage.value
113
+ })
114
+ clearPending()
115
+ } else if (chat.lastMessage?.role === 'user') {
116
+ chat.regenerate()
117
+ }
118
+ })
119
+ </script>
120
+
121
+ <template>
122
+ <DefineChatContent v-slot="{ showExpandButton = true }">
123
+ <div class="flex h-full flex-col">
124
+ <div class="flex h-16 shrink-0 items-center justify-between border-b border-muted/50 px-4">
125
+ <span class="font-medium text-highlighted">{{ aiChat.texts.title }}</span>
126
+ <div class="flex items-center gap-1">
127
+ <UTooltip
128
+ v-if="showExpandButton"
129
+ :text="isExpanded ? aiChat.texts.collapse : aiChat.texts.expand"
130
+ >
131
+ <UButton
132
+ :icon="isExpanded ? 'i-lucide-minimize-2' : 'i-lucide-maximize-2'"
133
+ color="neutral"
134
+ variant="ghost"
135
+ size="sm"
136
+ class="text-muted hover:text-highlighted"
137
+ aria-label="Toggle Expand Chat Panel"
138
+ @click="toggleExpanded"
139
+ />
140
+ </UTooltip>
141
+ <UTooltip
142
+ v-if="chat.messages.length > 0"
143
+ :text="aiChat.texts.clearChat"
144
+ >
145
+ <UButton
146
+ :icon="aiChat.icons.clearChat"
147
+ color="neutral"
148
+ variant="ghost"
149
+ size="sm"
150
+ class="text-muted hover:text-highlighted"
151
+ aria-label="Clear Chat"
152
+ @click="resetChat"
153
+ />
154
+ </UTooltip>
155
+ <UTooltip :text="aiChat.texts.close">
156
+ <UButton
157
+ :icon="aiChat.icons.close"
158
+ color="neutral"
159
+ variant="ghost"
160
+ size="sm"
161
+ class="text-muted hover:text-highlighted"
162
+ aria-label="Close Chat Panel"
163
+ @click="isOpen = false"
164
+ />
165
+ </UTooltip>
166
+ </div>
167
+ </div>
168
+
169
+ <div class="min-h-0 flex-1 overflow-y-auto">
170
+ <UChatMessages
171
+ v-if="chat.messages.length > 0"
172
+ should-auto-scroll
173
+ :messages="chat.messages"
174
+ compact
175
+ :status="chat.status"
176
+ :user="{ ui: { container: 'pb-2', content: 'text-sm' } }"
177
+ :ui="{ indicator: '*:bg-accented', root: 'h-auto!' }"
178
+ class="px-4 py-4"
179
+ >
180
+ <template #content="{ message }">
181
+ <div class="flex flex-col gap-2">
182
+ <template
183
+ v-for="(part, index) in message.parts"
184
+ :key="`${message.id}-${part.type}-${index}${'state' in part ? `-${part.state}` : ''}`"
185
+ >
186
+ <AiChatReasoning v-if="part.type === 'reasoning'" :text="part.text" :is-streaming="part.state !== 'done'" />
187
+
188
+ <MDCCached
189
+ v-else-if="part.type === 'text'"
190
+ :value="part.text"
191
+ :cache-key="`${message.id}-${index}`"
192
+ :components="components"
193
+ :parser-options="{ highlight: false }"
194
+ class="*:first:mt-0 *:last:mb-0"
195
+ />
196
+
197
+ <template v-else-if="part.type === 'data-tool-calls'">
198
+ <AiChatToolCall
199
+ v-for="tool in (part as any).data.tools"
200
+ :key="`${tool.toolCallId}-${JSON.stringify(tool.args)}`"
201
+ :text="getToolLabel(tool.toolName, tool.args)"
202
+ :is-loading="false"
203
+ />
204
+ </template>
205
+ </template>
206
+ <UButton
207
+ v-if="chat.status === 'streaming' && message.id === lastMessage?.id"
208
+ class="px-0"
209
+ color="neutral"
210
+ variant="link"
211
+ size="sm"
212
+ :label="aiChat.texts.loading"
213
+ loading
214
+ :loading-icon="aiChat.icons.loading"
215
+ />
216
+ </div>
217
+ </template>
218
+ </UChatMessages>
219
+
220
+ <div
221
+ v-else
222
+ class="p-4"
223
+ >
224
+ <div
225
+ v-if="!faqQuestions?.length"
226
+ class="flex h-full flex-col items-center justify-center py-12 text-center"
227
+ >
228
+ <div class="mb-4 flex size-12 items-center justify-center rounded-full bg-primary/10">
229
+ <UIcon
230
+ name="i-lucide-message-circle-question"
231
+ class="size-6 text-primary"
232
+ />
233
+ </div>
234
+ <h3 class="mb-2 text-base font-medium text-highlighted">
235
+ {{ aiChat.texts.askAnything }}
236
+ </h3>
237
+ <p class="max-w-xs text-sm text-muted">
238
+ {{ aiChat.texts.askMeAnythingDescription }}
239
+ </p>
240
+ </div>
241
+
242
+ <template v-else>
243
+ <p class="mb-4 text-sm font-medium text-muted">
244
+ {{ aiChat.texts.faq }}
245
+ </p>
246
+
247
+ <AiChatSlideoverFaq :faq-questions="faqQuestions" @ask-question="askQuestion" />
248
+ </template>
249
+ </div>
250
+ </div>
251
+
252
+ <div class="w-full shrink-0 p-3">
253
+ <UChatPrompt
254
+ v-model="input"
255
+ :rows="2"
256
+ class="text-sm"
257
+ variant="subtle"
258
+ :placeholder="aiChat.texts.placeholder"
259
+ :ui="{
260
+ root: 'shadow-none!',
261
+ body: '*:p-0! *:rounded-none!'
262
+ }"
263
+ @submit="handleSubmit"
264
+ >
265
+ <template #footer>
266
+ <div class="hidden items-center divide-x divide-muted/50 sm:flex">
267
+ <AiChatModelSelect v-model="model" />
268
+ <div class="flex gap-1 justify-between items-center px-1 text-xs text-muted">
269
+ <span>{{ aiChat.texts.lineBreak }}</span>
270
+ <UKbd value="shift" />
271
+ <UKbd value="enter" />
272
+ </div>
273
+ </div>
274
+
275
+ <UChatPromptSubmit
276
+ class="ml-auto"
277
+ size="xs"
278
+ :status="chat.status"
279
+ @stop="chat.stop()"
280
+ @reload="chat.regenerate()"
281
+ />
282
+ </template>
283
+ </UChatPrompt>
284
+ </div>
285
+ </div>
286
+ </DefineChatContent>
287
+
288
+ <aside
289
+ v-if="!isMobile"
290
+ class="fixed top-0 z-50 h-dvh overflow-hidden border-l border-muted/50 bg-default/95 backdrop-blur-xl transition-[right,width] duration-200 ease-linear will-change-[right,width]"
291
+ :style="{
292
+ width: `${panelWidth}px`,
293
+ right: isOpen ? '0' : `-${panelWidth}px`
294
+ }"
295
+ >
296
+ <div
297
+ class="h-full transition-[width] duration-200 ease-linear"
298
+ :style="{ width: `${panelWidth}px` }"
299
+ >
300
+ <ReuseChatContent :show-expand-button="true" />
301
+ </div>
302
+ </aside>
303
+
304
+ <USlideover
305
+ v-else
306
+ v-model:open="isOpen"
307
+ title=" "
308
+ description=" "
309
+ side="right"
310
+ :ui="{
311
+ content: 'ring-0 bg-default'
312
+ }"
313
+ >
314
+ <template #content>
315
+ <ReuseChatContent :show-expand-button="false" />
316
+ </template>
317
+ </USlideover>
318
+ </template>
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { ShikiCachedRenderer } from 'shiki-stream/vue'
3
+ import { useHighlighter } from '../composables/useHighlighter'
3
4
 
4
5
  const colorMode = useColorMode()
5
6
  const highlighter = await useHighlighter()
@@ -12,7 +12,7 @@ const { isStreaming = false } = defineProps<{
12
12
  }>()
13
13
 
14
14
  const open = ref(false)
15
- const { ui } = useAppConfig()
15
+ const { ui, aiChat } = useAppConfig()
16
16
 
17
17
  watch(() => isStreaming, () => {
18
18
  open.value = isStreaming
@@ -33,11 +33,11 @@ function cleanMarkdown(text: string): string {
33
33
  class="p-0 group"
34
34
  color="neutral"
35
35
  variant="link"
36
- :trailing-icon="ui.icons.chevronDown"
36
+ :trailing-icon="aiChat.icons.streaming || ui.icons.chevronDown"
37
37
  :ui="{
38
38
  trailingIcon: text.length > 0 ? 'group-data-[state=open]:rotate-180 transition-transform duration-200' : 'hidden'
39
39
  }"
40
- :label="isStreaming ? '思考中...' : '思考过程'"
40
+ :label="isStreaming ? aiChat.texts.streaming : aiChat.texts.streamed"
41
41
  />
42
42
 
43
43
  <template #content>
@@ -1,8 +1,5 @@
1
1
  <script lang="ts" setup>
2
- export interface FaqCategory {
3
- category: string
4
- items: string[]
5
- }
2
+ import type { FaqCategory } from '../types'
6
3
 
7
4
  defineProps<{
8
5
  /**
@@ -23,7 +20,7 @@ defineEmits<{
23
20
  :key="category.category"
24
21
  class="flex flex-col gap-1.5"
25
22
  >
26
- <h4 class="text-xs font-medium text-dimmed uppercase tracking-wide">
23
+ <h4 class="text-xs font-medium text-muted uppercase tracking-wide">
27
24
  {{ category.category }}
28
25
  </h4>
29
26
  <div class="flex flex-col">