| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549 |
- <!-- eslint-disable vue/no-v-html -->
- <template>
- <div class="btn" @click="showAIChat">
- <div style="display: flex; flex-direction: row" class="btn-header">
- <img src="@/assets/images/vent/home/wakeBtn.png" />
- </div>
- </div>
- <div class="container">
- <div v-if="isShowChatBroad" class="mini-chat">
- <!-- 左侧折叠区域 -->
- <div class="left-side" :class="{ collapsed: isFold }" id="leftSide">
- <div
- class="addBtn"
- :style="{
- backgroundColor: isFold ? '' : '#2cb6ff',
- width: isFold ? '20px' : 'auto',
- }"
- @click="addNew"
- >
- <SvgIcon v-if="isFold" name="add" size="20" :color="''" />
- <span v-if="!isFold" class="btn-text">添加新对话</span>
- </div>
- <div v-if="isFold" class="historyBtn" @click="addNew">
- <SvgIcon v-if="isFold" name="history" size="20" :color="''" />
- </div>
- <div v-else class="historyBtn1">
- <span v-if="!isFold" class="btn-text">历史对话</span>
- <a-list style="width: 136px" :split="false" :data-source="sessionHistory" :scroll="190" class="custom-list">
- <template #renderItem="{ item }">
- <a-list-item
- class="session-item"
- :style="{
- padding: '8px 10px 0 8px',
- color: '#5e7081',
- fontSize: '10px',
- position: 'relative', // 新增定位
- }"
- >
- <!-- 新增flex布局容器 -->
- <div style="width: 100%">
- <div v-if="editingId !== item.id" class="text-container">
- <span class="edit-text" @click="sessionsHistory(item.id)">{{ item.name }}</span>
- <div class="btn-container">
- <EditOutlined class="edit-icon" @click="startEditing(item)" />
- <DeleteOutlined class="delete-icon" @click="startDelete(item)" />
- </div>
- </div>
- <!-- 输入框 -->
- <a-input
- size="small"
- v-else
- v-model:value="editText"
- v-focus
- @blur="handleSave(item)"
- @keyup.enter="handleSave(item)"
- class="edit-input"
- />
- </div>
- </a-list-item>
- </template>
- </a-list>
- </div>
- <div class="foldBtn" @click="fold">
- <SvgIcon v-if="isFold" name="Fold1" size="20" :color="''" />
- <SvgIcon v-else name="unfold" size="20" :color="''" />
- </div>
- </div>
- <!-- 右侧对话框 -->
- <div class="right-side">
- <div class="title"> 智能问答 </div>
- <!-- 对话区域 -->
- <div class="dialog-area" ref="dialogAreaRef">
- <div
- v-for="message in messageHistory"
- :key="message.id"
- class="flex items-center w-100%"
- :style="{ alignSelf: message.type === 'user' ? 'flex-end' : 'flex-start' }"
- >
- <template v-if="message.type === 'user'">
- <div class="flex-grow-1"></div>
- <div class="message-wrapper user-message-wrapper">
- <div class="ask-message">{{ message.parsedContent }}</div>
- <div class="copy-icon-container">
- <CopyOutlined class="copy-icon" @click="copyToClipboard(message.parsedContent)" title="复制消息" />
- <EditOutlined class="copy-icon" @click="editAsk(message.parsedContent)" title="重新编辑" />
- </div>
- </div>
- </template>
- <template v-else>
- <SvgIcon size="40" class="answerIcon" name="ai-logo" />
- <div class="message-wrapper ai-message-wrapper">
- <div class="answer-message">
- <div v-if="message.parsedContentR1" class="thinking-section">
- <div class="thinking-header" @click="isShow(message)">
- <span class="thinking-title"
- >思考过程:<RightOutlined v-if="!message.isShowThink" /> <DownOutlined v-if="message.isShowThink"
- /></span>
- </div>
- <div v-show="message.isShowThink" class="color-gray font-size-12px" v-html="formatMessage(message.parsedContentR1)"></div>
- </div>
- <div v-if="message.parsedContent" v-html="formatMessage(message.parsedContent)"> </div>
- </div>
- <div class="copy-icon-container">
- <CopyOutlined class="copy-icon" @click="copyToClipboard(message.parsedContent)" title="复制消息" />
- <RedoOutlined class="copy-icon" @click="refresh()" title="重新生成" />
- </div>
- </div>
- </template>
- </div>
- <!-- 建议信息 -->
- <div v-for="(item, index) in suggestList" :key="index" class="suggestion-item" @click="handleSuggestClick(item)">
- <span class="suggestion-text">{{ item }}</span>
- <a-icon type="right" class="suggestion-arrow" />
- </div>
- </div>
- <!-- 底部输入区 -->
- <div class="input-area">
- <!-- <div class="file-preview" v-if="currentFile">
- <div class="file-info">
- 📄 已选择文件:{{ currentFile.name }} (大小:{{ (currentFile.size / 1024).toFixed(2) }}KB)
- <button @click="clearFile" class="clear-btn">×</button>
- </div>
- </div> -->
- <a-textarea v-model:value="inputText" placeholder="请输入你的问题" @keyup.enter="handleSend(inputText)" class="ant-input" auto-size />
- <div class="ctrl-btn">
- <div class="input-controls">
- <button class="control-btn" :class="{ active: isThinking }" @click="toggleThinking">深度思考</button>
- <button class="control-btn" @click="stopReq()">停止响应</button>
- </div>
- <div class="action-bar">
- <Space>
- <Button class="control-btn1" size="small" @mouseenter="showModal(true)">
- <template #icon>
- <SvgIcon name="send-file" />
- </template>
- </Button>
- <Button class="control-btn2" size="small" @click="handleSend(inputText)">
- <template #icon>
- <SvgIcon name="send" />
- </template>
- </Button>
- </Space>
- </div>
- </div>
- <!-- 右侧文件上传区 -->
- <div v-if="open" @mouseenter="showModal(true)" @mouseleave="showModal(false)" class="file-upload">
- <!-- 上传按钮 -->
- <a-upload
- class="custom-upload"
- name="file"
- :multiple="false"
- :before-upload="handleBeforeUpload"
- :file-list="fileList"
- :remove="handleRemove"
- accept=".pdf,.docx,.xlsx,.xls"
- >
- <a-button class="upload-btn">
- <UploadOutlined></UploadOutlined>
- 从本地上传
- </a-button>
- </a-upload>
- </div>
- </div>
- </div>
- </div>
- <div class="doc" v-if="isShowDoc">
- <div class="close"> <button class="closeBtn" @click="close">关闭</button></div>
- <!-- 已上传文件列表 -->
- <div class="file-list" v-if="uploadedFiles.length">
- <div class="file-item" v-for="file in uploadedFiles" :key="file.id">
- <div class="file-info">
- <div class="file-name" :title="file.name">{{ file.name }}</div>
- </div>
- <div class="file-actions">
- <button class="btn btn-preview" @click="previewFile(file)"> <i class="fas fa-eye"></i> 预览 </button>
- <button class="btn btn-delete" @click="deleteFile(file.id)"> <i class="fas fa-trash"></i> 删除 </button>
- </div>
- </div>
- </div>
- <!-- 预览内容 -->
- <div class="pre-container">
- <!-- PDF预览 -->
- <VueOfficePdf v-if="fileType === 'pdf'" :src="fileUrl" @rendered="handleRendered" @error="handleError" />
- <!-- Word文档预览 -->
- <VueOfficeDocx v-else-if="fileType === 'docx'" :src="fileUrl" @rendered="handleRendered" @error="handleError" />
- <!-- Excel预览 -->
- <VueOfficeExcel
- v-else-if="fileType === 'xlsx' || fileType === 'xls'"
- :options="{ xls: true }"
- :src="fileUrl"
- @rendered="handleRendered"
- @error="handleError"
- />
- <!-- 不支持的文件类型 -->
- <div v-else class="unsupported">
- <p>不支持预览该文件类型: {{ fileType }}</p>
- </div>
- <!-- 加载状态 -->
- <div v-if="loading" class="loading">
- <p>文件加载中...</p>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script lang="ts" setup>
- import VueOfficePdf from '@vue-office/pdf/lib/v3/vue-office-pdf.mjs';
- import VueOfficeDocx from '@vue-office/docx/lib/v3/vue-office-docx.mjs';
- import VueOfficeExcel from '@vue-office/excel/lib/v3/vue-office-excel.mjs';
- import { ref, onMounted, nextTick } from 'vue';
- import { SvgIcon } from '../Icon';
- import { Space, Button, Modal, Input, message } from 'ant-design-vue';
- // import AIChat from './index.vue';
- import { useUserStore } from '/@/store/modules/user';
- import {
- EditOutlined,
- DeleteOutlined,
- UploadOutlined,
- CopyOutlined,
- RightOutlined,
- DownOutlined,
- FastForwardFilled,
- RedoOutlined,
- } from '@ant-design/icons-vue';
- import { createVNode } from 'vue';
- import { marked } from 'marked';
- import katex from 'katex';
- import 'katex/dist/katex.min.css';
- const TextArea = Input.TextArea; // 直接导入TextArea组件使用时打包报错
- const inputText = ref(''); // 输入框内容
- const refreshText = ref(''); //重新生成文本
- const sessionHistory = ref([]);
- const isShowChatBroad = ref(false);
- const editingId = ref<number | null>(null);
- const editText = ref('');
- const currentSessionID = ref('');
- const taskID = ref('');
- const messageID = ref('');
- const open = ref<boolean>(false);
- const isThinking = ref(false); //深度思考是否开启
- const Thinking = ref(false);
- const isShowDoc = ref(false);
- const fileType = ref('');
- const fileUrl = ref('');
- const APIKEY = ref('Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd');
- interface ListItem {
- id: number;
- name?: string;
- }
- interface Message {
- id: string; // 唯一标识(可用时间戳生成)
- type: 'user' | 'system' | 'response';
- content: String; // 原始 Markdown 字符串(用于拼接)
- parsedContent: String; // 解析后的 HTML(用于渲染)
- contentR1: String; // 原始思考过程 Markdown
- parsedContentR1: String; // 解析后的思考过程 HTML
- timestamp: number; // 排序依据
- isShowThink: boolean; //深度思考展示
- }
- // 定义消息历史数组类型
- const messageHistory = ref<Message[]>([]);
- const isFold = ref(true); // 是否折叠
- const userid = useUserStore().getUserInfo.id as string;
- const filePath = ref(''); // 绑定输入框值
- const uploadedFiles = ref([]);
- const showConfirmBtn = ref(false); // 控制确认按钮显示状态
- const fileList = ref([]);
- const suggestList = ref([]); //建议列表
- const loading = ref(false);
- // 文件预览
- const handleRendered = () => {
- loading.value = false;
- console.log('文件渲染完成');
- };
- const handleError = (error) => {
- loading.value = false;
- console.error('文件预览错误:', error);
- };
- function previewFile(data) {
- fileType.value = data.name.split('.').pop().toLowerCase();
- fileUrl.value = data.source_url;
- }
- function deleteFile(fileId) {
- // 确认删除
- if (confirm('确定要删除这个文件吗?')) {
- uploadedFiles.value = uploadedFiles.value.filter((file) => file.id !== fileId);
- }
- }
- //启用深度思考
- const toggleThinking = () => {
- isThinking.value = !isThinking.value;
- if (isThinking.value) {
- APIKEY.value = 'Bearer app-kprgsFKtySM4Wjxs0ZGzaNFN';
- } else {
- APIKEY.value = 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd';
- }
- };
- // 折叠思考过程
- const isShow = (message) => {
- message.isShowThink = !message.isShowThink;
- };
- const dialogAreaRef = ref(null);
- // 滚动到底部的方法
- const scrollToBottom = async () => {
- // 等待 DOM 更新(如消息渲染完成)
- await nextTick();
- if (dialogAreaRef.value) {
- const el = dialogAreaRef.value;
- // 关键:scrollTop = scrollHeight(滚动内容总高度)
- el.scrollTop = el.scrollHeight;
- }
- };
- // 点击建议项时的处理函数
- const handleSuggestClick = (text) => {
- // 将选中的建议填充到文本框
- inputText.value = text;
- };
- function showAIChat() {
- isShowChatBroad.value = !isShowChatBroad.value;
- if (isShowChatBroad) {
- isShowDoc.value = false;
- }
- }
- //复制消息
- function copyToClipboard(text) {
- if (!text || text.trim() === '') {
- message.warn('没有可复制的内容');
- return;
- }
- // 2. 创建临时textarea 元素
- const textarea = document.createElement('textarea');
- textarea.value = text;
- textarea.style.position = 'fixed';
- textarea.style.top = '-999px';
- textarea.style.left = '-999px';
- textarea.style.width = '200px';
- textarea.style.height = '200px';
- document.body.appendChild(textarea);
- try {
- textarea.select();
- textarea.setSelectionRange(0, text.length);
- const isSuccessful = document.execCommand('copy');
- if (isSuccessful) {
- message.success('复制成功!');
- } else {
- throw new Error('复制命令执行失败');
- }
- } catch (err) {
- console.error('复制失败:', err);
- message.error('复制失败,请手动复制');
- } finally {
- document.body.removeChild(textarea);
- }
- }
- const initMarked = () => {
- marked.setOptions({
- gfm: true, // 启用GitHub风格的Markdown,包含表格
- breaks: false, // 禁用换行符转换
- });
- };
- // LaTeX 公式渲染(内部使用)
- const renderLatexInHtml = (html) => {
- // 匹配块级公式($$...$$)
- const blockRegex = /\$\$(.*?)\$\$/gs;
- // 匹配行内公式($...$)
- const inlineRegex = /\$(.*?)\$/g;
- // // 替换块级公式(居中显示)
- html = html.replace(blockRegex, (match, formula) => {
- return katex.renderToString(formula.trim(), {
- displayMode: true,
- throwOnError: false,
- strict: false,
- trust: true,
- });
- });
- // 替换行内公式(行内显示)
- html = html.replace(inlineRegex, (match, formula) => {
- return katex.renderToString(formula.trim(), {
- displayMode: false,
- throwOnError: false,
- strict: false,
- });
- });
- return html;
- };
- // Markdown + LaTeX 解析
- const parseMarkdownWithLatex = (mdStr) => {
- if (!mdStr) return '';
- try {
- let html = marked(mdStr); // Markdown → HTML
- html = renderLatexInHtml(html); // 替换公式
- return html;
- } catch (error) {
- console.error('解析失败:', error);
- return mdStr; // 降级显示原始字符串
- }
- };
- //重新生成
- const refresh = () => {
- handleSend(refreshText.value);
- };
- //重新编辑
- const editAsk = (data) => {
- inputText.value = data;
- };
- //获取消息列表
- async function handleSend(data) {
- refreshText.value = data;
- inputText.value = '';
- if (isThinking) {
- messageHistory.value.push({
- id: `user_${Date.now()}`,
- type: 'user',
- content: '',
- parsedContent: data,
- contentR1: '',
- parsedContentR1: '',
- timestamp: Date.now(),
- isShowThink: true,
- });
- } else {
- messageHistory.value.push({
- id: `user_${Date.now()}`,
- type: 'user',
- content: '',
- parsedContent: data,
- contentR1: '',
- parsedContentR1: '',
- timestamp: Date.now(),
- isShowThink: false,
- });
- }
- try {
- const response = await fetch('http://39.97.59.228:8000/v1/chat-messages', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: APIKEY.value,
- },
- body: JSON.stringify({
- conversation_id: currentSessionID.value ? currentSessionID.value : '',
- query: data,
- response_mode: 'streaming',
- user: userid,
- inputs: {},
- }),
- });
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- const decoder = new TextDecoder('utf-8');
- const reader = response.body.getReader();
- let textBuffer = ''; // 使用字符串缓冲区来累积数据
- // 在组件中定义
- const currentProcessingMessage = ref(null);
- if (isThinking) {
- const newMessage = {
- id: `response_${Date.now()}`,
- type: 'response',
- content: '',
- parsedContent: '',
- contentR1: '',
- parsedContentR1: '',
- timestamp: Date.now(),
- isShowThink: true,
- };
- messageHistory.value.push(newMessage);
- currentProcessingMessage.value = newMessage; // 保存引用
- } else {
- const newMessage = {
- id: `response_${Date.now()}`,
- type: 'response',
- content: '',
- parsedContent: '',
- contentR1: '',
- parsedContentR1: '',
- timestamp: Date.now(),
- isShowThink: false,
- };
- messageHistory.value.push(newMessage);
- currentProcessingMessage.value = newMessage; // 保存引用
- }
- while (true) {
- const { done, value } = await reader.read();
- if (done) {
- if (textBuffer) {
- processLine(textBuffer);
- }
- break;
- }
- textBuffer += decoder.decode(value, { stream: true });
- // 处理每一行数据
- let lineIndex;
- while ((lineIndex = textBuffer.indexOf('\n')) !== -1) {
- const line = textBuffer.substring(0, lineIndex).trim();
- textBuffer = textBuffer.substring(lineIndex + 1);
- if (line) {
- processLine(line);
- }
- }
- }
- function processLine(line) {
- if (line.startsWith('data: ')) {
- try {
- const jsonStr = line.substring('data: '.length);
- const data = JSON.parse(jsonStr);
- switch (data.event) {
- case 'message':
- if (data.answer) {
- const targetMessage = messageHistory.value.find((msg) => msg.id === currentProcessingMessage.value.id);
- if (!targetMessage) break;
- let currentChunk = data.answer;
- // 检查是否包含起始标签
- const startIndex = currentChunk.indexOf('<think>');
- if (startIndex !== -1) {
- // 找到起始标签:将标签前的内容作为正文
- if (startIndex > 0) {
- targetMessage.content += currentChunk.substring(0, startIndex);
- targetMessage.parsedContent = parseMarkdownWithLatex(targetMessage.content);
- }
- // 进入思考模式,将标签后的内容追加到contentR1
- Thinking.value = true;
- const remainingContent = currentChunk.substring(startIndex + '<think>'.length);
- if (remainingContent) {
- targetMessage.contentR1 += remainingContent;
- targetMessage.parsedContentR1 = parseMarkdownWithLatex(targetMessage.contentR1);
- }
- } else if (Thinking.value) {
- // 结束标签
- const endIndex = currentChunk.indexOf('</think>');
- if (endIndex !== -1) {
- // 找到结束标签:标签前的内容追加到contentR1
- if (endIndex > 0) {
- targetMessage.contentR1 += currentChunk.substring(0, endIndex);
- targetMessage.parsedContentR1 = parseMarkdownWithLatex(targetMessage.contentR1);
- }
- // 将标签后的内容作为正文
- const remainingContent = currentChunk.substring(endIndex + '</think>'.length);
- if (remainingContent) {
- targetMessage.content += remainingContent;
- targetMessage.parsedContent = parseMarkdownWithLatex(targetMessage.content);
- }
- targetMessage.content += currentChunk;
- Thinking.value = false;
- targetMessage.isShowThink = false;
- } else {
- // 没有结束标签,继续追加到contentR1
- targetMessage.contentR1 += currentChunk;
- targetMessage.parsedContentR1 = parseMarkdownWithLatex(targetMessage.contentR1);
- }
- } else {
- targetMessage.content += currentChunk;
- targetMessage.parsedContent = parseMarkdownWithLatex(targetMessage.content);
- }
- scrollToBottom(); // 每次接收消息都滚动
- }
- if (data.task_id && !taskID.value) taskID.value = data.task_id;
- if (data.conversation_id && !currentSessionID.value) currentSessionID.value = data.conversation_id;
- if (data.message_id && !messageID.value) {
- messageID.value = data.message_id;
- }
- break;
- }
- } catch (error) {
- console.warn('Error parsing stream chunk:', error, 'Chunk:', line);
- }
- }
- }
- getNextSuggest();
- } catch (error) {
- console.error('Error in handleSend:', error);
- // 在 UI 上显示错误信息
- messageHistory.value.push({
- id: `system_${Date.now()}`,
- type: 'system',
- content: '',
- parsedContent: '请求错误',
- contentR1: '',
- parsedContentR1: '',
- timestamp: Date.now(),
- isShowThink: false,
- });
- }
- }
- //创建新对话
- async function addNew() {
- messageHistory.value = [];
- currentSessionID.value = '';
- taskID.value = '';
- messageID.value = '';
- }
- // 上传文件
- const showModal = (data) => {
- open.value = data;
- };
- const close = () => {
- isShowDoc.value = false;
- };
- // 上传文件
- const handleBeforeUpload = async (file) => {
- const formData = new FormData();
- formData.append('file', file);
- formData.append('user', userid);
- try {
- const response = await fetch(`http://39.97.59.228:8000/v1/files/upload`, {
- method: 'POST',
- headers: {
- Authorization: APIKEY.value,
- },
- body: formData,
- });
- const result = await response.json();
- if (response.ok) {
- const linkText = `[${result.name}](${result.source_url})`;
- // inputText.value += `\n${linkText}`;
- uploadedFiles.value.push(result);
- isShowDoc.value = true;
- previewFile(result);
- } else {
- message.error(`上传失败: ${result.message || '网络错误'}`);
- }
- } catch (error) {
- console.error('保存失败:', error);
- message.error('上传失败,请重试');
- }
- return false;
- };
- const handleRemove = (file) => {
- const index = fileList.value.findIndex((item) => item.uid === file.uid);
- if (index !== -1) {
- fileList.value.splice(index, 1);
- }
- };
- //停止响应
- async function stopReq() {
- try {
- let response = await fetch(`http://39.97.59.228:8000/v1/chat-messages/${taskID.value}/stop`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: APIKEY.value,
- },
- body: JSON.stringify({
- user: userid,
- }),
- });
- if (!response) {
- throw new Error('Network response was not ok');
- }
- } catch (error) {
- console.error('保存失败:', error);
- }
- }
- //获取具体会话记录
- async function sessionsHistory(id: string, retryCount = 0) {
- const API_KEYS = ['Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd', 'Bearer app-kprgsFKtySM4Wjxs0ZGzaNFN'];
- // 最大重试次数
- const maxRetries = API_KEYS.length - 1;
- try {
- let response = await fetch(`http://39.97.59.228:8000/v1/messages?conversation_id=${id}&user=${userid}`, {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: API_KEYS[retryCount],
- },
- });
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
- }
- const data = await response.json();
- if (data.data.length > 0) {
- messageHistory.value = [];
- data.data.forEach((item: any) => {
- currentSessionID.value = item.conversation_id;
- messageHistory.value.push({
- id: `user_${Date.now()}`,
- type: 'user',
- content: '',
- parsedContent: item.query,
- contentR1: '',
- parsedContentR1: '',
- timestamp: Date.now(),
- isShowThink: false,
- });
- const answer = item.answer;
- // 查找<think>
- const startIndex = answer.indexOf('<think>');
- const endIndex = answer.indexOf('</think>');
- let content = '';
- let contentR1 = '';
- // 根据标签判断
- if (startIndex !== -1 && endIndex !== -1) {
- content = answer.substring(0, startIndex) + answer.substring(endIndex + '</think>'.length);
- contentR1 = answer.substring(startIndex + '<think>'.length, endIndex);
- } else if (startIndex !== -1) {
- // 只有起始标签
- content = answer.substring(0, startIndex);
- contentR1 = answer.substring(startIndex + '<think>'.length);
- } else if (endIndex !== -1) {
- // 只有结束标签
- content = answer.substring(endIndex + '</think>'.length);
- contentR1 = answer.substring(0, endIndex);
- } else {
- // 没有标签
- content = answer;
- }
- // 添加到消息历史
- messageHistory.value.push({
- id: `system_${Date.now()}`,
- type: 'system',
- content: '',
- parsedContent: parseMarkdownWithLatex(content.trim()),
- contentR1: '',
- parsedContentR1: parseMarkdownWithLatex(contentR1.trim()),
- timestamp: Date.now(),
- isShowThink: contentR1.length > 0, // 如果有思考内容则显示
- });
- });
- }
- } catch (error) {
- // 判断是否达到最大重试次数
- if (retryCount < maxRetries) {
- console.warn(`请求失败,正在尝试第 ${retryCount + 2} 个 API Key...`);
- return sessionsHistory(id, retryCount + 1);
- } else {
- console.error('所有 API Key 均尝试失败:', error);
- throw error;
- }
- }
- editingId.value = null;
- }
- //获取下一轮建议问题列表
- //停止响应
- async function getNextSuggest() {
- console.log(messageID.value, '123');
- try {
- let response = await fetch(`http://39.97.59.228:8000/v1/messages/${messageID.value}/suggested?user=${userid}`, {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: APIKEY.value,
- },
- });
- const data = await response.json();
- suggestList.value = data.data;
- if (!response) {
- throw new Error('Network response was not ok');
- }
- } catch (error) {
- console.error('保存失败:', error);
- }
- }
- //编辑标题
- const startEditing = (item: ListItem) => {
- editingId.value = item.id;
- editText.value = item.name || '';
- };
- // 保存修改
- const handleSave = async (item: ListItem) => {
- try {
- let response = await fetch(`http://39.97.59.228:8000/v1/conversations/${item.id}/name`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: APIKEY.value,
- },
- body: JSON.stringify({
- name: editText.value,
- user: userid,
- }),
- });
- if (!response.ok) {
- throw new Error('Network response was not ok');
- }
- item.name = editText.value;
- } catch (error) {
- console.error('保存失败:', error);
- }
- editingId.value = null;
- };
- // 删除会话
- const startDelete = async (item: ListItem) => {
- Modal.confirm({
- title: '确认删除',
- content: `确定要删除会话 "${item.name || '新会话'}" 吗?此操作不可撤销。`,
- okText: '确认',
- cancelText: '取消',
- onOk: async () => {
- // 原有删除逻辑不变
- try {
- let response = await fetch(`http://39.97.59.228:8000/v1/conversations/${item.id}`, {
- method: 'DELETE',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: APIKEY.value,
- },
- body: JSON.stringify({
- user: userid,
- }),
- });
- if (!response.ok) {
- throw new Error('Network response was not ok');
- }
- getHistoryList();
- } catch (error) {
- console.error('删除失败:', error);
- Modal.error({
- title: '删除失败',
- content: '删除会话时出现错误,请稍后重试。',
- });
- }
- },
- });
- };
- const fold = () => {
- isFold.value = !isFold.value;
- if (!isFold.value) {
- getHistoryList();
- }
- };
- // 获取历史会话列表
- async function getHistoryList() {
- sessionHistory.value = [];
- try {
- // 并行请求两个接口,提升效率
- const [response1, response2] = await Promise.all([
- // 第一个请求
- fetch(`http://39.97.59.228:8000/v1/conversations?user=${userid}`, {
- method: 'get',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
- },
- }),
- // 第二个请求
- fetch(`http://39.97.59.228:8000/v1/conversations?user=${userid}`, {
- method: 'get',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: 'Bearer app-kprgsFKtySM4Wjxs0ZGzaNFN',
- },
- }),
- ]);
- // 检查响应是否成功
- if (!response1.ok || !response2.ok) {
- throw new Error('接口请求失败');
- }
- // 解析两个响应的JSON数据
- const [data1, data2] = await Promise.all([response1.json(), response2.json()]);
- // 合并两个数组(核心修改:用扩展运算符合并)
- // 确保 data1.data 和 data2.data 都是数组(防止接口返回异常)
- const list1 = Array.isArray(data1.data) ? data1.data : [];
- const list2 = Array.isArray(data2.data) ? data2.data : [];
- // 合并数组并赋值给 sessionHistory
- sessionHistory.value = [...list1, ...list2];
- sessionHistory.value.forEach((item) => {
- currentSessionID.value = item.id;
- });
- console.log(sessionHistory.value, '历史数据');
- } catch (error) {
- console.error('获取历史数据失败:', error);
- // 错误处理(比如提示用户)
- }
- }
- //格式化消息
- const formatMessage = (text) => {
- if (!text) return '';
- let formatted = text
- // 处理换行
- .replace(/\n\n/g, '<br>')
- .replace(/\n###/g, '<br>')
- .replace(/###/g, '')
- .replace(/---/g, '')
- .replace(/^- /gm, '<br> - ')
- // 处理粗体
- .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
- // 处理斜体
- .replace(/\*(.*?)\*/g, '<em>$1</em>')
- // 处理行内代码
- .replace(/`([^`]+)`/g, '<code>$1</code>');
- return formatted;
- };
- // 初始化按钮定位
- onMounted(() => {
- getHistoryList();
- // 初始化(仅执行一次)
- initMarked();
- });
- </script>
- <style lang="less" scoped>
- .btn-header {
- width: 40px;
- height: 40px;
- margin-right: 5px;
- margin-bottom: 5px;
- }
- .container {
- display: flex;
- flex-direction: row;
- }
- .mini-chat {
- display: flex;
- min-width: none;
- width: 950px;
- height: 85%;
- border-radius: 4px;
- position: fixed;
- top: 50px;
- right: 20px;
- padding: 10px;
- background-color: #0a1a2f;
- background-image: linear-gradient(to bottom, #0b69b6 1%, #0a1a2f 100%), linear-gradient(to left, #0b69b6, #0a1a2f),
- linear-gradient(to top, #0b69b6, #0a1a2f), linear-gradient(to right, #0b69b6, #0a1a2f);
- background-size: 100% 5px, 5px 100%, 100% 5px, 5px 100%;
- background-position: top left, top right, bottom right, bottom left;
- background-repeat: no-repeat;
- z-index: 9999999;
- color: #fff;
- }
- .doc {
- display: flex;
- flex-direction: column;
- min-width: 600px;
- height: 85%;
- border-radius: 4px;
- position: fixed;
- top: 50px;
- right: 51%;
- padding: 10px;
- background-color: #0a1a2f;
- background-image: linear-gradient(to bottom, #0b69b6 1%, #0a1a2f 100%), linear-gradient(to left, #0b69b6, #0a1a2f),
- linear-gradient(to top, #0b69b6, #0a1a2f), linear-gradient(to right, #0b69b6, #0a1a2f);
- background-size: 100% 5px, 5px 100%, 100% 5px, 5px 100%;
- background-position: top left, top right, bottom right, bottom left;
- background-repeat: no-repeat;
- z-index: 9999999;
- color: #fff;
- }
- .close {
- display: flex;
- justify-content: flex-end;
- align-items: center;
- width: 100%;
- padding: 1px;
- border-radius: 8px;
- }
- .closeBtn {
- background-color: #007bff;
- color: white;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- transition: all 0.3s ease;
- }
- .closeBtn:hover {
- background-color: #0056b3;
- }
- .left-side {
- background: #0c2842;
- transition: width 0.5s ease; /* 平滑过渡动画 */
- width: 140px; /* 展开时宽度 */
- position: relative; /* 用于按钮定位 */
- }
- .left-side.collapsed {
- width: 40px; /* 折叠时宽度 */
- }
- .addBtn {
- height: 30px;
- position: absolute;
- background-size: 100% 100%;
- background-position: center;
- padding: 2px;
- right: 10px;
- bottom: 40px;
- left: 10px;
- align-items: center;
- border-radius: 3px;
- cursor: pointer;
- }
- .custom-list {
- height: 650px;
- overflow-y: auto;
- }
- .text-container {
- display: flex;
- justify-content: space-between;
- width: 100%;
- overflow: hidden;
- }
- .btn-container {
- display: flex;
- }
- .jeecg-layout-header-action span[role='img'] {
- padding: 0;
- }
- .text-ellipsis {
- flex: 1;
- }
- .edit-text {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- width: 90px;
- color: #fff;
- font-size: 12px;
- cursor: pointer;
- }
- .edit-icon {
- flex-shrink: 0;
- margin-left: auto;
- line-height: 23px;
- }
- .delete-icon {
- flex-shrink: 0;
- margin-left: auto;
- line-height: 23px;
- }
- .edit-icon:hover {
- color: #1890ff !important;
- cursor: pointer;
- }
- .delete-icon:hover {
- color: #1890ff !important;
- cursor: pointer;
- }
- .edit-input {
- font-size: 10px;
- }
- .btn-text-bg {
- width: 14px;
- height: 14px;
- position: absolute;
- background-size: 100% 100%;
- right: 10px;
- top: 10px;
- left: 10px;
- bottom: 10px;
- }
- .btn-text {
- margin-left: 3px;
- font-size: 12px;
- color: #fff;
- white-space: nowrap;
- margin-left: 30px;
- line-height: 29px;
- }
- .historyBtn {
- width: 20px;
- height: 20px;
- position: absolute;
- background-size: 100% 100%;
- background-position: center;
- padding: 2px;
- right: 10px;
- top: 10px;
- }
- .historyBtn1 {
- width: 20px;
- height: 20px;
- position: absolute;
- background-size: 100% 100%;
- background-position: center;
- left: 3px;
- }
- .divider0 {
- border-bottom: 1px solid #1074c1;
- width: auto;
- margin: 0 10px;
- height: 13%;
- display: block;
- background: transparent;
- }
- .foldBtn {
- width: 20px;
- height: 20px;
- position: absolute;
- background-size: 100% 100%;
- background-position: center;
- padding: 2px;
- right: 10px;
- bottom: 10px;
- cursor: pointer;
- }
- .right-side {
- flex: 1;
- display: flex;
- flex-direction: column;
- width: calc(100% - 134px) !important;
- .title {
- text-align: center;
- font-size: 14px;
- padding: 5px;
- }
- .dialog-area {
- flex: 1;
- gap: 30px;
- overflow-y: auto;
- padding: 5px;
- display: flex;
- flex-direction: column;
- color: #fff;
- .ask-message {
- padding: 10px;
- border-radius: 5px;
- background: #0c2842;
- max-width: 300px;
- min-width: 50px;
- white-space: pre-wrap;
- word-wrap: break-word;
- overflow-wrap: break-word;
- overflow: auto;
- line-height: 1.5;
- }
- .answer-message {
- padding: 10px;
- border-radius: 5px;
- background: #0c2842;
- max-width: 90%;
- }
- }
- .input-area {
- margin: 10px 10px 20px 10px;
- padding: 10px;
- background-color: #043256;
- border: 1px solid #2cb6ff;
- border-radius: 5px;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- gap: 5px;
- height: 25%;
- }
- /* 文件展示区域 */
- .file-preview {
- margin-bottom: 10px;
- padding: 8px;
- border: 1px solid #e5e7eb;
- border-radius: 4px;
- background: #f9fafb;
- }
- .file-info {
- display: flex;
- justify-content: space-between;
- align-items: center;
- font-size: 14px;
- color: #333;
- }
- .clear-btn {
- border: none;
- background: #ff4d4f;
- color: white;
- border-radius: 50%;
- width: 20px;
- height: 20px;
- cursor: pointer;
- font-size: 12px;
- }
- .file-content {
- margin-top: 6px;
- font-size: 13px;
- color: #666;
- line-height: 1.4;
- }
- /* 文件列表容器 */
- .uploaded-files {
- padding: 8px;
- background-color: #f5f5f5;
- border-radius: 4px;
- min-height: 40px;
- }
- /* 单个文件项 */
- .file-item {
- display: inline-flex;
- align-items: center;
- padding: 4px 8px;
- margin-right: 8px;
- margin-bottom: 8px;
- background-color: #fff;
- border: 1px solid #e9e9e9;
- border-radius: 4px;
- }
- /* 文件名 */
- .file-name {
- margin-left: 8px;
- margin-right: 8px;
- max-width: 150px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .ant-input {
- border: none;
- background-color: rgba(255, 255, 255, 0) !important;
- color: #fff;
- }
- .ant-input:focus {
- border: none; /* 聚焦时无边框 */
- outline: none; /* 聚焦时无轮廓 */
- box-shadow: none; /* 移除可能存在的阴影效果 */
- }
- .ctrl-btn {
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- }
- .question-input {
- background-color: #1e293b !important;
- border-color: #334155 !important;
- color: #e2e8f0 !important;
- border-radius: 8px !important;
- padding: 12px 16px !important;
- font-size: 14px !important;
- }
- .question-input::placeholder {
- color: #64748b !important;
- }
- .control-btn {
- height: 25px;
- background-color: #043256;
- border: 1px solid #2cb6ff;
- color: #fff;
- font-size: 10px;
- margin-right: 10px;
- cursor: pointer;
- transition: background-color 0.2s ease; /* 平滑过渡效果 */
- }
- /* 激活状态样式(点击后) */
- .control-btn.active {
- background-color: #2cb6ff; /* 蓝色背景(Ant Design 主色) */
- color: white; /* 白色文字 */
- }
- .control-btn1 {
- height: 20px;
- background-color: #234a6b;
- border: 1px solid #234a6b;
- color: #fff;
- font-size: 10px;
- margin-right: 10px;
- cursor: pointer;
- transition: all 0.2s;
- }
- .control-btn2 {
- height: 20px;
- background-color: #2cb6ff;
- border: 1px solid #2cb6ff;
- color: #fff;
- font-size: 10px;
- margin-right: 10px;
- cursor: pointer;
- transition: all 0.2s;
- }
- /* 文件上传区 */
- .file-upload {
- position: absolute;
- right: 20px;
- bottom: 70px;
- width: 180px;
- display: flex;
- flex-direction: column;
- gap: 10px;
- border: 1px solid #2cb6ff;
- background-color: #234a6b;
- border-radius: 6px;
- padding: 10px;
- }
- .input-container {
- position: relative;
- display: flex;
- align-items: center;
- width: 100%;
- }
- .file-input {
- flex: 1;
- background-color: #234a6b;
- border-color: #2cb6ff !important;
- color: #e2e8f0 !important;
- border-radius: 6px !important;
- font-size: 10px !important;
- padding-right: 70px !important;
- height: 36px !important;
- width: 100% !important;
- }
- .confirm-btn {
- position: absolute;
- right: 5px;
- background-color: #2cb6ff;
- border: none;
- color: #fff;
- border-radius: 4px;
- font-size: 12px;
- padding: 4px 10px;
- cursor: pointer;
- transition: all 0.2s;
- height: 28px;
- }
- .confirm-btn:hover {
- background-color: #2cb6ff;
- }
- .custom-upload {
- width: 100%;
- padding: 0 !important;
- }
- .upload-btn {
- background-color: #234a6b !important;
- border: 1px solid #2188c3 !important;
- color: #dbeafe !important;
- border-radius: 6px !important;
- font-size: 12px !important;
- cursor: pointer;
- transition: all 0.2s;
- padding: 8px 0 !important;
- width: 190% !important;
- height: 36px !important;
- box-sizing: border-box !important;
- }
- .upload-btn:hover {
- background-color: #1f84bd !important;
- color: #fff !important;
- }
- .custom-upload .ant-upload-select:hover .ant-btn {
- border-color: #1f84bd !important;
- }
- .message-wrapper.ai-message-wrapper {
- display: flex;
- align-items: flex-start;
- }
- .answerIcon {
- flex: 0 0 45px;
- }
- .suggestion-item {
- height: 30px;
- margin-left: 45px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 10px 16px;
- border: 1px solid #1890ff;
- color: white;
- border-radius: 4px;
- cursor: pointer;
- width: 33%;
- font-size: 12px;
- }
- .thinking-section {
- border-left: 3px solid #e5e7eb;
- padding-left: 12px;
- margin-bottom: 16px;
- }
- .thinking-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 8px 0;
- cursor: pointer;
- user-select: none;
- }
- .thinking-title {
- font-size: 14px;
- font-weight: 500;
- color: #6b7280;
- }
- }
- </style>
- <style scoped>
- .zxm-popover-inner-content {
- padding: 1px;
- }
- .message-wrapper {
- display: flex;
- align-items: flex-start;
- position: relative;
- }
- .user-message-wrapper {
- flex-direction: row-reverse;
- }
- .ai-message-wrapper {
- flex-direction: row;
- }
- /* 鼠标滑过 .message-wrapper 时,显示图标 */
- .ai-message-wrapper:hover .copy-icon-container {
- opacity: 1;
- visibility: visible;
- }
- .user-message-wrapper:hover .copy-icon-container {
- opacity: 1;
- visibility: visible;
- }
- .copy-icon {
- font-size: 16px;
- color: #666;
- cursor: pointer;
- }
- /* 默认隐藏,hover 时显示 */
- .copy-icon-container {
- position: absolute;
- display: flex;
- flex-direction: row;
- bottom: -20px;
- right: 15%;
- opacity: 0;
- visibility: hidden;
- transition: all 0.3s ease;
- z-index: 10;
- }
- /* 复制图标样式 */
- .copy-icon {
- font-size: 16px;
- cursor: pointer;
- margin-right: 20%;
- float: right;
- color: #fff;
- }
- .message-wrapper:hover .copy-icon {
- opacity: 1;
- }
- .copy-icon:hover {
- color: #1890ff;
- }
- ::v-deep table {
- border-collapse: collapse;
- width: 100%;
- margin: 10px 0;
- border: 1px solid #333;
- }
- ::v-deep th {
- border: 1px solid #333;
- background-color: #234a6b;
- padding: 5px;
- text-align: center;
- }
- ::v-deep td {
- border: 1px solid #ddd;
- padding: 8px 12px;
- text-align: center;
- }
- </style>
- <style scoped>
- /* 已上传文件列表 */
- .file-list {
- margin-top: 20px;
- }
- .pre-container {
- flex: 1;
- width: 100%;
- height: 100%;
- overflow: auto;
- }
- .vue-office-excel {
- background: #fff !important;
- }
- .file-item {
- display: flex;
- align-items: center;
- padding: 12px 15px;
- background-color: #ddd;
- border-radius: 6px;
- margin-bottom: 10px;
- transition: background-color 0.2s ease;
- }
- .file-item:hover {
- background-color: #f0f2f5;
- }
- .file-info {
- flex: 1;
- overflow: hidden;
- }
- .file-name {
- font-size: 15px;
- color: #1d2129;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- margin-bottom: 3px;
- }
- .file-actions {
- display: flex;
- gap: 10px;
- }
- .btn {
- padding: 0px 15px;
- margin-left: 10px;
- border-radius: 4px;
- border: none;
- font-size: 14px;
- cursor: pointer;
- transition: all 0.2s ease;
- }
- .btn-preview {
- background-color: #165dff;
- color: white;
- }
- .btn-preview:hover {
- background-color: #0d47a1;
- }
- .btn-delete {
- background-color: #f2f3f5;
- color: #4e5969;
- }
- .btn-delete:hover {
- background-color: #e5e6eb;
- color: #1d2129;
- }
- </style>
|