|
@@ -2,12 +2,15 @@
|
|
|
import {
|
|
|
onMounted,
|
|
|
ref,
|
|
|
- toRefs,
|
|
|
- reactive
|
|
|
+ nextTick,
|
|
|
+ watch,
|
|
|
+ onUnmounted
|
|
|
} from 'vue';
|
|
|
import {
|
|
|
- getChatList,
|
|
|
- createChat,
|
|
|
+ useRoute
|
|
|
+ } from 'vue-router';
|
|
|
+ import {
|
|
|
+ getFlowChatStatus,
|
|
|
getHistoryChatList
|
|
|
} from '@/api/ai'
|
|
|
import {
|
|
@@ -22,43 +25,138 @@
|
|
|
import hljs from 'highlight.js/lib/common';
|
|
|
// 引入 github 代码主题
|
|
|
import 'highlight.js/styles/github.css';
|
|
|
-
|
|
|
- const state = reactive({
|
|
|
- renderedMarkdown: ''
|
|
|
- });
|
|
|
- const {
|
|
|
- renderedMarkdown
|
|
|
- } = toRefs(state);
|
|
|
+ import config from '@/config'
|
|
|
const user = ref(useUserStore().userData);
|
|
|
- const messageList = ref([]);
|
|
|
+ // 对话历史
|
|
|
+ const chatHistory = ref([]);
|
|
|
+ // 加载状态
|
|
|
const loading = ref(false);
|
|
|
- const messageValue = ref('')
|
|
|
+ // 输入框内容
|
|
|
+ const prompt = ref('');
|
|
|
+ // 当前AI流式响应内容
|
|
|
+ const currentAIResponse = ref('');
|
|
|
+ const UUID = ref(useRoute().params.id);
|
|
|
+ const conversationId = ref('');
|
|
|
+ const chatBody = ref(null);
|
|
|
const formatTime = () => {
|
|
|
return ''
|
|
|
}
|
|
|
+ onMounted(() => {
|
|
|
+ if (UUID.value.trim()) init();
|
|
|
+ })
|
|
|
+ // 监听对话历史变化,自动滚动到底部
|
|
|
+ watch(chatHistory, () => {
|
|
|
+ scrollToBottom();
|
|
|
+ }, {
|
|
|
+ deep: true
|
|
|
+ })
|
|
|
+ let timerId = null;
|
|
|
const init = async () => {
|
|
|
- let chatData = await getChatList(8);
|
|
|
- if (chatData.state) {
|
|
|
- let data = chatData.data.data;
|
|
|
- if (data.length > 0) initChatList(data[0].id);
|
|
|
+ let flowChatData = await getFlowChatStatus(UUID.value);
|
|
|
+ if (flowChatData.state) {
|
|
|
+ if (flowChatData.data.status === 'succeeded') {
|
|
|
+ conversationId.value = flowChatData.data.conversationId;
|
|
|
+ loading.value = false;
|
|
|
+ initChatList();
|
|
|
+ } else {
|
|
|
+ let inputs = JSON.parse(flowChatData.data.inputs);
|
|
|
+ chatHistory.value = [{
|
|
|
+ query: inputs.query,
|
|
|
+ AIoutputs: ''
|
|
|
+ }]
|
|
|
+ loading.value = true;
|
|
|
+ startTimer();
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
- onMounted(() => {
|
|
|
- init();
|
|
|
- })
|
|
|
- const initChatList = async (id) => {
|
|
|
- let flowData = await getHistoryChatList(id);
|
|
|
+ // 启动定时器
|
|
|
+ const startTimer = () => {
|
|
|
+ if (timerId) return
|
|
|
+ timerId = setInterval(() => {
|
|
|
+ init();
|
|
|
+ }, 3000)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 停止定时器
|
|
|
+ const stopTimer = () => {
|
|
|
+ if (timerId) {
|
|
|
+ clearInterval(timerId)
|
|
|
+ timerId = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const initChatList = async () => {
|
|
|
+ let flowData = await getHistoryChatList(conversationId.value);
|
|
|
if (flowData.state) {
|
|
|
- messageList.value = flowData.data.data.map(node => {
|
|
|
+ chatHistory.value = flowData.data.data.map(node => {
|
|
|
node['AIoutputs'] = renderMarkdown(node.answer);
|
|
|
return node
|
|
|
});
|
|
|
- hljs.highlightAll()
|
|
|
+ hljs.highlightAll();
|
|
|
}
|
|
|
}
|
|
|
+ const reloadMessage = (value) => {
|
|
|
+ prompt.value = value;
|
|
|
+ sendMessage();
|
|
|
+ }
|
|
|
const sendMessage = async () => {
|
|
|
- let chatData = await createChat(8, {
|
|
|
- query: messageValue.value
|
|
|
+ if (!prompt.value.trim() || loading.value) {
|
|
|
+ ElMessage({
|
|
|
+ type: 'warning',
|
|
|
+ message: '输入不能为空!'
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ // 1. 添加用户消息到历史记录
|
|
|
+ const userMessage = prompt.value
|
|
|
+ chatHistory.value.push({
|
|
|
+ isUser: true,
|
|
|
+ query: userMessage,
|
|
|
+ AIoutputs: ''
|
|
|
+ })
|
|
|
+ // 2. 初始化状态
|
|
|
+ loading.value = true
|
|
|
+ currentAIResponse.value = ''
|
|
|
+ prompt.value = '' // 清空输入框
|
|
|
+ const response = await fetch(`${config.baseURL}/api/ai/chat/run/7`, {
|
|
|
+ method: "POST",
|
|
|
+ headers: {
|
|
|
+ "Content-Type": "application/json",
|
|
|
+ "token": useUserStore().token
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ query: userMessage,
|
|
|
+ conversationId: conversationId.value
|
|
|
+ })
|
|
|
+ });
|
|
|
+ const reader = response.body.getReader();
|
|
|
+ const decoder = new TextDecoder();
|
|
|
+ let done = false;
|
|
|
+ let result = '';
|
|
|
+ while (!done) {
|
|
|
+ const {
|
|
|
+ value,
|
|
|
+ done: doneReading
|
|
|
+ } = await reader.read();
|
|
|
+ done = doneReading;
|
|
|
+ let resultString = decoder.decode(value, {
|
|
|
+ stream: true
|
|
|
+ });
|
|
|
+ loading.value = false;
|
|
|
+ result += resultString;
|
|
|
+ result = result.replaceAll('data:', '');
|
|
|
+ chatHistory.value[chatHistory.value.length - 1].AIoutputs = renderMarkdown(result);
|
|
|
+ scrollToBottom();
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("请求失败:", error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const scrollToBottom = () => {
|
|
|
+ nextTick(() => {
|
|
|
+ if (chatBody.value) {
|
|
|
+ chatBody.value.scrollTop = chatBody.value.scrollHeight
|
|
|
+ }
|
|
|
})
|
|
|
}
|
|
|
const returnUserInputs = (data) => {
|
|
@@ -80,37 +178,39 @@
|
|
|
const highlighted = hljs.highlight(text, {
|
|
|
language
|
|
|
}).value;
|
|
|
- return `<pre><code class="hljs language-${language}">${highlighted}</code></pre>`;
|
|
|
+ return `<div class="hljs-box"><div class="hljs-title">${language.toLocaleUpperCase()}</div><pre><code class="hljs language-${language}">${highlighted}</code></pre></div>`;
|
|
|
};
|
|
|
return marked(markdown, {
|
|
|
renderer
|
|
|
});
|
|
|
};
|
|
|
+ // 组件卸载时清除定时器(重要!)
|
|
|
+ onUnmounted(() => {
|
|
|
+ stopTimer()
|
|
|
+ })
|
|
|
</script>
|
|
|
<template>
|
|
|
<div class="ai-chat-container">
|
|
|
<div class="chat-body" ref="chatBody">
|
|
|
- <div v-for="(message, index) in messageList" :key="index">
|
|
|
- <div class="message user-message">
|
|
|
- <div class="message-content" v-if="message.query" v-html="message.query"></div>
|
|
|
+ <div v-for="(message, index) in chatHistory" :key="index">
|
|
|
+ <div class="user-message">
|
|
|
+ <div class="message user-message-box">
|
|
|
+ <div class="message-content" v-if="message.query" v-html="message.query"></div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- <div class="message ai-message" v-if="!loading || message.status !== 'running'"
|
|
|
- style="margin-top: 15px;">
|
|
|
+ <div class="message ai-message" v-if="!loading || message.AIoutputs" style="margin-top: 15px;">
|
|
|
<div class="message-header">
|
|
|
<el-avatar :size="30"
|
|
|
src="https://file-node.oss-cn-shanghai.aliyuncs.com/youji/f9617c7f80da485cb3cc72b6accc62ed">
|
|
|
</el-avatar>
|
|
|
- <div class="message-header-label">AI助手</div>
|
|
|
+ <div class="message-header-label" style="width: 100px;">AI助手</div>
|
|
|
</div>
|
|
|
<div class="message-content">
|
|
|
- <div v-if="message.status === 'failed'">
|
|
|
- <p>服务器繁忙,请稍后重试</p>
|
|
|
- <el-button size="small" @click="reload(message)"
|
|
|
- :disabled="index !== messageList.length - 1" style="margin-top: 10px;">
|
|
|
- 点击重试
|
|
|
- </el-button>
|
|
|
- </div>
|
|
|
- <div class="ai-website-boxs" v-else v-html="message.AIoutputs"></div>
|
|
|
+ <div class="ai-website-boxs" v-html="message.AIoutputs"></div>
|
|
|
+ <el-button size="small" @click="reloadMessage(message.query)" :disabled="loading"
|
|
|
+ style="margin-top: 10px;" v-if="message.AIoutputs.indexOf('系统繁忙,请重试')>-1">
|
|
|
+ 点击重试
|
|
|
+ </el-button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
@@ -123,11 +223,11 @@
|
|
|
</div>
|
|
|
<div class="input-container">
|
|
|
<div class="input-box">
|
|
|
- <el-input type="textarea" v-model="messageValue" placeholder="给AI发送消息" resize="none" :rows="4"
|
|
|
+ <el-input type="textarea" v-model="prompt" placeholder="给AI发送消息" resize="none" :rows="4"
|
|
|
:autosize="{ minRows: 2, maxRows: 6 }">
|
|
|
</el-input>
|
|
|
<div class="input-button">
|
|
|
- <el-button circle type="primary" @click="sendMessage" :disabled="!messageValue.trim() || loading"
|
|
|
+ <el-button circle type="primary" @click="sendMessage" :disabled="!prompt.trim() || loading"
|
|
|
:icon="Top">
|
|
|
</el-button>
|
|
|
</div>
|
|
@@ -137,17 +237,33 @@
|
|
|
</template>
|
|
|
|
|
|
|
|
|
-<style>
|
|
|
+<style lang="scss">
|
|
|
.message-image {
|
|
|
width: 30px;
|
|
|
}
|
|
|
-</style>
|
|
|
-<style scoped lang="scss">
|
|
|
- .hljs{
|
|
|
- .language-html{
|
|
|
- background: #fafafa;
|
|
|
- }
|
|
|
+
|
|
|
+ .hljs-box {
|
|
|
+ border-radius: 5px;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ .hljs-title {
|
|
|
+ background: #f2f3f5;
|
|
|
+ padding: 5px 10px;
|
|
|
+ color: #354052;
|
|
|
+ font-weight: bold;
|
|
|
+ border-bottom: 1px solid #999999;
|
|
|
+ }
|
|
|
+
|
|
|
+ .language-html {
|
|
|
+ background: #f1f2f5;
|
|
|
+ }
|
|
|
+
|
|
|
+ pre {
|
|
|
+ margin-top: 0;
|
|
|
+ }
|
|
|
}
|
|
|
+</style>
|
|
|
+<style scoped lang="scss">
|
|
|
.ai-chat-container {
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
@@ -177,13 +293,19 @@
|
|
|
position: relative;
|
|
|
animation: fadeIn 0.3s ease;
|
|
|
line-height: 1.5;
|
|
|
+ display: inline-block;
|
|
|
+ max-width: 100%;
|
|
|
}
|
|
|
|
|
|
.user-message {
|
|
|
- background: #7a72cf;
|
|
|
- color: white;
|
|
|
- margin-left: auto;
|
|
|
- border-bottom-right-radius: 4px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: end;
|
|
|
+
|
|
|
+ .user-message-box {
|
|
|
+ background: #7a72cf;
|
|
|
+ color: white;
|
|
|
+ border-bottom-right-radius: 4px;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
.ai-message {
|