| 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> |