|
|
@@ -104,43 +104,20 @@
|
|
|
/>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 文件预览面板(与 chat-panel 并列,带滑入滑出动画) -->
|
|
|
+ <!-- 右侧辅助面板(标签页容器,与 chat-panel 并列,带滑入滑出动画) -->
|
|
|
<Transition name="right-panel-slide" @after-leave="onPanelLeaveEnd">
|
|
|
- <div v-if="previewFile" class="right-panel">
|
|
|
- <FilePreviewPanel
|
|
|
- :file="previewFile"
|
|
|
- :editable="previewEditable"
|
|
|
- :save-filename="previewFile?.name || ''"
|
|
|
- @close="removePreviewFile"
|
|
|
- @save-success="handleSaveSuccess"
|
|
|
- />
|
|
|
- </div>
|
|
|
- </Transition>
|
|
|
-
|
|
|
- <!-- Agent 详情面板(与 chat-panel 并列,带滑入滑出动画) -->
|
|
|
- <Transition name="right-panel-slide" @after-leave="onPanelLeaveEnd">
|
|
|
- <div v-if="activeAgentDetail" class="right-panel">
|
|
|
- <AgentDetailPanel
|
|
|
- :messageIndex="activeAgentDetail.messageIndex"
|
|
|
- :agentName="activeAgentDetail.agent"
|
|
|
- :agentId="activeAgentDetail.agentId"
|
|
|
- :messages="messages"
|
|
|
- @close="closeAgentDetail"
|
|
|
- />
|
|
|
- </div>
|
|
|
- </Transition>
|
|
|
-
|
|
|
- <!-- 定时任务详情面板(与 chat-panel 并列,带滑入滑出动画) -->
|
|
|
- <Transition name="right-panel-slide" @after-leave="onPanelLeaveEnd">
|
|
|
- <div v-if="currentScheduleTask" class="right-panel narrow">
|
|
|
- <ScheduleDetailPanel
|
|
|
- :task="currentScheduleTask"
|
|
|
- @close="showScheduleDetail = false"
|
|
|
- @deleted="handleScheduleDeleted"
|
|
|
- @updated="handleScheduleUpdated"
|
|
|
- @open-session="handleOpenScheduleSession"
|
|
|
- />
|
|
|
- </div>
|
|
|
+ <RightPanelTabs
|
|
|
+ v-if="currentRightPanels.tabs.length > 0"
|
|
|
+ :tabs="currentRightPanels.tabs"
|
|
|
+ :activeTabId="currentRightPanels.activeTabId"
|
|
|
+ :messages="messages"
|
|
|
+ @switch-tab="activateTab"
|
|
|
+ @close-tab="closeTab"
|
|
|
+ @save-success="handleSaveSuccess"
|
|
|
+ @schedule-deleted="handleScheduleDeleted"
|
|
|
+ @schedule-updated="handleScheduleUpdated"
|
|
|
+ @open-schedule-session="handleOpenScheduleSession"
|
|
|
+ />
|
|
|
</Transition>
|
|
|
</template>
|
|
|
|
|
|
@@ -193,17 +170,15 @@
|
|
|
import FileListPanel from './chatModal/FileListPanel.vue';
|
|
|
import ChatMessages from './chatModal/ChatMessages.vue';
|
|
|
import type { AgentDetailData } from './chatModal/ChatMessages.vue';
|
|
|
- import AgentDetailPanel from './chatModal/AgentDetailPanel.vue';
|
|
|
import ChatInputArea from './chatModal/ChatInputArea.vue';
|
|
|
- import FilePreviewPanel from './chatModal/FilePreviewPanel.vue';
|
|
|
import TodoListBar from './chatModal/TodoListBar.vue';
|
|
|
import SkillPanel from './chatModal/SkillPanel.vue';
|
|
|
import SubAgentPanel from './chatModal/SubAgentPanel.vue';
|
|
|
import SchedulePanel from './chatModal/SchedulePanel.vue';
|
|
|
- import ScheduleDetailPanel from './chatModal/ScheduleDetailPanel.vue';
|
|
|
+ import RightPanelTabs from './chatModal/RightPanelTabs.vue';
|
|
|
import { getScheduleList } from '../api';
|
|
|
import type { TodayUsage } from '../api';
|
|
|
- import type { AttachedFile, Message, Task, SseEvent } from './chatModal/types';
|
|
|
+ import type { AttachedFile, Message, Task, RightPanelTab, RightPanelState, SseEvent } from './chatModal/types';
|
|
|
import { isTextFile, isVideoFile, isAudioFile, isWordFile, isExcelFile, isCsvFile } from './chatModal/utils';
|
|
|
|
|
|
interface Props {
|
|
|
@@ -294,20 +269,9 @@
|
|
|
const chatInputRef = ref<InstanceType<typeof ChatInputArea>>();
|
|
|
const chatMessagesRef = ref<InstanceType<typeof ChatMessages>>();
|
|
|
|
|
|
- const previewFile = ref<AttachedFile | null>(null);
|
|
|
- const previewEditable = ref(false);
|
|
|
const showFileList = ref(false);
|
|
|
- const activeAgentDetail = ref<{ messageIndex: number; agent: string; agentId?: string } | null>(null);
|
|
|
const activePanel = ref<'chat' | 'skill' | 'sub-agents' | 'schedules'>('chat');
|
|
|
const scheduleListForDetail = ref<any[]>([]);
|
|
|
- const showScheduleDetail = ref(false);
|
|
|
-
|
|
|
- const currentScheduleTask = computed(() => {
|
|
|
- if (!showScheduleDetail.value || activePanel.value !== 'chat') return null;
|
|
|
- const sessionId = currentTask.value?.sessionId;
|
|
|
- if (!sessionId) return null;
|
|
|
- return scheduleListForDetail.value.find((t: any) => t.feedback_session_id === sessionId) || null;
|
|
|
- });
|
|
|
|
|
|
const fetchScheduleListForDetail = async () => {
|
|
|
try {
|
|
|
@@ -373,7 +337,6 @@
|
|
|
activePanel.value = 'chat';
|
|
|
} else {
|
|
|
activePanel.value = panel;
|
|
|
- previewFile.value = null;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -384,8 +347,6 @@
|
|
|
|
|
|
const handleOpenScheduleSession = async (sessionId: string, prompt?: string) => {
|
|
|
activePanel.value = 'chat';
|
|
|
- previewFile.value = null;
|
|
|
- showScheduleDetail.value = true;
|
|
|
await fetchSessionList();
|
|
|
await fetchScheduleListForDetail();
|
|
|
const existing = taskList.value.find((t) => t.sessionId === sessionId);
|
|
|
@@ -415,13 +376,24 @@
|
|
|
const schedulePanelRef = ref();
|
|
|
|
|
|
const handleScheduleDeleted = () => {
|
|
|
- showScheduleDetail.value = false;
|
|
|
+ const task = currentTask.value;
|
|
|
+ if (task?.rightPanels) {
|
|
|
+ const tab = task.rightPanels.tabs.find((t) => t.type === 'schedule');
|
|
|
+ if (tab) removeTab(task.rightPanels, tab.id);
|
|
|
+ }
|
|
|
fetchScheduleListForDetail();
|
|
|
schedulePanelRef.value?.fetchScheduleList?.();
|
|
|
};
|
|
|
|
|
|
- const handleScheduleUpdated = () => {
|
|
|
- fetchScheduleListForDetail();
|
|
|
+ const handleScheduleUpdated = async () => {
|
|
|
+ await fetchScheduleListForDetail();
|
|
|
+ // 同步标签内任务对象为最新数据(编辑/启停后)
|
|
|
+ const task = currentTask.value;
|
|
|
+ const tab = task?.rightPanels?.tabs.find((t) => t.type === 'schedule');
|
|
|
+ if (tab?.type === 'schedule') {
|
|
|
+ const fresh = scheduleListForDetail.value.find((s: any) => s.id === tab.task.id);
|
|
|
+ if (fresh) tab.task = fresh;
|
|
|
+ }
|
|
|
schedulePanelRef.value?.fetchScheduleList?.();
|
|
|
};
|
|
|
|
|
|
@@ -446,10 +418,17 @@
|
|
|
};
|
|
|
window.addEventListener('resize', onResize);
|
|
|
|
|
|
- // 是否有右侧辅助面板(文件预览/Agent详情/定时任务详情)与 chat-panel 并列显示
|
|
|
+ const currentTask = computed(() => taskList.value.find((t) => t.id === currentTaskId.value));
|
|
|
+
|
|
|
+ const currentRightPanels = computed<RightPanelState>(() => {
|
|
|
+ const task = currentTask.value;
|
|
|
+ return task?.rightPanels || { tabs: [], activeTabId: null };
|
|
|
+ });
|
|
|
+
|
|
|
+ // 是否有右侧辅助面板(标签页容器)与 chat-panel 并列显示
|
|
|
const hasRightPanel = computed(() => {
|
|
|
if (activePanel.value !== 'chat') return false;
|
|
|
- return !!(previewFile.value || activeAgentDetail.value || currentScheduleTask.value);
|
|
|
+ return currentRightPanels.value.tabs.length > 0;
|
|
|
});
|
|
|
|
|
|
// 面板离开动画标记:确保 modal 在面板退出动画结束后才收缩宽度
|
|
|
@@ -585,9 +564,6 @@
|
|
|
|
|
|
inputMessage.value = '';
|
|
|
pendingFile.value = null;
|
|
|
- previewFile.value = null;
|
|
|
- activeAgentDetail.value = null;
|
|
|
- showScheduleDetail.value = false;
|
|
|
showFileList.value = false;
|
|
|
|
|
|
if (newTask.sessionId && !historyLoadedTasks.has(taskId)) {
|
|
|
@@ -617,11 +593,19 @@
|
|
|
messages.value = newTask.messages && newTask.messages.length > 0 ? [...newTask.messages] : getDefaultMessages();
|
|
|
syncTodoBarFromMessages();
|
|
|
|
|
|
- // 切换任务时检测是否为定时任务会话
|
|
|
- if (newTask.sessionId && scheduleListForDetail.value.some((s: any) => s.feedback_session_id === newTask.sessionId)) {
|
|
|
- showScheduleDetail.value = true;
|
|
|
- } else {
|
|
|
- showScheduleDetail.value = false;
|
|
|
+ // 切换任务时检测是否为定时任务会话:自动打开/关闭定时任务详情标签
|
|
|
+ const scheduleItem = newTask.sessionId ? scheduleListForDetail.value.find((s: any) => s.feedback_session_id === newTask.sessionId) : undefined;
|
|
|
+ const panelState = getPanelState(newTask);
|
|
|
+ const existingScheduleTab = panelState.tabs.find((t) => t.type === 'schedule');
|
|
|
+ if (scheduleItem) {
|
|
|
+ if (existingScheduleTab) {
|
|
|
+ panelState.activeTabId = existingScheduleTab.id;
|
|
|
+ } else {
|
|
|
+ panelState.tabs.push({ id: 'schedule', type: 'schedule', title: scheduleItem.name || '定时任务', task: scheduleItem });
|
|
|
+ panelState.activeTabId = 'schedule';
|
|
|
+ }
|
|
|
+ } else if (existingScheduleTab) {
|
|
|
+ removeTab(panelState, existingScheduleTab.id);
|
|
|
}
|
|
|
|
|
|
await nextTick();
|
|
|
@@ -1046,40 +1030,48 @@
|
|
|
pendingFile.value = null;
|
|
|
};
|
|
|
|
|
|
- const removePreviewFile = () => {
|
|
|
- previewFile.value = null;
|
|
|
- previewEditable.value = false;
|
|
|
- // 关闭文件预览后,恢复定时任务详情面板
|
|
|
- const sessionId = currentTask.value?.sessionId;
|
|
|
- if (sessionId && scheduleListForDetail.value.some((s: any) => s.feedback_session_id === sessionId)) {
|
|
|
- showScheduleDetail.value = true;
|
|
|
+ const openFileTab = (file: AttachedFile, editable: boolean) => {
|
|
|
+ const task = currentTask.value;
|
|
|
+ if (!task) return;
|
|
|
+ const state = getPanelState(task);
|
|
|
+ const existing = state.tabs.find((t) => t.type === 'file' && t.file.id === file.id);
|
|
|
+ if (existing) {
|
|
|
+ // 编辑入口可升级已有标签为可编辑;预览入口不降级
|
|
|
+ if (editable) existing.editable = true;
|
|
|
+ state.activeTabId = existing.id;
|
|
|
+ return;
|
|
|
}
|
|
|
+ openTab({ id: nextTabId(), type: 'file', title: file.name, file, editable });
|
|
|
};
|
|
|
|
|
|
const handleAgentDetailClick = (data: AgentDetailData) => {
|
|
|
- // 同一个 agent 再次点击则关闭面板
|
|
|
- if (
|
|
|
- activeAgentDetail.value?.messageIndex === data.messageIndex &&
|
|
|
- activeAgentDetail.value?.agentId === data.agentId &&
|
|
|
- activeAgentDetail.value?.agent === data.agent
|
|
|
- ) {
|
|
|
- activeAgentDetail.value = null;
|
|
|
- } else {
|
|
|
- activeAgentDetail.value = { messageIndex: data.messageIndex, agent: data.agent, agentId: data.agentId };
|
|
|
- previewFile.value = null;
|
|
|
- showScheduleDetail.value = false;
|
|
|
+ const task = currentTask.value;
|
|
|
+ if (!task) return;
|
|
|
+ const state = getPanelState(task);
|
|
|
+ const existing = state.tabs.find(
|
|
|
+ (t) => t.type === 'agent' && t.messageIndex === data.messageIndex && (t.agentId || '') === (data.agentId || '') && t.agent === data.agent
|
|
|
+ );
|
|
|
+ if (existing) {
|
|
|
+ if (state.activeTabId === existing.id) {
|
|
|
+ // 保持原交互:再次点击已激活的 agent 则关闭
|
|
|
+ removeTab(state, existing.id);
|
|
|
+ } else {
|
|
|
+ state.activeTabId = existing.id;
|
|
|
+ }
|
|
|
+ return;
|
|
|
}
|
|
|
- };
|
|
|
-
|
|
|
- const closeAgentDetail = () => {
|
|
|
- activeAgentDetail.value = null;
|
|
|
+ openTab({
|
|
|
+ id: nextTabId(),
|
|
|
+ type: 'agent',
|
|
|
+ title: data.agent,
|
|
|
+ messageIndex: data.messageIndex,
|
|
|
+ agent: data.agent,
|
|
|
+ agentId: data.agentId,
|
|
|
+ });
|
|
|
};
|
|
|
|
|
|
const openFilePreview = (file: AttachedFile) => {
|
|
|
- previewFile.value = file;
|
|
|
- previewEditable.value = false;
|
|
|
- showScheduleDetail.value = false;
|
|
|
- activeAgentDetail.value = null;
|
|
|
+ openFileTab(file, false);
|
|
|
|
|
|
const msgIndex = messages.value.findIndex((m) => m.attachedFile?.id === file.id);
|
|
|
if (msgIndex !== -1) {
|
|
|
@@ -1100,7 +1092,47 @@
|
|
|
return currentTask?.attachedFiles || [];
|
|
|
};
|
|
|
|
|
|
- const currentTask = computed(() => taskList.value.find((t) => t.id === currentTaskId.value));
|
|
|
+ // 右侧辅助面板标签页状态(按任务维度保存,切换任务自动恢复)
|
|
|
+ let tabSeq = 0;
|
|
|
+ const nextTabId = () => `panel-${Date.now()}-${tabSeq++}`;
|
|
|
+
|
|
|
+ const getPanelState = (task: Task): RightPanelState => {
|
|
|
+ if (!task.rightPanels) {
|
|
|
+ task.rightPanels = { tabs: [], activeTabId: null };
|
|
|
+ }
|
|
|
+ return task.rightPanels;
|
|
|
+ };
|
|
|
+
|
|
|
+ const openTab = (tab: RightPanelTab) => {
|
|
|
+ const task = currentTask.value;
|
|
|
+ if (!task) return;
|
|
|
+ const state = getPanelState(task);
|
|
|
+ state.tabs.push(tab);
|
|
|
+ state.activeTabId = tab.id;
|
|
|
+ };
|
|
|
+
|
|
|
+ const activateTab = (tabId: string) => {
|
|
|
+ const task = currentTask.value;
|
|
|
+ if (!task?.rightPanels) return;
|
|
|
+ if (task.rightPanels.tabs.some((t) => t.id === tabId)) {
|
|
|
+ task.rightPanels.activeTabId = tabId;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const removeTab = (state: RightPanelState, tabId: string) => {
|
|
|
+ const idx = state.tabs.findIndex((t) => t.id === tabId);
|
|
|
+ if (idx === -1) return;
|
|
|
+ state.tabs.splice(idx, 1);
|
|
|
+ if (state.activeTabId === tabId) {
|
|
|
+ state.activeTabId = state.tabs.length ? (state.tabs[idx] || state.tabs[idx - 1]).id : null;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const closeTab = (tabId: string) => {
|
|
|
+ const task = currentTask.value;
|
|
|
+ if (!task?.rightPanels) return;
|
|
|
+ removeTab(task.rightPanels, tabId);
|
|
|
+ };
|
|
|
|
|
|
const currentWordUrl = computed(() => {
|
|
|
return currentTask.value?.wordUrl || '';
|
|
|
@@ -1136,15 +1168,14 @@
|
|
|
const downloadWordReport = () => downloadWordFile(currentWordUrl.value);
|
|
|
|
|
|
const handleOpenFilePreview = (file: AttachedFile) => {
|
|
|
- previewFile.value = file;
|
|
|
- previewEditable.value = true;
|
|
|
- showScheduleDetail.value = false;
|
|
|
- activeAgentDetail.value = null;
|
|
|
+ openFileTab(file, true);
|
|
|
};
|
|
|
|
|
|
const handleSaveSuccess = (content: string) => {
|
|
|
- if (previewFile.value) {
|
|
|
- previewFile.value = { ...previewFile.value, content };
|
|
|
+ const task = currentTask.value;
|
|
|
+ const tab = task?.rightPanels?.tabs.find((t) => t.id === task.rightPanels?.activeTabId);
|
|
|
+ if (tab?.type === 'file') {
|
|
|
+ tab.file = { ...tab.file, content };
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -1389,8 +1420,11 @@
|
|
|
timestamp: Date.now(),
|
|
|
});
|
|
|
}
|
|
|
- currentTodos.value = [...(data.todos || [])];
|
|
|
- showTodoBar.value = true;
|
|
|
+ // todos 与任务绑定:仅当用户正在查看该任务时才更新进度条,避免流式期间切换/新建任务后串台
|
|
|
+ if (isViewingTask) {
|
|
|
+ currentTodos.value = [...(data.todos || [])];
|
|
|
+ showTodoBar.value = true;
|
|
|
+ }
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
@@ -1885,8 +1919,8 @@
|
|
|
}
|
|
|
|
|
|
messages.value = targetTask.messages && targetTask.messages.length > 0 ? [...targetTask.messages] : getDefaultMessages();
|
|
|
+ syncTodoBarFromMessages();
|
|
|
|
|
|
- previewFile.value = null;
|
|
|
pendingFile.value = null;
|
|
|
showFileList.value = false;
|
|
|
inputMessage.value = '';
|
|
|
@@ -2078,11 +2112,6 @@
|
|
|
width: auto;
|
|
|
min-width: 0;
|
|
|
}
|
|
|
-
|
|
|
- &.narrow {
|
|
|
- width: 320px;
|
|
|
- flex-shrink: 0;
|
|
|
- }
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
@@ -2339,14 +2368,14 @@
|
|
|
.right-panel {
|
|
|
background-image: var(--img-chat-fs-panel-bg) !important;
|
|
|
}
|
|
|
+ :deep(.panel-tab-bar) {
|
|
|
+ padding: 14px 16px 0;
|
|
|
+ }
|
|
|
.schedule-panel,
|
|
|
.skill-panel,
|
|
|
.sub-agent-panel {
|
|
|
background-image: var(--img-chat-fs-panel-bg) !important;
|
|
|
}
|
|
|
- .schedule-detail-panel {
|
|
|
- background-image: var(--img-chat-fs-task-panel-bg) !important;
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
.zxm-modal-header {
|