docus 5.4.4 → 5.5.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.
- package/app/app.vue +16 -6
- package/app/components/app/AppHeader.vue +5 -0
- package/app/components/docs/DocsAsideRightBottom.vue +28 -1
- package/app/pages/[[lang]]/[...slug].vue +9 -2
- package/app/plugins/i18n.ts +28 -11
- package/app/types/index.d.ts +49 -0
- package/i18n/locales/en.json +27 -0
- package/i18n/locales/fr.json +28 -1
- package/modules/assistant/README.md +213 -0
- package/modules/assistant/index.ts +100 -0
- package/modules/assistant/runtime/components/AssistantChat.vue +21 -0
- package/modules/assistant/runtime/components/AssistantChatDisabled.vue +3 -0
- package/modules/assistant/runtime/components/AssistantFloatingInput.vue +105 -0
- package/modules/assistant/runtime/components/AssistantLoading.vue +164 -0
- package/modules/assistant/runtime/components/AssistantMatrix.vue +92 -0
- package/modules/assistant/runtime/components/AssistantPanel.vue +329 -0
- package/modules/assistant/runtime/components/AssistantPreStream.vue +46 -0
- package/modules/assistant/runtime/composables/useAssistant.ts +107 -0
- package/modules/assistant/runtime/composables/useHighlighter.ts +34 -0
- package/modules/assistant/runtime/server/api/search.ts +111 -0
- package/modules/assistant/runtime/types.ts +7 -0
- package/modules/config.ts +5 -3
- package/modules/css.ts +3 -1
- package/modules/markdown-rewrite.ts +130 -0
- package/nuxt.config.ts +13 -1
- package/nuxt.schema.ts +63 -0
- package/package.json +18 -9
- package/server/routes/sitemap.xml.ts +74 -0
- package/utils/meta.ts +9 -3
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { AnimatePresence, motion } from 'motion-v'
|
|
3
|
+
import { useDocusI18n } from '../../../../app/composables/useDocusI18n'
|
|
4
|
+
|
|
5
|
+
const route = useRoute()
|
|
6
|
+
const appConfig = useAppConfig()
|
|
7
|
+
const { open, isOpen } = useAssistant()
|
|
8
|
+
const { t } = useDocusI18n()
|
|
9
|
+
const input = ref('')
|
|
10
|
+
const isVisible = ref(true)
|
|
11
|
+
const inputRef = ref<{ inputRef: HTMLInputElement } | null>(null)
|
|
12
|
+
|
|
13
|
+
const isDocsRoute = computed(() => route.meta.layout === 'docs')
|
|
14
|
+
const isFloatingInputEnabled = computed(() => appConfig.assistant?.floatingInput !== false)
|
|
15
|
+
const focusInputShortcut = computed(() => appConfig.assistant?.shortcuts?.focusInput || 'meta_i')
|
|
16
|
+
const placeholder = computed(() => t('assistant.placeholder'))
|
|
17
|
+
|
|
18
|
+
const shortcutDisplayKeys = computed(() => {
|
|
19
|
+
const shortcut = focusInputShortcut.value
|
|
20
|
+
const parts = shortcut.split('_')
|
|
21
|
+
return parts.map(part => part === 'meta' ? 'meta' : part.toUpperCase())
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
function handleSubmit() {
|
|
25
|
+
if (!input.value.trim()) return
|
|
26
|
+
|
|
27
|
+
const message = input.value
|
|
28
|
+
isVisible.value = false
|
|
29
|
+
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
open(message, true)
|
|
32
|
+
input.value = ''
|
|
33
|
+
isVisible.value = true
|
|
34
|
+
}, 200)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const shortcuts = computed(() => ({
|
|
38
|
+
[focusInputShortcut.value]: {
|
|
39
|
+
usingInput: true,
|
|
40
|
+
handler: () => {
|
|
41
|
+
if (!isDocsRoute.value || !isFloatingInputEnabled.value) return
|
|
42
|
+
inputRef.value?.inputRef?.focus()
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
escape: {
|
|
46
|
+
usingInput: true,
|
|
47
|
+
handler: () => {
|
|
48
|
+
inputRef.value?.inputRef?.blur()
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
}))
|
|
52
|
+
|
|
53
|
+
defineShortcuts(shortcuts)
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<template>
|
|
57
|
+
<AnimatePresence>
|
|
58
|
+
<motion.div
|
|
59
|
+
v-if="isFloatingInputEnabled && isDocsRoute && isVisible && !isOpen"
|
|
60
|
+
key="floating-input"
|
|
61
|
+
:initial="{ y: 20, opacity: 0 }"
|
|
62
|
+
:animate="{ y: 0, opacity: 1 }"
|
|
63
|
+
:exit="{ y: 100, opacity: 0 }"
|
|
64
|
+
:transition="{ duration: 0.2, ease: 'easeOut' }"
|
|
65
|
+
class="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 px-4"
|
|
66
|
+
style="will-change: transform"
|
|
67
|
+
>
|
|
68
|
+
<form @submit.prevent="handleSubmit">
|
|
69
|
+
<UInput
|
|
70
|
+
ref="inputRef"
|
|
71
|
+
v-model="input"
|
|
72
|
+
:placeholder="placeholder"
|
|
73
|
+
size="lg"
|
|
74
|
+
maxlength="1000"
|
|
75
|
+
:ui="{
|
|
76
|
+
root: 'group w-72 focus-within:w-96 transition-all duration-300 ease-out hover:scale-105 focus-within:scale-105',
|
|
77
|
+
base: 'bg-default shadow-lg rounded-xl',
|
|
78
|
+
trailing: 'pe-2',
|
|
79
|
+
}"
|
|
80
|
+
@keydown.enter.exact.prevent="handleSubmit"
|
|
81
|
+
>
|
|
82
|
+
<template #trailing>
|
|
83
|
+
<div class="flex items-center gap-2">
|
|
84
|
+
<div class="hidden sm:flex group-focus-within:hidden items-center gap-1">
|
|
85
|
+
<UKbd
|
|
86
|
+
v-for="key in shortcutDisplayKeys"
|
|
87
|
+
:key="key"
|
|
88
|
+
:value="key"
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<UButton
|
|
93
|
+
type="submit"
|
|
94
|
+
icon="i-lucide-arrow-up"
|
|
95
|
+
color="primary"
|
|
96
|
+
size="xs"
|
|
97
|
+
:disabled="!input.trim()"
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
</template>
|
|
101
|
+
</UInput>
|
|
102
|
+
</form>
|
|
103
|
+
</motion.div>
|
|
104
|
+
</AnimatePresence>
|
|
105
|
+
</template>
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { motion } from 'motion-v'
|
|
3
|
+
import { useDocusI18n } from '../../../../app/composables/useDocusI18n'
|
|
4
|
+
|
|
5
|
+
interface ToolCall {
|
|
6
|
+
toolCallId: string
|
|
7
|
+
toolName: string
|
|
8
|
+
args: Record<string, unknown>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
text?: string
|
|
13
|
+
toolCalls?: ToolCall[]
|
|
14
|
+
isLoading?: boolean
|
|
15
|
+
}>()
|
|
16
|
+
|
|
17
|
+
const { t } = useDocusI18n()
|
|
18
|
+
|
|
19
|
+
const messages = computed(() => [
|
|
20
|
+
t('assistant.loading.searching'),
|
|
21
|
+
t('assistant.loading.reading'),
|
|
22
|
+
t('assistant.loading.analyzing'),
|
|
23
|
+
t('assistant.loading.finding'),
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
const finishedMessage = computed(() => t('assistant.loading.finished'))
|
|
27
|
+
|
|
28
|
+
const currentIndex = ref(0)
|
|
29
|
+
const targetText = computed(() => {
|
|
30
|
+
if (!props.isLoading) {
|
|
31
|
+
return finishedMessage.value
|
|
32
|
+
}
|
|
33
|
+
return props.text || messages.value[currentIndex.value]
|
|
34
|
+
})
|
|
35
|
+
const displayedText = ref(targetText.value)
|
|
36
|
+
|
|
37
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz'
|
|
38
|
+
|
|
39
|
+
function scrambleText(from: string, to: string) {
|
|
40
|
+
const maxLength = Math.max(from.length, to.length)
|
|
41
|
+
let frame = 0
|
|
42
|
+
const totalFrames = 15
|
|
43
|
+
|
|
44
|
+
const animate = () => {
|
|
45
|
+
frame++
|
|
46
|
+
let result = ''
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < maxLength; i++) {
|
|
49
|
+
const progress = frame / totalFrames
|
|
50
|
+
const charProgress = progress * maxLength
|
|
51
|
+
|
|
52
|
+
if (i < charProgress - 2) {
|
|
53
|
+
result += to[i] || ''
|
|
54
|
+
}
|
|
55
|
+
else if (i < charProgress) {
|
|
56
|
+
result += chars[Math.floor(Math.random() * chars.length)]
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
result += from[i] || ''
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
displayedText.value = result
|
|
64
|
+
|
|
65
|
+
if (frame < totalFrames) {
|
|
66
|
+
requestAnimationFrame(animate)
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
displayedText.value = to
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
requestAnimationFrame(animate)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let textInterval: ReturnType<typeof setInterval> | null = null
|
|
77
|
+
|
|
78
|
+
watch(targetText, (newText, oldText) => {
|
|
79
|
+
if (newText !== oldText && newText && oldText) {
|
|
80
|
+
scrambleText(oldText, newText)
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Stop text rotation when loading finishes
|
|
85
|
+
watch(() => props.isLoading, (isLoading) => {
|
|
86
|
+
if (!isLoading && textInterval) {
|
|
87
|
+
clearInterval(textInterval)
|
|
88
|
+
textInterval = null
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
93
|
+
function getToolLabel(toolName: string, args: any) {
|
|
94
|
+
const path = args?.path || ''
|
|
95
|
+
|
|
96
|
+
if (toolName === 'list-pages') {
|
|
97
|
+
return t('assistant.toolListPages')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (toolName === 'get-page') {
|
|
101
|
+
return `${t('assistant.toolReadPage')} ${path || '...'}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return toolName
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
onMounted(() => {
|
|
108
|
+
// Text rotation only when loading
|
|
109
|
+
if (!props.text && props.isLoading) {
|
|
110
|
+
textInterval = setInterval(() => {
|
|
111
|
+
currentIndex.value = (currentIndex.value + 1) % messages.value.length
|
|
112
|
+
}, 3500)
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
onUnmounted(() => {
|
|
117
|
+
if (textInterval) clearInterval(textInterval)
|
|
118
|
+
})
|
|
119
|
+
</script>
|
|
120
|
+
|
|
121
|
+
<template>
|
|
122
|
+
<div class="flex flex-col gap-2">
|
|
123
|
+
<!-- Main loader with matrix and text -->
|
|
124
|
+
<div class="flex items-center text-xs text-muted overflow-hidden">
|
|
125
|
+
<motion.div
|
|
126
|
+
v-if="isLoading"
|
|
127
|
+
:initial="{ opacity: 1, width: 'auto' }"
|
|
128
|
+
:exit="{ opacity: 0, width: 0 }"
|
|
129
|
+
:transition="{ duration: 0.2 }"
|
|
130
|
+
class="shrink-0 mr-2"
|
|
131
|
+
>
|
|
132
|
+
<AssistantMatrix />
|
|
133
|
+
</motion.div>
|
|
134
|
+
<motion.span
|
|
135
|
+
:animate="{ x: 0 }"
|
|
136
|
+
:transition="{ duration: 0.2 }"
|
|
137
|
+
class="font-mono tracking-tight"
|
|
138
|
+
>
|
|
139
|
+
{{ displayedText }}
|
|
140
|
+
</motion.span>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<!-- Tool calls displayed below -->
|
|
144
|
+
<div
|
|
145
|
+
v-if="toolCalls?.length"
|
|
146
|
+
class="flex flex-col gap-1"
|
|
147
|
+
:class="isLoading ? 'pl-[22px]' : 'pl-0'"
|
|
148
|
+
>
|
|
149
|
+
<motion.div
|
|
150
|
+
v-for="tool in toolCalls"
|
|
151
|
+
:key="`${tool.toolCallId}-${JSON.stringify(tool.args)}`"
|
|
152
|
+
:initial="{ opacity: 0, x: -4 }"
|
|
153
|
+
:animate="{ opacity: 1, x: 0 }"
|
|
154
|
+
:transition="{ duration: 0.15 }"
|
|
155
|
+
class="flex items-center gap-1.5"
|
|
156
|
+
>
|
|
157
|
+
<span class="size-1 rounded-full bg-current opacity-40" />
|
|
158
|
+
<span class="text-[11px] text-dimmed truncate max-w-[200px]">
|
|
159
|
+
{{ getToolLabel(tool.toolName, tool.args) }}
|
|
160
|
+
</span>
|
|
161
|
+
</motion.div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</template>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
size?: number
|
|
4
|
+
dotSize?: number
|
|
5
|
+
gap?: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
9
|
+
size: 4,
|
|
10
|
+
dotSize: 2,
|
|
11
|
+
gap: 2,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const totalDots = computed(() => props.size * props.size)
|
|
15
|
+
const activeDots = ref<Set<number>>(new Set())
|
|
16
|
+
|
|
17
|
+
// Patterns for 4x4 grid (indices 0-15)
|
|
18
|
+
// Grid layout:
|
|
19
|
+
// 0 1 2 3
|
|
20
|
+
// 4 5 6 7
|
|
21
|
+
// 8 9 10 11
|
|
22
|
+
// 12 13 14 15
|
|
23
|
+
const patterns = [
|
|
24
|
+
// Spiral inward
|
|
25
|
+
[[0], [1], [2], [3], [7], [11], [15], [14], [13], [12], [8], [4], [5], [6], [10], [9]],
|
|
26
|
+
// Wave horizontal
|
|
27
|
+
[[0, 4, 8, 12], [1, 5, 9, 13], [2, 6, 10, 14], [3, 7, 11, 15]],
|
|
28
|
+
// Diamond pulse
|
|
29
|
+
[[5, 6, 9, 10], [1, 4, 7, 8, 11, 14], [0, 3, 12, 15], [1, 4, 7, 8, 11, 14], [5, 6, 9, 10]],
|
|
30
|
+
// Loading bar
|
|
31
|
+
[[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11], [12, 13, 14, 15]],
|
|
32
|
+
// Corners rotate
|
|
33
|
+
[[0], [3], [15], [12]],
|
|
34
|
+
// Cross pulse
|
|
35
|
+
[[5, 6, 9, 10], [1, 2, 4, 7, 8, 11, 13, 14], [0, 3, 12, 15]],
|
|
36
|
+
// Snake
|
|
37
|
+
[[0], [1], [2], [3], [7], [6], [5], [4], [8], [9], [10], [11], [15], [14], [13], [12]],
|
|
38
|
+
// Diagonal wave
|
|
39
|
+
[[0], [1, 4], [2, 5, 8], [3, 6, 9, 12], [7, 10, 13], [11, 14], [15]],
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
let patternIndex = 0
|
|
43
|
+
let stepIndex = 0
|
|
44
|
+
let interval: ReturnType<typeof setInterval> | null = null
|
|
45
|
+
|
|
46
|
+
function nextStep() {
|
|
47
|
+
const pattern = patterns[patternIndex]
|
|
48
|
+
if (!pattern) return
|
|
49
|
+
|
|
50
|
+
activeDots.value = new Set(pattern[stepIndex])
|
|
51
|
+
stepIndex++
|
|
52
|
+
|
|
53
|
+
if (stepIndex >= pattern.length) {
|
|
54
|
+
stepIndex = 0
|
|
55
|
+
patternIndex = (patternIndex + 1) % patterns.length
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const gridStyle = computed(() => ({
|
|
60
|
+
display: 'grid',
|
|
61
|
+
gridTemplateColumns: `repeat(${props.size}, 1fr)`,
|
|
62
|
+
gap: `${props.gap}px`,
|
|
63
|
+
width: `${props.size * props.dotSize + (props.size - 1) * props.gap}px`,
|
|
64
|
+
height: `${props.size * props.dotSize + (props.size - 1) * props.gap}px`,
|
|
65
|
+
}))
|
|
66
|
+
|
|
67
|
+
const dotStyle = computed(() => ({
|
|
68
|
+
width: `${props.dotSize}px`,
|
|
69
|
+
height: `${props.dotSize}px`,
|
|
70
|
+
}))
|
|
71
|
+
|
|
72
|
+
onMounted(() => {
|
|
73
|
+
interval = setInterval(nextStep, 120)
|
|
74
|
+
nextStep()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
onUnmounted(() => {
|
|
78
|
+
if (interval) clearInterval(interval)
|
|
79
|
+
})
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<template>
|
|
83
|
+
<div :style="gridStyle">
|
|
84
|
+
<span
|
|
85
|
+
v-for="i in totalDots"
|
|
86
|
+
:key="i"
|
|
87
|
+
class="rounded-[0.5px] bg-current transition-opacity duration-100"
|
|
88
|
+
:class="activeDots.has(i - 1) ? 'opacity-100' : 'opacity-20'"
|
|
89
|
+
:style="dotStyle"
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
</template>
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { defineAsyncComponent } 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 { useDocusI18n } from '../../../../app/composables/useDocusI18n'
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
const components: Record<string, any> = {
|
|
11
|
+
pre: defineAsyncComponent(() => import('./AssistantPreStream.vue')),
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const [DefineChatContent, ReuseChatContent] = createReusableTemplate<{ showExpandButton?: boolean }>()
|
|
15
|
+
|
|
16
|
+
const { isOpen, isExpanded, isMobile, panelWidth, toggleExpanded, messages, pendingMessage, clearPending, faqQuestions } = useAssistant()
|
|
17
|
+
const config = useRuntimeConfig()
|
|
18
|
+
const toast = useToast()
|
|
19
|
+
const { t } = useDocusI18n()
|
|
20
|
+
const input = ref('')
|
|
21
|
+
|
|
22
|
+
const displayTitle = computed(() => t('assistant.title'))
|
|
23
|
+
const displayPlaceholder = computed(() => t('assistant.placeholder'))
|
|
24
|
+
|
|
25
|
+
const chat = new Chat({
|
|
26
|
+
messages: messages.value,
|
|
27
|
+
transport: new DefaultChatTransport({
|
|
28
|
+
api: config.public.assistant.apiPath,
|
|
29
|
+
}),
|
|
30
|
+
onError: (error: Error) => {
|
|
31
|
+
const message = (() => {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(error.message)
|
|
34
|
+
return parsed?.message || error.message
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return error.message
|
|
38
|
+
}
|
|
39
|
+
})()
|
|
40
|
+
|
|
41
|
+
toast.add({
|
|
42
|
+
description: message,
|
|
43
|
+
icon: 'i-lucide-alert-circle',
|
|
44
|
+
color: 'error',
|
|
45
|
+
duration: 0,
|
|
46
|
+
})
|
|
47
|
+
},
|
|
48
|
+
onFinish: () => {
|
|
49
|
+
messages.value = chat.messages
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
watch(pendingMessage, (message: string | undefined) => {
|
|
54
|
+
if (message) {
|
|
55
|
+
if (messages.value.length === 0 && chat.messages.length > 0) {
|
|
56
|
+
chat.messages.length = 0
|
|
57
|
+
}
|
|
58
|
+
chat.sendMessage({
|
|
59
|
+
text: message,
|
|
60
|
+
})
|
|
61
|
+
clearPending()
|
|
62
|
+
}
|
|
63
|
+
}, { immediate: true })
|
|
64
|
+
|
|
65
|
+
watch(messages, (newMessages: UIMessage[]) => {
|
|
66
|
+
if (newMessages.length === 0 && chat.messages.length > 0) {
|
|
67
|
+
chat.messages.length = 0
|
|
68
|
+
}
|
|
69
|
+
}, { deep: true })
|
|
70
|
+
|
|
71
|
+
const lastMessage = computed(() => chat.messages.at(-1))
|
|
72
|
+
const showThinking = computed(() =>
|
|
73
|
+
chat.status === 'streaming'
|
|
74
|
+
&& lastMessage.value?.role === 'assistant'
|
|
75
|
+
&& !lastMessage.value?.parts?.some((p: { type: string }) => p.type === 'text'),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
79
|
+
function getMessageToolCalls(message: any) {
|
|
80
|
+
if (!message?.parts) return []
|
|
81
|
+
return message.parts
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
83
|
+
.filter((p: any) => p.type === 'data-tool-calls')
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
85
|
+
.flatMap((p: any) => p.data?.tools || [])
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function handleSubmit(event?: Event) {
|
|
89
|
+
event?.preventDefault()
|
|
90
|
+
|
|
91
|
+
if (!input.value.trim()) {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
chat.sendMessage({
|
|
96
|
+
text: input.value,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
input.value = ''
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function askQuestion(question: string) {
|
|
103
|
+
chat.sendMessage({
|
|
104
|
+
text: question,
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resetChat() {
|
|
109
|
+
chat.stop()
|
|
110
|
+
messages.value = []
|
|
111
|
+
chat.messages.length = 0
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
onMounted(() => {
|
|
115
|
+
if (pendingMessage.value) {
|
|
116
|
+
chat.sendMessage({
|
|
117
|
+
text: pendingMessage.value,
|
|
118
|
+
})
|
|
119
|
+
clearPending()
|
|
120
|
+
}
|
|
121
|
+
else if (chat.lastMessage?.role === 'user') {
|
|
122
|
+
chat.regenerate()
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
</script>
|
|
126
|
+
|
|
127
|
+
<template>
|
|
128
|
+
<DefineChatContent v-slot="{ showExpandButton = true }">
|
|
129
|
+
<div class="flex h-full flex-col">
|
|
130
|
+
<div class="flex h-16 shrink-0 items-center justify-between border-b border-default px-4">
|
|
131
|
+
<span class="font-medium text-highlighted">{{ displayTitle }}</span>
|
|
132
|
+
<div class="flex items-center gap-1">
|
|
133
|
+
<UTooltip
|
|
134
|
+
v-if="showExpandButton"
|
|
135
|
+
:text="isExpanded ? t('assistant.collapse') : t('assistant.expand')"
|
|
136
|
+
>
|
|
137
|
+
<UButton
|
|
138
|
+
:icon="isExpanded ? 'i-lucide-minimize-2' : 'i-lucide-maximize-2'"
|
|
139
|
+
color="neutral"
|
|
140
|
+
variant="ghost"
|
|
141
|
+
size="sm"
|
|
142
|
+
class="text-muted hover:text-highlighted"
|
|
143
|
+
@click="toggleExpanded"
|
|
144
|
+
/>
|
|
145
|
+
</UTooltip>
|
|
146
|
+
<UTooltip
|
|
147
|
+
v-if="chat.messages.length > 0"
|
|
148
|
+
:text="t('assistant.clearChat')"
|
|
149
|
+
>
|
|
150
|
+
<UButton
|
|
151
|
+
icon="i-lucide-trash-2"
|
|
152
|
+
color="neutral"
|
|
153
|
+
variant="ghost"
|
|
154
|
+
size="sm"
|
|
155
|
+
class="text-muted hover:text-highlighted"
|
|
156
|
+
@click="resetChat"
|
|
157
|
+
/>
|
|
158
|
+
</UTooltip>
|
|
159
|
+
<UTooltip :text="t('assistant.close')">
|
|
160
|
+
<UButton
|
|
161
|
+
icon="i-lucide-x"
|
|
162
|
+
color="neutral"
|
|
163
|
+
variant="ghost"
|
|
164
|
+
size="sm"
|
|
165
|
+
class="text-muted hover:text-highlighted"
|
|
166
|
+
@click="isOpen = false"
|
|
167
|
+
/>
|
|
168
|
+
</UTooltip>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div class="min-h-0 flex-1 overflow-y-auto">
|
|
173
|
+
<UChatMessages
|
|
174
|
+
v-if="chat.messages.length > 0"
|
|
175
|
+
:messages="chat.messages"
|
|
176
|
+
compact
|
|
177
|
+
:status="chat.status"
|
|
178
|
+
:user="{ ui: { content: 'text-sm' } }"
|
|
179
|
+
:ui="{ indicator: '*:bg-accented', root: 'h-auto!' }"
|
|
180
|
+
class="px-4 py-4"
|
|
181
|
+
>
|
|
182
|
+
<template #content="{ message }">
|
|
183
|
+
<div class="flex flex-col gap-2">
|
|
184
|
+
<AssistantLoading
|
|
185
|
+
v-if="message.role === 'assistant' && (getMessageToolCalls(message).length > 0 || (showThinking && message.id === lastMessage?.id))"
|
|
186
|
+
:tool-calls="getMessageToolCalls(message)"
|
|
187
|
+
:is-loading="showThinking && message.id === lastMessage?.id"
|
|
188
|
+
/>
|
|
189
|
+
<template
|
|
190
|
+
v-for="(part, index) in message.parts"
|
|
191
|
+
:key="`${message.id}-${part.type}-${index}${'state' in part ? `-${part.state}` : ''}`"
|
|
192
|
+
>
|
|
193
|
+
<MDCCached
|
|
194
|
+
v-if="part.type === 'text' && part.text"
|
|
195
|
+
:value="part.text"
|
|
196
|
+
:cache-key="`${message.id}-${index}`"
|
|
197
|
+
:components="components"
|
|
198
|
+
:parser-options="{ highlight: false }"
|
|
199
|
+
class="*:first:mt-0 *:last:mb-0"
|
|
200
|
+
/>
|
|
201
|
+
</template>
|
|
202
|
+
</div>
|
|
203
|
+
</template>
|
|
204
|
+
</UChatMessages>
|
|
205
|
+
|
|
206
|
+
<div
|
|
207
|
+
v-else
|
|
208
|
+
class="p-4"
|
|
209
|
+
>
|
|
210
|
+
<div
|
|
211
|
+
v-if="!faqQuestions?.length"
|
|
212
|
+
class="flex h-full flex-col items-center justify-center py-12 text-center"
|
|
213
|
+
>
|
|
214
|
+
<div class="mb-4 flex size-12 items-center justify-center rounded-full bg-primary/10">
|
|
215
|
+
<UIcon
|
|
216
|
+
name="i-lucide-message-circle-question"
|
|
217
|
+
class="size-6 text-primary"
|
|
218
|
+
/>
|
|
219
|
+
</div>
|
|
220
|
+
<h3 class="mb-2 text-base font-medium text-highlighted">
|
|
221
|
+
{{ t('assistant.askMeAnything') }}
|
|
222
|
+
</h3>
|
|
223
|
+
<p class="max-w-xs text-sm text-muted">
|
|
224
|
+
{{ t('assistant.askMeAnythingDescription') }}
|
|
225
|
+
</p>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<template v-else>
|
|
229
|
+
<p class="mb-4 text-sm font-medium text-muted">
|
|
230
|
+
{{ t('assistant.faq') }}
|
|
231
|
+
</p>
|
|
232
|
+
|
|
233
|
+
<div class="flex flex-col gap-5">
|
|
234
|
+
<div
|
|
235
|
+
v-for="category in faqQuestions"
|
|
236
|
+
:key="category.category"
|
|
237
|
+
class="flex flex-col gap-1.5"
|
|
238
|
+
>
|
|
239
|
+
<h4 class="text-xs font-medium uppercase tracking-wide text-dimmed">
|
|
240
|
+
{{ category.category }}
|
|
241
|
+
</h4>
|
|
242
|
+
<div class="flex flex-col">
|
|
243
|
+
<button
|
|
244
|
+
v-for="question in category.items"
|
|
245
|
+
:key="question"
|
|
246
|
+
class="py-1.5 text-left text-sm text-muted transition-colors hover:text-highlighted"
|
|
247
|
+
@click="askQuestion(question)"
|
|
248
|
+
>
|
|
249
|
+
{{ question }}
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</template>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<div class="w-full shrink-0 p-3">
|
|
259
|
+
<UChatPrompt
|
|
260
|
+
v-model="input"
|
|
261
|
+
:rows="2"
|
|
262
|
+
:placeholder="displayPlaceholder"
|
|
263
|
+
maxlength="1000"
|
|
264
|
+
:ui="{
|
|
265
|
+
root: 'shadow-none!',
|
|
266
|
+
body: '*:p-0! *:rounded-none! *:text-sm!',
|
|
267
|
+
}"
|
|
268
|
+
@submit="handleSubmit"
|
|
269
|
+
>
|
|
270
|
+
<template #footer>
|
|
271
|
+
<div class="flex items-center gap-1 text-xs text-muted">
|
|
272
|
+
<span>{{ t('assistant.lineBreak') }}</span>
|
|
273
|
+
<UKbd
|
|
274
|
+
size="sm"
|
|
275
|
+
value="shift"
|
|
276
|
+
/>
|
|
277
|
+
<UKbd
|
|
278
|
+
size="sm"
|
|
279
|
+
value="enter"
|
|
280
|
+
/>
|
|
281
|
+
</div>
|
|
282
|
+
<UChatPromptSubmit
|
|
283
|
+
class="ml-auto"
|
|
284
|
+
size="xs"
|
|
285
|
+
:status="chat.status"
|
|
286
|
+
@stop="chat.stop()"
|
|
287
|
+
@reload="chat.regenerate()"
|
|
288
|
+
/>
|
|
289
|
+
</template>
|
|
290
|
+
</UChatPrompt>
|
|
291
|
+
<div class="mt-1 flex text-xs text-dimmed items-center justify-between">
|
|
292
|
+
<span>{{ t('assistant.chatCleared') }}</span>
|
|
293
|
+
<span>
|
|
294
|
+
{{ input.length }}/1000
|
|
295
|
+
</span>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
</DefineChatContent>
|
|
300
|
+
|
|
301
|
+
<aside
|
|
302
|
+
v-if="!isMobile"
|
|
303
|
+
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]"
|
|
304
|
+
:style="{
|
|
305
|
+
width: `${panelWidth}px`,
|
|
306
|
+
right: isOpen ? '0' : `-${panelWidth}px`,
|
|
307
|
+
}"
|
|
308
|
+
>
|
|
309
|
+
<div
|
|
310
|
+
class="h-full transition-[width] duration-200 ease-linear"
|
|
311
|
+
:style="{ width: `${panelWidth}px` }"
|
|
312
|
+
>
|
|
313
|
+
<ReuseChatContent :show-expand-button="true" />
|
|
314
|
+
</div>
|
|
315
|
+
</aside>
|
|
316
|
+
|
|
317
|
+
<USlideover
|
|
318
|
+
v-else
|
|
319
|
+
v-model:open="isOpen"
|
|
320
|
+
side="right"
|
|
321
|
+
:ui="{
|
|
322
|
+
content: 'ring-0 bg-default',
|
|
323
|
+
}"
|
|
324
|
+
>
|
|
325
|
+
<template #content>
|
|
326
|
+
<ReuseChatContent :show-expand-button="false" />
|
|
327
|
+
</template>
|
|
328
|
+
</USlideover>
|
|
329
|
+
</template>
|