|
@@ -21,13 +21,17 @@
|
|
|
<div class="collapse-btn" @click="togglePanel">
|
|
<div class="collapse-btn" @click="togglePanel">
|
|
|
<div :class="['collapse-icon', { collapsed: panelCollapsed }]"></div>
|
|
<div :class="['collapse-icon', { collapsed: panelCollapsed }]"></div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="ai-container">
|
|
|
|
|
|
|
+ <div class="ai-container" @dragenter="onDragEnter" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
|
|
|
|
|
+ <!-- 拖拽经过时的「可放置」提示。pointer-events: none 不能去掉:遮罩一旦能命中,
|
|
|
|
|
+ 它自己会连续触发 dragenter/dragleave,反馈会持续闪断 -->
|
|
|
|
|
+ <div v-if="isDragOver" class="drop-overlay">
|
|
|
|
|
+ <div class="drop-overlay-text">松开鼠标添加附件</div>
|
|
|
|
|
+ </div>
|
|
|
<!-- 左侧任务列表 -->
|
|
<!-- 左侧任务列表 -->
|
|
|
<div :class="['task-panel-wrapper', { collapsed: panelCollapsed }]">
|
|
<div :class="['task-panel-wrapper', { collapsed: panelCollapsed }]">
|
|
|
<TaskListPanel
|
|
<TaskListPanel
|
|
|
:taskList="taskList"
|
|
:taskList="taskList"
|
|
|
:currentTaskId="currentTaskId"
|
|
:currentTaskId="currentTaskId"
|
|
|
- :isAdmin="isAdmin"
|
|
|
|
|
@switch-task="switchTask"
|
|
@switch-task="switchTask"
|
|
|
@create-task="createNewTask"
|
|
@create-task="createNewTask"
|
|
|
@delete-task="handleDeleteTask"
|
|
@delete-task="handleDeleteTask"
|
|
@@ -36,9 +40,8 @@
|
|
|
@toggle-sub-agents="toggleSubAgentPanel"
|
|
@toggle-sub-agents="toggleSubAgentPanel"
|
|
|
@toggle-schedules="toggleSchedulePanel"
|
|
@toggle-schedules="toggleSchedulePanel"
|
|
|
/>
|
|
/>
|
|
|
- <!-- [审计-第四轮 M3] 重放自我方 fix/base-agent-token(M1/M2/M3)::isAdmin 透传给左侧任务面板,
|
|
|
|
|
- 由面板内部过滤掉「技能」「子智能体」两个管理入口(按钮渲染在 TaskListPanel 的 optionItems 里)。
|
|
|
|
|
- 注意「定时任务」不属于全局变更,保持对所有人开放。 -->
|
|
|
|
|
|
|
+ <!-- 「技能」「子智能体」两个入口对所有角色都显示:角色差异收敛在面板内部
|
|
|
|
|
+ (非管理员只读:隐藏创建/编辑/删除、禁用启停开关),而不是把入口整个藏掉。 -->
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 右侧内容区:根据 activePanel 切换显示 -->
|
|
<!-- 右侧内容区:根据 activePanel 切换显示 -->
|
|
@@ -74,6 +77,7 @@
|
|
|
<ChatMessages
|
|
<ChatMessages
|
|
|
ref="chatMessagesRef"
|
|
ref="chatMessagesRef"
|
|
|
:messages="messages"
|
|
:messages="messages"
|
|
|
|
|
+ :history-loading="isCurrentHistoryLoading"
|
|
|
@file-preview="openFilePreview"
|
|
@file-preview="openFilePreview"
|
|
|
@download-word="downloadWordFile"
|
|
@download-word="downloadWordFile"
|
|
|
@open-file-preview="handleOpenFilePreview"
|
|
@open-file-preview="handleOpenFilePreview"
|
|
@@ -116,6 +120,7 @@
|
|
|
@stop="stopStreaming"
|
|
@stop="stopStreaming"
|
|
|
@file-upload="triggerFileUpload"
|
|
@file-upload="triggerFileUpload"
|
|
|
@file-change="handleFileUpload"
|
|
@file-change="handleFileUpload"
|
|
|
|
|
+ @file-add="handleFileAdded"
|
|
|
@remove-pending-file="removePendingFile"
|
|
@remove-pending-file="removePendingFile"
|
|
|
@pending-file-preview="openFilePreview"
|
|
@pending-file-preview="openFilePreview"
|
|
|
@think-level-change="handleThinkLevelChange"
|
|
@think-level-change="handleThinkLevelChange"
|
|
@@ -143,14 +148,14 @@
|
|
|
</Transition>
|
|
</Transition>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
- <!-- 技能面板 -->
|
|
|
|
|
|
|
+ <!-- 技能面板:非管理员只读(面板内部按 isAdmin 隐藏/禁用写操作) -->
|
|
|
<div v-if="activePanel === 'skill'" class="right-panel full">
|
|
<div v-if="activePanel === 'skill'" class="right-panel full">
|
|
|
- <SkillPanel @close="activePanel = 'chat'" />
|
|
|
|
|
|
|
+ <SkillPanel :is-admin="isAdmin" @close="activePanel = 'chat'" />
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- 子智能体面板 -->
|
|
|
|
|
|
|
+ <!-- 子智能体面板:非管理员只读 -->
|
|
|
<div v-if="activePanel === 'sub-agents'" class="right-panel full">
|
|
<div v-if="activePanel === 'sub-agents'" class="right-panel full">
|
|
|
- <SubAgentPanel @close="activePanel = 'chat'" />
|
|
|
|
|
|
|
+ <SubAgentPanel :is-admin="isAdmin" @close="activePanel = 'chat'" />
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 定时任务面板 -->
|
|
<!-- 定时任务面板 -->
|
|
@@ -183,7 +188,7 @@
|
|
|
import SchedulePanel from '/@/views/ventAI/manageAssistent/components/panels/SchedulePanel.vue';
|
|
import SchedulePanel from '/@/views/ventAI/manageAssistent/components/panels/SchedulePanel.vue';
|
|
|
import RightPanelTabs from '/@/views/ventAI/manageAssistent/components/panels/RightPanelTabs.vue';
|
|
import RightPanelTabs from '/@/views/ventAI/manageAssistent/components/panels/RightPanelTabs.vue';
|
|
|
import { getScheduleList } from '/@/views/ventAI/manageAssistent/api';
|
|
import { getScheduleList } from '/@/views/ventAI/manageAssistent/api';
|
|
|
- import type { Message, Task } from '/@/views/ventAI/manageAssistent/types/index';
|
|
|
|
|
|
|
+ import type { Message, ScheduleTask, Task } from '/@/views/ventAI/manageAssistent/types/index';
|
|
|
import { buildCapsuleFiles, buildCapsuleLinks, buildCapsuleAgents } from '/@/views/ventAI/manageAssistent/lib/capsule';
|
|
import { buildCapsuleFiles, buildCapsuleLinks, buildCapsuleAgents } from '/@/views/ventAI/manageAssistent/lib/capsule';
|
|
|
import { useChatStream } from '../hooks/useChatStream';
|
|
import { useChatStream } from '../hooks/useChatStream';
|
|
|
import { useChatTasks } from '../hooks/useChatTasks';
|
|
import { useChatTasks } from '../hooks/useChatTasks';
|
|
@@ -193,6 +198,7 @@
|
|
|
import { useChatSend } from '../hooks/useChatSend';
|
|
import { useChatSend } from '../hooks/useChatSend';
|
|
|
import { useChatScroll } from '../hooks/useChatScroll';
|
|
import { useChatScroll } from '../hooks/useChatScroll';
|
|
|
import { useChatFiles } from '../hooks/useChatFiles';
|
|
import { useChatFiles } from '../hooks/useChatFiles';
|
|
|
|
|
+ import { useFileDrop } from '../hooks/useFileDrop';
|
|
|
import { useChatQuota } from '../hooks/useChatQuota';
|
|
import { useChatQuota } from '../hooks/useChatQuota';
|
|
|
import { useChatExport } from '../hooks/useChatExport';
|
|
import { useChatExport } from '../hooks/useChatExport';
|
|
|
|
|
|
|
@@ -238,6 +244,8 @@
|
|
|
currentTask,
|
|
currentTask,
|
|
|
messages,
|
|
messages,
|
|
|
historyLoadedTasks,
|
|
historyLoadedTasks,
|
|
|
|
|
+ historyLoadingTaskIds,
|
|
|
|
|
+ setHistoryLoading,
|
|
|
getDefaultMessages,
|
|
getDefaultMessages,
|
|
|
transformHistoryToMessages,
|
|
transformHistoryToMessages,
|
|
|
fetchSessionList,
|
|
fetchSessionList,
|
|
@@ -246,6 +254,10 @@
|
|
|
setCurrentSessionId,
|
|
setCurrentSessionId,
|
|
|
} = useChatTasks();
|
|
} = useChatTasks();
|
|
|
|
|
|
|
|
|
|
+ // 当前任务的历史是否正在拉取:切换任务时新任务的 messages 要等接口回来才填充,
|
|
|
|
|
+ // 消息区据此显示加载态,避免中间先闪一屏空会话缺省图。
|
|
|
|
|
+ const isCurrentHistoryLoading = computed(() => historyLoadingTaskIds.value.includes(currentTaskId.value));
|
|
|
|
|
+
|
|
|
// —— 右侧面板/标签页/全屏相关职责已下沉到 useChatPanels ——
|
|
// —— 右侧面板/标签页/全屏相关职责已下沉到 useChatPanels ——
|
|
|
const {
|
|
const {
|
|
|
showFileList,
|
|
showFileList,
|
|
@@ -390,12 +402,13 @@
|
|
|
const chatInputRef = ref<InstanceType<typeof ChatInputArea>>();
|
|
const chatInputRef = ref<InstanceType<typeof ChatInputArea>>();
|
|
|
const chatMessagesRef = ref<InstanceType<typeof ChatMessages>>();
|
|
const chatMessagesRef = ref<InstanceType<typeof ChatMessages>>();
|
|
|
|
|
|
|
|
- const scheduleListForDetail = ref<any[]>([]);
|
|
|
|
|
|
|
+ const scheduleListForDetail = ref<ScheduleTask[]>([]);
|
|
|
|
|
|
|
|
const fetchScheduleListForDetail = async () => {
|
|
const fetchScheduleListForDetail = async () => {
|
|
|
try {
|
|
try {
|
|
|
const res = await getScheduleList();
|
|
const res = await getScheduleList();
|
|
|
- scheduleListForDetail.value = res?.tasks || res?.data || (Array.isArray(res) ? res : []);
|
|
|
|
|
|
|
+ // getScheduleList 已按真实契约标注 { tasks, total }
|
|
|
|
|
+ scheduleListForDetail.value = res?.tasks ?? [];
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
console.error('获取定时任务列表失败:', e);
|
|
console.error('获取定时任务列表失败:', e);
|
|
|
scheduleListForDetail.value = [];
|
|
scheduleListForDetail.value = [];
|
|
@@ -470,7 +483,7 @@
|
|
|
const task = currentTask.value;
|
|
const task = currentTask.value;
|
|
|
const tab = task?.rightPanels?.tabs.find((t) => t.type === 'schedule');
|
|
const tab = task?.rightPanels?.tabs.find((t) => t.type === 'schedule');
|
|
|
if (tab?.type === 'schedule') {
|
|
if (tab?.type === 'schedule') {
|
|
|
- const fresh = scheduleListForDetail.value.find((s: any) => s.id === tab.task.id);
|
|
|
|
|
|
|
+ const fresh = scheduleListForDetail.value.find((s) => s.id === tab.task.id);
|
|
|
if (fresh) tab.task = fresh;
|
|
if (fresh) tab.task = fresh;
|
|
|
}
|
|
}
|
|
|
schedulePanelRef.value?.fetchScheduleList?.();
|
|
schedulePanelRef.value?.fetchScheduleList?.();
|
|
@@ -486,7 +499,24 @@
|
|
|
|
|
|
|
|
// —— 以下四组职责已下沉到 composables(额度/导出/滚动/文件),组件只做接线 ——
|
|
// —— 以下四组职责已下沉到 composables(额度/导出/滚动/文件),组件只做接线 ——
|
|
|
const { scrollToBottom, scrollToBottomThrottled } = useChatScroll(() => chatMessagesRef.value);
|
|
const { scrollToBottom, scrollToBottomThrottled } = useChatScroll(() => chatMessagesRef.value);
|
|
|
- const { pendingFile, activeFileReader, triggerFileUpload, handleFileUpload, removePendingFile } = useChatFiles(() => chatInputRef.value);
|
|
|
|
|
|
|
+ const { pendingFile, activeFileReader, triggerFileUpload, handleFileUpload, addFile, removePendingFile } = useChatFiles(() => chatInputRef.value);
|
|
|
|
|
+
|
|
|
|
|
+ // 拖拽 / 粘贴进来的文件与「选择文件」共用同一条入队路径:校验与读取分流都在 addFile 内
|
|
|
|
|
+ const handleFileAdded = (file: File) => {
|
|
|
|
|
+ addFile(file);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleDropFiles = (files: File[]) => {
|
|
|
|
|
+ // 待发附件是单值,多文件拖入时取第一个并说明,避免用户以为全都加上了
|
|
|
|
|
+ if (files.length > 1) message.warning('一次只能添加一个附件,已取第一个文件');
|
|
|
|
|
+ addFile(files[0]);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const { isDragOver, dragHandlers } = useFileDrop({
|
|
|
|
|
+ onFiles: handleDropFiles,
|
|
|
|
|
+ isVisible: () => props.visible,
|
|
|
|
|
+ });
|
|
|
|
|
+ const { onDragEnter, onDragOver, onDragLeave, onDrop } = dragHandlers;
|
|
|
const {
|
|
const {
|
|
|
usageToday,
|
|
usageToday,
|
|
|
fetchTodayUsage,
|
|
fetchTodayUsage,
|
|
@@ -537,9 +567,11 @@
|
|
|
// 流式中的任务不拉历史(避免覆盖在途消息),但必须继续走完下面的初始化:
|
|
// 流式中的任务不拉历史(避免覆盖在途消息),但必须继续走完下面的初始化:
|
|
|
// currentTaskId 已经切过来了,提前 return 会让上下文用量/模式/进度条/滚动全部停在上一个任务上。
|
|
// currentTaskId 已经切过来了,提前 return 会让上下文用量/模式/进度条/滚动全部停在上一个任务上。
|
|
|
if (!newTask.isStreaming) {
|
|
if (!newTask.isStreaming) {
|
|
|
|
|
+ // 标记加载中:消息区据 isCurrentHistoryLoading 显示加载态,而不是先闪一屏空会话
|
|
|
|
|
+ setHistoryLoading(taskId, true);
|
|
|
try {
|
|
try {
|
|
|
const res = await getDetail(newTask.sessionId);
|
|
const res = await getDetail(newTask.sessionId);
|
|
|
- const messages = res?.messages || (Array.isArray(res) ? res : res?.data);
|
|
|
|
|
|
|
+ const messages = res?.messages;
|
|
|
if (Array.isArray(messages)) {
|
|
if (Array.isArray(messages)) {
|
|
|
newTask.messages = transformHistoryToMessages(messages);
|
|
newTask.messages = transformHistoryToMessages(messages);
|
|
|
}
|
|
}
|
|
@@ -548,6 +580,8 @@
|
|
|
// 拉不到历史时界面会是空白会话,容易被误认为「对话丢了」,必须给出提示
|
|
// 拉不到历史时界面会是空白会话,容易被误认为「对话丢了」,必须给出提示
|
|
|
console.error('加载历史会话失败:', error);
|
|
console.error('加载历史会话失败:', error);
|
|
|
message.error('加载历史会话失败,请稍后重试');
|
|
message.error('加载历史会话失败,请稍后重试');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setHistoryLoading(taskId, false);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -567,7 +601,7 @@
|
|
|
syncTodoBarFromMessages();
|
|
syncTodoBarFromMessages();
|
|
|
|
|
|
|
|
// 切换任务时检测是否为定时任务会话:自动打开/关闭定时任务详情标签
|
|
// 切换任务时检测是否为定时任务会话:自动打开/关闭定时任务详情标签
|
|
|
- const scheduleItem = newTask.sessionId ? scheduleListForDetail.value.find((s: any) => s.feedback_session_id === newTask.sessionId) : undefined;
|
|
|
|
|
|
|
+ const scheduleItem = newTask.sessionId ? scheduleListForDetail.value.find((s) => s.feedback_session_id === newTask.sessionId) : undefined;
|
|
|
const panelState = getPanelState(newTask);
|
|
const panelState = getPanelState(newTask);
|
|
|
const existingScheduleTab = panelState.tabs.find((t) => t.type === 'schedule');
|
|
const existingScheduleTab = panelState.tabs.find((t) => t.type === 'schedule');
|
|
|
if (scheduleItem) {
|
|
if (scheduleItem) {
|
|
@@ -819,9 +853,10 @@
|
|
|
|
|
|
|
|
// 如果是历史任务,加载其消息
|
|
// 如果是历史任务,加载其消息
|
|
|
if (targetTask.sessionId && !historyLoadedTasks.has(targetTask.id)) {
|
|
if (targetTask.sessionId && !historyLoadedTasks.has(targetTask.id)) {
|
|
|
|
|
+ setHistoryLoading(targetTask.id, true);
|
|
|
try {
|
|
try {
|
|
|
const res = await getDetail(targetTask.sessionId);
|
|
const res = await getDetail(targetTask.sessionId);
|
|
|
- const messages = res?.messages || (Array.isArray(res) ? res : res?.data);
|
|
|
|
|
|
|
+ const messages = res?.messages;
|
|
|
if (Array.isArray(messages)) {
|
|
if (Array.isArray(messages)) {
|
|
|
targetTask.messages = transformHistoryToMessages(messages);
|
|
targetTask.messages = transformHistoryToMessages(messages);
|
|
|
}
|
|
}
|
|
@@ -829,6 +864,8 @@
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('加载历史会话失败:', error);
|
|
console.error('加载历史会话失败:', error);
|
|
|
message.error('加载历史会话失败,请稍后重试');
|
|
message.error('加载历史会话失败,请稍后重试');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setHistoryLoading(targetTask.id, false);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -872,12 +909,42 @@
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped lang="less">
|
|
<style scoped lang="less">
|
|
|
|
|
+ @import '../styles/tokens.less';
|
|
|
|
|
+
|
|
|
.ai-container {
|
|
.ai-container {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
height: 700px;
|
|
height: 700px;
|
|
|
gap: 10px;
|
|
gap: 10px;
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
|
|
|
+ /* 拖拽提示遮罩的定位基准 */
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 拖拽上传提示:z-index 低于弹框的收起/最大化按钮(10),拖拽时窗口控件仍保持可见 */
|
|
|
|
|
+ .drop-overlay {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ z-index: 5;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+ background: @color-primary-a10;
|
|
|
|
|
+ border: 2px dashed @color-primary-a50;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .drop-overlay-text {
|
|
|
|
|
+ padding: 8px 20px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ background: @color-panel-bg;
|
|
|
|
|
+ border: 1px solid @color-panel-border;
|
|
|
|
|
+ color: @color-text;
|
|
|
|
|
+ font-size: 13px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.collapse-btn {
|
|
.collapse-btn {
|