AiAssistantModal.vue 68 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196
  1. <template>
  2. <a-modal
  3. :visible="props.visible"
  4. @cancel="emit('close')"
  5. :width="isFullscreen ? '100vw' : 1400"
  6. :footer="null"
  7. destroyOnClose
  8. :class="['ai-assistant-modal', { fullscreen: isFullscreen }]"
  9. >
  10. <template #title>
  11. <div :class="['draggable-title', { 'no-drag': isFullscreen }]" @mousedown="onDragStart">"通安智风"大模型</div>
  12. </template>
  13. <!-- 最大化/还原按钮 -->
  14. <div class="maximize-btn" @click="toggleFullscreen">
  15. <div :class="['maximize-icon', { minimized: isFullscreen }]"></div>
  16. </div>
  17. <!-- 收起/展开按钮 -->
  18. <div class="collapse-btn" @click="togglePanel">
  19. <div :class="['collapse-icon', { collapsed: panelCollapsed }]"></div>
  20. </div>
  21. <div class="ai-container">
  22. <!-- 左侧任务列表 -->
  23. <div :class="['task-panel-wrapper', { collapsed: panelCollapsed }]">
  24. <TaskListPanel
  25. :taskList="taskList"
  26. :currentTaskId="currentTaskId"
  27. @switch-task="switchTask"
  28. @create-task="createNewTask"
  29. @delete-task="handleDeleteTask"
  30. @pin-task="handlePinTask"
  31. @toggle-skill="toggleSkillPanel"
  32. @toggle-schedules="toggleSchedulePanel"
  33. />
  34. </div>
  35. <!-- 右侧内容区:根据 activePanel 切换显示 -->
  36. <!-- 聊天区域 -->
  37. <template v-if="activePanel === 'chat'">
  38. <div class="chat-panel">
  39. <!-- 顶部任务选择器 -->
  40. <div class="chat-header">
  41. <template v-if="panelCollapsed">
  42. <div :class="['nav-btn', { disabled: !canGoPrev }]" @click="goToPrev">
  43. <div class="nav-icon prev-icon"></div>
  44. </div>
  45. <div :class="['nav-btn', { disabled: !canGoNext }]" @click="goToNext">
  46. <div class="nav-icon next-icon"></div>
  47. </div>
  48. <div class="nav-btn" @click="createNewTask">
  49. <div class="nav-icon add-icon"></div>
  50. </div>
  51. </template>
  52. <div v-if="currentTask?.sessionId" class="chat-header-title-wrapper">
  53. <template v-if="editingTitle">
  54. <input v-model="editTitleValue" class="title-edit-input" @keyup.enter="saveTitle" @keyup.escape="cancelEditTitle" @blur="saveTitle" />
  55. </template>
  56. <template v-else>
  57. <div class="title-edit-btn" @click="startEditTitle" title="编辑标题"></div>
  58. <span class="chat-header-title">{{ currentTask.name }}</span>
  59. </template>
  60. </div>
  61. <div v-if="currentWordUrl" class="upload-bg" @click="downloadWordReport">
  62. <div class="download-icon"></div>
  63. </div>
  64. </div>
  65. <!-- 附件列表面板 -->
  66. <FileListPanel :visible="showFileList" :files="getCurrentTaskFiles()" @close="showFileList = false" @file-click="openFilePreview" />
  67. <!-- 消息列表 -->
  68. <ChatMessages
  69. ref="chatMessagesRef"
  70. :messages="messages"
  71. @file-preview="openFilePreview"
  72. @download-word="downloadWordFile"
  73. @approve="handleApprove"
  74. @reject="handleReject"
  75. />
  76. <!-- 下载确认条 -->
  77. <DownloadConfirmBar
  78. v-if="downloadConfirm && downloadConfirmTaskId === currentTaskId"
  79. :filename="downloadConfirm.filename"
  80. @confirm="doDownload"
  81. @cancel="clearDownloadConfirm"
  82. />
  83. <!-- 任务进度条 -->
  84. <TodoListBar v-if="showTodoBar" :todos="currentTodos" />
  85. <!-- 输入区域 -->
  86. <ChatInputArea
  87. ref="chatInputRef"
  88. v-model="inputMessage"
  89. :pendingFile="pendingFile"
  90. :loading="loading || !!pendingApprovalSessionId"
  91. :streaming="streaming"
  92. :contextUsed="contextUsed"
  93. :contextMax="contextMax"
  94. :contextPercent="contextPercent"
  95. :contextBreakdown="contextBreakdown"
  96. :initialThinkLevel="thinkLevel"
  97. :initialEditMode="editMode"
  98. :taskList="taskList"
  99. :currentTaskId="currentTaskId"
  100. @send="handleSendMessage"
  101. @stop="stopStreaming"
  102. @file-upload="triggerFileUpload"
  103. @file-change="handleFileUpload"
  104. @remove-pending-file="removePendingFile"
  105. @think-level-change="handleThinkLevelChange"
  106. @edit-mode-change="handleEditModeChange"
  107. @update:selected-sessions="handleUpdateSelectedSessions"
  108. @update:selected-skill="handleUpdateSelectedSkill"
  109. />
  110. </div>
  111. <!-- 文件预览面板(与 chat-panel 并列) -->
  112. <div v-if="previewFile" class="right-panel">
  113. <FilePreviewPanel :file="previewFile" @close="removePreviewFile" />
  114. </div>
  115. <!-- 定时任务详情面板(与 chat-panel 并列) -->
  116. <div v-if="currentScheduleTask" class="right-panel narrow">
  117. <ScheduleDetailPanel
  118. :task="currentScheduleTask"
  119. @close="showScheduleDetail = false"
  120. @deleted="handleScheduleDeleted"
  121. @updated="fetchScheduleListForDetail"
  122. />
  123. </div>
  124. </template>
  125. <!-- 技能面板 -->
  126. <div v-if="activePanel === 'skill'" class="right-panel full">
  127. <SkillPanel @close="activePanel = 'chat'" />
  128. </div>
  129. <!-- 定时任务面板 -->
  130. <div v-if="activePanel === 'schedules'" class="right-panel full">
  131. <SchedulePanel
  132. ref="schedulePanelRef"
  133. @close="activePanel = 'chat'"
  134. @open-session="handleOpenScheduleSession"
  135. @data-changed="fetchScheduleListForDetail"
  136. />
  137. </div>
  138. </div>
  139. </a-modal>
  140. </template>
  141. <script setup lang="ts">
  142. import { ref, nextTick, watch, computed, onMounted, onBeforeUnmount } from 'vue';
  143. import dayjs from 'dayjs';
  144. import {
  145. unifiedStream,
  146. reviewPdfStream,
  147. chatResumeStream,
  148. getHistoryList,
  149. getDetail,
  150. deleteSession,
  151. pinSession,
  152. editSessionTitle,
  153. getContext,
  154. switchThinking,
  155. getModelMsg,
  156. getSessionMode,
  157. putSessionMode,
  158. } from '../api';
  159. import { message, Modal } from 'ant-design-vue';
  160. import TaskListPanel from './chatModal/TaskListPanel.vue';
  161. import FileListPanel from './chatModal/FileListPanel.vue';
  162. import ChatMessages from './chatModal/ChatMessages.vue';
  163. import ChatInputArea from './chatModal/ChatInputArea.vue';
  164. import FilePreviewPanel from './chatModal/FilePreviewPanel.vue';
  165. import TodoListBar from './chatModal/TodoListBar.vue';
  166. import DownloadConfirmBar from './chatModal/DownloadConfirmBar.vue';
  167. import SkillPanel from './chatModal/SkillPanel.vue';
  168. import SchedulePanel from './chatModal/SchedulePanel.vue';
  169. import ScheduleDetailPanel from './chatModal/ScheduleDetailPanel.vue';
  170. import { getScheduleList } from '../api';
  171. import type { AttachedFile, Message, Task } from './chatModal/types';
  172. import { isTextFile } from './chatModal/utils';
  173. interface Props {
  174. visible: boolean;
  175. }
  176. interface Emits {
  177. (e: 'close'): void;
  178. }
  179. const props = defineProps<Props>();
  180. const emit = defineEmits<Emits>();
  181. const taskList = ref<Task[]>([]);
  182. // 已从接口拉取过的会话 id,用于识别流式期间后端新建的会话
  183. const knownApiSessionIds = new Set<string>();
  184. // 从接口获取会话列表
  185. const fetchSessionList = async () => {
  186. try {
  187. const res = await getHistoryList({});
  188. const sessions = res?.sessions || res?.data || [];
  189. if (Array.isArray(sessions)) {
  190. const apiTasks: Task[] = sessions.map((item: { session_id: string; title: string; updated_at: string; sort_order?: number }) => ({
  191. id: item.session_id,
  192. name: item.title || '未命名会话',
  193. sessionId: item.session_id,
  194. messages: [],
  195. attachedFiles: [],
  196. updatedAt: item.updated_at,
  197. sortOrder: item.sort_order,
  198. }));
  199. // 流式中的空白任务:后端已为其创建会话,但前端要等流结束才拿到 session_id。
  200. // 该会话在接口里是一条全新记录,归并回空白任务而不是单独插入,避免列表出现重复条目
  201. let blankStreamingTask: Task | null = taskList.value.find((t) => !t.sessionId && t.isStreaming) || null;
  202. const mergedTasks: Task[] = [];
  203. for (const apiTask of apiTasks) {
  204. if (blankStreamingTask && (blankStreamingTask.pendingSessionId === apiTask.id || !knownApiSessionIds.has(apiTask.id))) {
  205. blankStreamingTask.pendingSessionId = apiTask.id;
  206. blankStreamingTask.name = apiTask.name;
  207. blankStreamingTask.updatedAt = apiTask.updatedAt;
  208. blankStreamingTask.sortOrder = apiTask.sortOrder;
  209. blankStreamingTask = null;
  210. continue;
  211. }
  212. const existing = taskList.value.find((t) => t.id === apiTask.id);
  213. if (existing) {
  214. mergedTasks.push({
  215. ...apiTask,
  216. messages: existing.messages,
  217. attachedFiles: existing.attachedFiles,
  218. wordUrl: existing.wordUrl,
  219. });
  220. } else {
  221. mergedTasks.push(apiTask);
  222. }
  223. }
  224. apiTasks.forEach((apiTask) => knownApiSessionIds.add(apiTask.id));
  225. const localOnlyTasks = taskList.value.filter((t) => !t.sessionId && !apiTasks.find((a) => a.id === t.id));
  226. taskList.value = [...localOnlyTasks, ...mergedTasks];
  227. return mergedTasks;
  228. }
  229. } catch (error) {
  230. console.error('获取会话列表失败:', error);
  231. }
  232. return [];
  233. };
  234. const currentTaskId = ref('');
  235. const chatInputRef = ref<InstanceType<typeof ChatInputArea>>();
  236. const chatMessagesRef = ref<InstanceType<typeof ChatMessages>>();
  237. const previewFile = ref<AttachedFile | null>(null);
  238. const showFileList = ref(false);
  239. const activePanel = ref<'chat' | 'skill' | 'schedules'>('chat');
  240. const scheduleListForDetail = ref<any[]>([]);
  241. const showScheduleDetail = ref(false);
  242. const currentScheduleTask = computed(() => {
  243. if (!showScheduleDetail.value || activePanel.value !== 'chat') return null;
  244. const sessionId = currentTask.value?.sessionId;
  245. if (!sessionId) return null;
  246. return scheduleListForDetail.value.find((t: any) => t.feedback_session_id === sessionId) || null;
  247. });
  248. const fetchScheduleListForDetail = async () => {
  249. try {
  250. const res = await getScheduleList();
  251. scheduleListForDetail.value = res?.tasks || res?.data || (Array.isArray(res) ? res : []);
  252. } catch {
  253. scheduleListForDetail.value = [];
  254. }
  255. };
  256. const pendingFile = ref<AttachedFile | null>(null);
  257. const messages = ref<Message[]>([]);
  258. const contextUsed = ref(0);
  259. const contextMax = ref(1000000);
  260. const contextPercent = ref(0);
  261. const thinkLevel = ref('off');
  262. const editMode = ref('full');
  263. const pendingApprovalSessionId = ref('');
  264. const pendingApprovalThreadId = ref('');
  265. const downloadConfirm = ref<{ url: string; filename: string } | null>(null);
  266. const downloadConfirmTaskId = ref('');
  267. let downloadConfirmTimer: ReturnType<typeof setTimeout> | null = null;
  268. const clearDownloadConfirm = () => {
  269. downloadConfirm.value = null;
  270. downloadConfirmTaskId.value = '';
  271. if (downloadConfirmTimer) {
  272. clearTimeout(downloadConfirmTimer);
  273. downloadConfirmTimer = null;
  274. }
  275. };
  276. const startDownloadConfirmTimer = () => {
  277. if (downloadConfirmTimer) clearTimeout(downloadConfirmTimer);
  278. downloadConfirmTimer = setTimeout(() => {
  279. clearDownloadConfirm();
  280. }, 10000);
  281. };
  282. const contextBreakdown = ref({ messages: 0, mcp: 0, skills: 0, system_prompt: 0, other: 0 });
  283. const currentTodos = ref<Array<{ content: string; status: string }>>([]);
  284. const showTodoBar = ref(false);
  285. const syncTodoBarFromMessages = () => {
  286. for (let i = messages.value.length - 1; i >= 0; i--) {
  287. const msg = messages.value[i];
  288. if (msg.type === 'ai' && msg.thinkingSteps) {
  289. for (let j = msg.thinkingSteps.length - 1; j >= 0; j--) {
  290. const step = msg.thinkingSteps[j];
  291. if (step.type === 'updated_todo_list' && step.todos) {
  292. currentTodos.value = [...step.todos];
  293. showTodoBar.value = true;
  294. return;
  295. }
  296. }
  297. }
  298. }
  299. showTodoBar.value = false;
  300. currentTodos.value = [];
  301. };
  302. const panelCollapsed = ref(false);
  303. const togglePanel = () => {
  304. panelCollapsed.value = !panelCollapsed.value;
  305. };
  306. const toggleSkillPanel = () => {
  307. if (activePanel.value === 'skill') {
  308. activePanel.value = 'chat';
  309. } else {
  310. activePanel.value = 'skill';
  311. previewFile.value = null;
  312. }
  313. };
  314. const toggleSchedulePanel = () => {
  315. if (activePanel.value === 'schedules') {
  316. activePanel.value = 'chat';
  317. } else {
  318. activePanel.value = 'schedules';
  319. previewFile.value = null;
  320. }
  321. };
  322. const handleOpenScheduleSession = async (sessionId: string) => {
  323. activePanel.value = 'chat';
  324. previewFile.value = null;
  325. showScheduleDetail.value = true;
  326. await fetchSessionList();
  327. await fetchScheduleListForDetail();
  328. const existing = taskList.value.find((t) => t.sessionId === sessionId);
  329. if (existing) {
  330. await switchTask(existing.id);
  331. } else {
  332. const newTask: Task = {
  333. id: sessionId,
  334. name: '定时任务',
  335. sessionId,
  336. messages: [],
  337. attachedFiles: [],
  338. };
  339. taskList.value.unshift(newTask);
  340. historyLoadedTasks.value.delete(sessionId);
  341. await switchTask(newTask.id);
  342. }
  343. };
  344. const schedulePanelRef = ref();
  345. const handleScheduleDeleted = () => {
  346. showScheduleDetail.value = false;
  347. fetchScheduleListForDetail();
  348. schedulePanelRef.value?.fetchScheduleList?.();
  349. };
  350. const editingTitle = ref(false);
  351. const editTitleValue = ref('');
  352. const startEditTitle = () => {
  353. if (!currentTask.value?.sessionId) return;
  354. editTitleValue.value = currentTask.value.name;
  355. editingTitle.value = true;
  356. nextTick(() => {
  357. const input = document.querySelector('.title-edit-input') as HTMLInputElement;
  358. input?.focus();
  359. input?.select();
  360. });
  361. };
  362. const saveTitle = async () => {
  363. const newTitle = editTitleValue.value.trim();
  364. if (!currentTask.value?.sessionId || !newTitle || newTitle === currentTask.value.name) {
  365. editingTitle.value = false;
  366. return;
  367. }
  368. try {
  369. await editSessionTitle(currentTask.value.sessionId, newTitle);
  370. currentTask.value.name = newTitle;
  371. message.success('标题修改成功');
  372. } catch {
  373. message.error('标题修改失败');
  374. }
  375. editingTitle.value = false;
  376. };
  377. const cancelEditTitle = () => {
  378. editingTitle.value = false;
  379. };
  380. const isFullscreen = ref(false);
  381. const toggleFullscreen = () => {
  382. isFullscreen.value = !isFullscreen.value;
  383. nextTick(() => {
  384. const wraps = document.querySelectorAll<HTMLElement>('.zxm-modal-wrap, .ant-modal-wrap, [class*="modal-wrap"]');
  385. wraps.forEach((wrap) => {
  386. if (isFullscreen.value) {
  387. wrap.style.position = '';
  388. wrap.style.left = '';
  389. wrap.style.top = '0';
  390. wrap.style.right = '';
  391. wrap.style.bottom = '';
  392. wrap.style.width = '';
  393. wrap.style.height = '';
  394. wrap.style.margin = '0';
  395. wrap.style.padding = '0';
  396. wrap.style.transform = '';
  397. } else {
  398. wrap.style.position = '';
  399. wrap.style.left = '';
  400. wrap.style.top = '';
  401. wrap.style.right = '';
  402. wrap.style.bottom = '';
  403. wrap.style.width = '';
  404. wrap.style.height = '';
  405. wrap.style.margin = '';
  406. wrap.style.padding = '';
  407. wrap.style.transform = '';
  408. }
  409. });
  410. draggedEl = null;
  411. if (!isFullscreen.value) {
  412. centerModal();
  413. }
  414. });
  415. };
  416. const inputMessage = ref('');
  417. const loading = ref(false);
  418. const streaming = ref(false);
  419. const activeAbortController = ref<AbortController | null>(null);
  420. const selectedSessions = ref<Array<{ id: string; name: string }>>([]);
  421. const selectedSkill = ref<{ name: string; display_name: string } | null>(null);
  422. const stopStreaming = () => {
  423. activeAbortController.value?.abort();
  424. };
  425. const getCurrentSessionId = (): string => {
  426. const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
  427. return currentTask?.sessionId || '';
  428. };
  429. const historyLoadedTasks = ref<Set<string>>(new Set());
  430. const setCurrentSessionId = (sessionId: string) => {
  431. const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
  432. if (currentTask && currentTask.id !== sessionId) {
  433. taskList.value = taskList.value.filter((t) => t.id !== sessionId);
  434. currentTask.id = sessionId;
  435. currentTask.sessionId = sessionId;
  436. currentTaskId.value = sessionId;
  437. historyLoadedTasks.value.add(sessionId);
  438. }
  439. };
  440. const switchTask = async (taskId: string) => {
  441. activePanel.value = 'chat';
  442. const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
  443. if (currentTask) {
  444. currentTask.messages = [...messages.value];
  445. }
  446. currentTaskId.value = taskId;
  447. const newTask = taskList.value.find((t) => t.id === taskId);
  448. if (!newTask) return;
  449. inputMessage.value = '';
  450. pendingFile.value = null;
  451. previewFile.value = null;
  452. showFileList.value = false;
  453. if (newTask.sessionId && !historyLoadedTasks.value.has(taskId)) {
  454. loading.value = true;
  455. try {
  456. const res = await getDetail(newTask.sessionId);
  457. const messages = res?.messages || (Array.isArray(res) ? res : res?.data);
  458. if (Array.isArray(messages)) {
  459. newTask.messages = transformHistoryToMessages(messages);
  460. }
  461. historyLoadedTasks.value.add(taskId);
  462. } catch (error) {
  463. console.error('加载历史会话失败:', error);
  464. } finally {
  465. loading.value = false;
  466. }
  467. }
  468. if (newTask.sessionId) {
  469. fetchContext(newTask.sessionId);
  470. fetchSessionMode(newTask.sessionId);
  471. } else {
  472. contextUsed.value = 0;
  473. contextPercent.value = 0;
  474. contextBreakdown.value = { messages: 0, mcp: 0, skills: 0, system_prompt: 0, other: 0 };
  475. editMode.value = 'full';
  476. }
  477. messages.value = newTask.messages && newTask.messages.length > 0 ? [...newTask.messages] : getDefaultMessages();
  478. syncTodoBarFromMessages();
  479. // 切换任务时检测是否为定时任务会话
  480. if (newTask.sessionId && scheduleListForDetail.value.some((s: any) => s.feedback_session_id === newTask.sessionId)) {
  481. showScheduleDetail.value = true;
  482. } else {
  483. showScheduleDetail.value = false;
  484. }
  485. await nextTick();
  486. chatMessagesRef.value?.forceScrollToBottom();
  487. };
  488. const createNewTask = () => {
  489. const existingBlank = taskList.value.find((t) => !t.sessionId);
  490. if (existingBlank) {
  491. switchTask(existingBlank.id);
  492. return;
  493. }
  494. const newTask: Task = {
  495. id: `task-${Date.now()}`,
  496. name: '新任务',
  497. sessionId: '',
  498. messages: getDefaultMessages(),
  499. attachedFiles: [],
  500. };
  501. contextUsed.value = 0;
  502. contextPercent.value = 0;
  503. contextBreakdown.value = { messages: 0, mcp: 0, skills: 0, system_prompt: 0, other: 0 };
  504. editMode.value = 'full';
  505. taskList.value.unshift(newTask);
  506. switchTask(newTask.id);
  507. };
  508. const handlePinTask = async (task: Task) => {
  509. if (!task.sessionId) {
  510. message.warning('请先发送消息后再置顶');
  511. return;
  512. }
  513. try {
  514. await pinSession(task.sessionId);
  515. await fetchSessionList();
  516. } catch {
  517. message.error('置顶操作失败');
  518. }
  519. };
  520. const handleThinkLevelChange = async (levelKey: string) => {
  521. try {
  522. await switchThinking(levelKey);
  523. } catch {
  524. message.error('切换思考级别失败');
  525. }
  526. };
  527. const handleUpdateSelectedSessions = (sessions: Array<{ id: string; name: string }>) => {
  528. selectedSessions.value = sessions;
  529. };
  530. const handleUpdateSelectedSkill = (skill: { name: string; display_name: string } | null) => {
  531. selectedSkill.value = skill;
  532. };
  533. const getSkillRefIconUrl = () => {
  534. const val = getComputedStyle(document.documentElement).getPropertyValue('--img-chat-options-skill-icon').trim();
  535. return val.replace(/^url\(["']?/, '').replace(/["']?\)$/, '');
  536. };
  537. const getSessionRefIconUrl = () => {
  538. const val = getComputedStyle(document.documentElement).getPropertyValue('--img-chat-popup-session-icon').trim();
  539. return val.replace(/^url\(["']?/, '').replace(/["']?\)$/, '');
  540. };
  541. const handleEditModeChange = async (modeKey: string) => {
  542. editMode.value = modeKey;
  543. const sessionId = getCurrentSessionId();
  544. if (!sessionId) return;
  545. try {
  546. await putSessionMode(sessionId, modeKey);
  547. } catch {
  548. message.error('切换模式失败');
  549. }
  550. };
  551. const handleApprove = async () => {
  552. const sessionId = pendingApprovalSessionId.value || getCurrentSessionId();
  553. if (!sessionId) return;
  554. const approveTask = taskList.value.find((t) => t.sessionId === sessionId);
  555. if (!approveTask) return;
  556. const taskMessages = approveTask.messages;
  557. const lastMsg = taskMessages[taskMessages.length - 1];
  558. if (!lastMsg || !lastMsg.isPendingApproval) return;
  559. // 复用同一条消息继续渲染:清除审批状态,恢复 loading,重置前端计时起点
  560. lastMsg.isPendingApproval = false;
  561. lastMsg.pendingApprovalData = undefined;
  562. lastMsg.isLoading = true;
  563. lastMsg.generateStartTime = Date.now();
  564. // thinkingSteps、thinkingContent、baseDurationMs 保持不变,后续继续累积
  565. const aiMsgIndex = taskMessages.length - 1;
  566. approveTask.messages = [...taskMessages];
  567. if (currentTaskId.value === approveTask.id) {
  568. messages.value = approveTask.messages;
  569. await scrollToBottom();
  570. }
  571. loading.value = true;
  572. approveTask.isStreaming = true;
  573. showTodoBar.value = false;
  574. currentTodos.value = [];
  575. const controller = new AbortController();
  576. activeAbortController.value = controller;
  577. streaming.value = true;
  578. try {
  579. const result = await chatResumeStream(
  580. {
  581. session_id: sessionId,
  582. thread_id: pendingApprovalThreadId.value || undefined,
  583. action: 'approve',
  584. signal: controller.signal,
  585. },
  586. (chunk: string) => {
  587. parseSSEData(chunk, aiMsgIndex, approveTask.id);
  588. if (currentTaskId.value === approveTask.id) scrollToBottom();
  589. }
  590. );
  591. if (result.session_id) {
  592. setCurrentSessionId(result.session_id);
  593. }
  594. } catch (error) {
  595. if ((error as Error)?.name === 'AbortError') {
  596. // 用户主动停止生成
  597. lastMsg.isLoading = false;
  598. // 后端未返回耗时,冻结前端伪计时
  599. lastMsg.durationMs = (lastMsg.baseDurationMs || 0) + (lastMsg.generateStartTime ? Date.now() - lastMsg.generateStartTime : 0);
  600. if (!lastMsg.content) {
  601. lastMsg.content = '已停止生成。';
  602. }
  603. } else {
  604. console.error('审批继续执行失败:', error);
  605. message.error('审批失败,请重试');
  606. lastMsg.isLoading = false;
  607. if (!lastMsg.content) {
  608. lastMsg.content = '抱歉,审批请求失败,请稍后重试。';
  609. }
  610. }
  611. } finally {
  612. if (activeAbortController.value === controller) {
  613. activeAbortController.value = null;
  614. }
  615. streaming.value = false;
  616. loading.value = false;
  617. approveTask.isStreaming = false;
  618. pendingApprovalSessionId.value = '';
  619. pendingApprovalThreadId.value = '';
  620. if (currentTaskId.value === approveTask.id) {
  621. await scrollToBottom();
  622. }
  623. }
  624. };
  625. const handleReject = async () => {
  626. const sessionId = pendingApprovalSessionId.value || getCurrentSessionId();
  627. if (!sessionId) return;
  628. const rejectTask = taskList.value.find((t) => t.sessionId === sessionId);
  629. if (!rejectTask) return;
  630. const taskMessages = rejectTask.messages;
  631. const lastMsg = taskMessages[taskMessages.length - 1];
  632. if (!lastMsg || !lastMsg.isPendingApproval) return;
  633. // 复用同一条消息:清除审批状态,标记为已拒绝
  634. lastMsg.isPendingApproval = false;
  635. lastMsg.pendingApprovalData = undefined;
  636. lastMsg.isLoading = false;
  637. lastMsg.content = (lastMsg.content ? lastMsg.content + '\n\n' : '') + '已拒绝执行计划。';
  638. rejectTask.messages = [...taskMessages];
  639. const threadId = pendingApprovalThreadId.value || undefined;
  640. pendingApprovalSessionId.value = '';
  641. pendingApprovalThreadId.value = '';
  642. try {
  643. await chatResumeStream(
  644. {
  645. session_id: sessionId,
  646. thread_id: threadId,
  647. action: 'reject',
  648. },
  649. () => {}
  650. );
  651. } catch (error) {
  652. console.error('拒绝操作失败:', error);
  653. }
  654. if (currentTaskId.value === rejectTask.id) {
  655. messages.value = rejectTask.messages;
  656. await scrollToBottom();
  657. }
  658. };
  659. const handleDeleteTask = (task: Task, _index: number) => {
  660. Modal.confirm({
  661. title: '提示',
  662. content: '是否删除该对话?',
  663. okText: '确认',
  664. cancelText: '取消',
  665. wrapClassName: 'delete-task-confirm-modal',
  666. onOk: async () => {
  667. try {
  668. if (task.sessionId) {
  669. await deleteSession(task.sessionId);
  670. }
  671. const realIndex = taskList.value.findIndex((t) => t.id === task.id);
  672. if (realIndex !== -1) {
  673. taskList.value.splice(realIndex, 1);
  674. }
  675. if (currentTaskId.value === task.id) {
  676. if (taskList.value.length > 0) {
  677. switchTask(taskList.value[0].id);
  678. } else {
  679. messages.value = [];
  680. currentTaskId.value = '';
  681. }
  682. }
  683. message.success('删除成功');
  684. } catch {
  685. message.error('删除失败');
  686. }
  687. },
  688. });
  689. };
  690. const getDefaultMessages = (): Message[] => {
  691. return [];
  692. };
  693. const normalizeLineBreaks = (str: string): string => {
  694. if (!str) return '';
  695. return str.replace(/\\n/g, '\n').replace(/\r\n/g, '\n');
  696. };
  697. const transformHistoryToMessages = (data: Array<{ role?: string; type?: string; content: string; created_at?: string }>): Message[] => {
  698. const messages: Message[] = [];
  699. for (const item of data) {
  700. const normalizedContent = normalizeLineBreaks(item.content);
  701. const time = item.created_at ? dayjs(item.created_at).format('HH:mm') : '';
  702. const createdAt = item.created_at || '';
  703. // 新格式:role 字段 (user/assistant)
  704. if (item.role === 'user') {
  705. messages.push({ type: 'user', content: normalizedContent, time, createdAt });
  706. } else if (item.role === 'assistant') {
  707. messages.push({ type: 'ai', content: normalizedContent, time, createdAt });
  708. }
  709. // 兼容旧格式:type 字段
  710. else if (item.type === 'user_message') {
  711. messages.push({ type: 'user', content: normalizedContent, time, createdAt });
  712. } else if (item.type === 'model_thinking') {
  713. const lastAiMsg = messages[messages.length - 1];
  714. if (lastAiMsg && lastAiMsg.type === 'ai') {
  715. lastAiMsg.content += normalizedContent;
  716. lastAiMsg.content = lastAiMsg.content.replace(/\|\|\|SPLIT_CONTENT\|\|\|/g, '');
  717. } else {
  718. messages.push({ type: 'ai', content: normalizedContent, time, createdAt });
  719. }
  720. }
  721. }
  722. return messages;
  723. };
  724. const triggerFileUpload = () => {
  725. chatInputRef.value?.fileInputRef?.click();
  726. };
  727. const handleFileUpload = async (event: Event) => {
  728. const target = event.target as HTMLInputElement;
  729. const file = target.files?.[0];
  730. if (!file) return;
  731. try {
  732. const newFile: AttachedFile = {
  733. id: `file-${Date.now()}`,
  734. name: file.name,
  735. size: file.size,
  736. type: file.type,
  737. uploadTime: dayjs().format('YYYY-MM-DD HH:mm'),
  738. originalFile: file,
  739. };
  740. const reader = new FileReader();
  741. if (isTextFile(file.name)) {
  742. reader.onload = (e) => {
  743. newFile.content = e.target?.result as string;
  744. };
  745. reader.readAsText(file);
  746. } else {
  747. reader.onload = (e) => {
  748. newFile.preview = e.target?.result as string;
  749. };
  750. reader.readAsDataURL(file);
  751. }
  752. pendingFile.value = newFile;
  753. message.success(`文件 "${file.name}" 已添加,点击发送按钮提交`);
  754. } catch (error) {
  755. console.error('文件处理失败:', error);
  756. message.error('文件处理失败,请重试');
  757. }
  758. target.value = '';
  759. };
  760. const removePendingFile = () => {
  761. pendingFile.value = null;
  762. };
  763. const removePreviewFile = () => {
  764. previewFile.value = null;
  765. };
  766. const openFilePreview = (file: AttachedFile) => {
  767. previewFile.value = file;
  768. const msgIndex = messages.value.findIndex((m) => m.attachedFile?.id === file.id);
  769. if (msgIndex !== -1) {
  770. nextTick(() => {
  771. const messageElements = chatMessagesRef.value?.messagesRef?.querySelectorAll('.message-wrapper');
  772. if (messageElements && messageElements[msgIndex]) {
  773. (messageElements[msgIndex] as HTMLElement).scrollIntoView({
  774. behavior: 'smooth',
  775. block: 'center',
  776. });
  777. }
  778. });
  779. }
  780. };
  781. const getCurrentTaskFiles = (): AttachedFile[] => {
  782. const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
  783. return currentTask?.attachedFiles || [];
  784. };
  785. const currentTask = computed(() => taskList.value.find((t) => t.id === currentTaskId.value));
  786. const currentWordUrl = computed(() => {
  787. return currentTask.value?.wordUrl || '';
  788. });
  789. const currentTaskIndex = computed(() => taskList.value.findIndex((t) => t.id === currentTaskId.value));
  790. const canGoPrev = computed(() => currentTaskIndex.value > 0);
  791. const canGoNext = computed(() => currentTaskIndex.value >= 0 && currentTaskIndex.value < taskList.value.length - 1);
  792. const goToPrev = () => {
  793. if (!canGoPrev.value) return;
  794. const prevTask = taskList.value[currentTaskIndex.value - 1];
  795. if (prevTask) switchTask(prevTask.id);
  796. };
  797. const goToNext = () => {
  798. if (!canGoNext.value) return;
  799. const nextTask = taskList.value[currentTaskIndex.value + 1];
  800. if (nextTask) switchTask(nextTask.id);
  801. };
  802. const downloadWordReport = () => {
  803. const url = currentWordUrl.value;
  804. if (!url) return;
  805. const a = document.createElement('a');
  806. a.href = url;
  807. a.download = '';
  808. a.target = '_blank';
  809. document.body.appendChild(a);
  810. a.click();
  811. document.body.removeChild(a);
  812. };
  813. const doDownload = () => {
  814. if (!downloadConfirm.value) return;
  815. const a = document.createElement('a');
  816. a.href = downloadConfirm.value.url;
  817. a.download = downloadConfirm.value.filename;
  818. document.body.appendChild(a);
  819. a.click();
  820. document.body.removeChild(a);
  821. clearDownloadConfirm();
  822. };
  823. const downloadWordFile = (url: string) => {
  824. if (!url) return;
  825. const a = document.createElement('a');
  826. a.href = url;
  827. a.download = '';
  828. a.target = '_blank';
  829. document.body.appendChild(a);
  830. a.click();
  831. document.body.removeChild(a);
  832. };
  833. const handleSendMessage = async () => {
  834. const hasText = inputMessage.value.trim();
  835. const hasFile = pendingFile.value;
  836. if (!hasText) {
  837. message.warning('请输入消息内容');
  838. return;
  839. }
  840. if (loading.value) return;
  841. if (pendingApprovalSessionId.value) {
  842. message.warning('请先审批执行计划');
  843. return;
  844. }
  845. const sessionPrefix = selectedSessions.value.length > 0 ? selectedSessions.value.map((s) => `#session:${s.id}`).join(' ') + ' ' : '';
  846. const skillPrefix = selectedSkill.value ? `#skill:${selectedSkill.value.name} ` : '';
  847. const userInput = skillPrefix + sessionPrefix + inputMessage.value.trim();
  848. let displayContent = inputMessage.value.trim();
  849. if (selectedSkill.value) {
  850. const skillIconUrl = getSkillRefIconUrl();
  851. displayContent = `[<img src="${skillIconUrl}" class="session-ref-icon" /> 技能:${selectedSkill.value.display_name}] ${displayContent}`;
  852. }
  853. if (selectedSessions.value.length > 0) {
  854. const names = selectedSessions.value.map((s) => s.name).join('、');
  855. const iconUrl = getSessionRefIconUrl();
  856. displayContent = `[<img src="${iconUrl}" class="session-ref-icon" /> 引用 ${selectedSessions.value.length} 个会话:${names}] ${displayContent}`;
  857. }
  858. const fileToSend = pendingFile.value;
  859. inputMessage.value = '';
  860. pendingFile.value = null;
  861. selectedSessions.value = [];
  862. selectedSkill.value = null;
  863. try {
  864. loading.value = true;
  865. await nextTick();
  866. chatMessagesRef.value?.forceScrollToBottom();
  867. if (hasFile) {
  868. await sendWithAttachment(fileToSend!, userInput, displayContent);
  869. } else {
  870. await sendTextOnly(userInput, displayContent);
  871. }
  872. } catch (error) {
  873. console.error('发送消息失败:', error);
  874. message.error('发送失败,请重试');
  875. const lastMsg = messages.value[messages.value.length - 1];
  876. if (lastMsg && lastMsg.type === 'ai' && lastMsg.isLoading) {
  877. messages.value.pop();
  878. }
  879. const errorMsg: Message = {
  880. type: 'ai',
  881. content: '抱歉,请求失败,请稍后重试。',
  882. time: dayjs().format('HH:mm'),
  883. };
  884. messages.value.push(errorMsg);
  885. const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
  886. if (currentTask) {
  887. currentTask.messages = [...messages.value];
  888. }
  889. } finally {
  890. loading.value = false;
  891. await scrollToBottom();
  892. }
  893. };
  894. const extractAgentName = (toolName: string): string => {
  895. const nameMap: Record<string, string> = {
  896. form_review: '形式审查',
  897. verify_coal_face_ventilation: '采煤工作面需风量计算核验',
  898. verify_tunneling_face_ventilation: '掘进工作面需风量计算核验',
  899. verify_chamber_ventilation: '硐室需风量计算核验',
  900. verify_other_points_ventilation: '其他地点需风量计算核验',
  901. check_data_by_gas_report: '瓦斯数据一致性审查',
  902. check_data_by_face_design: '设计规程一致性审查',
  903. check_data_by_vent_report: '测风报表一致性审查',
  904. check_current_month_plan: '日期有效性审查',
  905. get_key_translate: '计算参数对照',
  906. calculate_ventilation: '需风量计算',
  907. get_ventilation_data: '需风量查询',
  908. recalculate_ventilation: '需风量重计算',
  909. get_plan_required_wind: '需风量提取',
  910. get_model_calculated_wind: '三维模型解算风量提取',
  911. get_sensor_wind: '巷道监测风量提取',
  912. merge_wind_by_location: '地点风量智能合并',
  913. check_wind_compliance: '风量检查',
  914. };
  915. return nameMap[toolName] || nameMap[toolName.toLowerCase()] || '智能分析助手';
  916. };
  917. // 累积所有步骤,保留完整历史
  918. const upsertStep = (steps: any[], step: any) => {
  919. steps.push(step);
  920. };
  921. const fetchModelMsg = async () => {
  922. try {
  923. const res = await getModelMsg();
  924. if (res?.thinking_level) {
  925. thinkLevel.value = res.thinking_level;
  926. }
  927. } catch (e) {
  928. console.error('获取模型信息失败:', e);
  929. }
  930. };
  931. const fetchSessionMode = async (sessionId: string) => {
  932. try {
  933. const res = await getSessionMode(sessionId);
  934. if (res?.mode) {
  935. editMode.value = res.mode;
  936. }
  937. } catch (e) {
  938. console.error('获取会话模式失败:', e);
  939. }
  940. };
  941. const fetchContext = async (sessionId: string) => {
  942. try {
  943. const res = await getContext(sessionId);
  944. contextUsed.value = res?.current_usage ?? 0;
  945. contextMax.value = res?.total_limit ?? 0;
  946. contextPercent.value = res?.usage_pct ?? 0;
  947. contextBreakdown.value = res?.breakdown ?? { messages: 0, mcp: 0, skills: 0, system_prompt: 0, other: 0 };
  948. } catch (e) {
  949. console.error('获取上下文容量失败:', e);
  950. contextUsed.value = 0;
  951. contextMax.value = 0;
  952. contextPercent.value = 0;
  953. contextBreakdown.value = { messages: 0, mcp: 0, skills: 0, system_prompt: 0, other: 0 };
  954. }
  955. };
  956. const handleSSEMessage = (data: any, aiMsgIndex: number, taskId: string) => {
  957. const streamingTask = taskList.value.find((t) => t.id === taskId);
  958. if (!streamingTask) return;
  959. // 当前正在查看流式任务时,新消息被 push 到 messages.value 而非 task.messages
  960. // 所以需要从 messages.value 读取;否则从任务存储的 messages 读取
  961. const isViewingTask = currentTaskId.value === taskId;
  962. const sourceArr = isViewingTask ? messages.value : streamingTask.messages;
  963. const aiMsg = sourceArr[aiMsgIndex];
  964. if (!aiMsg) return;
  965. const normalizedContent = normalizeLineBreaks(data.content || '');
  966. // 初始化 thinkingSteps 数组
  967. if (!aiMsg.thinkingSteps) {
  968. aiMsg.thinkingSteps = [];
  969. }
  970. switch (data.type) {
  971. // token 流式内容 - 直接拼接到消息内容
  972. case 'token':
  973. if (normalizedContent) {
  974. aiMsg.content += normalizedContent;
  975. }
  976. break;
  977. // thinking_token - 模型思考过程内容
  978. case 'thinking_token':
  979. if (normalizedContent) {
  980. aiMsg.thinkingContent = (aiMsg.thinkingContent || '') + normalizedContent;
  981. }
  982. break;
  983. // 系统进度消息 - 仅当 agent 不是 system 时才作为 thinking step
  984. case 'progress': {
  985. if (data.agent && data.agent !== 'system') {
  986. upsertStep(aiMsg.thinkingSteps!, {
  987. type: 'executing',
  988. agent: data.agent,
  989. message: data.message || '',
  990. timestamp: Date.now(),
  991. });
  992. }
  993. break;
  994. }
  995. // agent 开始处理
  996. case 'agent_start': {
  997. upsertStep(aiMsg.thinkingSteps!, {
  998. type: 'agent_start',
  999. agent: data.cn_agent || data.agent || '智能助手',
  1000. message: data.message || '开始处理...',
  1001. timestamp: Date.now(),
  1002. });
  1003. break;
  1004. }
  1005. // agent 任务列表更新(同一 agent 的 todos 更新已有条目,不重复添加)
  1006. case 'agent_todos':
  1007. case 'updated_todo_list': {
  1008. const agentName = data.cn_agent || data.agent || '智能助手';
  1009. const existingIdx = aiMsg.thinkingSteps!.findIndex((s) => s.type === 'updated_todo_list' && s.agent === agentName);
  1010. if (existingIdx !== -1) {
  1011. aiMsg.thinkingSteps![existingIdx].todos = data.todos || [];
  1012. aiMsg.thinkingSteps![existingIdx].timestamp = Date.now();
  1013. } else {
  1014. aiMsg.thinkingSteps!.push({
  1015. type: 'updated_todo_list',
  1016. agent: agentName,
  1017. todos: data.todos || [],
  1018. message: data.message || '任务进度已更新',
  1019. timestamp: Date.now(),
  1020. });
  1021. }
  1022. currentTodos.value = [...(data.todos || [])];
  1023. showTodoBar.value = true;
  1024. break;
  1025. }
  1026. // agent 执行中(工具调用)
  1027. case 'agent_executing':
  1028. case 'executing': {
  1029. upsertStep(aiMsg.thinkingSteps!, {
  1030. type: 'executing',
  1031. agent: data.cn_agent || data.agent || '智能助手',
  1032. tool: (data.cn_tools || data.tools || []).join('、'),
  1033. message: data.message || '',
  1034. timestamp: Date.now(),
  1035. });
  1036. break;
  1037. }
  1038. // 工具结果
  1039. case 'agent_tool_result':
  1040. case 'tool_result': {
  1041. upsertStep(aiMsg.thinkingSteps!, {
  1042. type: 'tool_result',
  1043. agent: data.cn_agent || data.agent || '智能助手',
  1044. tool: data.cn_tool || data.tool || '',
  1045. message: data.message || '执行完成',
  1046. timestamp: Date.now(),
  1047. });
  1048. if (data.download_url) {
  1049. downloadConfirm.value = { url: data.download_url, filename: data.filename || '文件' };
  1050. downloadConfirmTaskId.value = taskId;
  1051. startDownloadConfirmTimer();
  1052. }
  1053. break;
  1054. }
  1055. // agent 完成
  1056. case 'agent_done': {
  1057. upsertStep(aiMsg.thinkingSteps!, {
  1058. type: 'agent_done',
  1059. agent: data.cn_agent || data.agent || '智能助手',
  1060. message: `${(data.message || '审查完成').replace(/(\d+ms)/g, '')}${data.duration_ms ? `(${(data.duration_ms / 1000).toFixed(0)}s)` : ''}${data.progress ? ` [${data.progress}]` : ''}`,
  1061. timestamp: Date.now(),
  1062. });
  1063. // 累加后端返回的各段耗时,并重置前端计时起点,避免重复计算已验证的时间段
  1064. if (data.duration_ms) {
  1065. aiMsg.baseDurationMs = (aiMsg.baseDurationMs || 0) + data.duration_ms;
  1066. aiMsg.generateStartTime = Date.now();
  1067. }
  1068. if (data.preview) {
  1069. aiMsg.content += data.preview;
  1070. }
  1071. break;
  1072. }
  1073. // 旧格式兼容
  1074. case 'model_thinking':
  1075. if (normalizedContent) {
  1076. aiMsg.content += normalizedContent;
  1077. aiMsg.content = aiMsg.content.replace(/\|\|\|SPLIT_CONTENT\|\|\|/g, '');
  1078. }
  1079. break;
  1080. case 'tool_select': {
  1081. upsertStep(aiMsg.thinkingSteps!, {
  1082. type: 'tool_call',
  1083. agent: extractAgentName(normalizedContent),
  1084. tool: normalizedContent,
  1085. message: `选择工具:${normalizedContent}`,
  1086. timestamp: Date.now(),
  1087. });
  1088. break;
  1089. }
  1090. case 'error':
  1091. upsertStep(aiMsg.thinkingSteps!, {
  1092. type: 'tool_result',
  1093. agent: '系统',
  1094. message: `❌ 执行失败:${normalizedContent}`,
  1095. timestamp: Date.now(),
  1096. });
  1097. aiMsg.isLoading = false;
  1098. // 后端未返回耗时,冻结前端伪计时
  1099. aiMsg.durationMs = (aiMsg.baseDurationMs || 0) + (aiMsg.generateStartTime ? Date.now() - aiMsg.generateStartTime : 0);
  1100. break;
  1101. case 'word_download':
  1102. if (normalizedContent) {
  1103. aiMsg.wordDownloadUrl = normalizedContent;
  1104. streamingTask.wordUrl = normalizedContent;
  1105. }
  1106. break;
  1107. case 'done':
  1108. if (data.session_id) {
  1109. aiMsg.sessionId = data.session_id;
  1110. fetchContext(data.session_id);
  1111. }
  1112. aiMsg.durationMs = data.duration_ms
  1113. ? (aiMsg.baseDurationMs || 0) + data.duration_ms
  1114. : (aiMsg.baseDurationMs || 0) + (aiMsg.generateStartTime ? Date.now() - aiMsg.generateStartTime : 0);
  1115. aiMsg.isLoading = false;
  1116. streamingTask.isStreaming = false;
  1117. break;
  1118. case 'system':
  1119. if (normalizedContent) {
  1120. aiMsg.content += normalizedContent.replace(/\n/g, ' \n');
  1121. }
  1122. break;
  1123. case 'interrupt': {
  1124. // 后端各段耗时 + 前端当前段计时,吸收进 baseDurationMs 保证审批继续后计时连续
  1125. const elapsedMs = (aiMsg.baseDurationMs || 0) + (aiMsg.generateStartTime ? Date.now() - aiMsg.generateStartTime : 0);
  1126. aiMsg.baseDurationMs = elapsedMs;
  1127. aiMsg.isLoading = false;
  1128. aiMsg.isPendingApproval = true;
  1129. const interruptSessionId = data.session_id || streamingTask.sessionId;
  1130. aiMsg.pendingApprovalData = {
  1131. message: data.message || '智能体制定了执行计划,等待您的审批...',
  1132. sessionId: interruptSessionId,
  1133. threadId: data.thread_id,
  1134. interrupts: data.interrupts,
  1135. elapsedMs,
  1136. prevThinkingContent: aiMsg.thinkingContent,
  1137. };
  1138. pendingApprovalSessionId.value = interruptSessionId;
  1139. pendingApprovalThreadId.value = data.thread_id || '';
  1140. break;
  1141. }
  1142. }
  1143. // 保存到流式任务的 messages,并同步 UI
  1144. if (isViewingTask) {
  1145. streamingTask.messages = [...messages.value];
  1146. } else {
  1147. streamingTask.messages = [...streamingTask.messages];
  1148. }
  1149. };
  1150. const parseSSEData = (chunk: string, aiMsgIndex: number, taskId: string) => {
  1151. const lines = chunk.split('\n');
  1152. for (const lineRaw of lines) {
  1153. const line = lineRaw.trim();
  1154. if (!line) continue;
  1155. if (line.startsWith('data:')) {
  1156. try {
  1157. const jsonStr = line.substring(5).trim();
  1158. const data = JSON.parse(jsonStr);
  1159. handleSSEMessage(data, aiMsgIndex, taskId);
  1160. } catch (error) {
  1161. console.error('解析 SSE data 行失败:', error, line);
  1162. }
  1163. continue;
  1164. }
  1165. try {
  1166. const data = JSON.parse(line);
  1167. handleSSEMessage(data, aiMsgIndex, taskId);
  1168. continue;
  1169. } catch {
  1170. // not json
  1171. }
  1172. handleSSEMessage({ type: 'model_thinking', content: line }, aiMsgIndex, taskId);
  1173. }
  1174. };
  1175. const sendTextOnly = async (userInput: string, displayContent?: string) => {
  1176. const originTaskId = currentTaskId.value;
  1177. const now = dayjs();
  1178. const userMsg: Message = {
  1179. type: 'user',
  1180. content: displayContent || userInput,
  1181. time: now.format('HH:mm'),
  1182. createdAt: now.toISOString(),
  1183. };
  1184. messages.value.push(userMsg);
  1185. await scrollToBottom();
  1186. const aiMsgIndex = messages.value.length;
  1187. const aiMsg: Message = {
  1188. type: 'ai',
  1189. content: '',
  1190. time: now.format('HH:mm'),
  1191. createdAt: now.toISOString(),
  1192. isLoading: true,
  1193. thinkingSteps: [],
  1194. generateStartTime: Date.now(),
  1195. };
  1196. messages.value.push(aiMsg);
  1197. await scrollToBottom();
  1198. showTodoBar.value = false;
  1199. currentTodos.value = [];
  1200. const originTask = taskList.value.find((t) => t.id === originTaskId);
  1201. if (originTask) originTask.isStreaming = true;
  1202. const controller = new AbortController();
  1203. activeAbortController.value = controller;
  1204. streaming.value = true;
  1205. try {
  1206. const result = await unifiedStream(
  1207. {
  1208. message: userInput,
  1209. session_id: getCurrentSessionId() || undefined,
  1210. mode: editMode.value,
  1211. signal: controller.signal,
  1212. },
  1213. (chunk: string) => {
  1214. parseSSEData(chunk, aiMsgIndex, originTaskId);
  1215. if (currentTaskId.value === originTaskId) scrollToBottom();
  1216. }
  1217. );
  1218. if (result.session_id) {
  1219. const originTask = taskList.value.find((t) => t.id === originTaskId);
  1220. if (originTask && !originTask.sessionId) {
  1221. originTask.sessionId = result.session_id;
  1222. // 流式期间若列表已用后端标题归并过该任务,则保留后端标题;否则用首条消息命名
  1223. if (originTask.pendingSessionId !== result.session_id) {
  1224. originTask.name = userInput.substring(0, 20) + (userInput.length > 20 ? '...' : '');
  1225. }
  1226. originTask.pendingSessionId = undefined;
  1227. if (originTask.id !== result.session_id) {
  1228. taskList.value = taskList.value.filter((t) => t.id !== result.session_id);
  1229. originTask.id = result.session_id;
  1230. historyLoadedTasks.value.add(result.session_id);
  1231. }
  1232. if (currentTaskId.value === originTaskId) {
  1233. currentTaskId.value = result.session_id;
  1234. }
  1235. }
  1236. }
  1237. } catch (error) {
  1238. // 用户主动停止生成
  1239. if ((error as Error)?.name === 'AbortError') {
  1240. const originTask = taskList.value.find((t) => t.id === originTaskId);
  1241. const arr = currentTaskId.value === originTaskId ? messages.value : originTask?.messages;
  1242. const aiMsg = arr?.[aiMsgIndex];
  1243. if (aiMsg) {
  1244. aiMsg.isLoading = false;
  1245. // 后端未返回耗时,冻结前端伪计时
  1246. aiMsg.durationMs = (aiMsg.baseDurationMs || 0) + (aiMsg.generateStartTime ? Date.now() - aiMsg.generateStartTime : 0);
  1247. if (!aiMsg.content) {
  1248. aiMsg.content = '已停止生成。';
  1249. }
  1250. }
  1251. if (originTask && currentTaskId.value !== originTaskId) {
  1252. originTask.messages = [...(arr || [])];
  1253. }
  1254. return;
  1255. }
  1256. throw error;
  1257. } finally {
  1258. if (activeAbortController.value === controller) {
  1259. activeAbortController.value = null;
  1260. }
  1261. streaming.value = false;
  1262. const originTask = taskList.value.find((t) => t.id === originTaskId);
  1263. if (originTask) originTask.isStreaming = false;
  1264. }
  1265. };
  1266. const sendWithAttachment = async (file: AttachedFile, userInput: string, displayContent?: string) => {
  1267. const originTaskId = currentTaskId.value;
  1268. let currentTask = taskList.value.find((t) => t.id === originTaskId);
  1269. if (currentTask) {
  1270. currentTask.attachedFiles.push(file);
  1271. } else {
  1272. throw new Error('未找到当前任务');
  1273. }
  1274. const now = dayjs();
  1275. const userMsg: Message = {
  1276. type: 'user',
  1277. content: displayContent || userInput || `上传了文件:${file.name}`,
  1278. time: now.format('HH:mm'),
  1279. createdAt: now.toISOString(),
  1280. attachedFile: file,
  1281. };
  1282. messages.value.push(userMsg);
  1283. await scrollToBottom();
  1284. const aiMsgIndex = messages.value.length;
  1285. const aiMsg: Message = {
  1286. type: 'ai',
  1287. content: '',
  1288. time: now.format('HH:mm'),
  1289. createdAt: now.toISOString(),
  1290. isLoading: true,
  1291. thinkingSteps: [],
  1292. generateStartTime: Date.now(),
  1293. };
  1294. messages.value.push(aiMsg);
  1295. await scrollToBottom();
  1296. showTodoBar.value = false;
  1297. currentTodos.value = [];
  1298. if (!file.originalFile) {
  1299. throw new Error('缺少原始文件对象');
  1300. }
  1301. const originTask = taskList.value.find((t) => t.id === originTaskId);
  1302. if (originTask) originTask.isStreaming = true;
  1303. const controller = new AbortController();
  1304. activeAbortController.value = controller;
  1305. streaming.value = true;
  1306. try {
  1307. const result = await reviewPdfStream(
  1308. {
  1309. file: file.originalFile!,
  1310. session_id: getCurrentSessionId() || undefined,
  1311. message: userInput || undefined,
  1312. mode: editMode.value,
  1313. signal: controller.signal,
  1314. },
  1315. (chunk: string) => {
  1316. parseSSEData(chunk, aiMsgIndex, originTaskId);
  1317. if (currentTaskId.value === originTaskId) scrollToBottom();
  1318. }
  1319. );
  1320. if (result.session_id) {
  1321. const originTask = taskList.value.find((t) => t.id === originTaskId);
  1322. if (originTask && !originTask.sessionId) {
  1323. originTask.sessionId = result.session_id;
  1324. if (originTask.pendingSessionId !== result.session_id) {
  1325. const taskName = userInput || `文件:${file.name}`;
  1326. originTask.name = taskName.substring(0, 20) + (taskName.length > 20 ? '...' : '');
  1327. }
  1328. originTask.pendingSessionId = undefined;
  1329. if (originTask.id !== result.session_id) {
  1330. taskList.value = taskList.value.filter((t) => t.id !== result.session_id);
  1331. originTask.id = result.session_id;
  1332. historyLoadedTasks.value.add(result.session_id);
  1333. }
  1334. if (currentTaskId.value === originTaskId) {
  1335. currentTaskId.value = result.session_id;
  1336. }
  1337. }
  1338. }
  1339. } catch (error) {
  1340. // 用户主动停止生成
  1341. if ((error as Error)?.name === 'AbortError') {
  1342. const originTask = taskList.value.find((t) => t.id === originTaskId);
  1343. const arr = currentTaskId.value === originTaskId ? messages.value : originTask?.messages;
  1344. const aiMsg = arr?.[aiMsgIndex];
  1345. if (aiMsg) {
  1346. aiMsg.isLoading = false;
  1347. // 后端未返回耗时,冻结前端伪计时
  1348. aiMsg.durationMs = (aiMsg.baseDurationMs || 0) + (aiMsg.generateStartTime ? Date.now() - aiMsg.generateStartTime : 0);
  1349. if (!aiMsg.content) {
  1350. aiMsg.content = '已停止生成。';
  1351. }
  1352. }
  1353. if (originTask && currentTaskId.value !== originTaskId) {
  1354. originTask.messages = [...(arr || [])];
  1355. }
  1356. return;
  1357. }
  1358. throw error;
  1359. } finally {
  1360. if (activeAbortController.value === controller) {
  1361. activeAbortController.value = null;
  1362. }
  1363. streaming.value = false;
  1364. const originTask = taskList.value.find((t) => t.id === originTaskId);
  1365. if (originTask) originTask.isStreaming = false;
  1366. }
  1367. };
  1368. const scrollToBottom = async () => {
  1369. await nextTick();
  1370. if (!chatMessagesRef.value?.isAtBottom) return;
  1371. const el = chatMessagesRef.value?.messagesRef;
  1372. if (el) {
  1373. el.scrollTop = el.scrollHeight;
  1374. }
  1375. };
  1376. watch(
  1377. () => props.visible,
  1378. async (newVisible) => {
  1379. if (!newVisible) {
  1380. const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
  1381. if (currentTask) {
  1382. currentTask.messages = [...messages.value];
  1383. }
  1384. } else {
  1385. await fetchSessionList();
  1386. fetchModelMsg();
  1387. fetchScheduleListForDetail();
  1388. // 去除重复的空白任务,只保留一个
  1389. taskList.value = taskList.value.filter((task, index) => {
  1390. if (task.sessionId) return true;
  1391. return index === taskList.value.findIndex((t) => !t.sessionId);
  1392. });
  1393. // 优先回到关闭前所在的任务(如流式中的新任务),否则默认选中第一条历史记录
  1394. const firstHistoryTask = taskList.value.find((t) => t.sessionId);
  1395. const prevTask = taskList.value.find((t) => t.id === currentTaskId.value);
  1396. let targetTask: Task;
  1397. if (prevTask) {
  1398. targetTask = prevTask;
  1399. } else if (firstHistoryTask) {
  1400. targetTask = firstHistoryTask;
  1401. } else {
  1402. // 没有历史记录时才创建空白新任务
  1403. targetTask = {
  1404. id: `task-${Date.now()}`,
  1405. name: '新任务',
  1406. sessionId: '',
  1407. messages: getDefaultMessages(),
  1408. attachedFiles: [],
  1409. };
  1410. taskList.value.unshift(targetTask);
  1411. }
  1412. currentTaskId.value = targetTask.id;
  1413. // 如果是历史任务,加载其消息
  1414. if (targetTask.sessionId && !historyLoadedTasks.value.has(targetTask.id)) {
  1415. loading.value = true;
  1416. try {
  1417. const res = await getDetail(targetTask.sessionId);
  1418. const messages = res?.messages || (Array.isArray(res) ? res : res?.data);
  1419. if (Array.isArray(messages)) {
  1420. targetTask.messages = transformHistoryToMessages(messages);
  1421. }
  1422. historyLoadedTasks.value.add(targetTask.id);
  1423. } catch (error) {
  1424. console.error('加载历史会话失败:', error);
  1425. } finally {
  1426. loading.value = false;
  1427. }
  1428. }
  1429. if (targetTask.sessionId) {
  1430. fetchContext(targetTask.sessionId);
  1431. fetchSessionMode(targetTask.sessionId);
  1432. } else {
  1433. contextUsed.value = 0;
  1434. contextPercent.value = 0;
  1435. contextBreakdown.value = { messages: 0, mcp: 0, skills: 0, system_prompt: 0, other: 0 };
  1436. editMode.value = 'full';
  1437. }
  1438. messages.value = targetTask.messages && targetTask.messages.length > 0 ? [...targetTask.messages] : getDefaultMessages();
  1439. previewFile.value = null;
  1440. pendingFile.value = null;
  1441. showFileList.value = false;
  1442. inputMessage.value = '';
  1443. await nextTick(() => {
  1444. chatMessagesRef.value?.forceScrollToBottom();
  1445. });
  1446. }
  1447. }
  1448. );
  1449. // 拖拽相关(使用普通变量避免 mousemove 触发 Vue 响应式导致卡顿)
  1450. let isDragging = false;
  1451. let dragStartX = 0;
  1452. let dragStartY = 0;
  1453. let modalStartLeft = 0;
  1454. let modalStartTop = 0;
  1455. let draggedEl: HTMLElement | null = null;
  1456. const findModalWrap = (target: HTMLElement): HTMLElement | null =>
  1457. target.closest<HTMLElement>('.zxm-modal-wrap, .ant-modal-wrap, [class*="modal-wrap"]');
  1458. const queryModalWrap = (): HTMLElement | null => document.querySelector<HTMLElement>('.zxm-modal-wrap, .ant-modal-wrap, [class*="modal-wrap"]');
  1459. const centerModal = () => {
  1460. nextTick(() => {
  1461. const wrap = queryModalWrap();
  1462. if (wrap) {
  1463. wrap.style.position = '';
  1464. wrap.style.left = '';
  1465. wrap.style.top = '';
  1466. wrap.style.right = '';
  1467. wrap.style.bottom = '';
  1468. wrap.style.width = '';
  1469. wrap.style.height = '';
  1470. wrap.style.margin = '';
  1471. wrap.style.transform = '';
  1472. }
  1473. draggedEl = null;
  1474. });
  1475. };
  1476. const onDragStart = (e: MouseEvent) => {
  1477. if (isFullscreen.value) return;
  1478. draggedEl = findModalWrap(e.target as HTMLElement) || queryModalWrap();
  1479. if (!draggedEl) return;
  1480. isDragging = true;
  1481. dragStartX = e.clientX;
  1482. dragStartY = e.clientY;
  1483. const rect = draggedEl.getBoundingClientRect();
  1484. modalStartLeft = rect.left;
  1485. modalStartTop = rect.top;
  1486. draggedEl.style.position = 'fixed';
  1487. draggedEl.style.left = `${rect.left}px`;
  1488. draggedEl.style.top = `${rect.top}px`;
  1489. draggedEl.style.margin = '0';
  1490. draggedEl.style.transform = 'none';
  1491. document.addEventListener('mousemove', onDragMove);
  1492. document.addEventListener('mouseup', onDragEnd);
  1493. };
  1494. const onDragMove = (e: MouseEvent) => {
  1495. if (!isDragging || !draggedEl) return;
  1496. draggedEl.style.left = `${modalStartLeft + (e.clientX - dragStartX)}px`;
  1497. draggedEl.style.top = `${modalStartTop + (e.clientY - dragStartY)}px`;
  1498. };
  1499. const onDragEnd = () => {
  1500. isDragging = false;
  1501. document.removeEventListener('mousemove', onDragMove);
  1502. document.removeEventListener('mouseup', onDragEnd);
  1503. };
  1504. watch(
  1505. () => props.visible,
  1506. (newVisible) => {
  1507. if (newVisible) {
  1508. centerModal();
  1509. }
  1510. }
  1511. );
  1512. onMounted(async () => {
  1513. // fetchSessionList moved to watch(visible) to avoid unnecessary API calls
  1514. });
  1515. onBeforeUnmount(() => {
  1516. document.removeEventListener('mousemove', onDragMove);
  1517. document.removeEventListener('mouseup', onDragEnd);
  1518. if (downloadConfirmTimer) {
  1519. clearTimeout(downloadConfirmTimer);
  1520. downloadConfirmTimer = null;
  1521. }
  1522. // 组件卸载时主动断开流式连接
  1523. activeAbortController.value?.abort();
  1524. });
  1525. </script>
  1526. <style scoped lang="less">
  1527. .ai-container {
  1528. display: flex;
  1529. height: 700px;
  1530. gap: 10px;
  1531. overflow: hidden;
  1532. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  1533. }
  1534. .collapse-btn {
  1535. position: absolute;
  1536. top: 16px;
  1537. left: 24px;
  1538. z-index: 10;
  1539. width: 32px;
  1540. height: 32px;
  1541. background: #1a6696;
  1542. border-radius: 4px;
  1543. display: flex;
  1544. align-items: center;
  1545. justify-content: center;
  1546. cursor: pointer;
  1547. transition: background 0.3s;
  1548. &:hover {
  1549. background: #1f7ab8;
  1550. }
  1551. .collapse-icon {
  1552. width: 16px;
  1553. height: 16px;
  1554. background-image: var(--img-chat-collapse-icon);
  1555. background-repeat: no-repeat;
  1556. background-size: 100% 100%;
  1557. &.collapsed {
  1558. background-image: var(--img-chat-expand-icon);
  1559. }
  1560. }
  1561. }
  1562. .task-panel-wrapper {
  1563. width: 300px;
  1564. flex-shrink: 0;
  1565. overflow: hidden;
  1566. transition:
  1567. width 0.35s cubic-bezier(0.4, 0, 0.2, 1),
  1568. margin 0.35s cubic-bezier(0.4, 0, 0.2, 1),
  1569. opacity 0.25s ease;
  1570. &.collapsed {
  1571. width: 0;
  1572. margin: 0;
  1573. opacity: 0;
  1574. }
  1575. }
  1576. .chat-panel {
  1577. flex: 1;
  1578. display: flex;
  1579. position: relative;
  1580. flex-direction: column;
  1581. background-image: var(--img-chat-panel-bg);
  1582. background-repeat: no-repeat;
  1583. background-size: 100% 100%;
  1584. border-radius: 4px;
  1585. overflow: hidden;
  1586. min-width: 0;
  1587. :deep(.chat-header) {
  1588. display: flex;
  1589. padding: 12px 20px;
  1590. flex-shrink: 0;
  1591. align-items: center;
  1592. background: rgba(2, 53, 100, 0.3);
  1593. .header-icon {
  1594. width: 35px;
  1595. height: 28px;
  1596. background-image: var(--img-chat-header-icon);
  1597. background-repeat: no-repeat;
  1598. background-size: 100% 100%;
  1599. margin-right: 10px;
  1600. }
  1601. .chat-header-title {
  1602. font-size: 16px;
  1603. color: #e0e6ed;
  1604. overflow: hidden;
  1605. text-overflow: ellipsis;
  1606. white-space: nowrap;
  1607. margin-left: 10px;
  1608. }
  1609. .chat-header-title-wrapper {
  1610. display: flex;
  1611. align-items: center;
  1612. flex: 1;
  1613. min-width: 0;
  1614. &:hover .title-edit-btn {
  1615. opacity: 1;
  1616. }
  1617. .title-edit-btn {
  1618. width: 28px;
  1619. height: 28px;
  1620. flex-shrink: 0;
  1621. background-image: var(--img-chat-edit-icon);
  1622. background-repeat: no-repeat;
  1623. background-size: 16px 16px;
  1624. background-position: center;
  1625. cursor: pointer;
  1626. opacity: 0;
  1627. transition: opacity 0.2s;
  1628. border-radius: 4px;
  1629. margin-right: 4px;
  1630. &:hover {
  1631. background-color: rgba(10, 132, 255, 0.2);
  1632. }
  1633. }
  1634. .chat-header-title {
  1635. margin-left: 0;
  1636. flex: 1;
  1637. min-width: 0;
  1638. }
  1639. .title-edit-input {
  1640. flex: 1;
  1641. min-width: 0;
  1642. height: 28px;
  1643. padding: 0 8px;
  1644. font-size: 14px;
  1645. color: #e0e6ed;
  1646. background: rgba(10, 132, 255, 0.1);
  1647. border: 1px solid rgba(10, 132, 255, 0.5);
  1648. border-radius: 4px;
  1649. outline: none;
  1650. &:focus {
  1651. border-color: rgba(10, 132, 255, 0.8);
  1652. }
  1653. }
  1654. }
  1655. .nav-btn {
  1656. width: 28px;
  1657. height: 28px;
  1658. display: flex;
  1659. align-items: center;
  1660. justify-content: center;
  1661. cursor: pointer;
  1662. border-radius: 4px;
  1663. transition: background 0.2s;
  1664. &:hover:not(.disabled) {
  1665. background: rgba(10, 132, 255, 0.15);
  1666. }
  1667. &.disabled {
  1668. opacity: 0.3;
  1669. cursor: not-allowed;
  1670. }
  1671. .nav-icon {
  1672. width: 16px;
  1673. height: 16px;
  1674. background-repeat: no-repeat;
  1675. background-size: 100% 100%;
  1676. }
  1677. .prev-icon {
  1678. background-image: var(--img-chat-prev-icon);
  1679. }
  1680. .next-icon {
  1681. background-image: var(--img-chat-next-icon);
  1682. }
  1683. .add-icon {
  1684. background-image: var(--img-chat-options-add-icon);
  1685. }
  1686. }
  1687. .upload-bg {
  1688. cursor: pointer;
  1689. width: 36px;
  1690. height: 36px;
  1691. background-image: var(--img-chat-close-bg);
  1692. background-repeat: no-repeat;
  1693. background-size: 100% 100%;
  1694. display: flex;
  1695. align-items: center;
  1696. justify-content: center;
  1697. margin-left: auto;
  1698. position: relative;
  1699. }
  1700. .download-icon {
  1701. width: 20px;
  1702. height: 20px;
  1703. background-image: var(--img-chat-download-icon);
  1704. background-repeat: no-repeat;
  1705. background-size: 100% 100%;
  1706. }
  1707. }
  1708. }
  1709. .right-panel {
  1710. width: calc(50% - 140px);
  1711. height: 100%;
  1712. display: flex;
  1713. flex-direction: column;
  1714. background-image: var(--img-chat-panel-bg);
  1715. background-repeat: no-repeat;
  1716. background-size: 100% 100%;
  1717. border-radius: 4px;
  1718. &.full {
  1719. flex: 1;
  1720. width: auto;
  1721. min-width: 0;
  1722. }
  1723. &.narrow {
  1724. width: 320px;
  1725. flex-shrink: 0;
  1726. }
  1727. flex-shrink: 0;
  1728. }
  1729. .draggable-title {
  1730. cursor: grab;
  1731. user-select: none;
  1732. &:active {
  1733. cursor: grabbing;
  1734. }
  1735. &.no-drag {
  1736. cursor: default;
  1737. &:active {
  1738. cursor: default;
  1739. }
  1740. }
  1741. }
  1742. </style>
  1743. <style lang="less">
  1744. .ai-assistant-modal {
  1745. .zxm-modal-content {
  1746. background-image: var(--img-chat-modal-bg);
  1747. background-repeat: no-repeat;
  1748. background-size: 100% 100%;
  1749. background-color: unset !important;
  1750. box-shadow: none;
  1751. padding: 0;
  1752. border: none !important;
  1753. }
  1754. .zxm-modal-close {
  1755. background-image: var(--img-chat-close-bg);
  1756. background-repeat: no-repeat;
  1757. background-size: 100% 100%;
  1758. top: 0px;
  1759. right: 0px;
  1760. width: 38px;
  1761. height: 38px;
  1762. .zxm-modal-close-x {
  1763. width: 38px;
  1764. height: 38px;
  1765. }
  1766. svg {
  1767. width: 26px;
  1768. height: 26px;
  1769. margin-bottom: 4px;
  1770. }
  1771. }
  1772. .maximize-btn {
  1773. position: absolute;
  1774. top: 0px;
  1775. right: 56px;
  1776. width: 38px;
  1777. height: 38px;
  1778. background-image: var(--img-chat-close-bg);
  1779. background-repeat: no-repeat;
  1780. background-size: 100% 100%;
  1781. display: flex;
  1782. align-items: center;
  1783. justify-content: center;
  1784. cursor: pointer;
  1785. z-index: 10;
  1786. .maximize-icon {
  1787. width: 22px;
  1788. height: 22px;
  1789. background-image: var(--img-chat-maximize-icon);
  1790. background-repeat: no-repeat;
  1791. background-size: 100% 100%;
  1792. &.minimized {
  1793. background-image: var(--img-chat-minimize-icon);
  1794. }
  1795. }
  1796. }
  1797. &.fullscreen {
  1798. top: 0 !important;
  1799. padding: 0 !important;
  1800. max-width: 100vw !important;
  1801. margin: 0 !important;
  1802. background-color: #09172c;
  1803. .zxm-modal-wrap,
  1804. .ant-modal-wrap {
  1805. padding: 0 !important;
  1806. margin: 0 !important;
  1807. top: 0 !important;
  1808. }
  1809. .zxm-modal-content {
  1810. width: 100vw !important;
  1811. height: 100vh !important;
  1812. max-width: 100vw !important;
  1813. margin: 0;
  1814. border-radius: 0;
  1815. overflow: hidden;
  1816. background-image: var(--img-chat-fs-modal-bg);
  1817. }
  1818. .zxm-modal-header {
  1819. height: 90px;
  1820. background-image: var(--img-chat-fs-header-bg) !important;
  1821. background-repeat: no-repeat;
  1822. background-size: 100% 100%;
  1823. margin-bottom: 16px;
  1824. }
  1825. .no-drag {
  1826. cursor: default;
  1827. font-size: 32px !important;
  1828. }
  1829. .zxm-modal-title {
  1830. height: 90px;
  1831. }
  1832. .zxm-modal-body {
  1833. height: calc(100vh - 120px);
  1834. overflow: hidden;
  1835. padding: 0 24px 12px;
  1836. }
  1837. .ai-container {
  1838. height: 100%;
  1839. overflow: hidden;
  1840. }
  1841. .chat-panel {
  1842. background-image: var(--img-chat-fs-panel-bg) !important;
  1843. }
  1844. .chat-header {
  1845. padding: 0 32px;
  1846. height: 50px;
  1847. }
  1848. .collapse-btn {
  1849. top: 60px;
  1850. background-image: var(--img-chat-fs-collapse-btn-bg) !important;
  1851. background-repeat: no-repeat;
  1852. background-size: 100% 100%;
  1853. }
  1854. .task-list-panel {
  1855. padding: 20px;
  1856. background-image: var(--img-chat-fs-task-panel-bg) !important;
  1857. background-repeat: no-repeat !important;
  1858. background-size: 100% 100% !important;
  1859. }
  1860. .options-area {
  1861. gap: 12px;
  1862. }
  1863. .options-area-item {
  1864. background-image: var(--img-chat-fs-options-item-bg) !important;
  1865. background-repeat: no-repeat !important;
  1866. background-size: 100% 100% !important;
  1867. &.active,
  1868. &:active {
  1869. background-image: var(--img-chat-fs-options-item-active-bg) !important;
  1870. }
  1871. .options-img {
  1872. background-image: none !important;
  1873. }
  1874. }
  1875. .chat-header-title {
  1876. height: 40px;
  1877. line-height: 40px;
  1878. position: relative;
  1879. background-image: var(--img-chat-fs-header-title-bg) !important;
  1880. background-repeat: no-repeat !important;
  1881. background-size: auto 100% !important;
  1882. padding: 0 28px;
  1883. &::before {
  1884. content: '';
  1885. position: absolute;
  1886. left: 0;
  1887. top: 50%;
  1888. transform: translateY(-50%);
  1889. width: 40px;
  1890. height: 40px;
  1891. background-image: var(--img-chat-fs-header-title-icon);
  1892. background-repeat: no-repeat;
  1893. background-size: 100% 100%;
  1894. }
  1895. }
  1896. .chat-header-title-wrapper {
  1897. .title-edit-input {
  1898. height: 36px;
  1899. font-size: 16px;
  1900. background: rgba(10, 132, 255, 0.15);
  1901. border-color: rgba(10, 132, 255, 0.6);
  1902. }
  1903. .title-edit-btn {
  1904. width: 24px;
  1905. height: 24px;
  1906. background-size: 16px 16px;
  1907. }
  1908. }
  1909. .chat-input-area {
  1910. margin: 24px;
  1911. }
  1912. .right-panel {
  1913. background-image: var(--img-chat-fs-panel-bg) !important;
  1914. }
  1915. .schedule-panel,
  1916. .skill-panel {
  1917. background-image: var(--img-chat-fs-panel-bg) !important;
  1918. }
  1919. .schedule-detail-panel {
  1920. background-image: var(--img-chat-fs-task-panel-bg) !important;
  1921. }
  1922. }
  1923. .zxm-modal-header {
  1924. background-color: unset !important;
  1925. background: unset;
  1926. border: none !important;
  1927. padding: 8px;
  1928. backdrop-filter: unset;
  1929. display: flex;
  1930. justify-content: center;
  1931. }
  1932. .zxm-modal-title {
  1933. width: 100%;
  1934. height: 60px;
  1935. display: flex;
  1936. align-items: center;
  1937. justify-content: center;
  1938. .draggable-title {
  1939. color: #fff;
  1940. font-family: var(--vet-font-family);
  1941. letter-spacing: 3px;
  1942. font-size: 24px;
  1943. font-weight: 100;
  1944. line-height: 1.4;
  1945. }
  1946. }
  1947. .zxm-modal-body {
  1948. padding: 12px 24px;
  1949. }
  1950. }
  1951. .delete-task-confirm-modal {
  1952. .zxm-modal-confirm-title,
  1953. .zxm-modal-confirm-content {
  1954. color: #fff;
  1955. }
  1956. }
  1957. </style>