|
@@ -0,0 +1,307 @@
|
|
|
+<script setup>
|
|
|
+ import {
|
|
|
+ onMounted,
|
|
|
+ ref,
|
|
|
+ toRefs,
|
|
|
+ reactive
|
|
|
+ } from 'vue';
|
|
|
+ import {
|
|
|
+ getChatList,
|
|
|
+ createChat,
|
|
|
+ getHistoryChatList
|
|
|
+ } from '@/api/ai'
|
|
|
+ import {
|
|
|
+ Top
|
|
|
+ } from '@element-plus/icons-vue'
|
|
|
+ import {
|
|
|
+ useUserStore
|
|
|
+ } from '@/store'
|
|
|
+ import {
|
|
|
+ marked
|
|
|
+ } from 'marked';
|
|
|
+ import hljs from 'highlight.js/lib/common';
|
|
|
+ // 引入 github 代码主题
|
|
|
+ import 'highlight.js/styles/github.css';
|
|
|
+
|
|
|
+ const state = reactive({
|
|
|
+ renderedMarkdown: ''
|
|
|
+ });
|
|
|
+ const {
|
|
|
+ renderedMarkdown
|
|
|
+ } = toRefs(state);
|
|
|
+ const user = ref(useUserStore().userData);
|
|
|
+ const messageList = ref([]);
|
|
|
+ const loading = ref(false);
|
|
|
+ const messageValue = ref('')
|
|
|
+ const formatTime = () => {
|
|
|
+ return ''
|
|
|
+ }
|
|
|
+ const init = async () => {
|
|
|
+ let chatData = await getChatList(8);
|
|
|
+ if (chatData.state) {
|
|
|
+ let data = chatData.data.data;
|
|
|
+ if (data.length > 0) initChatList(data[0].id);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ onMounted(() => {
|
|
|
+ init();
|
|
|
+ })
|
|
|
+ const initChatList = async (id) => {
|
|
|
+ let flowData = await getHistoryChatList(id);
|
|
|
+ if (flowData.state) {
|
|
|
+ messageList.value = flowData.data.data.map(node => {
|
|
|
+ node['AIoutputs'] = renderMarkdown(node.answer);
|
|
|
+ return node
|
|
|
+ });
|
|
|
+ hljs.highlightAll()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const sendMessage = async () => {
|
|
|
+ let chatData = await createChat(8, {
|
|
|
+ query: messageValue.value
|
|
|
+ })
|
|
|
+ }
|
|
|
+ const returnUserInputs = (data) => {
|
|
|
+ if (!data) return '';
|
|
|
+ return JSON.parse(data).query;
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 使用 marked 解析 Markdown
|
|
|
+ * @param markdown 解析的文本
|
|
|
+ */
|
|
|
+ const renderMarkdown = markdown => {
|
|
|
+ const renderer = new marked.Renderer();
|
|
|
+ renderer.code = ({
|
|
|
+ text,
|
|
|
+ lang,
|
|
|
+ escaped
|
|
|
+ }) => {
|
|
|
+ const language = hljs.getLanguage(lang) ? lang : 'plaintext';
|
|
|
+ const highlighted = hljs.highlight(text, {
|
|
|
+ language
|
|
|
+ }).value;
|
|
|
+ return `<pre><code class="hljs language-${language}">${highlighted}</code></pre>`;
|
|
|
+ };
|
|
|
+ return marked(markdown, {
|
|
|
+ renderer
|
|
|
+ });
|
|
|
+ };
|
|
|
+</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>
|
|
|
+ <div class="message ai-message" v-if="!loading || message.status !== 'running'"
|
|
|
+ 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>
|
|
|
+ <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>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="loading" class="typing-indicator">
|
|
|
+ <div class="typing-dot"></div>
|
|
|
+ <div class="typing-dot"></div>
|
|
|
+ <div class="typing-dot"></div>
|
|
|
+ <span style="margin-left: 10px;">思考中...</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="input-container">
|
|
|
+ <div class="input-box">
|
|
|
+ <el-input type="textarea" v-model="messageValue" 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"
|
|
|
+ :icon="Top">
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+
|
|
|
+<style>
|
|
|
+ .message-image {
|
|
|
+ width: 30px;
|
|
|
+ }
|
|
|
+</style>
|
|
|
+<style scoped lang="scss">
|
|
|
+ .hljs{
|
|
|
+ .language-html{
|
|
|
+ background: #fafafa;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .ai-chat-container {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background: white;
|
|
|
+ border-radius: 16px;
|
|
|
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
|
|
+ overflow: hidden;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 聊天内容区域 */
|
|
|
+ .chat-body {
|
|
|
+ flex: 1;
|
|
|
+ padding: 20px;
|
|
|
+ overflow-y: auto;
|
|
|
+ background: #f8fafc;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 15px;
|
|
|
+ overflow-x: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ .message {
|
|
|
+ padding: 12px 16px;
|
|
|
+ border-radius: 18px;
|
|
|
+ position: relative;
|
|
|
+ animation: fadeIn 0.3s ease;
|
|
|
+ line-height: 1.5;
|
|
|
+ }
|
|
|
+
|
|
|
+ .user-message {
|
|
|
+ background: #7a72cf;
|
|
|
+ color: white;
|
|
|
+ margin-left: auto;
|
|
|
+ border-bottom-right-radius: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ai-message {
|
|
|
+ background: #ffffff;
|
|
|
+ border: 1px solid #e9ecef;
|
|
|
+ margin-right: auto;
|
|
|
+ border-bottom-left-radius: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 5px;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-header-label {
|
|
|
+ flex: 1;
|
|
|
+ width: 0;
|
|
|
+ overflow: hidden;
|
|
|
+ margin-left: 5px;
|
|
|
+ font-weight: bold;
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-time {
|
|
|
+ font-size: 0.7rem;
|
|
|
+ opacity: 0.7;
|
|
|
+ margin-top: 5px;
|
|
|
+ text-align: right;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 加载指示器 */
|
|
|
+ .typing-indicator {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 12px 16px;
|
|
|
+ background: white;
|
|
|
+ border: 1px solid #e9ecef;
|
|
|
+ border-radius: 18px;
|
|
|
+ width: fit-content;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ border-bottom-left-radius: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .typing-dot {
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ background: #6c757d;
|
|
|
+ border-radius: 50%;
|
|
|
+ margin: 0 3px;
|
|
|
+ animation: typing 1.4s infinite;
|
|
|
+ }
|
|
|
+
|
|
|
+ .typing-dot:nth-child(1) {
|
|
|
+ animation-delay: 0s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .typing-dot:nth-child(2) {
|
|
|
+ animation-delay: 0.2s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .typing-dot:nth-child(3) {
|
|
|
+ animation-delay: 0.4s;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 输入区域 */
|
|
|
+ .input-container {
|
|
|
+ padding: 10px;
|
|
|
+ box-sizing: border-box;
|
|
|
+ background: #f8fafc;
|
|
|
+
|
|
|
+ .input-box {
|
|
|
+ background: #ffffff;
|
|
|
+ padding: 5px 0;
|
|
|
+ border: 1px solid var(--el-border-color);
|
|
|
+ border-radius: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-textarea__inner) {
|
|
|
+ border: none;
|
|
|
+ background: transparent;
|
|
|
+ box-shadow: none;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ box-shadow: none;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .input-button {
|
|
|
+ margin-top: 10px;
|
|
|
+ text-align: right;
|
|
|
+ padding: 5px 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 动画 */
|
|
|
+ @keyframes fadeIn {
|
|
|
+ from {
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateY(10px);
|
|
|
+ }
|
|
|
+
|
|
|
+ to {
|
|
|
+ opacity: 1;
|
|
|
+ transform: translateY(0);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes typing {
|
|
|
+
|
|
|
+ 0%,
|
|
|
+ 60%,
|
|
|
+ 100% {
|
|
|
+ transform: translateY(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ 30% {
|
|
|
+ transform: translateY(-5px);
|
|
|
+ }
|
|
|
+ }
|
|
|
+</style>
|