@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.
- package/app/app.config.ts +37 -0
- package/app/app.vue +8 -3
- package/app/components/DocsAsideRightBottom.vue +17 -22
- package/app/components/PageHeaderLinks.vue +6 -1
- package/app/components/content/PageLastCommit.vue +5 -5
- package/app/components/content/StarsBg.vue +119 -0
- package/app/components/header/Header.vue +1 -1
- package/app/components/header/HeaderBody.vue +12 -2
- package/app/components/header/HeaderBottom.vue +1 -0
- package/app/components/header/HeaderCTA.vue +2 -2
- package/app/components/header/HeaderCenter.vue +1 -1
- package/app/components/header/HeaderLogo.vue +1 -1
- package/app/layouts/default.vue +3 -1
- package/app/layouts/docs.vue +1 -1
- package/app/pages/docs/[...slug].vue +3 -2
- package/app/templates/releases.vue +98 -0
- package/app/types/index.d.ts +149 -0
- package/content.config.ts +24 -2
- package/modules/ai-chat/index.ts +53 -21
- package/modules/ai-chat/runtime/components/AiChat.vue +4 -10
- package/modules/ai-chat/runtime/components/AiChatDisabled.vue +3 -0
- package/modules/ai-chat/runtime/components/AiChatFloatingInput.vue +24 -9
- package/modules/ai-chat/runtime/components/AiChatModelSelect.vue +2 -0
- package/modules/ai-chat/runtime/components/AiChatPanel.vue +318 -0
- package/modules/ai-chat/runtime/components/AiChatPreStream.vue +1 -0
- package/modules/ai-chat/runtime/components/AiChatReasoning.vue +3 -3
- package/modules/ai-chat/runtime/components/AiChatSlideoverFaq.vue +2 -5
- package/modules/ai-chat/runtime/composables/useAIChat.ts +48 -0
- package/modules/ai-chat/runtime/composables/useModels.ts +3 -6
- package/modules/ai-chat/runtime/server/api/ai-chat.ts +40 -32
- package/modules/ai-chat/runtime/server/utils/docs_agent.ts +23 -15
- package/modules/ai-chat/runtime/types.ts +6 -0
- package/modules/css.ts +3 -2
- package/modules/routing.ts +26 -0
- package/nuxt.config.ts +2 -0
- package/nuxt.schema.ts +493 -0
- package/package.json +11 -9
- package/app/composables/useFaq.ts +0 -21
- package/modules/ai-chat/runtime/components/AiChatSlideover.vue +0 -255
- /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
|
})
|
package/modules/ai-chat/index.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
-
import {
|
|
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
|
-
|
|
54
|
+
enabled: hasApiKey,
|
|
52
55
|
apiPath: options.apiPath!,
|
|
53
56
|
model: options.model!,
|
|
54
57
|
models: options.models!
|
|
55
58
|
}
|
|
56
|
-
|
|
57
|
-
|
|
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 (!
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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="
|
|
7
|
+
<UTooltip :text="aiChat.texts.trigger">
|
|
15
8
|
<UButton
|
|
16
|
-
icon="
|
|
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>
|
|
@@ -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
|
-
|
|
24
|
-
|
|
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="
|
|
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>
|
|
@@ -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>
|
|
@@ -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
|
-
|
|
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-
|
|
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">
|