|
|
@@ -0,0 +1,1119 @@
|
|
|
+<template>
|
|
|
+ <a-modal :visible="props.visible" @cancel="emit('close')" :width="1400" :footer="null" destroyOnClose class="ai-assistant-modal">
|
|
|
+ <template #title>
|
|
|
+ <div class="draggable-title" @mousedown="onDragStart">"通安智风"大模型</div>
|
|
|
+ </template>
|
|
|
+ <div class="ai-container">
|
|
|
+ <!-- 左侧任务列表 -->
|
|
|
+ <TaskListPanel
|
|
|
+ :taskList="taskList"
|
|
|
+ :currentTaskId="currentTaskId"
|
|
|
+ @switch-task="switchTask"
|
|
|
+ @create-task="createNewTask"
|
|
|
+ @delete-task="handleDeleteTask"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 中间聊天区域 -->
|
|
|
+ <div class="chat-panel">
|
|
|
+ <!-- 顶部任务选择器 -->
|
|
|
+ <div class="chat-header">
|
|
|
+ <a-select
|
|
|
+ v-model:value="currentTaskId"
|
|
|
+ style="width: 300px"
|
|
|
+ @change="handleTaskChange"
|
|
|
+ :getContainer="false"
|
|
|
+ popup-class-name="ai-task-select-dropdown"
|
|
|
+ >
|
|
|
+ <a-select-option v-for="task in taskList" :key="task.id" :value="task.id">
|
|
|
+ {{ task.name }}
|
|
|
+ </a-select-option>
|
|
|
+ </a-select>
|
|
|
+ <div v-if="currentWordUrl" class="upload-bg" @click="downloadWordReport">
|
|
|
+ <div class="download-icon"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 附件列表面板 -->
|
|
|
+ <FileListPanel :visible="showFileList" :files="getCurrentTaskFiles()" @close="showFileList = false" @file-click="openFilePreview" />
|
|
|
+
|
|
|
+ <!-- 消息列表 -->
|
|
|
+ <ChatMessages ref="chatMessagesRef" :messages="messages" @file-preview="openFilePreview" @download-word="downloadWordFile" />
|
|
|
+
|
|
|
+ <!-- 输入区域 -->
|
|
|
+ <ChatInputArea
|
|
|
+ ref="chatInputRef"
|
|
|
+ v-model="inputMessage"
|
|
|
+ :pendingFile="pendingFile"
|
|
|
+ :loading="loading"
|
|
|
+ @send="handleSendMessage"
|
|
|
+ @file-upload="triggerFileUpload"
|
|
|
+ @file-change="handleFileUpload"
|
|
|
+ @remove-pending-file="removePendingFile"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 右侧文件预览面板 -->
|
|
|
+ <div v-if="previewFile" class="right-panel">
|
|
|
+ <FilePreviewPanel :file="previewFile" @close="removePreviewFile" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </a-modal>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+ import { ref, nextTick, watch, computed, onMounted, onBeforeUnmount } from 'vue';
|
|
|
+ import dayjs from 'dayjs';
|
|
|
+ import { unifiedStream, getHistoryList, getDetail, deleteSession } from '../api';
|
|
|
+ import { message, Modal } from 'ant-design-vue';
|
|
|
+ import { isPdfFile } from './chatModal/utils';
|
|
|
+ import TaskListPanel from './chatModal/TaskListPanel.vue';
|
|
|
+ import FileListPanel from './chatModal/FileListPanel.vue';
|
|
|
+ import ChatMessages from './chatModal/ChatMessages.vue';
|
|
|
+ import ChatInputArea from './chatModal/ChatInputArea.vue';
|
|
|
+ import FilePreviewPanel from './chatModal/FilePreviewPanel.vue';
|
|
|
+ import type { AttachedFile, Message, Task } from './chatModal/types';
|
|
|
+
|
|
|
+ interface Props {
|
|
|
+ visible: boolean;
|
|
|
+ }
|
|
|
+
|
|
|
+ interface Emits {
|
|
|
+ (e: 'close'): void;
|
|
|
+ }
|
|
|
+
|
|
|
+ const props = defineProps<Props>();
|
|
|
+ const emit = defineEmits<Emits>();
|
|
|
+
|
|
|
+ const taskList = ref<Task[]>([]);
|
|
|
+
|
|
|
+ // 从接口获取会话列表
|
|
|
+ const fetchSessionList = async () => {
|
|
|
+ try {
|
|
|
+ const res = await getHistoryList({});
|
|
|
+ const sessions = res?.sessions || res?.data || [];
|
|
|
+ if (Array.isArray(sessions)) {
|
|
|
+ const apiTasks: Task[] = sessions.map((item: { session_id: string; title: string }) => ({
|
|
|
+ id: item.session_id,
|
|
|
+ name: item.title || '未命名会话',
|
|
|
+ sessionId: item.session_id,
|
|
|
+ messages: [],
|
|
|
+ attachedFiles: [],
|
|
|
+ }));
|
|
|
+
|
|
|
+ const mergedTasks = apiTasks.map((apiTask) => {
|
|
|
+ const existing = taskList.value.find((t) => t.id === apiTask.id);
|
|
|
+ if (existing) {
|
|
|
+ return {
|
|
|
+ ...apiTask,
|
|
|
+ messages: existing.messages,
|
|
|
+ attachedFiles: existing.attachedFiles,
|
|
|
+ wordUrl: existing.wordUrl,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return apiTask;
|
|
|
+ });
|
|
|
+
|
|
|
+ const localOnlyTasks = taskList.value.filter((t) => !t.sessionId && !apiTasks.find((a) => a.id === t.id));
|
|
|
+
|
|
|
+ taskList.value = [...localOnlyTasks, ...mergedTasks];
|
|
|
+ return mergedTasks;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取会话列表失败:', error);
|
|
|
+ }
|
|
|
+ return [];
|
|
|
+ };
|
|
|
+
|
|
|
+ const currentTaskId = ref('');
|
|
|
+ const chatInputRef = ref<InstanceType<typeof ChatInputArea>>();
|
|
|
+ const chatMessagesRef = ref<InstanceType<typeof ChatMessages>>();
|
|
|
+
|
|
|
+ const previewFile = ref<AttachedFile | null>(null);
|
|
|
+ const showFileList = ref(false);
|
|
|
+ const pendingFile = ref<AttachedFile | null>(null);
|
|
|
+
|
|
|
+ const messages = ref<Message[]>([
|
|
|
+ {
|
|
|
+ type: 'ai',
|
|
|
+ content: '您好!我是通安智风助手,请问有什么可以帮助您的吗?',
|
|
|
+ time: dayjs().format('HH:mm'),
|
|
|
+ },
|
|
|
+ ]);
|
|
|
+
|
|
|
+ const inputMessage = ref('');
|
|
|
+ const loading = ref(false);
|
|
|
+
|
|
|
+ const getCurrentSessionId = (): string => {
|
|
|
+ const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
|
|
|
+ return currentTask?.sessionId || '';
|
|
|
+ };
|
|
|
+
|
|
|
+ const historyLoadedTasks = ref<Set<string>>(new Set());
|
|
|
+
|
|
|
+ const setCurrentSessionId = (sessionId: string) => {
|
|
|
+ const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
|
|
|
+ if (currentTask && currentTask.id !== sessionId) {
|
|
|
+ taskList.value = taskList.value.filter((t) => t.id !== sessionId);
|
|
|
+ currentTask.id = sessionId;
|
|
|
+ currentTask.sessionId = sessionId;
|
|
|
+ currentTaskId.value = sessionId;
|
|
|
+ historyLoadedTasks.value.add(sessionId);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const switchTask = async (taskId: string) => {
|
|
|
+ const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
|
|
|
+ if (currentTask) {
|
|
|
+ currentTask.messages = [...messages.value];
|
|
|
+ }
|
|
|
+
|
|
|
+ currentTaskId.value = taskId;
|
|
|
+ const newTask = taskList.value.find((t) => t.id === taskId);
|
|
|
+ if (!newTask) return;
|
|
|
+
|
|
|
+ if (newTask.sessionId) {
|
|
|
+ taskList.value = taskList.value.filter((t) => t.sessionId);
|
|
|
+ }
|
|
|
+
|
|
|
+ inputMessage.value = '';
|
|
|
+ pendingFile.value = null;
|
|
|
+ previewFile.value = null;
|
|
|
+ showFileList.value = false;
|
|
|
+
|
|
|
+ if (newTask.sessionId && !historyLoadedTasks.value.has(taskId)) {
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ const res = await getDetail(newTask.sessionId);
|
|
|
+ const messages = res?.messages || (Array.isArray(res) ? res : res?.data);
|
|
|
+ if (Array.isArray(messages)) {
|
|
|
+ newTask.messages = transformHistoryToMessages(messages);
|
|
|
+ }
|
|
|
+ historyLoadedTasks.value.add(taskId);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载历史会话失败:', error);
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ messages.value = newTask.messages && newTask.messages.length > 0 ? [...newTask.messages] : getDefaultMessages();
|
|
|
+
|
|
|
+ await nextTick();
|
|
|
+ scrollToBottom();
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleTaskChange = (taskId: string) => {
|
|
|
+ switchTask(taskId);
|
|
|
+ };
|
|
|
+
|
|
|
+ const createNewTask = () => {
|
|
|
+ const existingBlank = taskList.value.find((t) => !t.sessionId);
|
|
|
+ if (existingBlank) {
|
|
|
+ switchTask(existingBlank.id);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const newTask: Task = {
|
|
|
+ id: `task-${Date.now()}`,
|
|
|
+ name: '新任务',
|
|
|
+ sessionId: '',
|
|
|
+ messages: getDefaultMessages(),
|
|
|
+ attachedFiles: [],
|
|
|
+ };
|
|
|
+
|
|
|
+ taskList.value.unshift(newTask);
|
|
|
+ switchTask(newTask.id);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleDeleteTask = (task: Task, index: number) => {
|
|
|
+ Modal.confirm({
|
|
|
+ title: '提示',
|
|
|
+ content: '是否删除该对话?',
|
|
|
+ okText: '确认',
|
|
|
+ cancelText: '取消',
|
|
|
+ onOk: async () => {
|
|
|
+ try {
|
|
|
+ if (task.sessionId) {
|
|
|
+ await deleteSession(task.sessionId);
|
|
|
+ }
|
|
|
+ taskList.value.splice(index, 1);
|
|
|
+ if (currentTaskId.value === task.id) {
|
|
|
+ if (taskList.value.length > 0) {
|
|
|
+ switchTask(taskList.value[0].id);
|
|
|
+ } else {
|
|
|
+ messages.value = [];
|
|
|
+ currentTaskId.value = '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ message.success('删除成功');
|
|
|
+ } catch {
|
|
|
+ message.error('删除失败');
|
|
|
+ }
|
|
|
+ },
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const getDefaultMessages = (): Message[] => {
|
|
|
+ return [
|
|
|
+ {
|
|
|
+ type: 'ai',
|
|
|
+ content: '您好!我是通安智风助手,请问有什么可以帮助您的吗?',
|
|
|
+ time: dayjs().format('HH:mm'),
|
|
|
+ },
|
|
|
+ ];
|
|
|
+ };
|
|
|
+
|
|
|
+ const normalizeLineBreaks = (str: string): string => {
|
|
|
+ if (!str) return '';
|
|
|
+ return str.replace(/\\n/g, '\n').replace(/\r\n/g, '\n');
|
|
|
+ };
|
|
|
+
|
|
|
+ const transformHistoryToMessages = (data: Array<{ role?: string; type?: string; content: string; created_at?: string }>): Message[] => {
|
|
|
+ const messages: Message[] = [];
|
|
|
+
|
|
|
+ for (const item of data) {
|
|
|
+ const normalizedContent = normalizeLineBreaks(item.content);
|
|
|
+ const time = item.created_at ? dayjs(item.created_at).format('HH:mm') : '';
|
|
|
+ const createdAt = item.created_at || '';
|
|
|
+
|
|
|
+ // 新格式:role 字段 (user/assistant)
|
|
|
+ if (item.role === 'user') {
|
|
|
+ messages.push({ type: 'user', content: normalizedContent, time, createdAt });
|
|
|
+ } else if (item.role === 'assistant') {
|
|
|
+ messages.push({ type: 'ai', content: normalizedContent, time, createdAt });
|
|
|
+ }
|
|
|
+ // 兼容旧格式:type 字段
|
|
|
+ else if (item.type === 'user_message') {
|
|
|
+ messages.push({ type: 'user', content: normalizedContent, time, createdAt });
|
|
|
+ } else if (item.type === 'model_thinking') {
|
|
|
+ const lastAiMsg = messages[messages.length - 1];
|
|
|
+ if (lastAiMsg && lastAiMsg.type === 'ai') {
|
|
|
+ lastAiMsg.content += normalizedContent;
|
|
|
+ lastAiMsg.content = lastAiMsg.content.replace(/\|\|\|SPLIT_CONTENT\|\|\|/g, '');
|
|
|
+ } else {
|
|
|
+ messages.push({ type: 'ai', content: normalizedContent, time, createdAt });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return messages;
|
|
|
+ };
|
|
|
+
|
|
|
+ const triggerFileUpload = () => {
|
|
|
+ chatInputRef.value?.fileInputRef?.click();
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleFileUpload = async (event: Event) => {
|
|
|
+ const target = event.target as HTMLInputElement;
|
|
|
+ const file = target.files?.[0];
|
|
|
+ if (!file) return;
|
|
|
+
|
|
|
+ if (!isPdfFile(file.name)) {
|
|
|
+ message.error('仅支持 PDF 文件格式');
|
|
|
+ target.value = '';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const newFile: AttachedFile = {
|
|
|
+ id: `file-${Date.now()}`,
|
|
|
+ name: file.name,
|
|
|
+ size: file.size,
|
|
|
+ type: file.type,
|
|
|
+ uploadTime: dayjs().format('YYYY-MM-DD HH:mm'),
|
|
|
+ originalFile: file,
|
|
|
+ };
|
|
|
+
|
|
|
+ const reader = new FileReader();
|
|
|
+ reader.onload = (e) => {
|
|
|
+ newFile.preview = e.target?.result as string;
|
|
|
+ };
|
|
|
+ reader.readAsDataURL(file);
|
|
|
+
|
|
|
+ pendingFile.value = newFile;
|
|
|
+ message.success(`文件 "${file.name}" 已添加,点击发送按钮提交`);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('文件处理失败:', error);
|
|
|
+ message.error('文件处理失败,请重试');
|
|
|
+ }
|
|
|
+
|
|
|
+ target.value = '';
|
|
|
+ };
|
|
|
+
|
|
|
+ const removePendingFile = () => {
|
|
|
+ pendingFile.value = null;
|
|
|
+ };
|
|
|
+
|
|
|
+ const removePreviewFile = () => {
|
|
|
+ previewFile.value = null;
|
|
|
+ };
|
|
|
+
|
|
|
+ const openFilePreview = (file: AttachedFile) => {
|
|
|
+ previewFile.value = file;
|
|
|
+
|
|
|
+ const msgIndex = messages.value.findIndex((m) => m.attachedFile?.id === file.id);
|
|
|
+ if (msgIndex !== -1) {
|
|
|
+ nextTick(() => {
|
|
|
+ const messageElements = chatMessagesRef.value?.messagesRef?.querySelectorAll('.message-wrapper');
|
|
|
+ if (messageElements && messageElements[msgIndex]) {
|
|
|
+ (messageElements[msgIndex] as HTMLElement).scrollIntoView({
|
|
|
+ behavior: 'smooth',
|
|
|
+ block: 'center',
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const getCurrentTaskFiles = (): AttachedFile[] => {
|
|
|
+ const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
|
|
|
+ return currentTask?.attachedFiles || [];
|
|
|
+ };
|
|
|
+
|
|
|
+ const currentWordUrl = computed(() => {
|
|
|
+ const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
|
|
|
+ return currentTask?.wordUrl || '';
|
|
|
+ });
|
|
|
+
|
|
|
+ const downloadWordReport = () => {
|
|
|
+ const url = currentWordUrl.value;
|
|
|
+ if (!url) return;
|
|
|
+ const a = document.createElement('a');
|
|
|
+ a.href = url;
|
|
|
+ a.download = '';
|
|
|
+ a.target = '_blank';
|
|
|
+ document.body.appendChild(a);
|
|
|
+ a.click();
|
|
|
+ document.body.removeChild(a);
|
|
|
+ };
|
|
|
+
|
|
|
+ const downloadWordFile = (url: string) => {
|
|
|
+ if (!url) return;
|
|
|
+ const a = document.createElement('a');
|
|
|
+ a.href = url;
|
|
|
+ a.download = '';
|
|
|
+ a.target = '_blank';
|
|
|
+ document.body.appendChild(a);
|
|
|
+ a.click();
|
|
|
+ document.body.removeChild(a);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleSendMessage = async () => {
|
|
|
+ const hasText = inputMessage.value.trim();
|
|
|
+ const hasFile = pendingFile.value;
|
|
|
+
|
|
|
+ if (!hasText) {
|
|
|
+ message.warning('请输入消息内容');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (loading.value) return;
|
|
|
+
|
|
|
+ const userInput = inputMessage.value.trim();
|
|
|
+ const fileToSend = pendingFile.value;
|
|
|
+
|
|
|
+ inputMessage.value = '';
|
|
|
+ pendingFile.value = null;
|
|
|
+
|
|
|
+ try {
|
|
|
+ loading.value = true;
|
|
|
+
|
|
|
+ if (hasFile) {
|
|
|
+ await sendWithAttachment(fileToSend!, userInput);
|
|
|
+ } else {
|
|
|
+ await sendTextOnly(userInput);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('发送消息失败:', error);
|
|
|
+ message.error('发送失败,请重试');
|
|
|
+
|
|
|
+ const lastMsg = messages.value[messages.value.length - 1];
|
|
|
+ if (lastMsg && lastMsg.type === 'ai' && lastMsg.isLoading) {
|
|
|
+ messages.value.pop();
|
|
|
+ }
|
|
|
+
|
|
|
+ const errorMsg: Message = {
|
|
|
+ type: 'ai',
|
|
|
+ content: '抱歉,请求失败,请稍后重试。',
|
|
|
+ time: dayjs().format('HH:mm'),
|
|
|
+ };
|
|
|
+ messages.value.push(errorMsg);
|
|
|
+
|
|
|
+ const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
|
|
|
+ if (currentTask) {
|
|
|
+ currentTask.messages = [...messages.value];
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ await scrollToBottom();
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const extractAgentName = (toolName: string): string => {
|
|
|
+ const nameMap: Record<string, string> = {
|
|
|
+ form_review: '形式审查',
|
|
|
+ verify_coal_face_ventilation: '采煤工作面需风量计算核验',
|
|
|
+ verify_tunneling_face_ventilation: '掘进工作面需风量计算核验',
|
|
|
+ verify_chamber_ventilation: '硐室需风量计算核验',
|
|
|
+ verify_other_points_ventilation: '其他地点需风量计算核验',
|
|
|
+ check_data_by_gas_report: '瓦斯数据一致性审查',
|
|
|
+ check_data_by_face_design: '设计规程一致性审查',
|
|
|
+ check_data_by_vent_report: '测风报表一致性审查',
|
|
|
+ check_current_month_plan: '日期有效性审查',
|
|
|
+ get_key_translate: '计算参数对照',
|
|
|
+ calculate_ventilation: '需风量计算',
|
|
|
+ get_ventilation_data: '需风量查询',
|
|
|
+ recalculate_ventilation: '需风量重计算',
|
|
|
+ get_plan_required_wind: '需风量提取',
|
|
|
+ get_model_calculated_wind: '三维模型解算风量提取',
|
|
|
+ get_sensor_wind: '巷道监测风量提取',
|
|
|
+ merge_wind_by_location: '地点风量智能合并',
|
|
|
+ check_wind_compliance: '风量检查',
|
|
|
+ };
|
|
|
+
|
|
|
+ return nameMap[toolName] || nameMap[toolName.toLowerCase()] || '智能分析助手';
|
|
|
+ };
|
|
|
+
|
|
|
+ // 每个 agent 只保留最新一条步骤,新数据替换旧数据
|
|
|
+ const upsertStep = (steps: any[], step: any) => {
|
|
|
+ const agentName = step.agent || '系统';
|
|
|
+ const idx = steps.findIndex((s) => (s.agent || '系统') === agentName && s.type !== 'updated_todo_list');
|
|
|
+ if (idx !== -1) {
|
|
|
+ steps.splice(idx, 1, step);
|
|
|
+ } else {
|
|
|
+ steps.push(step);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleSSEMessage = (data: any, aiMsgIndex: number) => {
|
|
|
+ const aiMsg = messages.value[aiMsgIndex];
|
|
|
+ const normalizedContent = normalizeLineBreaks(data.content || '');
|
|
|
+
|
|
|
+ // 初始化 thinkingSteps 数组
|
|
|
+ if (!aiMsg.thinkingSteps) {
|
|
|
+ aiMsg.thinkingSteps = [];
|
|
|
+ }
|
|
|
+
|
|
|
+ switch (data.type) {
|
|
|
+ // token 流式内容 - 直接拼接到消息内容
|
|
|
+ case 'token':
|
|
|
+ if (normalizedContent) {
|
|
|
+ aiMsg.content += normalizedContent;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+
|
|
|
+ // 系统进度消息 - 仅当 agent 不是 system 时才作为 thinking step
|
|
|
+ case 'progress': {
|
|
|
+ if (data.agent && data.agent !== 'system') {
|
|
|
+ upsertStep(aiMsg.thinkingSteps!, {
|
|
|
+ type: 'executing',
|
|
|
+ agent: data.agent,
|
|
|
+ message: data.message || '',
|
|
|
+ timestamp: Date.now(),
|
|
|
+ });
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // agent 开始处理
|
|
|
+ case 'agent_start': {
|
|
|
+ upsertStep(aiMsg.thinkingSteps!, {
|
|
|
+ type: 'agent_start',
|
|
|
+ agent: data.cn_agent || data.agent || '智能助手',
|
|
|
+ message: data.message || '开始处理...',
|
|
|
+ timestamp: Date.now(),
|
|
|
+ });
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // agent 任务列表更新(同一 agent 的 todos 更新已有条目,不重复添加)
|
|
|
+ case 'agent_todos':
|
|
|
+ case 'updated_todo_list': {
|
|
|
+ const agentName = data.cn_agent || data.agent || '智能助手';
|
|
|
+ const existingIdx = aiMsg.thinkingSteps!.findIndex((s) => s.type === 'updated_todo_list' && s.agent === agentName);
|
|
|
+ if (existingIdx !== -1) {
|
|
|
+ aiMsg.thinkingSteps![existingIdx].todos = data.todos || [];
|
|
|
+ aiMsg.thinkingSteps![existingIdx].timestamp = Date.now();
|
|
|
+ } else {
|
|
|
+ aiMsg.thinkingSteps!.push({
|
|
|
+ type: 'updated_todo_list',
|
|
|
+ agent: agentName,
|
|
|
+ todos: data.todos || [],
|
|
|
+ message: data.message || '任务进度已更新',
|
|
|
+ timestamp: Date.now(),
|
|
|
+ });
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // agent 执行中(工具调用)
|
|
|
+ case 'agent_executing':
|
|
|
+ case 'executing': {
|
|
|
+ upsertStep(aiMsg.thinkingSteps!, {
|
|
|
+ type: 'executing',
|
|
|
+ agent: data.cn_agent || data.agent || '智能助手',
|
|
|
+ tool: (data.cn_tools || data.tools || []).join('、'),
|
|
|
+ message: data.message || '',
|
|
|
+ timestamp: Date.now(),
|
|
|
+ });
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 工具结果
|
|
|
+ case 'agent_tool_result':
|
|
|
+ case 'tool_result': {
|
|
|
+ upsertStep(aiMsg.thinkingSteps!, {
|
|
|
+ type: 'tool_result',
|
|
|
+ agent: data.cn_agent || data.agent || '智能助手',
|
|
|
+ tool: data.cn_tool || data.tool || '',
|
|
|
+ message: data.message || '执行完成',
|
|
|
+ timestamp: Date.now(),
|
|
|
+ });
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // agent 完成
|
|
|
+ case 'agent_done': {
|
|
|
+ upsertStep(aiMsg.thinkingSteps!, {
|
|
|
+ type: 'agent_done',
|
|
|
+ agent: data.cn_agent || data.agent || '智能助手',
|
|
|
+ message: `${data.message || '审查完成'}${data.duration_ms ? `(${(data.duration_ms / 1000).toFixed(0)}s)` : ''}${data.progress ? ` [${data.progress}]` : ''}`,
|
|
|
+ timestamp: Date.now(),
|
|
|
+ });
|
|
|
+ if (data.preview) {
|
|
|
+ aiMsg.content += data.preview;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 旧格式兼容
|
|
|
+ case 'model_thinking':
|
|
|
+ if (normalizedContent) {
|
|
|
+ aiMsg.content += normalizedContent;
|
|
|
+ aiMsg.content = aiMsg.content.replace(/\|\|\|SPLIT_CONTENT\|\|\|/g, '');
|
|
|
+ }
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'tool_select': {
|
|
|
+ upsertStep(aiMsg.thinkingSteps!, {
|
|
|
+ type: 'tool_call',
|
|
|
+ agent: extractAgentName(normalizedContent),
|
|
|
+ tool: normalizedContent,
|
|
|
+ message: `选择工具:${normalizedContent}`,
|
|
|
+ timestamp: Date.now(),
|
|
|
+ });
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ case 'error':
|
|
|
+ upsertStep(aiMsg.thinkingSteps!, {
|
|
|
+ type: 'tool_result',
|
|
|
+ agent: '系统',
|
|
|
+ message: `❌ 执行失败:${normalizedContent}`,
|
|
|
+ timestamp: Date.now(),
|
|
|
+ });
|
|
|
+ aiMsg.isLoading = false;
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'word_download':
|
|
|
+ if (normalizedContent) {
|
|
|
+ aiMsg.wordDownloadUrl = normalizedContent;
|
|
|
+ const wordTask = taskList.value.find((t) => t.id === currentTaskId.value);
|
|
|
+ if (wordTask) wordTask.wordUrl = normalizedContent;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'done':
|
|
|
+ if (data.session_id) {
|
|
|
+ aiMsg.sessionId = data.session_id;
|
|
|
+ }
|
|
|
+ if (data.duration_ms) {
|
|
|
+ aiMsg.durationMs = data.duration_ms;
|
|
|
+ }
|
|
|
+ aiMsg.isLoading = false;
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'system':
|
|
|
+ if (normalizedContent) {
|
|
|
+ aiMsg.content += normalizedContent.replace(/\n/g, ' \n');
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
|
|
|
+ if (currentTask) {
|
|
|
+ currentTask.messages = [...messages.value];
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const parseSSEData = (chunk: string, aiMsgIndex: number) => {
|
|
|
+ const lines = chunk.split('\n');
|
|
|
+
|
|
|
+ for (const lineRaw of lines) {
|
|
|
+ const line = lineRaw.trim();
|
|
|
+ if (!line) continue;
|
|
|
+
|
|
|
+ if (line.startsWith('data:')) {
|
|
|
+ try {
|
|
|
+ const jsonStr = line.substring(5).trim();
|
|
|
+ const data = JSON.parse(jsonStr);
|
|
|
+ handleSSEMessage(data, aiMsgIndex);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('解析 SSE data 行失败:', error, line);
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const data = JSON.parse(line);
|
|
|
+ handleSSEMessage(data, aiMsgIndex);
|
|
|
+ continue;
|
|
|
+ } catch {
|
|
|
+ // not json
|
|
|
+ }
|
|
|
+
|
|
|
+ handleSSEMessage({ type: 'model_thinking', content: line }, aiMsgIndex);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const sendTextOnly = async (userInput: string) => {
|
|
|
+ const now = dayjs();
|
|
|
+ const userMsg: Message = {
|
|
|
+ type: 'user',
|
|
|
+ content: userInput,
|
|
|
+ time: now.format('HH:mm'),
|
|
|
+ createdAt: now.toISOString(),
|
|
|
+ };
|
|
|
+ messages.value.push(userMsg);
|
|
|
+ await scrollToBottom();
|
|
|
+
|
|
|
+ const aiMsgIndex = messages.value.length;
|
|
|
+ const aiMsg: Message = {
|
|
|
+ type: 'ai',
|
|
|
+ content: '',
|
|
|
+ time: now.format('HH:mm'),
|
|
|
+ createdAt: now.toISOString(),
|
|
|
+ isLoading: true,
|
|
|
+ thinkingSteps: [],
|
|
|
+ generateStartTime: Date.now(),
|
|
|
+ };
|
|
|
+ messages.value.push(aiMsg);
|
|
|
+ await scrollToBottom();
|
|
|
+
|
|
|
+ const result = await unifiedStream(
|
|
|
+ {
|
|
|
+ message: userInput,
|
|
|
+ session_id: getCurrentSessionId() || undefined,
|
|
|
+ },
|
|
|
+ (chunk: string) => {
|
|
|
+ parseSSEData(chunk, aiMsgIndex);
|
|
|
+ scrollToBottom();
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ if (result.session_id) {
|
|
|
+ const taskBeforeUpdate = taskList.value.find((t) => t.id === currentTaskId.value);
|
|
|
+ const isNewTask = taskBeforeUpdate && !taskBeforeUpdate.sessionId;
|
|
|
+
|
|
|
+ setCurrentSessionId(result.session_id);
|
|
|
+
|
|
|
+ if (isNewTask) {
|
|
|
+ taskBeforeUpdate.name = userInput.substring(0, 20) + (userInput.length > 20 ? '...' : '');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
|
|
|
+ if (currentTask) {
|
|
|
+ currentTask.messages = [...messages.value];
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const sendWithAttachment = async (file: AttachedFile, userInput: string) => {
|
|
|
+ let currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
|
|
|
+ if (currentTask) {
|
|
|
+ currentTask.attachedFiles.push(file);
|
|
|
+ } else {
|
|
|
+ throw new Error('未找到当前任务');
|
|
|
+ }
|
|
|
+
|
|
|
+ const now = dayjs();
|
|
|
+ const userMsg: Message = {
|
|
|
+ type: 'user',
|
|
|
+ content: userInput || `上传了文件:${file.name}`,
|
|
|
+ time: now.format('HH:mm'),
|
|
|
+ createdAt: now.toISOString(),
|
|
|
+ attachedFile: file,
|
|
|
+ };
|
|
|
+ messages.value.push(userMsg);
|
|
|
+ await scrollToBottom();
|
|
|
+
|
|
|
+ const aiMsgIndex = messages.value.length;
|
|
|
+ const aiMsg: Message = {
|
|
|
+ type: 'ai',
|
|
|
+ content: '',
|
|
|
+ time: now.format('HH:mm'),
|
|
|
+ createdAt: now.toISOString(),
|
|
|
+ isLoading: true,
|
|
|
+ thinkingSteps: [],
|
|
|
+ generateStartTime: Date.now(),
|
|
|
+ };
|
|
|
+ messages.value.push(aiMsg);
|
|
|
+ await scrollToBottom();
|
|
|
+
|
|
|
+ if (!file.originalFile) {
|
|
|
+ throw new Error('缺少原始文件对象');
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = await unifiedStream(
|
|
|
+ {
|
|
|
+ message: userInput || `上传了文件:${file.name}`,
|
|
|
+ session_id: getCurrentSessionId() || undefined,
|
|
|
+ file: file.originalFile,
|
|
|
+ },
|
|
|
+ (chunk: string) => {
|
|
|
+ parseSSEData(chunk, aiMsgIndex);
|
|
|
+ scrollToBottom();
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ if (result.session_id) {
|
|
|
+ setCurrentSessionId(result.session_id);
|
|
|
+ }
|
|
|
+
|
|
|
+ currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
|
|
|
+ if (currentTask) {
|
|
|
+ currentTask.messages = [...messages.value];
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const scrollToBottom = async () => {
|
|
|
+ await nextTick();
|
|
|
+ if (!chatMessagesRef.value?.isAtBottom) return;
|
|
|
+ const el = chatMessagesRef.value?.messagesRef;
|
|
|
+ if (el) {
|
|
|
+ el.scrollTop = el.scrollHeight;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ watch(
|
|
|
+ () => props.visible,
|
|
|
+ async (newVisible) => {
|
|
|
+ if (!newVisible) {
|
|
|
+ const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
|
|
|
+ if (currentTask) {
|
|
|
+ currentTask.messages = [...messages.value];
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ await fetchSessionList();
|
|
|
+
|
|
|
+ // 去除重复的空白任务,只保留一个
|
|
|
+ taskList.value = taskList.value.filter((task, index) => {
|
|
|
+ if (task.sessionId) return true;
|
|
|
+ return index === taskList.value.findIndex((t) => !t.sessionId);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 默认选中第一条历史记录
|
|
|
+ const firstHistoryTask = taskList.value.find((t) => t.sessionId);
|
|
|
+
|
|
|
+ let targetTask: Task;
|
|
|
+ if (firstHistoryTask) {
|
|
|
+ targetTask = firstHistoryTask;
|
|
|
+ } else {
|
|
|
+ // 没有历史记录时才创建空白新任务
|
|
|
+ targetTask = {
|
|
|
+ id: `task-${Date.now()}`,
|
|
|
+ name: '新任务',
|
|
|
+ sessionId: '',
|
|
|
+ messages: getDefaultMessages(),
|
|
|
+ attachedFiles: [],
|
|
|
+ };
|
|
|
+ taskList.value.unshift(targetTask);
|
|
|
+ }
|
|
|
+
|
|
|
+ currentTaskId.value = targetTask.id;
|
|
|
+
|
|
|
+ // 如果是历史任务,加载其消息
|
|
|
+ if (targetTask.sessionId && !historyLoadedTasks.value.has(targetTask.id)) {
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ const res = await getDetail(targetTask.sessionId);
|
|
|
+ const messages = res?.messages || (Array.isArray(res) ? res : res?.data);
|
|
|
+ if (Array.isArray(messages)) {
|
|
|
+ targetTask.messages = transformHistoryToMessages(messages);
|
|
|
+ }
|
|
|
+ historyLoadedTasks.value.add(targetTask.id);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载历史会话失败:', error);
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ messages.value = targetTask.messages && targetTask.messages.length > 0 ? [...targetTask.messages] : getDefaultMessages();
|
|
|
+
|
|
|
+ previewFile.value = null;
|
|
|
+ pendingFile.value = null;
|
|
|
+ showFileList.value = false;
|
|
|
+ inputMessage.value = '';
|
|
|
+
|
|
|
+ await nextTick(() => {
|
|
|
+ scrollToBottom();
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ // 拖拽相关(使用普通变量避免 mousemove 触发 Vue 响应式导致卡顿)
|
|
|
+ let isDragging = false;
|
|
|
+ let dragStartX = 0;
|
|
|
+ let dragStartY = 0;
|
|
|
+ let modalStartLeft = 0;
|
|
|
+ let modalStartTop = 0;
|
|
|
+ let draggedEl: HTMLElement | null = null;
|
|
|
+
|
|
|
+ const findModalWrap = (target: HTMLElement): HTMLElement | null =>
|
|
|
+ target.closest<HTMLElement>('.zxm-modal-wrap, .ant-modal-wrap, [class*="modal-wrap"]');
|
|
|
+
|
|
|
+ const queryModalWrap = (): HTMLElement | null => document.querySelector<HTMLElement>('.zxm-modal-wrap, .ant-modal-wrap, [class*="modal-wrap"]');
|
|
|
+
|
|
|
+ const centerModal = () => {
|
|
|
+ nextTick(() => {
|
|
|
+ const wrap = queryModalWrap();
|
|
|
+ if (wrap) {
|
|
|
+ wrap.style.position = '';
|
|
|
+ wrap.style.left = '';
|
|
|
+ wrap.style.top = '';
|
|
|
+ wrap.style.right = '';
|
|
|
+ wrap.style.bottom = '';
|
|
|
+ wrap.style.width = '';
|
|
|
+ wrap.style.height = '';
|
|
|
+ wrap.style.margin = '';
|
|
|
+ wrap.style.transform = '';
|
|
|
+ }
|
|
|
+ draggedEl = null;
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const onDragStart = (e: MouseEvent) => {
|
|
|
+ draggedEl = findModalWrap(e.target as HTMLElement) || queryModalWrap();
|
|
|
+ if (!draggedEl) return;
|
|
|
+
|
|
|
+ isDragging = true;
|
|
|
+ dragStartX = e.clientX;
|
|
|
+ dragStartY = e.clientY;
|
|
|
+
|
|
|
+ const rect = draggedEl.getBoundingClientRect();
|
|
|
+ modalStartLeft = rect.left;
|
|
|
+ modalStartTop = rect.top;
|
|
|
+
|
|
|
+ draggedEl.style.position = 'fixed';
|
|
|
+ draggedEl.style.left = `${rect.left}px`;
|
|
|
+ draggedEl.style.top = `${rect.top}px`;
|
|
|
+ draggedEl.style.margin = '0';
|
|
|
+ draggedEl.style.transform = 'none';
|
|
|
+
|
|
|
+ document.addEventListener('mousemove', onDragMove);
|
|
|
+ document.addEventListener('mouseup', onDragEnd);
|
|
|
+ };
|
|
|
+
|
|
|
+ const onDragMove = (e: MouseEvent) => {
|
|
|
+ if (!isDragging || !draggedEl) return;
|
|
|
+ draggedEl.style.left = `${modalStartLeft + (e.clientX - dragStartX)}px`;
|
|
|
+ draggedEl.style.top = `${modalStartTop + (e.clientY - dragStartY)}px`;
|
|
|
+ };
|
|
|
+
|
|
|
+ const onDragEnd = () => {
|
|
|
+ isDragging = false;
|
|
|
+ document.removeEventListener('mousemove', onDragMove);
|
|
|
+ document.removeEventListener('mouseup', onDragEnd);
|
|
|
+ };
|
|
|
+
|
|
|
+ watch(
|
|
|
+ () => props.visible,
|
|
|
+ (newVisible) => {
|
|
|
+ if (newVisible) {
|
|
|
+ centerModal();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ onMounted(async () => {
|
|
|
+ // fetchSessionList moved to watch(visible) to avoid unnecessary API calls
|
|
|
+ });
|
|
|
+
|
|
|
+ onBeforeUnmount(() => {
|
|
|
+ document.removeEventListener('mousemove', onDragMove);
|
|
|
+ document.removeEventListener('mouseup', onDragEnd);
|
|
|
+ });
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="less">
|
|
|
+ .ai-container {
|
|
|
+ display: flex;
|
|
|
+ height: 700px;
|
|
|
+ gap: 10px;
|
|
|
+ overflow: hidden;
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chat-panel {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ position: relative;
|
|
|
+ flex-direction: column;
|
|
|
+ background-image: var(--img-chat-panel-bg);
|
|
|
+ background-repeat: no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+ min-width: 0;
|
|
|
+
|
|
|
+ :deep(.chat-header) {
|
|
|
+ display: flex;
|
|
|
+ padding: 12px 20px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+ .header-icon {
|
|
|
+ width: 35px;
|
|
|
+ height: 28px;
|
|
|
+ background-image: var(--img-chat-header-icon);
|
|
|
+ background-repeat: no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ margin-right: 10px;
|
|
|
+ }
|
|
|
+ .zxm-select-selector {
|
|
|
+ height: 40px;
|
|
|
+ background-image: var(--img-chat-selector-bg);
|
|
|
+ background-repeat: no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ border: none;
|
|
|
+ border-color: unset !important;
|
|
|
+ box-shadow: unset !important;
|
|
|
+ color: #e0e6ed;
|
|
|
+ background-color: unset;
|
|
|
+ border-radius: 0;
|
|
|
+ padding-left: 40px;
|
|
|
+ padding-top: 10px;
|
|
|
+ }
|
|
|
+ .zxm-select-arrow {
|
|
|
+ color: #e0e6ed;
|
|
|
+ }
|
|
|
+
|
|
|
+ .zxm-select-selection-item {
|
|
|
+ color: #e0e6ed;
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-bg {
|
|
|
+ cursor: pointer;
|
|
|
+ width: 36px;
|
|
|
+ height: 36px;
|
|
|
+ background-image: var(--img-chat-upload-bg);
|
|
|
+ background-repeat: no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ margin-left: auto;
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+ .download-icon {
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ background-image: var(--img-chat-download-icon);
|
|
|
+ background-repeat: no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .right-panel {
|
|
|
+ width: calc(50% - 140px);
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ background: linear-gradient(to bottom, rgba(10, 132, 255, 0.08), rgba(2, 22, 50, 0.95));
|
|
|
+ border: 2px solid rgba(63, 80, 106, 0.5);
|
|
|
+ border-radius: 4px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .draggable-title {
|
|
|
+ cursor: grab;
|
|
|
+ user-select: none;
|
|
|
+
|
|
|
+ &:active {
|
|
|
+ cursor: grabbing;
|
|
|
+ }
|
|
|
+ }
|
|
|
+</style>
|
|
|
+
|
|
|
+<style lang="less">
|
|
|
+ .ai-assistant-modal {
|
|
|
+ .zxm-modal-content {
|
|
|
+ background-image: var(--img-chat-modal-bg);
|
|
|
+ background-repeat: no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ background-color: unset !important;
|
|
|
+ box-shadow: none;
|
|
|
+ padding: 0;
|
|
|
+ border: none !important;
|
|
|
+ }
|
|
|
+ .zxm-modal-close {
|
|
|
+ background-image: var(--img-chat-close-bg);
|
|
|
+ background-repeat: no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ top: -6px;
|
|
|
+ right: -6px;
|
|
|
+ width: 38px;
|
|
|
+ height: 38px;
|
|
|
+ .zxm-modal-close-x {
|
|
|
+ width: 38px;
|
|
|
+ height: 38px;
|
|
|
+ }
|
|
|
+ svg {
|
|
|
+ width: 26px;
|
|
|
+ height: 26px;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .zxm-modal-header {
|
|
|
+ background-color: unset !important;
|
|
|
+ background: unset;
|
|
|
+ border: none !important;
|
|
|
+ padding: 8px;
|
|
|
+ backdrop-filter: unset;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+ .zxm-modal-title {
|
|
|
+ width: 100%;
|
|
|
+ height: 60px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+
|
|
|
+ .draggable-title {
|
|
|
+ color: #fff;
|
|
|
+ font-family: var(--vet-font-family);
|
|
|
+ letter-spacing: 3px;
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: 100;
|
|
|
+ line-height: 1.4;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .zxm-modal-body {
|
|
|
+ padding: 12px 24px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .ai-task-select-dropdown {
|
|
|
+ background-color: rgba(10, 132, 255, 0.1);
|
|
|
+ .zxm-select-item {
|
|
|
+ color: #e0e6ed;
|
|
|
+ }
|
|
|
+ .zxm-select-item-option-selected {
|
|
|
+ color: #e0e6ed !important;
|
|
|
+ background-color: rgba(10, 132, 255, 0.15) !important;
|
|
|
+ }
|
|
|
+ }
|
|
|
+</style>
|