@movk/nuxt-docs 1.14.1 → 1.15.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 (42) hide show
  1. package/README.md +1 -1
  2. package/app/app.config.ts +5 -32
  3. package/app/app.vue +54 -32
  4. package/app/components/DocsAsideRightBottom.vue +1 -1
  5. package/app/components/OgImage/OgImageDocs.takumi.vue +90 -0
  6. package/app/components/PageHeaderLinks.vue +6 -16
  7. package/app/components/content/CommitChangelog.vue +1 -0
  8. package/app/components/content/Mermaid.vue +3 -1
  9. package/app/components/header/HeaderCTA.vue +1 -10
  10. package/app/components/theme-picker/ThemePicker.vue +22 -33
  11. package/app/composables/useTheme.ts +64 -84
  12. package/app/composables/useToolCall.ts +5 -8
  13. package/app/error.vue +1 -1
  14. package/app/pages/docs/[...slug].vue +6 -6
  15. package/app/plugins/theme.ts +39 -68
  16. package/app/templates/landing.vue +5 -2
  17. package/app/templates/releases.vue +5 -2
  18. package/app/types/index.d.ts +8 -57
  19. package/content.config.ts +1 -0
  20. package/modules/ai-chat/index.ts +16 -26
  21. package/modules/ai-chat/runtime/components/AiChat.vue +3 -3
  22. package/modules/ai-chat/runtime/components/AiChatFloatingInput.vue +8 -8
  23. package/modules/ai-chat/runtime/components/AiChatPanel.vue +216 -231
  24. package/modules/ai-chat/runtime/components/AiChatPreStream.vue +0 -14
  25. package/modules/ai-chat/runtime/composables/useAIChat.ts +25 -73
  26. package/modules/ai-chat/runtime/composables/useModels.ts +0 -19
  27. package/modules/ai-chat/runtime/server/api/ai-chat.ts +74 -48
  28. package/modules/ai-chat/runtime/server/utils/getModel.ts +1 -9
  29. package/modules/ai-chat/runtime/types.ts +5 -0
  30. package/nuxt.config.ts +42 -36
  31. package/nuxt.schema.ts +14 -99
  32. package/package.json +25 -29
  33. package/server/mcp/tools/get-page.ts +5 -47
  34. package/server/mcp/tools/list-getting-started-guides.ts +1 -3
  35. package/server/mcp/tools/list-pages.ts +9 -44
  36. package/utils/git.ts +26 -79
  37. package/app/components/OgImage/Nuxt.vue +0 -247
  38. package/app/composables/useAnalytics.ts +0 -7
  39. package/modules/ai-chat/runtime/components/AiChatReasoning.vue +0 -49
  40. package/modules/ai-chat/runtime/components/AiChatSlideoverFaq.vue +0 -38
  41. package/modules/ai-chat/runtime/components/AiChatToolCall.vue +0 -31
  42. package/modules/ai-chat/runtime/server/utils/docs_agent.ts +0 -54
