|
|
@@ -74,16 +74,15 @@
|
|
|
/>
|
|
|
|
|
|
<!-- 下载确认条 -->
|
|
|
- <div v-if="downloadConfirm" class="download-confirm-bar">
|
|
|
- <span class="download-confirm-text">是否下载 {{ downloadConfirm.filename }} 到本地?</span>
|
|
|
- <div class="download-confirm-actions">
|
|
|
- <button class="download-confirm-btn download-btn" @click="doDownload">下载</button>
|
|
|
- <button class="download-confirm-btn cancel-btn" @click="downloadConfirm = null">取消</button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <DownloadConfirmBar
|
|
|
+ v-if="downloadConfirm && downloadConfirmTaskId === currentTaskId"
|
|
|
+ :filename="downloadConfirm.filename"
|
|
|
+ @confirm="doDownload"
|
|
|
+ @cancel="clearDownloadConfirm"
|
|
|
+ />
|
|
|
|
|
|
<!-- 任务进度条 -->
|
|
|
- <TodoListBar v-if="currentTodos.length > 0" :todos="currentTodos" />
|
|
|
+ <TodoListBar v-if="showTodoBar" :todos="currentTodos" />
|
|
|
|
|
|
<!-- 输入区域 -->
|
|
|
<ChatInputArea
|
|
|
@@ -91,6 +90,7 @@
|
|
|
v-model="inputMessage"
|
|
|
:pendingFile="pendingFile"
|
|
|
:loading="loading || !!pendingApprovalSessionId"
|
|
|
+ :streaming="streaming"
|
|
|
:contextUsed="contextUsed"
|
|
|
:contextMax="contextMax"
|
|
|
:contextPercent="contextPercent"
|
|
|
@@ -98,13 +98,16 @@
|
|
|
:initialThinkLevel="thinkLevel"
|
|
|
:initialEditMode="editMode"
|
|
|
:taskList="taskList"
|
|
|
+ :currentTaskId="currentTaskId"
|
|
|
@send="handleSendMessage"
|
|
|
+ @stop="stopStreaming"
|
|
|
@file-upload="triggerFileUpload"
|
|
|
@file-change="handleFileUpload"
|
|
|
@remove-pending-file="removePendingFile"
|
|
|
@think-level-change="handleThinkLevelChange"
|
|
|
@edit-mode-change="handleEditModeChange"
|
|
|
- @insert-session="handleInsertSession"
|
|
|
+ @update:selected-sessions="handleUpdateSelectedSessions"
|
|
|
+ @update:selected-skill="handleUpdateSelectedSkill"
|
|
|
/>
|
|
|
</div>
|
|
|
|
|
|
@@ -141,7 +144,9 @@
|
|
|
import ChatInputArea from './chatModal/ChatInputArea.vue';
|
|
|
import FilePreviewPanel from './chatModal/FilePreviewPanel.vue';
|
|
|
import TodoListBar from './chatModal/TodoListBar.vue';
|
|
|
+ import DownloadConfirmBar from './chatModal/DownloadConfirmBar.vue';
|
|
|
import type { AttachedFile, Message, Task } from './chatModal/types';
|
|
|
+ import { isTextFile } from './chatModal/utils';
|
|
|
|
|
|
interface Props {
|
|
|
visible: boolean;
|
|
|
@@ -214,9 +219,28 @@
|
|
|
const pendingApprovalSessionId = ref('');
|
|
|
const pendingApprovalThreadId = ref('');
|
|
|
const downloadConfirm = ref<{ url: string; filename: string } | null>(null);
|
|
|
+ const downloadConfirmTaskId = ref('');
|
|
|
+ let downloadConfirmTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
+
|
|
|
+ const clearDownloadConfirm = () => {
|
|
|
+ downloadConfirm.value = null;
|
|
|
+ downloadConfirmTaskId.value = '';
|
|
|
+ if (downloadConfirmTimer) {
|
|
|
+ clearTimeout(downloadConfirmTimer);
|
|
|
+ downloadConfirmTimer = null;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const startDownloadConfirmTimer = () => {
|
|
|
+ if (downloadConfirmTimer) clearTimeout(downloadConfirmTimer);
|
|
|
+ downloadConfirmTimer = setTimeout(() => {
|
|
|
+ clearDownloadConfirm();
|
|
|
+ }, 10000);
|
|
|
+ };
|
|
|
const contextBreakdown = ref({ messages: 0, mcp: 0, skills: 0, system_prompt: 0, other: 0 });
|
|
|
|
|
|
const currentTodos = ref<Array<{ content: string; status: string }>>([]);
|
|
|
+ const showTodoBar = ref(false);
|
|
|
|
|
|
const syncTodoBarFromMessages = () => {
|
|
|
for (let i = messages.value.length - 1; i >= 0; i--) {
|
|
|
@@ -226,11 +250,13 @@
|
|
|
const step = msg.thinkingSteps[j];
|
|
|
if (step.type === 'updated_todo_list' && step.todos) {
|
|
|
currentTodos.value = [...step.todos];
|
|
|
+ showTodoBar.value = true;
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+ showTodoBar.value = false;
|
|
|
currentTodos.value = [];
|
|
|
};
|
|
|
|
|
|
@@ -312,6 +338,14 @@
|
|
|
|
|
|
const inputMessage = ref('');
|
|
|
const loading = ref(false);
|
|
|
+ const streaming = ref(false);
|
|
|
+ const activeAbortController = ref<AbortController | null>(null);
|
|
|
+ const selectedSessions = ref<Array<{ id: string; name: string }>>([]);
|
|
|
+ const selectedSkill = ref<{ name: string; display_name: string } | null>(null);
|
|
|
+
|
|
|
+ const stopStreaming = () => {
|
|
|
+ activeAbortController.value?.abort();
|
|
|
+ };
|
|
|
|
|
|
const getCurrentSessionId = (): string => {
|
|
|
const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
|
|
|
@@ -423,8 +457,22 @@
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- const handleInsertSession = (_taskId: string, _taskName: string) => {
|
|
|
- // 会话引用已由 ChatInputArea 内部以 chip 形式管理
|
|
|
+ const handleUpdateSelectedSessions = (sessions: Array<{ id: string; name: string }>) => {
|
|
|
+ selectedSessions.value = sessions;
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleUpdateSelectedSkill = (skill: { name: string; display_name: string } | null) => {
|
|
|
+ selectedSkill.value = skill;
|
|
|
+ };
|
|
|
+
|
|
|
+ const getSkillRefIconUrl = () => {
|
|
|
+ const val = getComputedStyle(document.documentElement).getPropertyValue('--img-chat-options-skill-icon').trim();
|
|
|
+ return val.replace(/^url\(["']?/, '').replace(/["']?\)$/, '');
|
|
|
+ };
|
|
|
+
|
|
|
+ const getSessionRefIconUrl = () => {
|
|
|
+ const val = getComputedStyle(document.documentElement).getPropertyValue('--img-chat-popup-session-icon').trim();
|
|
|
+ return val.replace(/^url\(["']?/, '').replace(/["']?\)$/, '');
|
|
|
};
|
|
|
|
|
|
const handleEditModeChange = async (modeKey: string) => {
|
|
|
@@ -445,29 +493,18 @@
|
|
|
const approveTask = taskList.value.find((t) => t.sessionId === sessionId);
|
|
|
if (!approveTask) return;
|
|
|
|
|
|
- // 清除中断消息的 pendingApproval 状态,获取中断前累计时长和思考内容
|
|
|
const taskMessages = approveTask.messages;
|
|
|
const lastMsg = taskMessages[taskMessages.length - 1];
|
|
|
- const prevElapsedMs = lastMsg?.pendingApprovalData?.elapsedMs || 0;
|
|
|
- const prevThinkingContent = lastMsg?.pendingApprovalData?.prevThinkingContent || lastMsg?.thinkingContent || '';
|
|
|
- if (lastMsg && lastMsg.isPendingApproval) {
|
|
|
- lastMsg.isPendingApproval = false;
|
|
|
- }
|
|
|
+ if (!lastMsg || !lastMsg.isPendingApproval) return;
|
|
|
|
|
|
- const now = dayjs();
|
|
|
- const aiMsgIndex = taskMessages.length;
|
|
|
- const aiMsg: Message = {
|
|
|
- type: 'ai',
|
|
|
- content: '',
|
|
|
- thinkingContent: prevThinkingContent,
|
|
|
- time: now.format('HH:mm'),
|
|
|
- createdAt: now.toISOString(),
|
|
|
- isLoading: true,
|
|
|
- thinkingSteps: [],
|
|
|
- generateStartTime: Date.now(),
|
|
|
- baseDurationMs: prevElapsedMs,
|
|
|
- };
|
|
|
- taskMessages.push(aiMsg);
|
|
|
+ // 复用同一条消息继续渲染:清除审批状态,恢复 loading,重置前端计时起点
|
|
|
+ lastMsg.isPendingApproval = false;
|
|
|
+ lastMsg.pendingApprovalData = undefined;
|
|
|
+ lastMsg.isLoading = true;
|
|
|
+ lastMsg.generateStartTime = Date.now();
|
|
|
+ // thinkingSteps、thinkingContent、baseDurationMs 保持不变,后续继续累积
|
|
|
+
|
|
|
+ const aiMsgIndex = taskMessages.length - 1;
|
|
|
approveTask.messages = [...taskMessages];
|
|
|
if (currentTaskId.value === approveTask.id) {
|
|
|
messages.value = approveTask.messages;
|
|
|
@@ -476,6 +513,12 @@
|
|
|
|
|
|
loading.value = true;
|
|
|
approveTask.isStreaming = true;
|
|
|
+ showTodoBar.value = false;
|
|
|
+ currentTodos.value = [];
|
|
|
+
|
|
|
+ const controller = new AbortController();
|
|
|
+ activeAbortController.value = controller;
|
|
|
+ streaming.value = true;
|
|
|
|
|
|
try {
|
|
|
const result = await chatResumeStream(
|
|
|
@@ -483,6 +526,7 @@
|
|
|
session_id: sessionId,
|
|
|
thread_id: pendingApprovalThreadId.value || undefined,
|
|
|
action: 'approve',
|
|
|
+ signal: controller.signal,
|
|
|
},
|
|
|
(chunk: string) => {
|
|
|
parseSSEData(chunk, aiMsgIndex, approveTask.id);
|
|
|
@@ -494,13 +538,27 @@
|
|
|
setCurrentSessionId(result.session_id);
|
|
|
}
|
|
|
} catch (error) {
|
|
|
- console.error('审批继续执行失败:', error);
|
|
|
- message.error('审批失败,请重试');
|
|
|
- if (aiMsg.isLoading) {
|
|
|
- aiMsg.isLoading = false;
|
|
|
- aiMsg.content = '抱歉,审批请求失败,请稍后重试。';
|
|
|
+ if ((error as Error)?.name === 'AbortError') {
|
|
|
+ // 用户主动停止生成
|
|
|
+ lastMsg.isLoading = false;
|
|
|
+ // 后端未返回耗时,冻结前端伪计时
|
|
|
+ lastMsg.durationMs = (lastMsg.baseDurationMs || 0) + (lastMsg.generateStartTime ? Date.now() - lastMsg.generateStartTime : 0);
|
|
|
+ if (!lastMsg.content) {
|
|
|
+ lastMsg.content = '已停止生成。';
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ console.error('审批继续执行失败:', error);
|
|
|
+ message.error('审批失败,请重试');
|
|
|
+ lastMsg.isLoading = false;
|
|
|
+ if (!lastMsg.content) {
|
|
|
+ lastMsg.content = '抱歉,审批请求失败,请稍后重试。';
|
|
|
+ }
|
|
|
}
|
|
|
} finally {
|
|
|
+ if (activeAbortController.value === controller) {
|
|
|
+ activeAbortController.value = null;
|
|
|
+ }
|
|
|
+ streaming.value = false;
|
|
|
loading.value = false;
|
|
|
approveTask.isStreaming = false;
|
|
|
pendingApprovalSessionId.value = '';
|
|
|
@@ -518,18 +576,27 @@
|
|
|
const rejectTask = taskList.value.find((t) => t.sessionId === sessionId);
|
|
|
if (!rejectTask) return;
|
|
|
|
|
|
- // 清除中断消息的 pendingApproval 状态
|
|
|
const taskMessages = rejectTask.messages;
|
|
|
const lastMsg = taskMessages[taskMessages.length - 1];
|
|
|
- if (lastMsg && lastMsg.isPendingApproval) {
|
|
|
- lastMsg.isPendingApproval = false;
|
|
|
- }
|
|
|
+ if (!lastMsg || !lastMsg.isPendingApproval) return;
|
|
|
+
|
|
|
+ // 复用同一条消息:清除审批状态,标记为已拒绝
|
|
|
+ lastMsg.isPendingApproval = false;
|
|
|
+ lastMsg.pendingApprovalData = undefined;
|
|
|
+ lastMsg.isLoading = false;
|
|
|
+ lastMsg.content = (lastMsg.content ? lastMsg.content + '\n\n' : '') + '已拒绝执行计划。';
|
|
|
+
|
|
|
+ rejectTask.messages = [...taskMessages];
|
|
|
+
|
|
|
+ const threadId = pendingApprovalThreadId.value || undefined;
|
|
|
+ pendingApprovalSessionId.value = '';
|
|
|
+ pendingApprovalThreadId.value = '';
|
|
|
|
|
|
try {
|
|
|
await chatResumeStream(
|
|
|
{
|
|
|
session_id: sessionId,
|
|
|
- thread_id: pendingApprovalThreadId.value || undefined,
|
|
|
+ thread_id: threadId,
|
|
|
action: 'reject',
|
|
|
},
|
|
|
() => {}
|
|
|
@@ -538,19 +605,6 @@
|
|
|
console.error('拒绝操作失败:', error);
|
|
|
}
|
|
|
|
|
|
- const now = dayjs();
|
|
|
- const rejectMsg: Message = {
|
|
|
- type: 'ai',
|
|
|
- content: '已拒绝执行计划。',
|
|
|
- time: now.format('HH:mm'),
|
|
|
- createdAt: now.toISOString(),
|
|
|
- };
|
|
|
- taskMessages.push(rejectMsg);
|
|
|
- rejectTask.messages = [...taskMessages];
|
|
|
-
|
|
|
- pendingApprovalSessionId.value = '';
|
|
|
- pendingApprovalThreadId.value = '';
|
|
|
-
|
|
|
if (currentTaskId.value === rejectTask.id) {
|
|
|
messages.value = rejectTask.messages;
|
|
|
await scrollToBottom();
|
|
|
@@ -649,10 +703,17 @@
|
|
|
};
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
- reader.onload = (e) => {
|
|
|
- newFile.preview = e.target?.result as string;
|
|
|
- };
|
|
|
- reader.readAsDataURL(file);
|
|
|
+ if (isTextFile(file.name)) {
|
|
|
+ reader.onload = (e) => {
|
|
|
+ newFile.content = e.target?.result as string;
|
|
|
+ };
|
|
|
+ reader.readAsText(file);
|
|
|
+ } else {
|
|
|
+ reader.onload = (e) => {
|
|
|
+ newFile.preview = e.target?.result as string;
|
|
|
+ };
|
|
|
+ reader.readAsDataURL(file);
|
|
|
+ }
|
|
|
|
|
|
pendingFile.value = newFile;
|
|
|
message.success(`文件 "${file.name}" 已添加,点击发送按钮提交`);
|
|
|
@@ -736,7 +797,7 @@
|
|
|
document.body.appendChild(a);
|
|
|
a.click();
|
|
|
document.body.removeChild(a);
|
|
|
- downloadConfirm.value = null;
|
|
|
+ clearDownloadConfirm();
|
|
|
};
|
|
|
|
|
|
const downloadWordFile = (url: string) => {
|
|
|
@@ -764,11 +825,27 @@
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const userInput = inputMessage.value.trim();
|
|
|
+ const sessionPrefix = selectedSessions.value.length > 0 ? selectedSessions.value.map((s) => `#session:${s.id}`).join(' ') + ' ' : '';
|
|
|
+ const skillPrefix = selectedSkill.value ? `#skill:${selectedSkill.value.name} ` : '';
|
|
|
+ const userInput = skillPrefix + sessionPrefix + inputMessage.value.trim();
|
|
|
+
|
|
|
+ let displayContent = inputMessage.value.trim();
|
|
|
+ if (selectedSkill.value) {
|
|
|
+ const skillIconUrl = getSkillRefIconUrl();
|
|
|
+ displayContent = `[<img src="${skillIconUrl}" class="session-ref-icon" /> 技能:${selectedSkill.value.display_name}] ${displayContent}`;
|
|
|
+ }
|
|
|
+ if (selectedSessions.value.length > 0) {
|
|
|
+ const names = selectedSessions.value.map((s) => s.name).join('、');
|
|
|
+ const iconUrl = getSessionRefIconUrl();
|
|
|
+ displayContent = `[<img src="${iconUrl}" class="session-ref-icon" /> 引用 ${selectedSessions.value.length} 个会话:${names}] ${displayContent}`;
|
|
|
+ }
|
|
|
+
|
|
|
const fileToSend = pendingFile.value;
|
|
|
|
|
|
inputMessage.value = '';
|
|
|
pendingFile.value = null;
|
|
|
+ selectedSessions.value = [];
|
|
|
+ selectedSkill.value = null;
|
|
|
|
|
|
try {
|
|
|
loading.value = true;
|
|
|
@@ -776,9 +853,9 @@
|
|
|
chatMessagesRef.value?.forceScrollToBottom();
|
|
|
|
|
|
if (hasFile) {
|
|
|
- await sendWithAttachment(fileToSend!, userInput);
|
|
|
+ await sendWithAttachment(fileToSend!, userInput, displayContent);
|
|
|
} else {
|
|
|
- await sendTextOnly(userInput);
|
|
|
+ await sendTextOnly(userInput, displayContent);
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('发送消息失败:', error);
|
|
|
@@ -953,6 +1030,7 @@
|
|
|
});
|
|
|
}
|
|
|
currentTodos.value = [...(data.todos || [])];
|
|
|
+ showTodoBar.value = true;
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
@@ -981,6 +1059,8 @@
|
|
|
});
|
|
|
if (data.download_url) {
|
|
|
downloadConfirm.value = { url: data.download_url, filename: data.filename || '文件' };
|
|
|
+ downloadConfirmTaskId.value = taskId;
|
|
|
+ startDownloadConfirmTimer();
|
|
|
}
|
|
|
break;
|
|
|
}
|
|
|
@@ -993,9 +1073,10 @@
|
|
|
message: `${(data.message || '审查完成').replace(/(\d+ms)/g, '')}${data.duration_ms ? `(${(data.duration_ms / 1000).toFixed(0)}s)` : ''}${data.progress ? ` [${data.progress}]` : ''}`,
|
|
|
timestamp: Date.now(),
|
|
|
});
|
|
|
- // 用后端返回的时间替换前端计时,作为中断前的累计基准
|
|
|
+ // 累加后端返回的各段耗时,并重置前端计时起点,避免重复计算已验证的时间段
|
|
|
if (data.duration_ms) {
|
|
|
- aiMsg.baseDurationMs = data.duration_ms;
|
|
|
+ aiMsg.baseDurationMs = (aiMsg.baseDurationMs || 0) + data.duration_ms;
|
|
|
+ aiMsg.generateStartTime = Date.now();
|
|
|
}
|
|
|
if (data.preview) {
|
|
|
aiMsg.content += data.preview;
|
|
|
@@ -1030,6 +1111,8 @@
|
|
|
timestamp: Date.now(),
|
|
|
});
|
|
|
aiMsg.isLoading = false;
|
|
|
+ // 后端未返回耗时,冻结前端伪计时
|
|
|
+ aiMsg.durationMs = (aiMsg.baseDurationMs || 0) + (aiMsg.generateStartTime ? Date.now() - aiMsg.generateStartTime : 0);
|
|
|
break;
|
|
|
|
|
|
case 'word_download':
|
|
|
@@ -1044,9 +1127,9 @@
|
|
|
aiMsg.sessionId = data.session_id;
|
|
|
fetchContext(data.session_id);
|
|
|
}
|
|
|
- if (data.duration_ms) {
|
|
|
- aiMsg.durationMs = (aiMsg.baseDurationMs || 0) + data.duration_ms;
|
|
|
- }
|
|
|
+ aiMsg.durationMs = data.duration_ms
|
|
|
+ ? (aiMsg.baseDurationMs || 0) + data.duration_ms
|
|
|
+ : (aiMsg.baseDurationMs || 0) + (aiMsg.generateStartTime ? Date.now() - aiMsg.generateStartTime : 0);
|
|
|
aiMsg.isLoading = false;
|
|
|
break;
|
|
|
|
|
|
@@ -1057,8 +1140,9 @@
|
|
|
break;
|
|
|
|
|
|
case 'interrupt': {
|
|
|
- // 优先使用 agent_done 已设置的后端时间,否则用前端计时
|
|
|
- const elapsedMs = aiMsg.baseDurationMs || (aiMsg.generateStartTime ? Date.now() - aiMsg.generateStartTime : 0);
|
|
|
+ // 后端各段耗时 + 前端当前段计时,吸收进 baseDurationMs 保证审批继续后计时连续
|
|
|
+ const elapsedMs = (aiMsg.baseDurationMs || 0) + (aiMsg.generateStartTime ? Date.now() - aiMsg.generateStartTime : 0);
|
|
|
+ aiMsg.baseDurationMs = elapsedMs;
|
|
|
aiMsg.isLoading = false;
|
|
|
aiMsg.isPendingApproval = true;
|
|
|
const interruptSessionId = data.session_id || streamingTask.sessionId;
|
|
|
@@ -1114,13 +1198,13 @@
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- const sendTextOnly = async (userInput: string) => {
|
|
|
+ const sendTextOnly = async (userInput: string, displayContent?: string) => {
|
|
|
const originTaskId = currentTaskId.value;
|
|
|
|
|
|
const now = dayjs();
|
|
|
const userMsg: Message = {
|
|
|
type: 'user',
|
|
|
- content: userInput,
|
|
|
+ content: displayContent || userInput,
|
|
|
time: now.format('HH:mm'),
|
|
|
createdAt: now.toISOString(),
|
|
|
};
|
|
|
@@ -1140,15 +1224,23 @@
|
|
|
messages.value.push(aiMsg);
|
|
|
await scrollToBottom();
|
|
|
|
|
|
+ showTodoBar.value = false;
|
|
|
+ currentTodos.value = [];
|
|
|
+
|
|
|
const originTask = taskList.value.find((t) => t.id === originTaskId);
|
|
|
if (originTask) originTask.isStreaming = true;
|
|
|
|
|
|
+ const controller = new AbortController();
|
|
|
+ activeAbortController.value = controller;
|
|
|
+ streaming.value = true;
|
|
|
+
|
|
|
try {
|
|
|
const result = await unifiedStream(
|
|
|
{
|
|
|
message: userInput,
|
|
|
session_id: getCurrentSessionId() || undefined,
|
|
|
mode: editMode.value,
|
|
|
+ signal: controller.signal,
|
|
|
},
|
|
|
(chunk: string) => {
|
|
|
parseSSEData(chunk, aiMsgIndex, originTaskId);
|
|
|
@@ -1171,13 +1263,37 @@
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+ } catch (error) {
|
|
|
+ // 用户主动停止生成
|
|
|
+ if ((error as Error)?.name === 'AbortError') {
|
|
|
+ const originTask = taskList.value.find((t) => t.id === originTaskId);
|
|
|
+ const arr = currentTaskId.value === originTaskId ? messages.value : originTask?.messages;
|
|
|
+ const aiMsg = arr?.[aiMsgIndex];
|
|
|
+ if (aiMsg) {
|
|
|
+ aiMsg.isLoading = false;
|
|
|
+ // 后端未返回耗时,冻结前端伪计时
|
|
|
+ aiMsg.durationMs = (aiMsg.baseDurationMs || 0) + (aiMsg.generateStartTime ? Date.now() - aiMsg.generateStartTime : 0);
|
|
|
+ if (!aiMsg.content) {
|
|
|
+ aiMsg.content = '已停止生成。';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (originTask && currentTaskId.value !== originTaskId) {
|
|
|
+ originTask.messages = [...(arr || [])];
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ throw error;
|
|
|
} finally {
|
|
|
+ if (activeAbortController.value === controller) {
|
|
|
+ activeAbortController.value = null;
|
|
|
+ }
|
|
|
+ streaming.value = false;
|
|
|
const originTask = taskList.value.find((t) => t.id === originTaskId);
|
|
|
if (originTask) originTask.isStreaming = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- const sendWithAttachment = async (file: AttachedFile, userInput: string) => {
|
|
|
+ const sendWithAttachment = async (file: AttachedFile, userInput: string, displayContent?: string) => {
|
|
|
const originTaskId = currentTaskId.value;
|
|
|
let currentTask = taskList.value.find((t) => t.id === originTaskId);
|
|
|
if (currentTask) {
|
|
|
@@ -1189,7 +1305,7 @@
|
|
|
const now = dayjs();
|
|
|
const userMsg: Message = {
|
|
|
type: 'user',
|
|
|
- content: userInput || `上传了文件:${file.name}`,
|
|
|
+ content: displayContent || userInput || `上传了文件:${file.name}`,
|
|
|
time: now.format('HH:mm'),
|
|
|
createdAt: now.toISOString(),
|
|
|
attachedFile: file,
|
|
|
@@ -1210,6 +1326,9 @@
|
|
|
messages.value.push(aiMsg);
|
|
|
await scrollToBottom();
|
|
|
|
|
|
+ showTodoBar.value = false;
|
|
|
+ currentTodos.value = [];
|
|
|
+
|
|
|
if (!file.originalFile) {
|
|
|
throw new Error('缺少原始文件对象');
|
|
|
}
|
|
|
@@ -1217,6 +1336,10 @@
|
|
|
const originTask = taskList.value.find((t) => t.id === originTaskId);
|
|
|
if (originTask) originTask.isStreaming = true;
|
|
|
|
|
|
+ const controller = new AbortController();
|
|
|
+ activeAbortController.value = controller;
|
|
|
+ streaming.value = true;
|
|
|
+
|
|
|
try {
|
|
|
const result = await reviewPdfStream(
|
|
|
{
|
|
|
@@ -1224,6 +1347,7 @@
|
|
|
session_id: getCurrentSessionId() || undefined,
|
|
|
message: userInput || undefined,
|
|
|
mode: editMode.value,
|
|
|
+ signal: controller.signal,
|
|
|
},
|
|
|
(chunk: string) => {
|
|
|
parseSSEData(chunk, aiMsgIndex, originTaskId);
|
|
|
@@ -1247,7 +1371,31 @@
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+ } catch (error) {
|
|
|
+ // 用户主动停止生成
|
|
|
+ if ((error as Error)?.name === 'AbortError') {
|
|
|
+ const originTask = taskList.value.find((t) => t.id === originTaskId);
|
|
|
+ const arr = currentTaskId.value === originTaskId ? messages.value : originTask?.messages;
|
|
|
+ const aiMsg = arr?.[aiMsgIndex];
|
|
|
+ if (aiMsg) {
|
|
|
+ aiMsg.isLoading = false;
|
|
|
+ // 后端未返回耗时,冻结前端伪计时
|
|
|
+ aiMsg.durationMs = (aiMsg.baseDurationMs || 0) + (aiMsg.generateStartTime ? Date.now() - aiMsg.generateStartTime : 0);
|
|
|
+ if (!aiMsg.content) {
|
|
|
+ aiMsg.content = '已停止生成。';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (originTask && currentTaskId.value !== originTaskId) {
|
|
|
+ originTask.messages = [...(arr || [])];
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ throw error;
|
|
|
} finally {
|
|
|
+ if (activeAbortController.value === controller) {
|
|
|
+ activeAbortController.value = null;
|
|
|
+ }
|
|
|
+ streaming.value = false;
|
|
|
const originTask = taskList.value.find((t) => t.id === originTaskId);
|
|
|
if (originTask) originTask.isStreaming = false;
|
|
|
}
|
|
|
@@ -1423,6 +1571,12 @@
|
|
|
onBeforeUnmount(() => {
|
|
|
document.removeEventListener('mousemove', onDragMove);
|
|
|
document.removeEventListener('mouseup', onDragEnd);
|
|
|
+ if (downloadConfirmTimer) {
|
|
|
+ clearTimeout(downloadConfirmTimer);
|
|
|
+ downloadConfirmTimer = null;
|
|
|
+ }
|
|
|
+ // 组件卸载时主动断开流式连接
|
|
|
+ activeAbortController.value?.abort();
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
@@ -1495,61 +1649,6 @@
|
|
|
overflow: hidden;
|
|
|
min-width: 0;
|
|
|
|
|
|
- .download-confirm-bar {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- padding: 10px 16px;
|
|
|
- margin: 0 16px;
|
|
|
- background: #0a2a3f;
|
|
|
- border: 1px solid #1a4a6f;
|
|
|
- border-radius: 8px;
|
|
|
- flex-shrink: 0;
|
|
|
-
|
|
|
- .download-confirm-text {
|
|
|
- font-size: 13px;
|
|
|
- color: #e0e6ed;
|
|
|
- overflow: hidden;
|
|
|
- text-overflow: ellipsis;
|
|
|
- white-space: nowrap;
|
|
|
- margin-right: 12px;
|
|
|
- }
|
|
|
-
|
|
|
- .download-confirm-actions {
|
|
|
- display: flex;
|
|
|
- gap: 8px;
|
|
|
- flex-shrink: 0;
|
|
|
- }
|
|
|
-
|
|
|
- .download-confirm-btn {
|
|
|
- padding: 4px 14px;
|
|
|
- border-radius: 4px;
|
|
|
- font-size: 12px;
|
|
|
- cursor: pointer;
|
|
|
- border: none;
|
|
|
- transition: all 0.2s;
|
|
|
-
|
|
|
- &.download-btn {
|
|
|
- background: #0a84ff;
|
|
|
- color: #fff;
|
|
|
-
|
|
|
- &:hover {
|
|
|
- background: #3a9bfd;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- &.cancel-btn {
|
|
|
- background: rgba(255, 255, 255, 0.08);
|
|
|
- color: #e0e6ed;
|
|
|
- border: 1px solid rgba(63, 80, 106, 0.5);
|
|
|
-
|
|
|
- &:hover {
|
|
|
- background: rgba(255, 255, 255, 0.15);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
:deep(.chat-header) {
|
|
|
display: flex;
|
|
|
padding: 12px 20px;
|