main
vue 199 lines 5 KB
Raw
1 <template>
2 <div class="flex flex-col overflow-hidden">
3 <div class="relative grow overflow-hidden">
4 <n-scrollbar ref="scrollbar">
5 <div v-if="list.length" class="flex flex-col gap-6 p-4 pb-50">
6 <ChatBubbleBlock
7 v-for="item of list"
8 :key="item.id"
9 class="animate-fade"
10 :entity="item"
11 @update="scrollChat()"
12 @edit="editMessage(item)"
13 />
14 <div v-if="loading" class="animate-fade">
15 <Icon name="svg-spinners:pulse-rings-3" :size="20" />
16 </div>
17 </div>
18 <div v-else class="text-secondary flex flex-col items-center justify-center py-12 text-center">
19 <p class="text-lg">
20 Your chat is empty.
21 <br />
22 Ask your first question!
23 </p>
24 </div>
25 </n-scrollbar>
26 <CollapseKeepAlive :show="!input && options.showQuestions && !!server" class="absolute! bottom-0">
27 <ChatQuestions v-if="server" :server @select="input = $event" />
28 </CollapseKeepAlive>
29 </div>
30 <div class="flex flex-col">
31 <ChatQuery
32 v-model:input="input"
33 :edit-message-body="editMessageContext?.body || null"
34 :loading
35 @cancel-edit="handleCancelEdit()"
36 @update-options="options = $event"
37 @message="sendQuery"
38 @select-server="server = $event"
39 @server-loaded="serverLoadedHandler()"
40 @stop="stopQuery()"
41 />
42 </div>
43 </div>
44 </template>
45
46 <script setup lang="ts">
47 import type { ScrollbarInst } from "naive-ui"
48 import type { ChatBubble } from "./ChatBubble.vue"
49 import type { Message } from "./ChatQuery.vue"
50 import { useStorage } from "@vueuse/core"
51 import axios from "axios"
52 import { NScrollbar, useMessage } from "naive-ui"
53 import { nanoid } from "nanoid"
54 import { nextTick, onBeforeMount, onMounted, ref } from "vue"
55 import Api from "@/api"
56 import CollapseKeepAlive from "@/components/common/CollapseKeepAlive.vue"
57 import Icon from "@/components/common/Icon.vue"
58 import { secureLocalStorage } from "@/utils/secure-storage"
59 import ChatBubbleBlock from "./ChatBubble.vue"
60 import ChatQuery from "./ChatQuery.vue"
61 import ChatQuestions from "./ChatQuestions.vue"
62
63 const emit = defineEmits<{
64 (e: "mounted", value: { clearHistory: () => void }): void
65 }>()
66
67 const message = useMessage()
68
69 const list = useStorage<ChatBubble[]>("ai-chatbot-list-messages", [], secureLocalStorage({ session: true }))
70 const loading = ref(false)
71 const server = ref<string | null>(null)
72 const input = ref<string | null>(null)
73 const options = ref<{ verbose: boolean; showQuestions: boolean }>({ verbose: false, showQuestions: false })
74 const scrollbar = ref<ScrollbarInst | null>(null)
75 const editMessageContext = ref<ChatBubble | null>(null)
76
77 let abortController: AbortController | null = null
78
79 function scrollChat() {
80 nextTick(() => {
81 scrollbar.value?.scrollTo({ top: 99999999999999, behavior: "smooth" })
82 })
83 }
84
85 function serverLoadedHandler() {
86 setTimeout(() => {
87 scrollChat()
88 }, 500)
89 }
90
91 function setAllOld() {
92 list.value.forEach(o => (o.new = false))
93 }
94
95 function addBubble(payload: Omit<ChatBubble, "datetime" | "id">) {
96 setAllOld()
97 list.value.push({ ...payload, datetime: new Date(), id: nanoid(), new: true })
98 scrollChat()
99 }
100
101 function editMessage(item: ChatBubble) {
102 editMessageContext.value = item
103 input.value = item.body
104 }
105
106 function handleCancelEdit() {
107 editMessageContext.value = null
108 }
109
110 function stopQuery() {
111 abortController?.abort()
112 }
113
114 function updateChatFromEdit(id: string) {
115 const index = list.value.findIndex(item => item.id === id)
116 if (index !== -1) {
117 list.value.splice(index)
118 }
119 }
120
121 function sendQuery(payload: Message) {
122 if (editMessageContext.value) {
123 updateChatFromEdit(editMessageContext.value.id)
124 editMessageContext.value = null
125 }
126
127 addBubble({
128 body: payload.input,
129 server: payload.server,
130 sender: "user"
131 })
132
133 loading.value = true
134
135 abortController = new AbortController()
136
137 Api.copilotMCP
138 .query(
139 {
140 input: payload.input,
141 mcp_server: payload.server,
142 verbose: payload.verbose
143 },
144 abortController.signal
145 )
146 .then(res => {
147 if (res.data.success) {
148 let body = `${res.data.result}`
149
150 if (typeof res.data.result !== "string" && "response" in res.data.result) {
151 body = res.data.result.response
152 }
153 if (res.data.structured_result?.response) {
154 body = res.data.structured_result.response
155 }
156
157 let thought
158
159 if (typeof res.data.result !== "string" && "thinking_process" in res.data.result) {
160 thought = res.data.result.thinking_process
161 }
162 if (res.data.structured_result?.thinking_process) {
163 thought = res.data.structured_result.thinking_process
164 }
165
166 addBubble({
167 body,
168 thought,
169 server: payload.server,
170 sender: "server"
171 })
172 } else {
173 message.warning(res.data?.message || "An error occurred. Please try again later.")
174 }
175 })
176 .catch(err => {
177 if (!axios.isCancel(err)) {
178 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
179 }
180 })
181 .finally(() => {
182 loading.value = false
183 })
184 }
185
186 function clearHistory() {
187 list.value = []
188 }
189
190 onBeforeMount(() => {
191 setAllOld()
192 })
193
194 onMounted(() => {
195 scrollChat()
196
197 emit("mounted", { clearHistory })
198 })
199 </script>