2025-03-21 17:16:12 +08:00
|
|
|
|
<template>
|
2025-03-26 18:08:51 +08:00
|
|
|
|
<div class="chat-app">
|
|
|
|
|
|
<!-- 历史会话侧边栏 -->
|
2026-05-12 23:33:55 +08:00
|
|
|
|
<div class="history-sessions" :class="{ collapsed: !sessionIsShow }">
|
|
|
|
|
|
<div class="session-header">
|
|
|
|
|
|
<el-button type="primary" @click="clearCurrent" class="new-session-btn">
|
|
|
|
|
|
<el-icon><Plus /></el-icon>
|
|
|
|
|
|
新会话
|
|
|
|
|
|
</el-button>
|
2025-03-28 21:34:11 +08:00
|
|
|
|
</div>
|
2026-05-12 23:33:55 +08:00
|
|
|
|
|
|
|
|
|
|
<el-card class="session-card" shadow="never">
|
2025-03-26 18:08:51 +08:00
|
|
|
|
<template #header>
|
2026-05-12 23:33:55 +08:00
|
|
|
|
<div class="card-header">
|
|
|
|
|
|
<el-icon><ChatDotRound /></el-icon>
|
|
|
|
|
|
<span>当前会话</span>
|
|
|
|
|
|
</div>
|
2025-09-23 20:35:33 +08:00
|
|
|
|
</template>
|
2026-05-12 23:33:55 +08:00
|
|
|
|
<div class="current-session">
|
|
|
|
|
|
<el-tooltip :content="sessionName" placement="top">
|
|
|
|
|
|
<div class="session-name">{{ getShortenedName(sessionName) || '新会话' }}</div>
|
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
|
</div>
|
2025-03-26 18:08:51 +08:00
|
|
|
|
</el-card>
|
2026-05-12 23:33:55 +08:00
|
|
|
|
|
|
|
|
|
|
<el-card class="session-card history-card" shadow="never">
|
2025-03-26 18:08:51 +08:00
|
|
|
|
<template #header>
|
2026-05-12 23:33:55 +08:00
|
|
|
|
<div class="card-header">
|
|
|
|
|
|
<el-icon><Clock /></el-icon>
|
|
|
|
|
|
<span>历史会话</span>
|
|
|
|
|
|
</div>
|
2025-03-26 18:08:51 +08:00
|
|
|
|
</template>
|
2026-05-12 23:33:55 +08:00
|
|
|
|
<el-scrollbar class="history-scroll">
|
|
|
|
|
|
<div class="history-list">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="(session, index) in historySessions"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
class="history-item"
|
|
|
|
|
|
:class="{ active: session.ID === sessionID }"
|
|
|
|
|
|
@click="loadSession(session.ID)"
|
|
|
|
|
|
>
|
2025-05-12 13:49:08 +08:00
|
|
|
|
<el-tooltip :content="session.Name" placement="top">
|
2026-05-12 23:33:55 +08:00
|
|
|
|
<span class="history-name">{{ getShortenedName(session.Name) }}</span>
|
2025-05-12 13:49:08 +08:00
|
|
|
|
</el-tooltip>
|
2026-05-12 23:33:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-03-26 18:08:51 +08:00
|
|
|
|
</el-scrollbar>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</div>
|
2026-05-12 23:33:55 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 折叠按钮 -->
|
|
|
|
|
|
<div class="toggle-sidebar" @click="showSession">
|
|
|
|
|
|
<el-icon v-if="sessionIsShow"><DArrowLeft /></el-icon>
|
|
|
|
|
|
<el-icon v-else><DArrowRight /></el-icon>
|
2025-03-28 21:34:11 +08:00
|
|
|
|
</div>
|
2025-03-26 18:08:51 +08:00
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
<!-- 聊天区域 -->
|
2025-03-26 18:08:51 +08:00
|
|
|
|
<div class="chat-container">
|
2026-05-12 23:33:55 +08:00
|
|
|
|
<!-- 顶部工具栏 -->
|
|
|
|
|
|
<div class="chat-header">
|
|
|
|
|
|
<el-select v-model="selectModel" placeholder="选择模型" class="model-select">
|
|
|
|
|
|
<el-option v-for="item in ModelList" :key="item.ID" :label="item.Type + ' - ' + item.Description" :value="item.ID" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
|
|
|
|
|
|
<el-dropdown trigger="click" class="params-dropdown">
|
|
|
|
|
|
<el-button link>
|
|
|
|
|
|
<el-icon><Setting /></el-icon>
|
|
|
|
|
|
模型参数
|
|
|
|
|
|
<el-icon><ArrowDown /></el-icon>
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<template #dropdown>
|
|
|
|
|
|
<div class="dropdown-content">
|
|
|
|
|
|
<div class="model-params">
|
|
|
|
|
|
<div class="params-tip">
|
|
|
|
|
|
<el-icon><InfoFilled /></el-icon>
|
|
|
|
|
|
<span>建议仅调整 temperature 或 top_p 其中之一</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 温度参数 -->
|
|
|
|
|
|
<div class="param-item">
|
|
|
|
|
|
<div class="param-label">
|
|
|
|
|
|
<span>温度 (Temperature)</span>
|
|
|
|
|
|
<el-tooltip effect="dark" placement="top" content="采样温度,控制生成随机性(0: 保守,2: 随机)">
|
|
|
|
|
|
<el-icon class="tip-icon"><QuestionFilled /></el-icon>
|
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="param-control">
|
|
|
|
|
|
<el-slider v-model="temperature" :min="0" :max="2" :step="0.1" :show-tooltip="false" />
|
|
|
|
|
|
<span class="param-value">{{ temperature }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Top P 参数 -->
|
|
|
|
|
|
<div class="param-item">
|
|
|
|
|
|
<div class="param-label">
|
|
|
|
|
|
<span>Top P</span>
|
|
|
|
|
|
<el-tooltip effect="dark" placement="top" content="限制候选词范围(0: 严格,1: 宽松)">
|
|
|
|
|
|
<el-icon class="tip-icon"><QuestionFilled /></el-icon>
|
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="param-control">
|
|
|
|
|
|
<el-slider v-model="topP" :min="0" :max="1" :step="0.1" :show-tooltip="false" />
|
|
|
|
|
|
<span class="param-value">{{ topP }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dropdown>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-03-26 18:08:51 +08:00
|
|
|
|
<!-- 消息列表 -->
|
2026-05-12 23:33:55 +08:00
|
|
|
|
<div class="chat-messages" ref="messagesContainer">
|
|
|
|
|
|
<!-- 空状态 -->
|
|
|
|
|
|
<div v-if="messages.length === 0" class="empty-state">
|
|
|
|
|
|
<div class="empty-icon">
|
|
|
|
|
|
<el-icon :size="80"><ChatLineRound /></el-icon>
|
2025-03-26 18:08:51 +08:00
|
|
|
|
</div>
|
2026-05-12 23:33:55 +08:00
|
|
|
|
<div class="empty-text">开始一段对话吧</div>
|
|
|
|
|
|
<div class="empty-hint">输入消息或选择文件,与 AI 进行交流</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 消息列表 -->
|
|
|
|
|
|
<div v-else class="messages-list">
|
|
|
|
|
|
<div v-for="(message, index) in messages" :key="index" :class="['message-item', message.role]">
|
|
|
|
|
|
<div class="message-avatar">
|
|
|
|
|
|
<el-icon v-if="message.role === 'assistant'" class="ai-icon"><Promotion /></el-icon>
|
|
|
|
|
|
<el-icon v-else class="user-icon"><User /></el-icon>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="message-wrapper">
|
|
|
|
|
|
<div class="message-content">
|
|
|
|
|
|
<div v-html="renderMarkdown(message, index)" class="markdown-body"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="message-actions">
|
|
|
|
|
|
<el-tooltip content="复制" placement="top">
|
|
|
|
|
|
<el-button type="text" :icon="DocumentCopy" @click="copyMessage(message.content)" />
|
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
|
<el-tooltip content="保存为文件" placement="top">
|
|
|
|
|
|
<el-button type="text" :icon="Document" @click="MessageTextToDoc(message.content)" />
|
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
|
</div>
|
2025-03-28 21:34:11 +08:00
|
|
|
|
</div>
|
2025-03-26 18:08:51 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
<!-- 加载状态 -->
|
|
|
|
|
|
<div v-if="loading" class="message-item assistant">
|
|
|
|
|
|
<div class="message-avatar">
|
|
|
|
|
|
<el-icon class="ai-icon"><Promotion /></el-icon>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="message-wrapper">
|
|
|
|
|
|
<div class="message-content">
|
|
|
|
|
|
<div class="typing-indicator">
|
|
|
|
|
|
<span></span>
|
|
|
|
|
|
<span></span>
|
|
|
|
|
|
<span></span>
|
2025-04-23 15:11:16 +08:00
|
|
|
|
</div>
|
2026-05-12 23:33:55 +08:00
|
|
|
|
<div class="typing-text">正在思考... ({{ currentAIMessage.length }} 字符)</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-04-23 15:11:16 +08:00
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
<!-- 输入区域 -->
|
|
|
|
|
|
<div class="chat-input-wrapper">
|
|
|
|
|
|
<!-- 已选文件 -->
|
|
|
|
|
|
<div v-if="selectedFiles.length > 0" class="selected-files">
|
|
|
|
|
|
<el-tag
|
|
|
|
|
|
v-for="(file, index) in selectedFiles"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
closable
|
|
|
|
|
|
@close="removeFile(index)"
|
|
|
|
|
|
class="file-tag"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-icon><Document /></el-icon>
|
|
|
|
|
|
{{ file.UserFileName }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</div>
|
2025-04-23 15:11:16 +08:00
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
<div class="input-area">
|
|
|
|
|
|
<div class="input-tools">
|
|
|
|
|
|
<el-tooltip content="选择文件" placement="top">
|
|
|
|
|
|
<el-button type="text" :icon="FolderOpened" @click="handleSelectFileVisible" />
|
2025-05-08 14:16:39 +08:00
|
|
|
|
</el-tooltip>
|
2026-05-12 23:33:55 +08:00
|
|
|
|
</div>
|
2025-04-01 14:40:20 +08:00
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
<el-input
|
|
|
|
|
|
v-model="inputMessage"
|
|
|
|
|
|
type="textarea"
|
|
|
|
|
|
placeholder="输入消息... (Enter 发送,Shift+Enter 换行)"
|
|
|
|
|
|
@keydown="handleKeydown"
|
|
|
|
|
|
class="chat-textarea"
|
|
|
|
|
|
:autosize="{ minRows: 1, maxRows: 6 }"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
:icon="loading ? VideoPause : Check"
|
|
|
|
|
|
@click="sendMessage"
|
|
|
|
|
|
class="send-btn"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ loading ? '停止' : '发送' }}
|
|
|
|
|
|
</el-button>
|
2025-04-01 16:07:25 +08:00
|
|
|
|
</div>
|
2026-05-12 23:33:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 文件对话框 -->
|
|
|
|
|
|
<el-dialog v-model="selectFileVisible" title="从上传文件中选择" width="50%" class="file-dialog">
|
|
|
|
|
|
<div class="dialog-search">
|
|
|
|
|
|
<el-input placeholder="搜索文件" v-model="searchFileQuery" prefix-icon="Search" clearable />
|
|
|
|
|
|
<el-button type="primary" @click="uploadMessageFile" class="upload-btn">
|
|
|
|
|
|
<el-icon><Upload /></el-icon>
|
|
|
|
|
|
上传文件
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="file-list">
|
|
|
|
|
|
<el-checkbox-group v-model="selectedFiles">
|
|
|
|
|
|
<div v-for="(item, index) in filteredFiles" :key="index" class="file-item">
|
|
|
|
|
|
<el-checkbox :label="item" />
|
|
|
|
|
|
<div class="file-info">
|
|
|
|
|
|
<el-icon class="file-icon">
|
|
|
|
|
|
<Picture v-if="item.UploadType === 'image'" />
|
|
|
|
|
|
<Document v-else />
|
|
|
|
|
|
</el-icon>
|
|
|
|
|
|
<span class="file-name">{{ item.UserFileName }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-checkbox-group>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="footer-bar">
|
|
|
|
|
|
<span class="selected-count">已选 {{ selectedFiles.length }} 个文件</span>
|
|
|
|
|
|
<div>
|
2025-04-01 16:07:25 +08:00
|
|
|
|
<el-button @click="selectFileVisible = false">取消</el-button>
|
2026-05-12 23:33:55 +08:00
|
|
|
|
<el-button type="primary" @click="handleSelectFileConfirm">确认添加</el-button>
|
2025-04-01 16:07:25 +08:00
|
|
|
|
</div>
|
2026-05-12 23:33:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
2025-04-01 16:07:25 +08:00
|
|
|
|
<!-- 上传文件对话框 -->
|
2026-05-12 23:33:55 +08:00
|
|
|
|
<el-dialog title="上传文件" v-model="uploadFileVisible" width="50%" :before-close="handleUploadFileClose">
|
|
|
|
|
|
<UploadFile />
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 文本创建文件对话 -->
|
|
|
|
|
|
<el-dialog title="文本创建文件" v-model="textToDocFileVisible" width="70%" class="doc-dialog" :before-close="handleMessageTextToDOCClose">
|
|
|
|
|
|
<div class="doc-header">
|
|
|
|
|
|
<el-input v-model="textToDocFileName" placeholder="输入文件名..." class="doc-name-input">
|
|
|
|
|
|
<template #append>
|
|
|
|
|
|
<el-select v-model="selectFileDocType" placeholder="类型">
|
|
|
|
|
|
<el-option label="docx" value="docx" />
|
|
|
|
|
|
<el-option label="txt" value="txt" />
|
|
|
|
|
|
<el-option label="md" value="md" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-input>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div ref="vditorRef" class="vditor-container"></div>
|
|
|
|
|
|
<div class="doc-footer">
|
2025-05-08 14:16:39 +08:00
|
|
|
|
<el-button @click="textToDocFileVisible = false">取消</el-button>
|
2026-05-12 23:33:55 +08:00
|
|
|
|
<el-button type="primary" @click="HandleTextToDocFile">确认创建</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-dialog>
|
2025-04-01 14:40:20 +08:00
|
|
|
|
</div>
|
2025-03-25 19:51:06 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
2026-04-03 20:58:10 +08:00
|
|
|
|
<script setup lang="ts" name="gen-chat">
|
2025-09-23 20:35:33 +08:00
|
|
|
|
import { ref, onMounted, onUnmounted, reactive, nextTick, watch } from "vue";
|
2026-05-12 23:33:55 +08:00
|
|
|
|
import {
|
|
|
|
|
|
Check,
|
|
|
|
|
|
DocumentCopy,
|
|
|
|
|
|
Document,
|
|
|
|
|
|
Plus,
|
|
|
|
|
|
ChatDotRound,
|
|
|
|
|
|
Clock,
|
|
|
|
|
|
DArrowLeft,
|
|
|
|
|
|
DArrowRight,
|
|
|
|
|
|
Setting,
|
|
|
|
|
|
ArrowDown,
|
|
|
|
|
|
QuestionFilled,
|
|
|
|
|
|
ChatLineRound,
|
|
|
|
|
|
Promotion,
|
|
|
|
|
|
User,
|
|
|
|
|
|
FolderOpened,
|
|
|
|
|
|
VideoPause,
|
|
|
|
|
|
Upload,
|
|
|
|
|
|
Picture,
|
|
|
|
|
|
InfoFilled,
|
|
|
|
|
|
} from "@element-plus/icons-vue";
|
|
|
|
|
|
import { ElMessage } from "element-plus";
|
|
|
|
|
|
import MarkdownIt from "markdown-it";
|
|
|
|
|
|
import hljs from "highlight.js";
|
|
|
|
|
|
import "highlight.js/styles/github.css";
|
|
|
|
|
|
import markdownItHighlightjs from "markdown-it-highlightjs";
|
|
|
|
|
|
import markdownItKatex from "markdown-it-katex";
|
|
|
|
|
|
import mermaidPlugin from "@agoose77/markdown-it-mermaid";
|
|
|
|
|
|
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
|
|
|
|
|
import "katex/dist/katex.min.css";
|
|
|
|
|
|
import Vditor from 'vditor';
|
|
|
|
|
|
import 'vditor/dist/index.css';
|
|
|
|
|
|
|
2025-04-01 16:07:25 +08:00
|
|
|
|
import { WSMessage, GenMessage } from "@/types/im";
|
2025-03-25 19:51:06 +08:00
|
|
|
|
import { GetMessageService } from "@/api/im";
|
2025-04-01 14:40:20 +08:00
|
|
|
|
import { FindUserFileService } from "@/api/file";
|
2025-04-01 16:07:25 +08:00
|
|
|
|
import { Model } from "@/types/model";
|
2025-09-23 20:35:33 +08:00
|
|
|
|
import { UserUISettings } from '@/types/user';
|
2025-04-01 16:07:25 +08:00
|
|
|
|
import { File, fileUrl } from "@/types/file";
|
2025-03-26 18:08:51 +08:00
|
|
|
|
import { Session } from "@/types/session";
|
|
|
|
|
|
import { FindSessionService } from "@/api/session";
|
2025-05-08 14:16:39 +08:00
|
|
|
|
import { SetMessageTextToDocService } from "@/api/tool";
|
2025-04-01 16:07:25 +08:00
|
|
|
|
import UploadFile from "@/components/upload-file.vue";
|
2025-05-20 20:38:36 +08:00
|
|
|
|
import { updateUserUIconfigInfoService } from "@/api/user";
|
2025-03-31 13:58:13 +08:00
|
|
|
|
import { FindModelListByFunctionName } from "@/api/function";
|
2025-05-08 14:16:39 +08:00
|
|
|
|
|
2025-03-25 19:51:06 +08:00
|
|
|
|
interface Message {
|
|
|
|
|
|
role: "user" | "assistant";
|
|
|
|
|
|
content: string;
|
|
|
|
|
|
finished?: boolean;
|
|
|
|
|
|
}
|
2026-05-12 23:33:55 +08:00
|
|
|
|
|
2025-04-07 19:31:41 +08:00
|
|
|
|
interface FileMessage {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
file_content: File;
|
|
|
|
|
|
file_type: string | null;
|
2025-04-07 19:31:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface GenerationMessage {
|
|
|
|
|
|
text: string;
|
|
|
|
|
|
file_content: FileMessage[];
|
|
|
|
|
|
}
|
2025-03-26 18:08:51 +08:00
|
|
|
|
|
2025-03-27 16:09:22 +08:00
|
|
|
|
const md = new MarkdownIt();
|
2025-03-27 17:33:35 +08:00
|
|
|
|
md.use(markdownItHighlightjs, {
|
2025-03-27 16:09:22 +08:00
|
|
|
|
hljs,
|
|
|
|
|
|
auto: true,
|
2025-03-27 17:33:35 +08:00
|
|
|
|
code: true,
|
|
|
|
|
|
});
|
2025-04-01 21:31:59 +08:00
|
|
|
|
md.renderer.rules.image = function (tokens, idx, options, env, self) {
|
|
|
|
|
|
const token = tokens[idx];
|
2025-09-23 20:35:33 +08:00
|
|
|
|
token.attrSet('width', '400');
|
2026-05-12 23:33:55 +08:00
|
|
|
|
token.attrSet('style', 'max-width: 100%; height: auto; border-radius: 8px;');
|
2025-04-01 21:31:59 +08:00
|
|
|
|
return self.renderToken(tokens, idx, options, env, self);
|
|
|
|
|
|
};
|
2025-03-27 18:49:14 +08:00
|
|
|
|
md.use(markdownItKatex);
|
|
|
|
|
|
md.use(mermaidPlugin);
|
|
|
|
|
|
|
2025-03-26 18:08:51 +08:00
|
|
|
|
const historySessions = ref<Session[]>([]);
|
2025-03-25 19:51:06 +08:00
|
|
|
|
const loading = ref(false);
|
2026-05-12 23:33:55 +08:00
|
|
|
|
const canStop = ref(false);
|
2025-03-25 19:51:06 +08:00
|
|
|
|
const isUserScrolling = ref<boolean>(false);
|
2026-05-12 23:33:55 +08:00
|
|
|
|
const messages = reactive<Message[]>([]);
|
2025-03-25 19:51:06 +08:00
|
|
|
|
const inputMessage = ref("");
|
|
|
|
|
|
const currentAIMessage = ref("");
|
|
|
|
|
|
const sessionID = ref(0);
|
|
|
|
|
|
const messagesContainer = ref<HTMLDivElement | null>(null);
|
2026-05-12 23:33:55 +08:00
|
|
|
|
const sessionIsShow = ref(true);
|
2025-03-26 18:08:51 +08:00
|
|
|
|
const sessionName = ref("");
|
2025-03-31 13:58:13 +08:00
|
|
|
|
const ModelList = ref<Model[]>([]);
|
|
|
|
|
|
const selectModel = ref(0);
|
2025-04-23 15:11:16 +08:00
|
|
|
|
const temperature = ref(0.8);
|
2025-03-31 19:06:57 +08:00
|
|
|
|
const topP = ref(0.9);
|
2026-05-12 23:33:55 +08:00
|
|
|
|
const selectedFiles = ref<File[]>([]);
|
|
|
|
|
|
const selectFileVisible = ref(false);
|
|
|
|
|
|
const searchFileQuery = ref("");
|
|
|
|
|
|
const filteredFiles = ref<File[]>([]);
|
|
|
|
|
|
const uploadFileVisible = ref(false);
|
|
|
|
|
|
const textToDocFileVisible = ref(false);
|
|
|
|
|
|
const selectFileDocType = ref("docx");
|
|
|
|
|
|
const textToDocFileName = ref("");
|
|
|
|
|
|
const textToDocFileContent = ref("");
|
|
|
|
|
|
const vditor = ref();
|
2025-05-19 19:14:22 +08:00
|
|
|
|
const vditorRef = ref(null);
|
2026-05-12 23:33:55 +08:00
|
|
|
|
const userUIconfigInfo = ref<UserUISettings>({} as UserUISettings);
|
|
|
|
|
|
const historyMsgHtml = ref([]);
|
2025-05-20 20:38:36 +08:00
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
// 监听需要保存的配置
|
2025-05-20 20:38:36 +08:00
|
|
|
|
watch(
|
2025-09-23 20:35:33 +08:00
|
|
|
|
[selectModel, temperature, topP, sessionID],
|
2026-05-12 23:33:55 +08:00
|
|
|
|
() => {
|
2025-05-20 20:38:36 +08:00
|
|
|
|
updateUserUIconfigInfo();
|
2025-09-23 20:35:33 +08:00
|
|
|
|
}
|
2026-05-12 23:33:55 +08:00
|
|
|
|
);
|
2025-03-25 19:51:06 +08:00
|
|
|
|
|
2025-09-23 20:35:33 +08:00
|
|
|
|
const renderMarkdown = (message: Message, index: number) => {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
if (message.finished === false) {
|
2025-04-13 15:51:15 +08:00
|
|
|
|
return message.content;
|
|
|
|
|
|
}
|
2025-09-23 20:35:33 +08:00
|
|
|
|
if (historyMsgHtml.value[index]) {
|
2025-04-13 15:51:15 +08:00
|
|
|
|
return historyMsgHtml.value[index];
|
|
|
|
|
|
}
|
|
|
|
|
|
const html = md.render(message.content);
|
|
|
|
|
|
historyMsgHtml.value.push(html);
|
|
|
|
|
|
return html;
|
2025-03-25 19:51:06 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const scrollToBottom = () => {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
if (messagesContainer.value) {
|
|
|
|
|
|
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-03-25 19:51:06 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-03-27 16:09:22 +08:00
|
|
|
|
const copyCode = (code: string) => {
|
|
|
|
|
|
navigator.clipboard.writeText(code).then(() => {
|
|
|
|
|
|
ElMessage.success("代码已复制到剪贴板");
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
2026-05-12 23:33:55 +08:00
|
|
|
|
|
2025-04-01 14:40:20 +08:00
|
|
|
|
const removeFile = (index: number) => {
|
|
|
|
|
|
selectedFiles.value.splice(index, 1);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSelectFileVisible = async () => {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
await getFileListData();
|
|
|
|
|
|
selectFileVisible.value = true;
|
2025-04-01 14:40:20 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-04-01 16:07:25 +08:00
|
|
|
|
const handleUploadFileClose = async () => {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
uploadFileVisible.value = false;
|
|
|
|
|
|
await getFileListData();
|
2025-04-01 15:45:43 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-05-08 14:16:39 +08:00
|
|
|
|
const handleMessageTextToDOCClose = async () => {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
textToDocFileVisible.value = false;
|
2025-04-01 14:40:20 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-04-01 15:45:43 +08:00
|
|
|
|
const uploadMessageFile = () => {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
uploadFileVisible.value = true;
|
2025-04-01 15:45:43 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-04-01 14:40:20 +08:00
|
|
|
|
const handleSelectFileConfirm = () => {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
selectFileVisible.value = false;
|
2025-04-01 14:40:20 +08:00
|
|
|
|
};
|
2025-03-27 16:09:22 +08:00
|
|
|
|
|
|
|
|
|
|
const doButtonD = () => {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
const codeBlocks = document.querySelectorAll<HTMLElement>(".markdown-body pre");
|
|
|
|
|
|
codeBlocks.forEach((pre) => {
|
|
|
|
|
|
if (pre.querySelector(".code-header")) return;
|
|
|
|
|
|
|
|
|
|
|
|
const code = pre.querySelector("code");
|
|
|
|
|
|
const lang = code?.className.match(/language-(\w+)/)?.[1] || "code";
|
|
|
|
|
|
|
|
|
|
|
|
const header = document.createElement("div");
|
|
|
|
|
|
header.className = "code-header";
|
|
|
|
|
|
header.innerHTML = `
|
|
|
|
|
|
<span class="code-lang">${lang}</span>
|
|
|
|
|
|
<button class="copy-btn" title="复制代码">
|
|
|
|
|
|
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
|
|
|
|
|
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
|
|
|
|
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
pre.style.position = "relative";
|
|
|
|
|
|
pre.style.marginTop = "0";
|
|
|
|
|
|
pre.insertBefore(header, pre.firstChild);
|
|
|
|
|
|
|
|
|
|
|
|
header.querySelector(".copy-btn")?.addEventListener("click", () => {
|
|
|
|
|
|
copyCode(code?.textContent || "");
|
|
|
|
|
|
ElMessage.success("复制成功");
|
|
|
|
|
|
});
|
2025-03-27 16:09:22 +08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
// 处理键盘事件
|
|
|
|
|
|
const handleKeydown = (e: KeyboardEvent) => {
|
|
|
|
|
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
sendMessage();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-05-20 20:38:36 +08:00
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
onMounted(async () => {
|
2025-05-20 20:38:36 +08:00
|
|
|
|
userUIconfigInfo.value = JSON.parse(
|
|
|
|
|
|
localStorage.getItem("userUIconfigInfo") || "{}"
|
|
|
|
|
|
);
|
2025-05-21 20:00:58 +08:00
|
|
|
|
messagesContainer.value = document.querySelector(".chat-messages");
|
2026-05-12 23:33:55 +08:00
|
|
|
|
// 页面加载时获取历史会话
|
|
|
|
|
|
await fetchHistorySessions();
|
2025-03-25 19:51:06 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
// 获取历史会话
|
|
|
|
|
|
const fetchHistorySessions = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
let req = {
|
|
|
|
|
|
token: localStorage.getItem("token"),
|
|
|
|
|
|
type: "UserID",
|
|
|
|
|
|
session_type: 1,
|
|
|
|
|
|
};
|
|
|
|
|
|
let result = await FindSessionService(req);
|
|
|
|
|
|
historySessions.value = result.data;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error("获取历史会话失败:", e);
|
2025-09-23 20:35:33 +08:00
|
|
|
|
}
|
2026-05-12 23:33:55 +08:00
|
|
|
|
};
|
2025-09-23 20:35:33 +08:00
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
// 清理工作可以在这里添加
|
|
|
|
|
|
});
|
2025-04-27 13:09:57 +08:00
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
const doReceiveMessageSSE = (event: any) => {
|
2025-09-23 20:35:33 +08:00
|
|
|
|
let msg: WSMessage = JSON.parse(event.data.replace("data: ", ""));
|
|
|
|
|
|
const existingMessage = messages.find(
|
2026-05-12 23:33:55 +08:00
|
|
|
|
(m) => m.role === "assistant" && !m.finished
|
2025-09-23 20:35:33 +08:00
|
|
|
|
);
|
|
|
|
|
|
if (existingMessage) {
|
|
|
|
|
|
existingMessage.content += msg.msg.msg.response;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
messages.push({
|
|
|
|
|
|
role: "assistant",
|
|
|
|
|
|
content: msg.msg.msg.response,
|
|
|
|
|
|
finished: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sessionID.value = msg.session_id;
|
|
|
|
|
|
currentAIMessage.value += msg.msg.msg.response;
|
|
|
|
|
|
if (msg.msg.msg.done) {
|
|
|
|
|
|
const assistantMessage = messages[messages.length - 1];
|
|
|
|
|
|
assistantMessage.finished = true;
|
|
|
|
|
|
loading.value = false;
|
2026-05-12 23:33:55 +08:00
|
|
|
|
canStop.value = false;
|
2025-09-23 20:35:33 +08:00
|
|
|
|
currentAIMessage.value = "";
|
|
|
|
|
|
doButtonD();
|
|
|
|
|
|
}
|
2026-05-12 23:33:55 +08:00
|
|
|
|
scrollToBottom();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-05-20 20:38:36 +08:00
|
|
|
|
const updateUserUIconfigInfo = () => {
|
2025-09-23 20:35:33 +08:00
|
|
|
|
let req: UserUISettings = JSON.parse(localStorage.getItem("userUIconfigInfo") || "{}");
|
|
|
|
|
|
if (req.user_id == 0) {
|
|
|
|
|
|
req.user_id = parseInt(localStorage.getItem("user_id") || "0");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
req.gen_ai_function.model_id = selectModel.value;
|
|
|
|
|
|
req.gen_ai_function.temperature = temperature.value;
|
|
|
|
|
|
req.gen_ai_function.top_p = topP.value;
|
|
|
|
|
|
req.gen_ai_function.session_id = sessionID.value;
|
|
|
|
|
|
}
|
|
|
|
|
|
updateUserUIconfigInfoService(req, localStorage.getItem("token")).then((res) => {
|
|
|
|
|
|
if (res["code"] === 0) {
|
|
|
|
|
|
console.log("保存成功");
|
2025-05-20 20:38:36 +08:00
|
|
|
|
}
|
2025-09-23 20:35:33 +08:00
|
|
|
|
});
|
2026-05-12 23:33:55 +08:00
|
|
|
|
};
|
2025-09-23 20:35:33 +08:00
|
|
|
|
|
2025-05-21 20:00:58 +08:00
|
|
|
|
const sendMessage = async () => {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
if (loading.value) {
|
|
|
|
|
|
ElMessage.info("正在停止生成...");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (inputMessage.value.trim() === "" && selectedFiles.value.length === 0) {
|
|
|
|
|
|
ElMessage.warning("消息不能为空");
|
2025-05-21 20:00:58 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
await nextTick();
|
2025-09-23 20:35:33 +08:00
|
|
|
|
sendMessageWithFileUseSSE();
|
2025-03-26 18:08:51 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-23 20:35:33 +08:00
|
|
|
|
const sendMessageWithFileUseSSE = async () => {
|
|
|
|
|
|
checkTokenIsValidAndRefresh();
|
|
|
|
|
|
let url = "https://pm.ljsea.top/im/chat_completion";
|
2026-05-12 23:33:55 +08:00
|
|
|
|
let headers = { "token": localStorage.getItem("token") || "" };
|
|
|
|
|
|
|
|
|
|
|
|
let end_msg: any = {
|
2025-09-23 20:35:33 +08:00
|
|
|
|
msg: inputMessage.value,
|
|
|
|
|
|
type: "null",
|
|
|
|
|
|
function: "gen-ai-chat",
|
|
|
|
|
|
session_id: sessionID.value,
|
|
|
|
|
|
model_id: selectModel.value,
|
|
|
|
|
|
temperature: temperature.value,
|
|
|
|
|
|
top_p: topP.value,
|
|
|
|
|
|
};
|
2026-05-12 23:33:55 +08:00
|
|
|
|
|
2025-09-23 20:35:33 +08:00
|
|
|
|
if (selectedFiles.value.length > 0) {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
let file_contents: FileMessage[] = [];
|
2025-09-23 20:35:33 +08:00
|
|
|
|
for (let i = 0; i < selectedFiles.value.length; i++) {
|
|
|
|
|
|
let file: File = selectedFiles.value[i];
|
2026-05-12 23:33:55 +08:00
|
|
|
|
let file_type = file.UserFileName.match(/\.(jpg|jpeg|png|gif|bmp|webp)$/i) ? "image_file" : "text_file";
|
|
|
|
|
|
file_contents.push({
|
2025-09-23 20:35:33 +08:00
|
|
|
|
file_content: file,
|
|
|
|
|
|
file_type: file_type,
|
2026-05-12 23:33:55 +08:00
|
|
|
|
});
|
2025-09-23 20:35:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
let msg: GenerationMessage = {
|
|
|
|
|
|
text: inputMessage.value,
|
|
|
|
|
|
file_content: file_contents,
|
|
|
|
|
|
};
|
2026-05-12 23:33:55 +08:00
|
|
|
|
end_msg.msg = JSON.stringify(msg);
|
|
|
|
|
|
end_msg.is_file = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let pMsgContent = "";
|
|
|
|
|
|
if (end_msg.is_file) {
|
|
|
|
|
|
let file_msg: GenerationMessage = JSON.parse(end_msg.msg);
|
|
|
|
|
|
for (let i = 0; i < file_msg.file_content.length; i++) {
|
|
|
|
|
|
const fc = file_msg.file_content[i];
|
|
|
|
|
|
if (fc.file_type === "image_file") {
|
|
|
|
|
|
pMsgContent += `\n\n`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
pMsgContent += `📎 文件:[${fc.file_content.UserFileName}](${fileUrl}${fc.file_content.file_store_name})\n\n`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
pMsgContent += file_msg.text;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
pMsgContent = end_msg.msg;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
messages.push({ role: "user", content: pMsgContent, finished: true });
|
|
|
|
|
|
inputMessage.value = "";
|
|
|
|
|
|
selectedFiles.value = [];
|
|
|
|
|
|
loading.value = true;
|
|
|
|
|
|
canStop.value = true;
|
|
|
|
|
|
if (sessionID.value === 0) {
|
|
|
|
|
|
sessionName.value = pMsgContent.slice(0, 50);
|
2025-09-23 20:35:33 +08:00
|
|
|
|
}
|
2026-05-12 23:33:55 +08:00
|
|
|
|
scrollToBottom();
|
|
|
|
|
|
|
2025-09-23 20:35:33 +08:00
|
|
|
|
fetchEventSource(url, {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
...headers,
|
2025-09-23 20:35:33 +08:00
|
|
|
|
},
|
2026-05-12 23:33:55 +08:00
|
|
|
|
body: JSON.stringify(end_msg),
|
|
|
|
|
|
openWhenHidden: true,
|
2025-09-23 20:35:33 +08:00
|
|
|
|
onmessage: (event) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
doReceiveMessageSSE(event);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('Failed to parse SSE data:', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
onerror: (error) => {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
ElMessage.error("发送失败! 请刷新页面重试");
|
|
|
|
|
|
loading.value = false;
|
|
|
|
|
|
canStop.value = false;
|
2025-09-23 20:35:33 +08:00
|
|
|
|
},
|
|
|
|
|
|
onclose: () => {
|
|
|
|
|
|
console.log('SSE connection closed');
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-04-07 19:31:41 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-03-26 18:08:51 +08:00
|
|
|
|
const loadSession = async (session_id: number) => {
|
|
|
|
|
|
sessionID.value = session_id;
|
2026-05-12 23:33:55 +08:00
|
|
|
|
messages.length = 0;
|
|
|
|
|
|
historyMsgHtml.value.length = 0;
|
2025-03-26 18:08:51 +08:00
|
|
|
|
sessionName.value = historySessions.value.find(
|
2026-05-12 23:33:55 +08:00
|
|
|
|
(session) => session.ID === session_id
|
|
|
|
|
|
)?.Name || "";
|
2025-03-26 18:08:51 +08:00
|
|
|
|
await getMessage(session_id);
|
2025-03-27 16:09:22 +08:00
|
|
|
|
scrollToBottom();
|
2025-03-27 17:33:35 +08:00
|
|
|
|
doButtonD();
|
2025-03-26 18:08:51 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const clearCurrent = () => {
|
|
|
|
|
|
sessionID.value = 0;
|
2026-05-12 23:33:55 +08:00
|
|
|
|
messages.length = 0;
|
|
|
|
|
|
historyMsgHtml.value.length = 0;
|
2025-03-26 18:08:51 +08:00
|
|
|
|
sessionName.value = "新会话";
|
2026-05-12 23:33:55 +08:00
|
|
|
|
ElMessage.success("新会话已创建! 可以开始聊天了");
|
2025-03-25 19:51:06 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-03-26 18:08:51 +08:00
|
|
|
|
const showSession = async () => {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
if (!sessionIsShow.value) {
|
|
|
|
|
|
await fetchHistorySessions();
|
|
|
|
|
|
}
|
2025-03-26 18:08:51 +08:00
|
|
|
|
sessionIsShow.value = !sessionIsShow.value;
|
|
|
|
|
|
};
|
2026-05-12 23:33:55 +08:00
|
|
|
|
|
2025-03-26 18:08:51 +08:00
|
|
|
|
const getShortenedName = (name: string) => {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
if (!name) return "";
|
|
|
|
|
|
if (name.length > 20) {
|
|
|
|
|
|
return name.slice(0, 20) + "...";
|
2025-03-26 18:08:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
return name;
|
|
|
|
|
|
};
|
2025-03-25 19:51:06 +08:00
|
|
|
|
|
2025-03-26 18:08:51 +08:00
|
|
|
|
const getMessage = async (session_id: number) => {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
historyMsgHtml.value.length = 0;
|
|
|
|
|
|
let result: any = {};
|
2025-04-07 19:31:41 +08:00
|
|
|
|
try {
|
|
|
|
|
|
let req = {
|
|
|
|
|
|
token: localStorage.getItem("token"),
|
|
|
|
|
|
session_id: session_id,
|
|
|
|
|
|
};
|
|
|
|
|
|
result = await GetMessageService(req);
|
|
|
|
|
|
if (result["code"] === 0) {
|
|
|
|
|
|
let data = result["data"];
|
|
|
|
|
|
for (let i = 0; i < data.length; i++) {
|
|
|
|
|
|
if (data[i]["Type"] === 3) {
|
|
|
|
|
|
let msg: GenMessage = data[i];
|
2025-09-23 20:35:33 +08:00
|
|
|
|
let pMsgContent = "";
|
2026-05-12 23:33:55 +08:00
|
|
|
|
if (msg.Status === 3) {
|
2025-04-07 19:31:41 +08:00
|
|
|
|
let file_msg: GenerationMessage = JSON.parse(msg.Msg);
|
2026-05-12 23:33:55 +08:00
|
|
|
|
for (let j = 0; j < file_msg.file_content.length; j++) {
|
|
|
|
|
|
const fc = file_msg.file_content[j];
|
|
|
|
|
|
if (fc.file_type === "image_file") {
|
|
|
|
|
|
pMsgContent += `\n\n`;
|
2025-09-23 20:35:33 +08:00
|
|
|
|
} else {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
pMsgContent += `📎 文件:[${fc.file_content.UserFileName}](${fileUrl}${fc.file_content.file_store_name})\n\n`;
|
2025-09-23 20:35:33 +08:00
|
|
|
|
}
|
2025-04-07 19:31:41 +08:00
|
|
|
|
}
|
2026-05-12 23:33:55 +08:00
|
|
|
|
pMsgContent += file_msg.text;
|
2025-04-07 19:31:41 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
pMsgContent = msg.Msg;
|
|
|
|
|
|
}
|
|
|
|
|
|
messages.push({
|
|
|
|
|
|
role: "user",
|
|
|
|
|
|
content: pMsgContent,
|
|
|
|
|
|
finished: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
} else if (data[i]["Type"] === 4) {
|
|
|
|
|
|
messages.push({
|
|
|
|
|
|
role: "assistant",
|
|
|
|
|
|
content: data[i]["Msg"],
|
|
|
|
|
|
finished: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.log(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-05-08 14:16:39 +08:00
|
|
|
|
const MessageTextToDoc = async (content: string) => {
|
|
|
|
|
|
textToDocFileContent.value = content;
|
2025-05-19 19:14:22 +08:00
|
|
|
|
textToDocFileVisible.value = true;
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
vditor.value = new Vditor(vditorRef.value, {
|
2025-05-08 14:16:39 +08:00
|
|
|
|
mode: 'sv',
|
2026-05-12 23:33:55 +08:00
|
|
|
|
height: '500px',
|
2025-05-08 14:16:39 +08:00
|
|
|
|
width: '100%',
|
|
|
|
|
|
cache: { enable: false },
|
|
|
|
|
|
value: textToDocFileContent.value,
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-03-27 16:09:22 +08:00
|
|
|
|
const copyMessage = (content: string) => {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
navigator.clipboard.writeText(content).then(() => {
|
|
|
|
|
|
ElMessage.success("复制成功");
|
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
|
ElMessage.error("复制失败");
|
|
|
|
|
|
});
|
2025-03-27 16:09:22 +08:00
|
|
|
|
};
|
2025-03-31 13:58:13 +08:00
|
|
|
|
|
2025-09-23 20:35:33 +08:00
|
|
|
|
const checkTokenIsValidAndRefresh = () => {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
let last_refresh = localStorage.getItem("refresh_time");
|
2025-09-23 20:35:33 +08:00
|
|
|
|
let now = Date.now();
|
|
|
|
|
|
let isValid = false;
|
|
|
|
|
|
if (last_refresh) {
|
|
|
|
|
|
let diff = now - parseInt(last_refresh);
|
|
|
|
|
|
if (diff < 60 * 60 * 1000) {
|
|
|
|
|
|
isValid = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-12 23:33:55 +08:00
|
|
|
|
if (!isValid) {
|
2025-09-23 20:35:33 +08:00
|
|
|
|
GetModelListByFunctionName();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-03-31 13:58:13 +08:00
|
|
|
|
const GetModelListByFunctionName = async () => {
|
|
|
|
|
|
let req = {
|
|
|
|
|
|
function: "gen-ai-chat",
|
|
|
|
|
|
token: localStorage.getItem("token"),
|
|
|
|
|
|
};
|
2025-04-01 16:07:25 +08:00
|
|
|
|
try {
|
2025-03-31 13:58:13 +08:00
|
|
|
|
let result = await FindModelListByFunctionName(req);
|
|
|
|
|
|
if (result["code"] === 0) {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
ModelList.value = result.data;
|
|
|
|
|
|
if (userUIconfigInfo.value.gen_ai_function?.model_id) {
|
2025-09-23 20:35:33 +08:00
|
|
|
|
selectModel.value = userUIconfigInfo.value.gen_ai_function.model_id;
|
2026-05-12 23:33:55 +08:00
|
|
|
|
temperature.value = userUIconfigInfo.value.gen_ai_function.temperature || 0.8;
|
|
|
|
|
|
topP.value = userUIconfigInfo.value.gen_ai_function.top_p || 0.9;
|
|
|
|
|
|
sessionID.value = userUIconfigInfo.value.gen_ai_function.session_id || 0;
|
|
|
|
|
|
if (sessionID.value !== 0) {
|
2025-09-23 20:35:33 +08:00
|
|
|
|
getMessage(sessionID.value);
|
|
|
|
|
|
}
|
2026-05-12 23:33:55 +08:00
|
|
|
|
} else if (ModelList.value.length > 0) {
|
|
|
|
|
|
selectModel.value = ModelList.value[0].ID;
|
2025-05-19 19:14:22 +08:00
|
|
|
|
}
|
2025-03-31 13:58:13 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
ElMessage.error(result["msg"]);
|
|
|
|
|
|
}
|
2025-04-01 16:07:25 +08:00
|
|
|
|
} catch (e) {
|
2025-03-31 13:58:13 +08:00
|
|
|
|
console.log(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
GetModelListByFunctionName();
|
2025-04-01 14:40:20 +08:00
|
|
|
|
|
|
|
|
|
|
const getFileListData = async () => {
|
2025-04-01 16:07:25 +08:00
|
|
|
|
let req = {
|
|
|
|
|
|
token: localStorage.getItem("token"),
|
|
|
|
|
|
type: "all",
|
|
|
|
|
|
};
|
|
|
|
|
|
let result = await FindUserFileService(req);
|
|
|
|
|
|
if (result["code"] === 0) {
|
|
|
|
|
|
filteredFiles.value = result["data"];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ElMessage.error(result["msg"]);
|
|
|
|
|
|
}
|
2025-04-01 14:40:20 +08:00
|
|
|
|
};
|
2025-05-08 14:16:39 +08:00
|
|
|
|
|
|
|
|
|
|
const HandleTextToDocFile = async () => {
|
2025-05-08 14:19:10 +08:00
|
|
|
|
if (textToDocFileName.value.trim() === "") {
|
|
|
|
|
|
ElMessage.warning("文件名不能为空");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-05-08 14:16:39 +08:00
|
|
|
|
let req = {
|
|
|
|
|
|
token: localStorage.getItem("token"),
|
|
|
|
|
|
file_name: textToDocFileName.value,
|
|
|
|
|
|
text: vditor.value.getValue(),
|
|
|
|
|
|
file_type: selectFileDocType.value,
|
|
|
|
|
|
};
|
|
|
|
|
|
let result = await SetMessageTextToDocService(req);
|
|
|
|
|
|
if (result["code"] === 0) {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
ElMessage.success("文件已加入创建队列,请在文件列表中查看");
|
2025-05-08 14:16:39 +08:00
|
|
|
|
textToDocFileVisible.value = false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ElMessage.error(result["msg"]);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-03-25 19:51:06 +08:00
|
|
|
|
</script>
|
2026-05-12 23:33:55 +08:00
|
|
|
|
|
2025-03-25 19:51:06 +08:00
|
|
|
|
<style scoped>
|
2025-03-26 18:08:51 +08:00
|
|
|
|
.chat-app {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
height: 100%;
|
2026-05-12 23:33:55 +08:00
|
|
|
|
background-color: #f7f8fa;
|
2025-03-26 18:08:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
/* 侧边栏 */
|
2025-03-26 18:08:51 +08:00
|
|
|
|
.history-sessions {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
width: 280px;
|
2025-03-26 18:08:51 +08:00
|
|
|
|
height: 100%;
|
2026-05-12 23:33:55 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-right: 1px solid #e5e6eb;
|
|
|
|
|
|
transition: all 0.3s ease;
|
2025-03-26 18:08:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
.history-sessions.collapsed {
|
|
|
|
|
|
width: 0;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
border-right: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.session-header {
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
border-bottom: 1px solid #e5e6eb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.new-session-btn {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.session-card {
|
|
|
|
|
|
margin: 12px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.session-card :deep(.el-card__header) {
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
background: #f7f8fa;
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #1d2129;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.current-session {
|
|
|
|
|
|
padding: 8px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.session-name {
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
background: #f2f3f5;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
color: #4e5969;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.history-card {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
margin-top: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.history-scroll {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.history-list {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.history-item {
|
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
color: #4e5969;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.history-item:hover {
|
|
|
|
|
|
background: #f2f3f5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.history-item.active {
|
|
|
|
|
|
background: #e8f3ff;
|
|
|
|
|
|
color: #1677ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.history-name {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
2025-03-26 18:08:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
/* 折叠按钮 */
|
|
|
|
|
|
.toggle-sidebar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 20px;
|
2025-03-26 18:08:51 +08:00
|
|
|
|
cursor: pointer;
|
2026-05-12 23:33:55 +08:00
|
|
|
|
color: #86909c;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-right: 1px solid #e5e6eb;
|
2025-03-26 18:08:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
.toggle-sidebar:hover {
|
|
|
|
|
|
color: #4e5969;
|
|
|
|
|
|
background: #f7f8fa;
|
2025-03-26 18:08:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
/* 聊天容器 */
|
2025-03-25 19:51:06 +08:00
|
|
|
|
.chat-container {
|
2025-03-26 18:08:51 +08:00
|
|
|
|
flex: 1;
|
2025-03-25 19:51:06 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2025-03-28 21:34:11 +08:00
|
|
|
|
height: 100%;
|
2026-05-12 23:33:55 +08:00
|
|
|
|
min-width: 0;
|
2025-03-25 19:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
/* 顶部工具栏 */
|
|
|
|
|
|
.chat-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
padding: 12px 24px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-bottom: 1px solid #e5e6eb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.model-select {
|
|
|
|
|
|
width: 300px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 消息列表 */
|
2025-03-25 19:51:06 +08:00
|
|
|
|
.chat-messages {
|
|
|
|
|
|
flex: 1;
|
2025-09-23 20:35:33 +08:00
|
|
|
|
overflow-y: auto;
|
2026-05-12 23:33:55 +08:00
|
|
|
|
padding: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 空状态 */
|
|
|
|
|
|
.empty-state {
|
2025-03-25 19:51:06 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-05-12 23:33:55 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
color: #86909c;
|
2025-03-25 19:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
.empty-icon {
|
|
|
|
|
|
color: #c9cdd4;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-text {
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #1d2129;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-hint {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #86909c;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 消息列表 */
|
|
|
|
|
|
.messages-list {
|
|
|
|
|
|
max-width: 1000px;
|
|
|
|
|
|
margin: 0 auto;
|
2025-03-25 19:51:06 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-05-12 23:33:55 +08:00
|
|
|
|
gap: 24px;
|
2025-03-25 19:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
.message-item {
|
2025-03-25 19:51:06 +08:00
|
|
|
|
display: flex;
|
2026-05-12 23:33:55 +08:00
|
|
|
|
gap: 12px;
|
2025-03-25 19:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
.message-item.user {
|
|
|
|
|
|
flex-direction: row-reverse;
|
2025-03-25 19:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.message-avatar {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
flex-shrink: 0;
|
2025-03-25 19:51:06 +08:00
|
|
|
|
width: 40px;
|
2026-05-12 23:33:55 +08:00
|
|
|
|
height: 40px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2025-03-25 19:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
.ai-icon {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
padding: 8px;
|
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-icon {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
padding: 8px;
|
|
|
|
|
|
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.message-wrapper {
|
|
|
|
|
|
max-width: calc(100% - 80px);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.message-item.user .message-wrapper {
|
|
|
|
|
|
align-items: flex-end;
|
2025-03-25 19:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.message-content {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
word-break: break-word;
|
2025-03-25 19:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
.message-item.user .message-content {
|
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
|
color: #fff;
|
2025-03-25 19:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
.message-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transition: opacity 0.2s;
|
2025-03-25 19:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
.message-item:hover .message-actions {
|
|
|
|
|
|
opacity: 1;
|
2025-03-25 19:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
/* 打字指示器 */
|
|
|
|
|
|
.typing-indicator {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
padding: 8px 0;
|
2025-03-25 19:51:06 +08:00
|
|
|
|
}
|
2025-03-26 18:08:51 +08:00
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
.typing-indicator span {
|
|
|
|
|
|
width: 8px;
|
|
|
|
|
|
height: 8px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: #86909c;
|
|
|
|
|
|
animation: typing 1.4s infinite;
|
2025-03-26 18:08:51 +08:00
|
|
|
|
}
|
2025-09-23 20:35:33 +08:00
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
.typing-indicator span:nth-child(2) {
|
|
|
|
|
|
animation-delay: 0.2s;
|
2025-03-26 18:08:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
.typing-indicator span:nth-child(3) {
|
|
|
|
|
|
animation-delay: 0.4s;
|
2025-03-26 18:08:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
@keyframes typing {
|
|
|
|
|
|
0%, 60%, 100% {
|
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
|
opacity: 0.4;
|
|
|
|
|
|
}
|
|
|
|
|
|
30% {
|
|
|
|
|
|
transform: translateY(-6px);
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
2025-03-26 18:08:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
.typing-text {
|
|
|
|
|
|
color: #86909c;
|
|
|
|
|
|
font-size: 14px;
|
2025-03-26 18:08:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
/* 输入区域 */
|
|
|
|
|
|
.chat-input-wrapper {
|
|
|
|
|
|
padding: 16px 24px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-top: 1px solid #e5e6eb;
|
2025-03-26 18:08:51 +08:00
|
|
|
|
}
|
2025-03-27 16:09:22 +08:00
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
.selected-files {
|
2025-03-27 17:33:35 +08:00
|
|
|
|
display: flex;
|
2026-05-12 23:33:55 +08:00
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-bottom: 12px;
|
2025-03-27 17:33:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
.file-tag {
|
2025-03-27 17:33:35 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-05-12 23:33:55 +08:00
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.input-area {
|
|
|
|
|
|
max-width: 1000px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.input-tools {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
padding-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-textarea {
|
|
|
|
|
|
flex: 1;
|
2025-03-27 16:09:22 +08:00
|
|
|
|
}
|
2025-03-27 17:33:35 +08:00
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
.chat-textarea :deep(.el-textarea__inner) {
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
resize: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.send-btn {
|
|
|
|
|
|
padding: 12px 24px;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 文件对话框 */
|
|
|
|
|
|
.file-dialog :deep(.el-dialog__body) {
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dialog-search {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
|
border-bottom: 1px solid #e5e6eb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dialog-search .el-input {
|
|
|
|
|
|
flex: 1;
|
2025-03-27 17:33:35 +08:00
|
|
|
|
}
|
2025-04-01 14:40:20 +08:00
|
|
|
|
|
|
|
|
|
|
.file-list {
|
|
|
|
|
|
max-height: 400px;
|
|
|
|
|
|
overflow-y: auto;
|
2026-05-12 23:33:55 +08:00
|
|
|
|
padding: 12px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
padding: 10px 20px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: background 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-item:hover {
|
|
|
|
|
|
background: #f7f8fa;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-info {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
flex: 1;
|
2025-04-01 14:40:20 +08:00
|
|
|
|
}
|
2025-09-23 20:35:33 +08:00
|
|
|
|
|
2025-04-01 14:40:20 +08:00
|
|
|
|
.file-icon {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
color: #86909c;
|
2025-04-01 14:40:20 +08:00
|
|
|
|
}
|
2025-09-23 20:35:33 +08:00
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
.file-name {
|
|
|
|
|
|
color: #1d2129;
|
2025-04-01 14:40:20 +08:00
|
|
|
|
}
|
2025-09-23 20:35:33 +08:00
|
|
|
|
|
2025-04-01 14:40:20 +08:00
|
|
|
|
.footer-bar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
2026-05-12 23:33:55 +08:00
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
|
border-top: 1px solid #e5e6eb;
|
2025-04-01 14:40:20 +08:00
|
|
|
|
}
|
2025-09-23 20:35:33 +08:00
|
|
|
|
|
2025-04-01 14:40:20 +08:00
|
|
|
|
.selected-count {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
color: #86909c;
|
2025-04-01 14:40:20 +08:00
|
|
|
|
}
|
2025-09-23 20:35:33 +08:00
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
/* 文档对话框 */
|
|
|
|
|
|
.doc-dialog :deep(.el-dialog__body) {
|
|
|
|
|
|
padding: 0;
|
2025-04-01 14:40:20 +08:00
|
|
|
|
}
|
2025-04-23 15:11:16 +08:00
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
.doc-header {
|
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
|
border-bottom: 1px solid #e5e6eb;
|
2025-04-23 15:11:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
.doc-name-input {
|
2025-04-23 15:11:16 +08:00
|
|
|
|
width: 400px;
|
2026-05-12 23:33:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.vditor-container {
|
|
|
|
|
|
min-height: 500px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.doc-footer {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
|
border-top: 1px solid #e5e6eb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 参数下拉 */
|
|
|
|
|
|
.dropdown-content {
|
|
|
|
|
|
width: 360px;
|
|
|
|
|
|
padding: 8px;
|
2025-04-23 15:11:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.model-params {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
padding: 8px;
|
2025-04-23 15:11:16 +08:00
|
|
|
|
}
|
2025-09-23 20:35:33 +08:00
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
.params-tip {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
background: #e8f3ff;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
color: #1677ff;
|
|
|
|
|
|
font-size: 13px;
|
2025-04-23 15:11:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.param-item {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
margin-bottom: 20px;
|
2025-04-23 15:11:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.param-label {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-05-12 23:33:55 +08:00
|
|
|
|
gap: 6px;
|
2025-04-23 15:11:16 +08:00
|
|
|
|
margin-bottom: 8px;
|
2026-05-12 23:33:55 +08:00
|
|
|
|
color: #4e5969;
|
|
|
|
|
|
font-size: 14px;
|
2025-04-23 15:11:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tip-icon {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
color: #86909c;
|
2025-04-23 15:11:16 +08:00
|
|
|
|
cursor: help;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
.param-control {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.param-control .el-slider {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-23 15:11:16 +08:00
|
|
|
|
.param-value {
|
2026-05-12 23:33:55 +08:00
|
|
|
|
min-width: 40px;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #1677ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
|
/* Markdown 样式 */
|
|
|
|
|
|
.markdown-body {
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.markdown-body p {
|
|
|
|
|
|
margin: 8px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.markdown-body pre {
|
|
|
|
|
|
margin: 16px 0;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.markdown-body code {
|
|
|
|
|
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.markdown-body pre code {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
padding-top: 12px;
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.markdown-body :not(pre) > code {
|
|
|
|
|
|
background: #f2f3f5;
|
|
|
|
|
|
padding: 2px 6px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
color: #d63384;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.code-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
|
background: #1e1e1e;
|
|
|
|
|
|
border-bottom: 1px solid #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.code-lang {
|
|
|
|
|
|
color: #888;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.copy-btn {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
padding: 4px;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: #888;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.copy-btn:hover {
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
background: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.markdown-body blockquote {
|
|
|
|
|
|
margin: 12px 0;
|
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
|
border-left: 4px solid #667eea;
|
|
|
|
|
|
background: #f7f8fa;
|
|
|
|
|
|
color: #4e5969;
|
|
|
|
|
|
border-radius: 0 6px 6px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.markdown-body ul,
|
|
|
|
|
|
.markdown-body ol {
|
|
|
|
|
|
padding-left: 24px;
|
|
|
|
|
|
margin: 12px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.markdown-body li {
|
|
|
|
|
|
margin: 4px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.markdown-body h1,
|
|
|
|
|
|
.markdown-body h2,
|
|
|
|
|
|
.markdown-body h3,
|
|
|
|
|
|
.markdown-body h4,
|
|
|
|
|
|
.markdown-body h5,
|
|
|
|
|
|
.markdown-body h6 {
|
|
|
|
|
|
margin: 16px 0 8px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #1d2129;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.markdown-body h1 {
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
border-bottom: 1px solid #e5e6eb;
|
|
|
|
|
|
padding-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.markdown-body h2 {
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.markdown-body h3 {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.markdown-body a {
|
|
|
|
|
|
color: #667eea;
|
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.markdown-body a:hover {
|
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.markdown-body table {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
|
margin: 12px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.markdown-body th,
|
|
|
|
|
|
.markdown-body td {
|
|
|
|
|
|
border: 1px solid #e5e6eb;
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.markdown-body th {
|
|
|
|
|
|
background: #f7f8fa;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.message-item.user .markdown-body :not(pre) > code {
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.2);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.message-item.user .markdown-body a {
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 滚动条样式 */
|
|
|
|
|
|
.chat-messages::-webkit-scrollbar,
|
|
|
|
|
|
.history-scroll::-webkit-scrollbar,
|
|
|
|
|
|
.file-list::-webkit-scrollbar {
|
|
|
|
|
|
width: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-messages::-webkit-scrollbar-track,
|
|
|
|
|
|
.history-scroll::-webkit-scrollbar-track,
|
|
|
|
|
|
.file-list::-webkit-scrollbar-track {
|
|
|
|
|
|
background: transparent;
|
2025-04-23 15:11:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
.chat-messages::-webkit-scrollbar-thumb,
|
|
|
|
|
|
.history-scroll::-webkit-scrollbar-thumb,
|
|
|
|
|
|
.file-list::-webkit-scrollbar-thumb {
|
|
|
|
|
|
background: #c9cdd4;
|
|
|
|
|
|
border-radius: 3px;
|
2025-04-23 15:11:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 23:33:55 +08:00
|
|
|
|
.chat-messages::-webkit-scrollbar-thumb:hover,
|
|
|
|
|
|
.history-scroll::-webkit-scrollbar-thumb:hover,
|
|
|
|
|
|
.file-list::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
|
background: #86909c;
|
2025-04-23 15:11:16 +08:00
|
|
|
|
}
|
2025-03-25 19:51:06 +08:00
|
|
|
|
</style>
|