@@ -1,10 +1,9 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { join } from 'pathe'
3
3
  import {
4
- addComponent,
5
- addComponentsDir,
6
4
  addImports,
7
5
  addServerHandler,
6
+ addComponent,
8
7
  createResolver,
9
8
  defineNuxtModule,
10
9
  logger
@@ -46,7 +45,7 @@ export default defineNuxtModule<AiChatModuleOptions>({
46
45
  models: []
47
46
  },
48
47
  setup(options, nuxt) {
49
- const hasApiKey = !!(process.env.AI_GATEWAY_API_KEY || process.env.OPENROUTER_API_KEY || process.env.ZHIPU_API_KEY)
48
+ const hasApiKey = !!process.env.AI_GATEWAY_API_KEY
50
49
 
51
50
  const { resolve } = createResolver(import.meta.url)
52
51
 
@@ -64,17 +63,22 @@ export default defineNuxtModule<AiChatModuleOptions>({
64
63
  }
65
64
  ])
66
65
 
67
- if (hasApiKey) {
68
- addComponentsDir({
69
- path: resolve('./runtime/components'),
70
- ignore: ['AiChatDisabled']
71
- })
72
- } else {
66
+ const components = [
67
+ 'AiChat',
68
+ 'AiChatFloatingInput',
69
+ 'AiChatModelSelect',
70
+ 'AiChatPanel',
71
+ 'AiChatPreStream'
72
+ ]
73
+
74
+ components.forEach(name =>
73
75
  addComponent({
74
- name: 'AiChatDisabled',
75
- filePath: resolve('./runtime/components/AiChatDisabled.vue')
76
+ name,
77
+ filePath: hasApiKey
78
+ ? resolve(`./runtime/components/${name}.vue`)
79
+ : resolve('./runtime/components/AiChatDisabled.vue')
76
80
  })
77
- }
81
+ )
78
82
 
79
83
  if (!hasApiKey) {
80
84
  log.warn('[movk-nuxt-docs] Ai Chat Module disabled: no API key found in environment variables.')
@@ -85,20 +89,6 @@ export default defineNuxtModule<AiChatModuleOptions>({
85
89
  mcpPath: options.mcpPath!
86
90
  }
87
91
 
88
- addImports([
89
- {
90
- name: 'useHighlighter',
91
- from: resolve('./runtime/composables/useHighlighter')
92
- }
93
- ])
94
-
95
- addImports([
96
- {
97
- name: 'useModels',
98
- from: resolve('./runtime/composables/useModels')
99
- }
100
- ])
101
-
102
92
  /**
103
93
  * 检查用户项目中是否存在自定义 handler
104
94
  * '/api/ai-chat' -> 'api/ai-chat','/custom' -> 'custom'
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  const { aiChat } = useAppConfig()
3
- const { toggle } = useAIChat()
3
+ const { toggleChat } = useAIChat()
4
4
  </script>
5
5
 
6
6
  <template>
@@ -9,8 +9,8 @@ const { toggle } = useAIChat()
9
9
  :icon="aiChat.icons.trigger"
10
10
  variant="ghost"
11
11
  class="rounded-full"
12
- aria-label="AI Chat Trigger"
13
- @click="toggle"
12
+ :aria-label="aiChat.texts.trigger"
13
+ @click="toggleChat"
14
14
  />
15
15
  </UTooltip>
16
16
  </template>
@@ -20,17 +20,17 @@ const shortcutDisplayKeys = computed(() => {
20
20
  return parts.map(part => part === 'meta' ? 'meta' : part.toUpperCase())
21
21
  })
22
22
 
23
- function handleSubmit() {
23
+ async function handleSubmit() {
24
24
  if (!input.value.trim()) return
25
25
 
26
26
  const message = input.value
27
27
  isVisible.value = false
28
28
 
29
- sleep(200).then(() => {
30
- open(message, true)
31
- input.value = ''
32
- isVisible.value = true
33
- })
29
+ await sleep(200)
30
+
31
+ open(message)
32
+ input.value = ''
33
+ isVisible.value = true
34
34
  }
35
35
 
36
36
  const shortcuts = computed(() => ({
@@ -61,14 +61,14 @@ defineShortcuts(shortcuts)
61
61
  :animate="{ y: 0, opacity: 1 }"
62
62
  :exit="{ y: 100, opacity: 0 }"
63
63
  :transition="{ duration: 0.2, ease: 'easeOut' }"
64
- class="fixed inset-x-0 z-10 px-4 sm:px-80 bottom-[max(1.5rem,env(safe-area-inset-bottom))]"
64
+ class="fixed inset-x-0 z-10 px-4 sm:px-80 bottom-[max(1.5rem,env(safe-area-inset-bottom))] pointer-events-none"
65
65
  style="will-change: transform"
66
66
  >
67
67
  <form
68
68
  class="flex w-full justify-center"
69
69
  @submit.prevent="handleSubmit"
70
70
  >
71
- <div class="w-full max-w-96">
71
+ <div class="w-full max-w-96 pointer-events-auto">
72
72
  <UInput
73
73
  ref="inputRef"
74
74
  v-model="input"
@@ -1,25 +1,29 @@
1
1
  <script setup lang="ts">
2
2
  import type { DefineComponent } from 'vue'
3
- import type { UIMessage } from 'ai'
3
+ import type { FaqCategory, FaqQuestions, ToolPart, ToolState } from '../types'
4
4
  import { Chat } from '@ai-sdk/vue'
5
- import { DefaultChatTransport } from 'ai'
6
- import { createReusableTemplate } from '@vueuse/core'
7
- import AiChatPreStream from './AiChatPreStream.vue'
5
+ import { DefaultChatTransport, getToolName, isReasoningUIPart, isTextUIPart, isToolUIPart } from 'ai'
6
+ import { computed } from 'vue'
7
+ import { isReasoningStreaming, isToolStreaming } from '@nuxt/ui/utils/ai'
8
8
  import { useModels } from '../composables/useModels'
9
+ import { splitByCase, upperFirst } from 'scule'
10
+ import AiChatPreStream from './AiChatPreStream.vue'
11
+ import { useMemoize } from '@vueuse/core'
9
12
 
10
13
  const components = {
11
14
  pre: AiChatPreStream as unknown as DefineComponent
12
15
  }
13
16
 
14
- const [DefineChatContent, ReuseChatContent] = createReusableTemplate<{ showExpandButton?: boolean }>()
15
-
16
- const { isOpen, isExpanded, isMobile, panelWidth, toggleExpanded, messages, pendingMessage, clearPending, faqQuestions } = useAIChat()
17
+ const { isOpen, messages } = useAIChat()
18
+ const toast = useToast()
17
19
  const config = useRuntimeConfig()
18
20
  const { aiChat } = useAppConfig()
19
- const toast = useToast()
20
21
  const { model } = useModels()
21
22
 
23
+ const canClear = computed(() => messages.value.length > 0)
24
+
22
25
  const input = ref('')
26
+ let _skipSync = false
23
27
 
24
28
  const chat = new Chat({
25
29
  messages: messages.value,
@@ -28,14 +32,14 @@ const chat = new Chat({
28
32
  body: () => ({ model: model.value })
29
33
  }),
30
34
  onError: (error: Error) => {
31
- const message = (() => {
35
+ let message = error.message
36
+ if (typeof message === 'string' && message[0] === '{') {
32
37
  try {
33
- const parsed = JSON.parse(error.message)
34
- return parsed?.message || error.message
38
+ message = JSON.parse(message).message || message
35
39
  } catch {
36
- return error.message
40
+ // keep original message on malformed JSON
37
41
  }
38
- })()
42
+ }
39
43
 
40
44
  toast.add({
41
45
  description: message,
@@ -45,263 +49,244 @@ const chat = new Chat({
45
49
  })
46
50
  },
47
51
  onFinish: () => {
52
+ _skipSync = true
48
53
  messages.value = chat.messages
54
+ nextTick(() => {
55
+ _skipSync = false
56
+ })
49
57
  }
50
58
  })
51
59
 
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 })
60
+ watch(messages, (newMessages) => {
61
+ if (_skipSync) return
63
62
 
64
- watch(messages, (newMessages: UIMessage[]) => {
65
- if (newMessages.length === 0 && chat.messages.length > 0) {
66
- chat.messages.length = 0
63
+ chat.messages = newMessages
64
+ if (chat.lastMessage?.role === 'user') {
65
+ chat.regenerate()
67
66
  }
68
- }, { deep: true })
67
+ })
69
68
 
70
- const lastMessage = computed(() => chat.messages.at(-1))
69
+ function upperName(name: string) {
70
+ return splitByCase(name).map(p => upperFirst(p)).join('')
71
+ }
71
72
 
72
- const { tools } = useToolCall()
73
- function getToolLabel(toolName: string, args?: any): string {
74
- const label = tools[toolName]
73
+ function getToolMessage(state: ToolState, toolName: string, input: Record<string, string | undefined>) {
74
+ const { toolMessage } = useToolCall(state, toolName, input)
75
+ const searchVerb = state === 'output-available' ? '已搜索' : '搜索中'
76
+ const readVerb = state === 'output-available' ? '已读取' : '读取中'
75
77
 
76
- if (!label) {
77
- return toolName
78
- }
78
+ return {
79
+ 'list-getting-started-guides': `${searchVerb} 入门指南`,
80
+ 'list-pages': `${searchVerb} 所有文档页面`,
81
+ 'list-examples': `${searchVerb} 所有示例`,
82
+ 'get-page': `${readVerb} ${input.path || ''} 页面`,
83
+ 'get-example': `${readVerb} ${upperName(input.exampleName || '')} 示例`,
84
+ ...toolMessage
85
+ }[toolName] || `${searchVerb} ${toolName}`
86
+ }
87
+
88
+ const getCachedToolMessage = useMemoize((state: ToolState, toolName: string, input: string) =>
89
+ getToolMessage(state, toolName, JSON.parse(input))
90
+ )
79
91
 
80
- return typeof label === 'function' ? label(args) : label
92
+ function getToolText(part: ToolPart) {
93
+ return getCachedToolMessage(part.state, getToolName(part), JSON.stringify(part.input || {}))
81
94
  }
82
95
 
83
- function handleSubmit(event?: Event) {
84
- event?.preventDefault()
96
+ function getToolIcon(part: ToolPart): string {
97
+ const toolName = getToolName(part)
98
+ const { toolIcon } = useToolCall(part.state, toolName, part.input || {} as any)
99
+
100
+ const iconMap: Record<string, string> = {
101
+ 'get-page': 'i-lucide-file-text',
102
+ 'get-example': 'i-lucide-file-text',
103
+ ...toolIcon
104
+ }
85
105
 
106
+ return iconMap[toolName] || 'i-lucide-search'
107
+ }
108
+
109
+ function onSubmit() {
86
110
  if (!input.value.trim()) {
87
111
  return
88
112
  }
89
113
 
90
- chat.sendMessage({
91
- text: input.value
92
- })
114
+ chat.sendMessage({ text: input.value })
93
115
 
94
116
  input.value = ''
95
117
  }
96
118
 
97
119
  function askQuestion(question: string) {
98
- chat.sendMessage({
99
- text: question
100
- })
120
+ input.value = question
121
+ onSubmit()
101
122
  }
102
123
 
103
- function resetChat() {
104
- chat.stop()
124
+ function clearMessages() {
125
+ if (chat.status === 'streaming') {
126
+ chat.stop()
127
+ }
105
128
  messages.value = []
106
- chat.messages.length = 0
129
+ chat.messages = []
107
130
  }
108
131
 
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()
132
+ function normalizeFaqQuestions(questions: FaqQuestions): FaqCategory[] {
133
+ if (!questions || (Array.isArray(questions) && questions.length === 0)) {
134
+ return []
135
+ }
136
+
137
+ if (typeof questions[0] === 'string') {
138
+ return [{
139
+ category: '问题',
140
+ items: questions as string[]
141
+ }]
117
142
  }
143
+
144
+ return questions as FaqCategory[]
145
+ }
146
+
147
+ const faqQuestions = computed<FaqCategory[]>(() => {
148
+ const faqConfig = aiChat?.faqQuestions
149
+ if (!faqConfig) return []
150
+
151
+ return normalizeFaqQuestions(faqConfig)
118
152
  })
119
153
  </script>
120
154
 
121
155
  <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-default px-4">
125
- <span class="font-medium text-highlighted">{{ aiChat.texts.title }}</span>
126
- <div class="flex items-center gap-1">
127
- <UTooltip v-if="showExpandButton" :text="isExpanded ? aiChat.texts.collapse : aiChat.texts.expand">
128
- <UButton
129
- :icon="isExpanded ? 'i-lucide-minimize-2' : 'i-lucide-maximize-2'"
130
- color="neutral"
131
- variant="ghost"
132
- size="sm"
133
- class="text-muted hover:text-highlighted"
134
- aria-label="Toggle Expand Chat Panel"
135
- @click="toggleExpanded"
136
- />
137
- </UTooltip>
138
- <UTooltip v-if="chat.messages.length > 0" :text="aiChat.texts.clearChat">
139
- <UButton
140
- :icon="aiChat.icons.clearChat"
141
- color="neutral"
142
- variant="ghost"
143
- size="sm"
144
- class="text-muted hover:text-highlighted"
145
- aria-label="Clear Chat"
146
- @click="resetChat"
147
- />
148
- </UTooltip>
149
- <UTooltip :text="aiChat.texts.close">
150
- <UButton
151
- :icon="aiChat.icons.close"
152
- color="neutral"
153
- variant="ghost"
154
- size="sm"
155
- class="text-muted hover:text-highlighted"
156
- aria-label="Close Chat Panel"
157
- @click="isOpen = false"
158
- />
159
- </UTooltip>
160
- </div>
161
- </div>
156
+ <USidebar
157
+ v-model:open="isOpen"
158
+ side="right"
159
+ :title="aiChat.texts.title"
160
+ rail
161
+ :style="{ '--sidebar-width': '24rem' }"
162
+ :ui="{ footer: 'p-0', actions: 'gap-0.5' }"
163
+ >
164
+ <template #actions>
165
+ <UTooltip v-if="canClear" :text="aiChat.texts.clearChat">
166
+ <UButton
167
+ :icon="aiChat.icons.clearChat"
168
+ color="neutral"
169
+ variant="ghost"
170
+ :aria-label="aiChat.texts.clearChat"
171
+ @click="clearMessages"
172
+ />
173
+ </UTooltip>
174
+ </template>
175
+
176
+ <template #close>
177
+ <UTooltip :text="aiChat.texts.close">
178
+ <UButton
179
+ :icon="aiChat.icons.close"
180
+ color="neutral"
181
+ variant="ghost"
182
+ :aria-label="aiChat.texts.close"
183
+ @click="isOpen = false"
184
+ />
185
+ </UTooltip>
186
+ </template>
162
187
 
163
- <div class="min-h-0 flex-1 overflow-y-auto">
164
- <UChatMessages
165
- v-if="chat.messages.length > 0"
166
- should-auto-scroll
167
- :messages="chat.messages"
168
- compact
169
- :status="chat.status"
170
- :user="{ ui: { container: 'pb-2', content: 'text-sm' } }"
171
- :ui="{ indicator: '*:bg-accented', root: 'h-auto!' }"
172
- class="px-4 py-4"
173
- >
174
- <template #content="{ message }">
175
- <div class="flex flex-col gap-2">
176
- <template
177
- v-for="(part, index) in message.parts"
178
- :key="`${message.id}-${part.type}-${index}${'state' in part ? `-${part.state}` : ''}`"
179
- >
180
- <AiChatReasoning
181
- v-if="part.type === 'reasoning'"
182
- :text="part.text"
183
- :is-streaming="part.state !== 'done'"
184
- />
185
-
186
- <MDCCached
187
- v-else-if="part.type === 'text'"
188
- :value="part.text"
189
- :cache-key="`${message.id}-${index}`"
190
- :components="components"
191
- :parser-options="{ highlight: false }"
192
- class="*:first:mt-0 *:last:mb-0"
193
- />
194
-
195
- <template v-else-if="part.type === 'data-tool-calls'">
196
- <AiChatToolCall
197
- v-for="tool in (part as any).data.tools"
198
- :key="`${tool.toolCallId}-${JSON.stringify(tool.args)}`"
199
- :text="getToolLabel(tool.toolName, tool.args)"
200
- :is-loading="false"
201
- />
202
- </template>
203
- </template>
204
- <UButton
205
- v-if="chat.status === 'streaming' && message.id === lastMessage?.id"
206
- class="px-0"
207
- color="neutral"
208
- variant="link"
209
- size="sm"
210
- :label="aiChat.texts.loading"
211
- loading
212
- :loading-icon="aiChat.icons.loading"
188
+ <UTheme
189
+ :ui="{
190
+ prose: {
191
+ p: { base: 'my-2 text-sm/6' },
192
+ li: { base: 'my-0.5 text-sm/6' },
193
+ ul: { base: 'my-2' },
194
+ ol: { base: 'my-2' },
195
+ h1: { base: 'text-xl mb-4' },
196
+ h2: { base: 'text-lg mt-6 mb-3' },
197
+ h3: { base: 'text-base mt-4 mb-2' },
198
+ h4: { base: 'text-sm mt-3 mb-1.5' },
199
+ code: { base: 'text-xs' },
200
+ pre: { root: 'my-2', base: 'text-xs/5' },
201
+ table: { root: 'my-2' },
202
+ hr: { base: 'my-4' }
203
+ }
204
+ }"
205
+ >
206
+ <UChatMessages
207
+ v-if="chat.messages.length"
208
+ should-auto-scroll
209
+ :messages="chat.messages"
210
+ :status="chat.status"
211
+ compact
212
+ class="px-0 gap-2"
213
+ :user="{ ui: { container: 'max-w-full' } }"
214
+ >
215
+ <template #content="{ message }">
216
+ <template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
217
+ <UChatReasoning
218
+ v-if="isReasoningUIPart(part)"
219
+ :text="part.text"
220
+ :streaming="isReasoningStreaming(message, index, chat)"
221
+ :icon="aiChat.icons.reasoning"
222
+ >
223
+ <MDCCached
224
+ :value="part.text"
225
+ :cache-key="`reasoning-${message.id}-${index}`"
226
+ :parser-options="{ highlight: false }"
227
+ class="*:first:mt-0 *:last:mb-0"
213
228
  />
214
- </div>
229
+ </UChatReasoning>
230
+ <MDCCached
231
+ v-else-if="isTextUIPart(part) && part.text.length > 0"
232
+ :value="part.text"
233
+ :cache-key="`${message.id}-${index}`"
234
+ :components="components"
235
+ :parser-options="{ highlight: false }"
236
+ class="*:first:mt-0 *:last:mb-0"
237
+ />
238
+ <UChatTool
239
+ v-else-if="isToolUIPart(part)"
240
+ :text="getToolText(part)"
241
+ :icon="getToolIcon(part)"
242
+ :streaming="isToolStreaming(part)"
243
+ />
215
244
  </template>
216
- </UChatMessages>
245
+ </template>
246
+ </UChatMessages>
217
247
 
218
- <div v-else class="p-4">
219
- <div v-if="!faqQuestions?.length" class="flex h-full flex-col items-center justify-center py-12 text-center">
220
- <div class="mb-4 flex size-12 items-center justify-center rounded-full bg-primary/10">
221
- <UIcon name="i-lucide-message-circle-question" class="size-6 text-primary" />
222
- </div>
223
- <h3 class="mb-2 text-base font-medium text-highlighted">
224
- {{ aiChat.texts.askAnything }}
225
- </h3>
226
- <p class="max-w-xs text-sm text-muted">
227
- {{ aiChat.texts.askMeAnythingDescription }}
228
- </p>
229
- </div>
230
-
231
- <template v-else>
232
- <p class="mb-4 text-sm font-medium text-muted">
233
- {{ aiChat.texts.faq }}
234
- </p>
235
-
236
- <AiChatSlideoverFaq :faq-questions="faqQuestions" @ask-question="askQuestion" />
237
- </template>
238
- </div>
248
+ <div v-else class="flex flex-col gap-6">
249
+ <UPageLinks
250
+ v-for="category in faqQuestions"
251
+ :key="category.category"
252
+ :title="category.category"
253
+ :links="category.items.map(item => ({ label: item, onClick: () => askQuestion(item) }))"
254
+ />
239
255
  </div>
256
+ </UTheme>
240
257
 
241
- <div class="w-full shrink-0 p-3">
242
- <UChatPrompt
243
- v-model="input"
244
- :rows="2"
245
- :placeholder="aiChat.texts.placeholder"
246
- maxlength="1000"
247
- :ui="{
248
- root: 'shadow-none!',
249
- body: '*:p-0! *:rounded-none! *:text-sm!'
250
- }"
251
- @submit="handleSubmit"
252
- >
253
- <template #footer>
254
- <div class="flex items-center gap-1 text-xs text-muted">
255
- <AiChatModelSelect v-model="model" />
256
- <div class="flex gap-1 justify-between items-center px-1 text-xs text-muted">
257
- <span>{{ aiChat.texts.lineBreak }}</span>
258
- <UKbd value="shift" />
259
- <UKbd value="enter" />
260
- </div>
261
- </div>
258
+ <template #footer>
259
+ <UChatPrompt
260
+ v-model="input"
261
+ :error="chat.error"
262
+ :placeholder="aiChat.texts.placeholder"
263
+ variant="naked"
264
+ size="sm"
265
+ autofocus
266
+ :ui="{ base: 'px-0' }"
267
+ class="px-4"
268
+ @submit="onSubmit"
269
+ >
270
+ <template #footer>
271
+ <div class="flex items-center gap-x-1 text-xs text-muted">
272
+ <AiChatModelSelect v-model="model" />
262
273
 
263
- <UChatPromptSubmit
264
- class="ml-auto"
265
- size="xs"
266
- :status="chat.status"
267
- @stop="chat.stop()"
268
- @reload="chat.regenerate()"
269
- />
270
- </template>
271
- </UChatPrompt>
272
- <div class="mt-1 flex text-xs text-dimmed items-center justify-between">
273
- <span>刷新时聊天会被清除</span>
274
- <span>
275
- {{ input.length }}/1000
276
- </span>
277
- </div>
278
- </div>
279
- </div>
280
- </DefineChatContent>
281
-
282
- <aside
283
- v-if="!isMobile"
284
- class="left-auto! fixed top-0 z-50 h-dvh overflow-hidden border-l border-default bg-default/95 backdrop-blur-xl transition-[right,width] duration-200 ease-linear will-change-[right,width]"
285
- :style="{
286
- width: `${panelWidth}px`,
287
- right: isOpen ? '0' : `-${panelWidth}px`
288
- }"
289
- >
290
- <div class="h-full transition-[width] duration-200 ease-linear" :style="{ width: `${panelWidth}px` }">
291
- <ReuseChatContent :show-expand-button="true" />
292
- </div>
293
- </aside>
274
+ <div class="flex gap-1 justify-between items-center px-1 text-xs text-muted">
275
+ <span>{{ aiChat.texts.lineBreak }}</span>
276
+ <UKbd value="shift" />
277
+ <UKbd value="enter" />
278
+ </div>
279
+ </div>
294
280
 
295
- <USlideover
296
- v-else
297
- v-model:open="isOpen"
298
- side="right"
299
- :ui="{
300
- content: 'ring-0 bg-default'
301
- }"
302
- >
303
- <template #content>
304
- <ReuseChatContent :show-expand-button="false" />
281
+ <UChatPromptSubmit
282
+ class="ml-auto"
283
+ size="xs"
284
+ :status="chat.status"
285
+ @stop="chat.stop()"
286
+ @reload="chat.regenerate()"
287
+ />
288
+ </template>
289
+ </UChatPrompt>
305
290
  </template>
306
- </USlideover>
291
+ </USidebar>
307
292
  </template>