@movk/nuxt-docs 1.5.2 → 1.6.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 (51) hide show
  1. package/README.md +54 -24
  2. package/app/components/DocsAsideLeftBody.vue +13 -0
  3. package/app/components/DocsAsideLeftTop.vue +6 -0
  4. package/app/components/DocsAsideRightBottom.vue +29 -0
  5. package/app/components/OgImage/Nuxt.vue +40 -16
  6. package/app/components/PageHeaderLinks.vue +35 -9
  7. package/app/components/content/CommitChangelog.vue +10 -10
  8. package/app/components/content/ComponentEmits.vue +2 -2
  9. package/app/components/content/ComponentExample.vue +10 -10
  10. package/app/components/content/ComponentProps.vue +5 -3
  11. package/app/components/content/ComponentPropsSchema.vue +5 -1
  12. package/app/components/content/ComponentSlots.vue +2 -2
  13. package/app/components/content/HighlightInlineType.vue +1 -1
  14. package/app/components/content/PageLastCommit.vue +12 -8
  15. package/app/components/footer/Footer.vue +22 -0
  16. package/app/components/footer/FooterLeft.vue +7 -0
  17. package/app/components/footer/FooterRight.vue +14 -0
  18. package/app/components/header/Header.vue +4 -7
  19. package/app/components/header/HeaderCTA.vue +18 -0
  20. package/app/components/header/HeaderCenter.vue +7 -0
  21. package/app/components/theme-picker/ThemePicker.vue +24 -201
  22. package/app/composables/useFaq.ts +21 -0
  23. package/app/composables/useHighlighter.ts +22 -0
  24. package/app/composables/useTheme.ts +223 -0
  25. package/app/layouts/docs.vue +2 -15
  26. package/app/pages/docs/[...slug].vue +1 -1
  27. package/app/utils/shiki-transformer-icon-highlight.ts +1 -1
  28. package/app/utils/unicode.ts +12 -0
  29. package/modules/ai-chat/index.ts +90 -0
  30. package/modules/ai-chat/runtime/components/AiChat.vue +22 -0
  31. package/modules/ai-chat/runtime/components/AiChatFloatingInput.vue +85 -0
  32. package/modules/ai-chat/runtime/components/AiChatModelSelect.vue +24 -0
  33. package/modules/ai-chat/runtime/components/AiChatPreStream.vue +58 -0
  34. package/modules/ai-chat/runtime/components/AiChatReasoning.vue +49 -0
  35. package/modules/ai-chat/runtime/components/AiChatSlideover.vue +245 -0
  36. package/modules/ai-chat/runtime/components/AiChatSlideoverFaq.vue +41 -0
  37. package/modules/ai-chat/runtime/components/AiChatToolCall.vue +31 -0
  38. package/modules/ai-chat/runtime/composables/useAIChat.ts +45 -0
  39. package/modules/ai-chat/runtime/composables/useModels.ts +58 -0
  40. package/modules/ai-chat/runtime/composables/useTools.ts +31 -0
  41. package/modules/ai-chat/runtime/server/api/search.ts +84 -0
  42. package/modules/ai-chat/runtime/server/utils/docs_agent.ts +49 -0
  43. package/modules/ai-chat/runtime/server/utils/getModel.ts +25 -0
  44. package/modules/css.ts +2 -0
  45. package/nuxt.config.ts +27 -39
  46. package/package.json +16 -7
  47. package/server/mcp/tools/get-page.ts +60 -0
  48. package/server/mcp/tools/list-pages.ts +49 -0
  49. package/app/components/AdsCarbon.vue +0 -3
  50. package/app/components/Footer.vue +0 -32
  51. package/modules/llms.ts +0 -27
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 解码字符串中的 Unicode 转义序列
3
+ * @param str 包含 Unicode 转义序列的字符串(如 '\u5411 AI \u63D0\u95EE')
4
+ * @returns 解码后的字符串(如 '向 AI 提问')
5
+ * @example
6
+ * decodeUnicodeEscapes("'\\u5411 AI \\u63D0\\u95EE'") // => "'向 AI 提问'"
7
+ */
8
+ export function decodeUnicodeEscapes(str: string): string {
9
+ return str.replace(/\\u[\dA-Fa-f]{4}/g, (match) => {
10
+ return String.fromCharCode(Number.parseInt(match.replace('\\u', ''), 16))
11
+ })
12
+ }
@@ -0,0 +1,90 @@
1
+ import { addComponentsDir, addImportsDir, addServerHandler, createResolver, defineNuxtModule } from '@nuxt/kit'
2
+
3
+ export interface AiChatModuleOptions {
4
+ /**
5
+ * 是否启用 AI 聊天功能
6
+ * @default import.meta.env.AI_GATEWAY_API_KEY || import.meta.env.OPENROUTER_API_KEY
7
+ */
8
+ enable?: boolean
9
+ /**
10
+ * 聊天 API 端点路径
11
+ * @default '/api/ai-chat'
12
+ */
13
+ apiPath?: string
14
+ /**
15
+ * MCP 服务器连接路径
16
+ * @default '/mcp'
17
+ */
18
+ mcpPath?: string
19
+ /**
20
+ * 通过 AI SDK Gateway 、OpenRouter 使用的 AI 模型
21
+ */
22
+ model?: string
23
+ /**
24
+ * 可用模型列表
25
+ * - 格式为 "provider/model" 或 "model"
26
+ */
27
+ models?: string[]
28
+ }
29
+
30
+ export default defineNuxtModule<AiChatModuleOptions>({
31
+ meta: {
32
+ name: 'ai-chat',
33
+ configKey: 'aiChat'
34
+ },
35
+ defaults: {
36
+ enable: !!(
37
+ import.meta.env.AI_GATEWAY_API_KEY
38
+ || import.meta.env.OPENROUTER_API_KEY
39
+ ),
40
+ apiPath: '/api/ai-chat',
41
+ mcpPath: '/mcp',
42
+ model: '',
43
+ models: []
44
+ },
45
+ setup(options, nuxt) {
46
+ const { resolve } = createResolver(import.meta.url)
47
+
48
+ nuxt.options.runtimeConfig.public.aiChat = {
49
+ enable: options.enable!,
50
+ apiPath: options.apiPath!,
51
+ model: options.model!,
52
+ models: options.models!
53
+ }
54
+ nuxt.options.runtimeConfig.aiChat = {
55
+ mcpPath: options.mcpPath!
56
+ }
57
+
58
+ if (!options.enable) {
59
+ console.info('[ai-chat] Module disabled: no AI_GATEWAY_API_KEY or OPENROUTER_API_KEY found')
60
+ }
61
+
62
+ addComponentsDir({
63
+ path: resolve('runtime/components')
64
+ })
65
+
66
+ addImportsDir(resolve('runtime/composables'))
67
+
68
+ const routePath = options.apiPath!.replace(/^\//, '')
69
+ addServerHandler({
70
+ route: `/${routePath}`,
71
+ handler: resolve('./runtime/server/api/search')
72
+ })
73
+ }
74
+ })
75
+
76
+ declare module 'nuxt/schema' {
77
+ interface PublicRuntimeConfig {
78
+ aiChat: {
79
+ enable: boolean
80
+ apiPath: string
81
+ model: string
82
+ models: string[]
83
+ }
84
+ }
85
+ interface RuntimeConfig {
86
+ aiChat: {
87
+ mcpPath: string
88
+ }
89
+ }
90
+ }
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ const { tooltipText = '向 AI 提问' } = defineProps<{
3
+ /**
4
+ * 鼠标悬停时的提示文本
5
+ * @defaultValue 向 AI 提问
6
+ */
7
+ tooltipText?: string
8
+ }>()
9
+
10
+ const { toggle } = useAIChat()
11
+ </script>
12
+
13
+ <template>
14
+ <UTooltip :text="tooltipText">
15
+ <UButton
16
+ icon="i-lucide-sparkles"
17
+ variant="ghost"
18
+ class="rounded-full"
19
+ @click="toggle"
20
+ />
21
+ </UTooltip>
22
+ </template>
@@ -0,0 +1,85 @@
1
+ <script setup lang="ts">
2
+ import { sleep } from '@movk/core'
3
+ import { AnimatePresence, motion } from 'motion-v'
4
+
5
+ const { open, isOpen } = useAIChat()
6
+ const input = ref('')
7
+ const isVisible = ref(true)
8
+ const inputRef = ref<{ inputRef: HTMLInputElement } | null>(null)
9
+
10
+ function handleSubmit() {
11
+ if (!input.value.trim()) return
12
+
13
+ const message = input.value
14
+ isVisible.value = false
15
+
16
+ sleep(200).then(() => {
17
+ open(message, true)
18
+ input.value = ''
19
+ isVisible.value = true
20
+ })
21
+ }
22
+
23
+ defineShortcuts({
24
+ meta_i: {
25
+ usingInput: true,
26
+ handler: () => {
27
+ inputRef.value?.inputRef?.focus()
28
+ }
29
+ },
30
+ escape: {
31
+ usingInput: true,
32
+ handler: () => {
33
+ inputRef.value?.inputRef?.blur()
34
+ }
35
+ }
36
+ })
37
+ </script>
38
+
39
+ <template>
40
+ <AnimatePresence>
41
+ <motion.div
42
+ v-if="isVisible && !isOpen"
43
+ key="floating-input"
44
+ :initial="{ y: 20, opacity: 0 }"
45
+ :animate="{ y: 0, opacity: 1 }"
46
+ :exit="{ y: 100, opacity: 0 }"
47
+ :transition="{ duration: 0.2, ease: 'easeOut' }"
48
+ class="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 px-4"
49
+ style="will-change: transform"
50
+ >
51
+ <UForm @submit="handleSubmit">
52
+ <UInput
53
+ ref="inputRef"
54
+ v-model="input"
55
+ placeholder="输入你的问题..."
56
+ size="lg"
57
+ :ui="{
58
+ root: 'w-72 py-0.5 focus-within:w-96 transition-all duration-300 ease-out',
59
+ base: 'bg-default/80 backdrop-blur-xl shadow-lg',
60
+ trailing: 'pe-2'
61
+ }"
62
+ @keydown.enter.exact.prevent="handleSubmit"
63
+ >
64
+ <template #trailing>
65
+ <div class="flex items-center gap-2">
66
+ <div class="hidden sm:!flex items-center gap-1">
67
+ <UKbd value="meta" />
68
+ <UKbd value="I" />
69
+ </div>
70
+
71
+ <UButton
72
+ type="submit"
73
+ icon="i-lucide-arrow-up"
74
+ color="primary"
75
+ size="xs"
76
+ :disabled="!input.trim()"
77
+ class="rounded-lg"
78
+ />
79
+ </div>
80
+ </template>
81
+ </UInput>
82
+ </UForm>
83
+ </motion.div>
84
+ </AnimatePresence>
85
+ </template>
@@ -0,0 +1,24 @@
1
+ <script setup lang="ts">
2
+ const { model, models, formatModelName, getModelIcon } = useModels()
3
+
4
+ const items = computed(() => models.map(model => ({
5
+ label: formatModelName(model),
6
+ value: model,
7
+ icon: getModelIcon(model)
8
+ })))
9
+ </script>
10
+
11
+ <template>
12
+ <USelectMenu
13
+ v-model="model"
14
+ :items="items"
15
+ size="sm"
16
+ :icon="getModelIcon(model)"
17
+ variant="ghost"
18
+ value-key="value"
19
+ class="hover:bg-default focus:bg-default data-[state=open]:bg-default"
20
+ :ui="{
21
+ trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200'
22
+ }"
23
+ />
24
+ </template>
@@ -0,0 +1,58 @@
1
+ <script setup lang="ts">
2
+ import { ShikiCachedRenderer } from 'shiki-stream/vue'
3
+
4
+ const colorMode = useColorMode()
5
+ const highlighter = await useHighlighter()
6
+ const props = defineProps<{
7
+ /**
8
+ * 要高亮显示的代码内容
9
+ */
10
+ code: string
11
+ /**
12
+ * 代码语言(如 'vue'、'javascript'、'typescript')
13
+ */
14
+ language: string
15
+ /**
16
+ * 自定义 CSS 类名
17
+ */
18
+ class?: string
19
+ /**
20
+ * 代码块元数据
21
+ */
22
+ meta?: string
23
+ }>()
24
+
25
+ const trimmedCode = computed(() => {
26
+ return props.code.trim().replace(/`+$/, '')
27
+ })
28
+
29
+ const lang = computed(() => {
30
+ switch (props.language) {
31
+ case 'vue':
32
+ return 'vue'
33
+ case 'javascript':
34
+ return 'js'
35
+ case 'typescript':
36
+ return 'ts'
37
+ case 'css':
38
+ return 'css'
39
+ default:
40
+ return props.language
41
+ }
42
+ })
43
+ const key = computed(() => {
44
+ return `${lang.value}-${colorMode.value}`
45
+ })
46
+ </script>
47
+
48
+ <template>
49
+ <ProsePre v-bind="props">
50
+ <ShikiCachedRenderer
51
+ :key="key"
52
+ :highlighter="highlighter"
53
+ :code="trimmedCode"
54
+ :lang="lang"
55
+ :theme="colorMode.value === 'dark' ? 'material-theme-palenight' : 'material-theme-lighter'"
56
+ />
57
+ </ProsePre>
58
+ </template>
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ const { isStreaming = false } = defineProps<{
3
+ /**
4
+ * 思考过程的文本内容
5
+ */
6
+ text: string
7
+ /**
8
+ * 是否正在流式接收思考内容
9
+ * @defaultValue false
10
+ */
11
+ isStreaming?: boolean
12
+ }>()
13
+
14
+ const open = ref(false)
15
+ const { ui } = useAppConfig()
16
+
17
+ watch(() => isStreaming, () => {
18
+ open.value = isStreaming
19
+ }, { immediate: true })
20
+
21
+ function cleanMarkdown(text: string): string {
22
+ return text
23
+ .replace(/\*\*(.+?)\*\*/g, '$1') // Remove bold
24
+ .replace(/\*(.+?)\*/g, '$1') // Remove italic
25
+ .replace(/`(.+?)`/g, '$1') // Remove inline code
26
+ .replace(/^#+\s+/gm, '') // Remove headers
27
+ }
28
+ </script>
29
+
30
+ <template>
31
+ <UCollapsible v-model:open="open" class="flex flex-col gap-1 my-5">
32
+ <UButton
33
+ class="p-0 group"
34
+ color="neutral"
35
+ variant="link"
36
+ :trailing-icon="ui.icons.chevronDown"
37
+ :ui="{
38
+ trailingIcon: text.length > 0 ? 'group-data-[state=open]:rotate-180 transition-transform duration-200' : 'hidden'
39
+ }"
40
+ :label="isStreaming ? '思考中...' : '思考过程'"
41
+ />
42
+
43
+ <template #content>
44
+ <div v-for="(value, index) in cleanMarkdown(text).split('\n').filter(Boolean)" :key="index">
45
+ <span class="whitespace-pre-wrap text-sm text-muted font-normal">{{ value }}</span>
46
+ </div>
47
+ </template>
48
+ </UCollapsible>
49
+ </template>
@@ -0,0 +1,245 @@
1
+ <script setup lang="ts">
2
+ import type { DefineComponent } from 'vue'
3
+ import { Chat } from '@ai-sdk/vue'
4
+ import { DefaultChatTransport } from 'ai'
5
+ import AiChatPreStream from './AiChatPreStream.vue'
6
+ import type { FaqCategory } from './AiChatSlideoverFaq.vue'
7
+
8
+ const {
9
+ title = 'AI 助手',
10
+ description = ' ',
11
+ placeholder = '输入你的问题...',
12
+ faqQuestions = []
13
+ } = defineProps<{
14
+ /**
15
+ * 标题栏显示的标题
16
+ * @defaultValue AI 助手
17
+ */
18
+ title?: string
19
+ /**
20
+ * 标题栏显示的描述
21
+ * @defaultValue ' '
22
+ */
23
+ description?: string
24
+ /**
25
+ * 输入框占位符文本
26
+ * @defaultValue 输入你的问题...
27
+ */
28
+ placeholder?: string
29
+ /**
30
+ * 聊天为空时显示的常见问题分类
31
+ * @defaultValue []
32
+ */
33
+ faqQuestions?: FaqCategory[]
34
+ }>()
35
+
36
+ const components = {
37
+ pre: AiChatPreStream as unknown as DefineComponent
38
+ }
39
+
40
+ const { messages, isOpen, pendingMessage, clearPending } = useAIChat()
41
+ const { apiPath } = useRuntimeConfig().public.aiChat
42
+
43
+ const { getToolLabel } = useTools()
44
+ const { model } = useModels()
45
+
46
+ const input = ref('')
47
+
48
+ watch(pendingMessage, (message) => {
49
+ if (message) {
50
+ if (messages.value.length === 0 && chat.messages.length > 0) {
51
+ chat.messages.length = 0
52
+ }
53
+ chat.sendMessage({
54
+ text: message
55
+ })
56
+ clearPending()
57
+ }
58
+ }, { immediate: true })
59
+
60
+ watch(messages, (newMessages) => {
61
+ if (newMessages.length === 0 && chat.messages.length > 0) {
62
+ chat.messages.length = 0
63
+ }
64
+ }, { deep: true })
65
+
66
+ const toast = useToast()
67
+ const lastMessage = computed(() => chat.messages.at(-1))
68
+
69
+ const chat = new Chat({
70
+ messages: messages.value,
71
+ transport: new DefaultChatTransport({
72
+ api: apiPath,
73
+ body: () => ({ model: model.value })
74
+ }),
75
+ onError(error) {
76
+ const { message } = typeof error.message === 'string' && error.message[0] === '{' ? JSON.parse(error.message) : error
77
+ toast.add({
78
+ description: message,
79
+ icon: 'i-lucide-circle-alert',
80
+ color: 'error',
81
+ duration: 0
82
+ })
83
+ },
84
+ onFinish: () => {
85
+ messages.value = chat.messages
86
+ }
87
+ })
88
+
89
+ function handleSubmit(event?: Event) {
90
+ event?.preventDefault()
91
+
92
+ if (!input.value.trim()) {
93
+ return
94
+ }
95
+
96
+ chat.sendMessage({
97
+ text: input.value
98
+ })
99
+
100
+ input.value = ''
101
+ }
102
+
103
+ function askQuestion(question: string) {
104
+ chat.sendMessage({
105
+ text: question
106
+ })
107
+ }
108
+
109
+ function resetChat() {
110
+ chat.stop()
111
+ messages.value = []
112
+ chat.messages.length = 0
113
+ }
114
+
115
+ onMounted(() => {
116
+ if (pendingMessage.value) {
117
+ chat.sendMessage({
118
+ text: pendingMessage.value
119
+ })
120
+ clearPending()
121
+ } else if (chat.lastMessage?.role === 'user') {
122
+ chat.regenerate()
123
+ }
124
+ })
125
+ </script>
126
+
127
+ <template>
128
+ <USlideover
129
+ v-model:open="isOpen"
130
+ :description="description"
131
+ :close="{ size: 'sm' }"
132
+ :ui="{
133
+ body: 'flex p-4!',
134
+ title: 'flex w-100 pr-6',
135
+ overlay: 'bg-default/60 backdrop-blur-sm',
136
+ content: 'w-full sm:max-w-md bg-default/95 backdrop-blur-xl shadow-2xl'
137
+ }"
138
+ >
139
+ <template #title>
140
+ <div class="flex items-center gap-2 flex-1">
141
+ <UBadge icon="i-lucide-sparkles" variant="soft" square />
142
+ <span class="font-medium text-highlighted">{{ title }}</span>
143
+ </div>
144
+
145
+ <div class="flex items-center gap-2">
146
+ <UTooltip v-if="chat.messages.length > 0" text="清空聊天">
147
+ <UButton
148
+ icon="i-lucide-trash-2"
149
+ color="neutral"
150
+ variant="ghost"
151
+ size="sm"
152
+ @click="resetChat"
153
+ />
154
+ </UTooltip>
155
+ </div>
156
+ </template>
157
+
158
+ <template #body>
159
+ <UChatPalette class="flex-1" :ui="{ prompt: 'border-0 px-2.5' }">
160
+ <UChatMessages
161
+ v-if="chat.messages.length > 0"
162
+ should-auto-scroll
163
+ :messages="chat.messages"
164
+ compact
165
+ :status="chat.status"
166
+ :user="{ ui: { container: 'pb-2', content: 'text-sm' } }"
167
+ :ui="{ indicator: '*:bg-accented' }"
168
+ >
169
+ <template #content="{ message }">
170
+ <div class="flex flex-col gap-2">
171
+ <template
172
+ v-for="(part, index) in message.parts"
173
+ :key="`${message.id}-${part.type}-${index}${'state' in part ? `-${part.state}` : ''}`"
174
+ >
175
+ <AiChatReasoning v-if="part.type === 'reasoning'" :text="part.text" :is-streaming="part.state !== 'done'" />
176
+ <MDCCached
177
+ v-else-if="part.type === 'text'"
178
+ :value="part.text"
179
+ :cache-key="`${message.id}-${index}`"
180
+ :components="components"
181
+ :parser-options="{ highlight: false }"
182
+ class="*:first:mt-0 *:last:mb-0"
183
+ />
184
+ <template v-else-if="part.type === 'data-tool-calls'">
185
+ <AiChatToolCall
186
+ v-for="tool in (part as any).data.tools"
187
+ :key="tool.toolCallId"
188
+ :text="getToolLabel(tool.toolName, tool.input)"
189
+ :is-loading="false"
190
+ />
191
+ </template>
192
+ </template>
193
+ <UButton
194
+ v-if="chat.status === 'streaming' && message.id === lastMessage?.id"
195
+ class="px-0"
196
+ color="neutral"
197
+ variant="link"
198
+ size="sm"
199
+ label="Loading..."
200
+ loading
201
+ loading-icon="i-lucide-loader"
202
+ />
203
+ </div>
204
+ </template>
205
+ </UChatMessages>
206
+ <div v-else class="flex-1 overflow-y-auto px-4 py-4">
207
+ <p class="text-sm font-medium text-muted mb-4">
208
+ FAQ 建议
209
+ </p>
210
+ <AiChatSlideoverFaq :faq-questions="faqQuestions" @ask-question="askQuestion" />
211
+ </div>
212
+ <template #prompt>
213
+ <UChatPrompt
214
+ v-model="input"
215
+ :error="chat.error"
216
+ :placeholder="placeholder"
217
+ variant="subtle"
218
+ class="[view-transition-name:chat-prompt]"
219
+ :ui="{ base: 'px-1.5 text-sm' }"
220
+ @submit="handleSubmit"
221
+ >
222
+ <template #footer>
223
+ <div class="flex items-center gap-1">
224
+ <AiChatModelSelect v-model="model" />
225
+ <div class="flex gap-1 justify-between items-center px-1 text-xs text-dimmed">
226
+ 换行
227
+ <UKbd value="shift" />
228
+ <UKbd value="enter" />
229
+ </div>
230
+ </div>
231
+
232
+ <UChatPromptSubmit
233
+ :status="chat.status"
234
+ color="neutral"
235
+ size="sm"
236
+ @stop="chat.stop()"
237
+ @reload="chat.regenerate()"
238
+ />
239
+ </template>
240
+ </UChatPrompt>
241
+ </template>
242
+ </UChatPalette>
243
+ </template>
244
+ </USlideover>
245
+ </template>
@@ -0,0 +1,41 @@
1
+ <script lang="ts" setup>
2
+ export interface FaqCategory {
3
+ category: string
4
+ items: string[]
5
+ }
6
+
7
+ defineProps<{
8
+ /**
9
+ * 聊天为空时显示的常见问题分类
10
+ */
11
+ faqQuestions: FaqCategory[]
12
+ }>()
13
+
14
+ defineEmits<{
15
+ (e: 'ask-question', question: string): void
16
+ }>()
17
+ </script>
18
+
19
+ <template>
20
+ <div class="flex flex-col gap-5">
21
+ <div
22
+ v-for="category in faqQuestions"
23
+ :key="category.category"
24
+ class="flex flex-col gap-1.5"
25
+ >
26
+ <h4 class="text-xs font-medium text-dimmed uppercase tracking-wide">
27
+ {{ category.category }}
28
+ </h4>
29
+ <div class="flex flex-col">
30
+ <button
31
+ v-for="question in category.items"
32
+ :key="question"
33
+ class="text-left text-sm text-muted hover:text-highlighted py-1.5 transition-colors"
34
+ @click="$emit('ask-question', question)"
35
+ >
36
+ {{ question }}
37
+ </button>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </template>
@@ -0,0 +1,31 @@
1
+ <script setup lang="ts">
2
+ import { motion } from 'motion-v'
3
+
4
+ defineProps<{
5
+ /**
6
+ * 工具调用的标签文本
7
+ */
8
+ text: string
9
+ /**
10
+ * 为 true 时显示加载旋转圈
11
+ * @defaultValue false
12
+ */
13
+ isLoading?: boolean
14
+ }>()
15
+ </script>
16
+
17
+ <template>
18
+ <motion.div
19
+ :initial="{ opacity: 0, y: 4 }"
20
+ :animate="{ opacity: 1, y: 0 }"
21
+ :transition="{ duration: 0.2 }"
22
+ class="flex items-center gap-2 px-2.5 py-1.5 rounded-lg bg-elevated/50 border border-default text-xs text-muted"
23
+ >
24
+ <UIcon
25
+ :name="isLoading ? 'i-lucide-loader-circle' : 'i-lucide-file-text'"
26
+ :class="[isLoading && 'animate-spin']"
27
+ class="size-4 shrink-0 text-muted"
28
+ />
29
+ <span class="truncate">{{ text }}</span>
30
+ </motion.div>
31
+ </template>