|
|
@@ -1,352 +0,0 @@
|
|
|
-<template>
|
|
|
- <a-drawer :visible="props.visible" class="drawer" width="570px" @cancel="closeAI" placement="left" :footer="false">
|
|
|
- <template #title>
|
|
|
- <div class="aiTitle">
|
|
|
- <img alt="AI" src="@/assets/ai.svg" height="25">
|
|
|
- <span>AI助手</span>
|
|
|
- <a-button size="small" class="button" @click="deleteMessages">清空记录</a-button>
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- <div class="container">
|
|
|
- <div class="messages" ref="messagesContainer">
|
|
|
- <a-spin :loading="messagesLoading">
|
|
|
- <div v-for="(item, index) in messages">
|
|
|
- <div class="time"
|
|
|
- v-if="!messages[index - 1] || (messages[index].time - messages[index - 1].time) > 300000">{{
|
|
|
- stramptoTime(item.time) }}</div>
|
|
|
- <div :class="['message', item.type === 'user' ? 'right' : 'left']">
|
|
|
- <a-avatar class="avatar">
|
|
|
- <img alt="avatar" src="@/assets/img/avatar/assistant.png" v-if="item.type !== 'user'" />
|
|
|
- <img alt="avatar" :src="user.avatar" v-else-if="item.type === 'user'" />
|
|
|
- </a-avatar>
|
|
|
- <a-dropdown trigger="contextMenu" alignPoint :style="{ display: 'block', zIndex: 99 }">
|
|
|
- <div :class="['content', item.type === 'user' ? 'user' : 'ai']"
|
|
|
- v-html="renderMarkdown(item.content)">
|
|
|
- </div>
|
|
|
- <template #content>
|
|
|
- <a-doption>
|
|
|
- <icon-play-arrow /> 打开仓库
|
|
|
- </a-doption>
|
|
|
- </template>
|
|
|
- </a-dropdown>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </a-spin>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="inputBox">
|
|
|
- <a-textarea placeholder="您的专属AI助手~" :max-length="300" :auto-size="{
|
|
|
- minRows: 1,
|
|
|
- maxRows: 6
|
|
|
- }" allow-clear v-model="input" />
|
|
|
- <a-button type="primary" :loading="send" @click="sendMessage">发送</a-button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </a-drawer>
|
|
|
-</template>
|
|
|
-
|
|
|
-<script setup>
|
|
|
-import { ref, onMounted, nextTick, watch, onUnmounted } from 'vue'
|
|
|
-import { Message, Modal } from '@arco-design/web-vue'
|
|
|
-import { eventBus } from '@/utils/eventBus'
|
|
|
-import { AIChat, GetAIChatMessages, GetAIChatMessage, DeleteAIChatMessages } from '@/api/ai'
|
|
|
-import { useUserStore } from '@/store/modules/user'
|
|
|
-import MarkdownIt from 'markdown-it'
|
|
|
-
|
|
|
-const md = new MarkdownIt()
|
|
|
-
|
|
|
-const renderMarkdown = (text) => {
|
|
|
- return md.render(text || '')
|
|
|
-}
|
|
|
-
|
|
|
-const props = defineProps({
|
|
|
- visible: {
|
|
|
- type: Boolean,
|
|
|
- default: false
|
|
|
- }
|
|
|
-})
|
|
|
-
|
|
|
-const user = ref('')
|
|
|
-const input = ref('')
|
|
|
-const messagesLoading = ref(false)
|
|
|
-const send = ref(false)
|
|
|
-const messages = ref([])
|
|
|
-const msgid = ref('')
|
|
|
-const newIndex = ref()
|
|
|
-
|
|
|
-const messagesContainer = ref(null)
|
|
|
-
|
|
|
-const scrollToBottom = () => {
|
|
|
- if (messagesContainer.value) {
|
|
|
- // 等 DOM 渲染完成再滚动
|
|
|
- nextTick(() => {
|
|
|
- messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
|
|
- })
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-const getAIChatMessages = async () => {
|
|
|
- try {
|
|
|
- messagesLoading.value = true
|
|
|
- const res = await GetAIChatMessages()
|
|
|
- if (!res || res.code !== 0)
|
|
|
- return Message.error(`获取历史对话消息失败!${res?.msg ?? ''}`)
|
|
|
- messages.value = res.data
|
|
|
- messages.value.push({
|
|
|
- type: 'system', time: new Date().getTime(), content: '你好!我是你的专属AI智能助手“小吉”,你可以问我任何问题哦~~\n试着问问:\n- 我拥有哪些Git仓库?\n- 仓库GitNexus最近一次提交的信息是什么?\n- 请对比electron仓库的最后两次提交。'
|
|
|
- })
|
|
|
- } catch (error) {
|
|
|
- Message.error(`获取历史对话消息失败!`)
|
|
|
- } finally {
|
|
|
- scrollToBottom()
|
|
|
- messagesLoading.value = false
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-const getAIChatMessage = async () => {
|
|
|
- try {
|
|
|
- const res = await GetAIChatMessage({ id: msgid.value })
|
|
|
- if (!res || res.code !== 0) {
|
|
|
- send.value = false
|
|
|
- return messages.value[newIndex.value].content = '获取对话消息失败,请稍后再试'
|
|
|
- }
|
|
|
-
|
|
|
- if (!res.data)
|
|
|
- return
|
|
|
-
|
|
|
- stopPolling()
|
|
|
- messages.value[newIndex.value].content = ''
|
|
|
- for (let i = 0; i < res.data.length; i++) {
|
|
|
- await new Promise(resolve => setTimeout(resolve, 30))
|
|
|
- messages.value[newIndex.value].content += res.data[i]
|
|
|
- scrollToBottom()
|
|
|
- }
|
|
|
- send.value = false
|
|
|
- } catch (error) {
|
|
|
- Message.error(`获取对话消息失败!`)
|
|
|
- } finally {
|
|
|
- scrollToBottom()
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-let timer = null
|
|
|
-
|
|
|
-// 轮询
|
|
|
-const startPolling = () => {
|
|
|
- if (!timer) {
|
|
|
- timer = setInterval(async () => {
|
|
|
- await getAIChatMessage()
|
|
|
- }, 1000)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 停止轮询
|
|
|
-const stopPolling = () => {
|
|
|
- if (timer) {
|
|
|
- clearInterval(timer)
|
|
|
- timer = null
|
|
|
- }
|
|
|
- send.value = false
|
|
|
-}
|
|
|
-
|
|
|
-const sendMessage = async () => {
|
|
|
- if (input.value === '') return
|
|
|
- send.value = true
|
|
|
- let content = input.value
|
|
|
- input.value = ''
|
|
|
- try {
|
|
|
- messages.value.push({ type: 'user', time: new Date().getTime(), content })
|
|
|
- newIndex.value = messages.value.push({ type: 'ai', time: new Date().getTime(), content: '小吉正在思考哦~请稍候...' }) - 1
|
|
|
- scrollToBottom()
|
|
|
- const res = await AIChat({ message: content })
|
|
|
- if (!res || res.code !== 0) {
|
|
|
- send.value = false
|
|
|
- return messages.value[newIndex.value].content = '获取对话消息失败,请稍后再试'
|
|
|
- }
|
|
|
-
|
|
|
- msgid.value = res.id
|
|
|
- startPolling()
|
|
|
- } catch (error) {
|
|
|
- console.log(error)
|
|
|
- Message.error('获取对话消息失败!请稍后再试')
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-const closeAI = () => {
|
|
|
- eventBus.emit('closeAI')
|
|
|
-}
|
|
|
-
|
|
|
-const getuser = async () => {
|
|
|
- const userStore = useUserStore()
|
|
|
- let userInfo = await userStore.getInfo()
|
|
|
- user.value = userInfo
|
|
|
-}
|
|
|
-
|
|
|
-onMounted(async () => {
|
|
|
- getuser()
|
|
|
- await getAIChatMessages()
|
|
|
-})
|
|
|
-
|
|
|
-// 组件销毁时停止轮询
|
|
|
-onUnmounted(() => {
|
|
|
- stopPolling()
|
|
|
-})
|
|
|
-
|
|
|
-watch(
|
|
|
- () => props.visible,
|
|
|
- (val) => {
|
|
|
- if (val) {
|
|
|
- nextTick(() => {
|
|
|
- scrollToBottom()
|
|
|
- })
|
|
|
- }
|
|
|
- }
|
|
|
-)
|
|
|
-
|
|
|
-const stramptoTime = (time) => {
|
|
|
- return new Date(time).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
|
|
|
-}
|
|
|
-
|
|
|
-const deleteMessages = () => {
|
|
|
- Modal.confirm({
|
|
|
- title: '清空消息记录',
|
|
|
- content: '您即将清空所有消息记录,是否继续?',
|
|
|
- onOk: async () => {
|
|
|
- const res = await DeleteAIChatMessages()
|
|
|
- if (!res || res.code !== 0)
|
|
|
- return Message.error('清空消息记录失败!')
|
|
|
- getAIChatMessages()
|
|
|
- Message.success('清空消息记录成功!')
|
|
|
- },
|
|
|
- onCancel: () => {
|
|
|
-
|
|
|
- }
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-</script>
|
|
|
-
|
|
|
-<style lang="less" scoped>
|
|
|
-.container {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- height: calc(100% + 20px);
|
|
|
- gap: 10px;
|
|
|
- margin-top: -20px;
|
|
|
-
|
|
|
- /* 对于 Webkit 浏览器(Chrome、Safari) */
|
|
|
- .messages::-webkit-scrollbar {
|
|
|
- width: 8px;
|
|
|
- }
|
|
|
-
|
|
|
- .messages::-webkit-scrollbar-track {
|
|
|
- background: transparent;
|
|
|
- }
|
|
|
-
|
|
|
- .messages::-webkit-scrollbar-thumb {
|
|
|
- background-color: rgba(0, 0, 0, 0.2);
|
|
|
- /* 滚动条滑块背景 */
|
|
|
- border-radius: 10px;
|
|
|
- /* 滚动条滑块圆角 */
|
|
|
- }
|
|
|
-
|
|
|
- .messages::-webkit-scrollbar-thumb:hover {
|
|
|
- background-color: rgba(0, 0, 0, 0.3);
|
|
|
- /* 滚动条滑块悬停时的背景 */
|
|
|
- }
|
|
|
-
|
|
|
- .messages {
|
|
|
- flex: 1;
|
|
|
- overflow-y: auto;
|
|
|
- padding: 10px;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
-
|
|
|
- .time {
|
|
|
- color: #888;
|
|
|
- font-size: 0.9em;
|
|
|
- text-align: center;
|
|
|
- margin: 10px 0 0;
|
|
|
- }
|
|
|
-
|
|
|
- .message {
|
|
|
- display: flex;
|
|
|
- margin-top: 10px;
|
|
|
-
|
|
|
- &.right {
|
|
|
- flex-direction: row-reverse;
|
|
|
- justify-content: end;
|
|
|
- }
|
|
|
-
|
|
|
- .avatar {
|
|
|
- user-select: none;
|
|
|
- width: 36px;
|
|
|
- height: 36px;
|
|
|
- background-color: #fff;
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
- /* 对于 Webkit 浏览器(Chrome、Safari) */
|
|
|
- .content::-webkit-scrollbar {
|
|
|
- height: 6px;
|
|
|
- }
|
|
|
-
|
|
|
- .content::-webkit-scrollbar-track {
|
|
|
- background: transparent;
|
|
|
- }
|
|
|
-
|
|
|
- .content::-webkit-scrollbar-thumb {
|
|
|
- background-color: rgba(0, 0, 0, 0.2);
|
|
|
- /* 滚动条滑块背景 */
|
|
|
- border-radius: 10px;
|
|
|
- /* 滚动条滑块圆角 */
|
|
|
- }
|
|
|
-
|
|
|
- .content::-webkit-scrollbar-thumb:hover {
|
|
|
- background-color: rgba(0, 0, 0, 0.3);
|
|
|
- /* 滚动条滑块悬停时的背景 */
|
|
|
- }
|
|
|
-
|
|
|
- .content {
|
|
|
- max-width: 70%;
|
|
|
- padding: 0 14px;
|
|
|
- border-radius: 10px;
|
|
|
- margin: 0 10px;
|
|
|
- font-size: 14px;
|
|
|
- line-height: 1.5;
|
|
|
- word-break: break-word;
|
|
|
- overflow-x: auto;
|
|
|
-
|
|
|
- &.user {
|
|
|
- background-color: #4e88ff;
|
|
|
- color: white;
|
|
|
- }
|
|
|
-
|
|
|
- &.ai {
|
|
|
- background-color: #eee;
|
|
|
- color: #333;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- }
|
|
|
-
|
|
|
- .inputBox {
|
|
|
- display: flex;
|
|
|
- gap: 10px;
|
|
|
- min-height: 15px;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-.aiTitle {
|
|
|
- display: flex;
|
|
|
- font-size: 1.1em;
|
|
|
- gap: 10px;
|
|
|
- width: 100%;
|
|
|
-
|
|
|
- .button {
|
|
|
- position: absolute;
|
|
|
- left: 75%;
|
|
|
- }
|
|
|
-}
|
|
|
-</style>
|