Forráskód Böngészése

[Feat 0000] 系统增加AI智能聊天助手(初版待完善)

wangkeyi 4 napja
szülő
commit
fdb3bdc63d
44 módosított fájl, 3881 hozzáadás és 1 törlés
  1. BIN
      src/assets/images/ventAI/chatModalBlue/1-1.png
  2. 10 0
      src/assets/images/ventAI/chatModalBlue/1-10.svg
  3. 10 0
      src/assets/images/ventAI/chatModalBlue/1-11.svg
  4. 9 0
      src/assets/images/ventAI/chatModalBlue/1-12.svg
  5. 9 0
      src/assets/images/ventAI/chatModalBlue/1-13.svg
  6. 10 0
      src/assets/images/ventAI/chatModalBlue/1-14.svg
  7. 10 0
      src/assets/images/ventAI/chatModalBlue/1-15.svg
  8. BIN
      src/assets/images/ventAI/chatModalBlue/1-2.png
  9. BIN
      src/assets/images/ventAI/chatModalBlue/1-3.png
  10. BIN
      src/assets/images/ventAI/chatModalBlue/1-4.png
  11. BIN
      src/assets/images/ventAI/chatModalBlue/1-5.png
  12. BIN
      src/assets/images/ventAI/chatModalBlue/1-6.png
  13. BIN
      src/assets/images/ventAI/chatModalBlue/1-7.png
  14. 3 0
      src/assets/images/ventAI/chatModalBlue/2-10.svg
  15. 3 0
      src/assets/images/ventAI/chatModalBlue/2-11.svg
  16. BIN
      src/assets/images/ventAI/chatModalBlue/2-13.png
  17. BIN
      src/assets/images/ventAI/chatModalBlue/2-15.png
  18. BIN
      src/assets/images/ventAI/chatModalBlue/2-6.png
  19. BIN
      src/assets/images/ventAI/chatModalBlue/2-7.png
  20. BIN
      src/assets/images/ventAI/chatModalBlue/2-8.png
  21. BIN
      src/assets/images/ventAI/chatModalBlue/2-9.png
  22. BIN
      src/assets/images/ventAI/chatModalBlue/3-1.png
  23. BIN
      src/assets/images/ventAI/chatModalBlue/3-2.png
  24. BIN
      src/assets/images/ventAI/chatModalBlue/3-5.png
  25. BIN
      src/assets/images/ventAI/chatModalBlue/3-6.png
  26. 7 0
      src/assets/images/ventAI/chatModalBlue/3-7.svg
  27. BIN
      src/assets/images/ventAI/chatModalBlue/3-8.png
  28. BIN
      src/assets/images/ventAI/chatModalBlue/4-1.png
  29. 1 0
      src/assets/images/ventAI/chatModalBlue/4-2.svg
  30. 1 0
      src/assets/images/ventAI/chatModalBlue/4-3.svg
  31. 1 0
      src/assets/images/ventAI/chatModalBlue/4-4.svg
  32. 34 0
      src/design/themify/deepblue.less
  33. 39 0
      src/design/themify/default.less
  34. 121 1
      src/layouts/default/index.vue
  35. 105 0
      src/views/ventAI/manageAssistent/api.ts
  36. 1119 0
      src/views/ventAI/manageAssistent/components/AiAssistantModal.vue
  37. 224 0
      src/views/ventAI/manageAssistent/components/chatModal/ChatInputArea.vue
  38. 1050 0
      src/views/ventAI/manageAssistent/components/chatModal/ChatMessages.vue
  39. 133 0
      src/views/ventAI/manageAssistent/components/chatModal/FileListPanel.vue
  40. 141 0
      src/views/ventAI/manageAssistent/components/chatModal/FilePreviewPanel.vue
  41. 290 0
      src/views/ventAI/manageAssistent/components/chatModal/TaskListPanel.vue
  42. 397 0
      src/views/ventAI/manageAssistent/components/chatModal/ThinkingPanel.vue
  43. 64 0
      src/views/ventAI/manageAssistent/components/chatModal/types.ts
  44. 90 0
      src/views/ventAI/manageAssistent/components/chatModal/utils.ts

BIN
src/assets/images/ventAI/chatModalBlue/1-1.png


+ 10 - 0
src/assets/images/ventAI/chatModalBlue/1-10.svg

@@ -0,0 +1,10 @@
+<svg id="组_16756" data-name="组 16756" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12.442" height="12.482" viewBox="0 0 12.442 12.482">
+  <defs>
+    <linearGradient id="linear-gradient" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox">
+      <stop offset="0" stop-color="#659ebc"/>
+      <stop offset="1" stop-color="#207aab"/>
+    </linearGradient>
+  </defs>
+  <path id="路径_57050" data-name="路径 57050" d="M6.37,0A4.74,4.74,0,1,1,1.63,4.74,4.741,4.741,0,0,1,6.37,0Zm0,1.435a3.305,3.305,0,1,0,3.3,3.305,3.305,3.305,0,0,0-3.3-3.305ZM9.653,8.888c-.113.082-.14.11-.054.222.666.871,1.386,1.8,2.049,2.665,1.4,1.827,3.541-.293,1.73-1.71-.86-.673-1.777-1.4-2.64-2.08-.111-.087-.138-.06-.222.052a3.921,3.921,0,0,1-.862.852Z" transform="translate(-1.63)" fill="url(#linear-gradient)"/>
+  <path id="路径_57051" data-name="路径 57051" d="M195.648,194.975h4.522a.12.12,0,0,1,.12.12v1.043a.12.12,0,0,1-.12.12h-4.522a.12.12,0,0,1-.12-.12v-1.043a.12.12,0,0,1,.12-.12Z" transform="translate(-193.143 -190.863)" fill="url(#linear-gradient)"/>
+</svg>

+ 10 - 0
src/assets/images/ventAI/chatModalBlue/1-11.svg

@@ -0,0 +1,10 @@
+<svg id="组_16759" data-name="组 16759" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12.442" height="12.482" viewBox="0 0 12.442 12.482">
+  <defs>
+    <linearGradient id="linear-gradient" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox">
+      <stop offset="0" stop-color="#bce8ff"/>
+      <stop offset="1" stop-color="#28a6ea"/>
+    </linearGradient>
+  </defs>
+  <path id="路径_57050" data-name="路径 57050" d="M6.37,0A4.74,4.74,0,1,1,1.63,4.74,4.741,4.741,0,0,1,6.37,0Zm0,1.435a3.305,3.305,0,1,0,3.3,3.305,3.305,3.305,0,0,0-3.3-3.305ZM9.653,8.888c-.113.082-.14.11-.054.222.666.871,1.386,1.8,2.049,2.665,1.4,1.827,3.541-.293,1.73-1.71-.86-.673-1.777-1.4-2.64-2.08-.111-.087-.138-.06-.222.052a3.921,3.921,0,0,1-.862.852Z" transform="translate(-1.63)" fill="url(#linear-gradient)"/>
+  <path id="路径_57051" data-name="路径 57051" d="M195.648,194.975h4.522a.12.12,0,0,1,.12.12v1.043a.12.12,0,0,1-.12.12h-4.522a.12.12,0,0,1-.12-.12v-1.043a.12.12,0,0,1,.12-.12Z" transform="translate(-193.143 -190.863)" fill="url(#linear-gradient)"/>
+</svg>

+ 9 - 0
src/assets/images/ventAI/chatModalBlue/1-12.svg

@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12.442" height="12.414" viewBox="0 0 12.442 12.414">
+  <defs>
+    <linearGradient id="linear-gradient" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox">
+      <stop offset="0" stop-color="#659ebc"/>
+      <stop offset="1" stop-color="#207aab"/>
+    </linearGradient>
+  </defs>
+  <path id="路径_58051" data-name="路径 58051" d="M114.815,184.972a5.445,5.445,0,0,0-.124,1,4.328,4.328,0,0,0,4.355,4.355,3.757,3.757,0,0,0,1.866-.436v2.364a1.673,1.673,0,0,1-1.68,1.68h-2.551l-.933,1.12a.532.532,0,0,1-.809.062.061.061,0,0,1-.062-.062l-.933-1.12h-2.551a1.673,1.673,0,0,1-1.68-1.68v-5.6a1.673,1.673,0,0,1,1.68-1.68Zm4.23,4.106a3.111,3.111,0,1,1,3.111-3.111A3.08,3.08,0,0,1,119.045,189.078Zm.373-3.484v-1.306a.378.378,0,1,0-.747,0v1.618a.229.229,0,0,0,.062.187.342.342,0,0,0,.311.187h1.12a.378.378,0,1,0,0-.747h-.747Z" transform="translate(-109.713 -182.857)" fill="url(#linear-gradient)"/>
+</svg>

+ 9 - 0
src/assets/images/ventAI/chatModalBlue/1-13.svg

@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12.442" height="12.414" viewBox="0 0 12.442 12.414">
+  <defs>
+    <linearGradient id="linear-gradient" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox">
+      <stop offset="0" stop-color="#bce8ff"/>
+      <stop offset="1" stop-color="#28a6ea"/>
+    </linearGradient>
+  </defs>
+  <path id="路径_58052" data-name="路径 58052" d="M114.815,184.972a5.445,5.445,0,0,0-.124,1,4.328,4.328,0,0,0,4.355,4.355,3.757,3.757,0,0,0,1.866-.436v2.364a1.673,1.673,0,0,1-1.68,1.68h-2.551l-.933,1.12a.532.532,0,0,1-.809.062.061.061,0,0,1-.062-.062l-.933-1.12h-2.551a1.673,1.673,0,0,1-1.68-1.68v-5.6a1.673,1.673,0,0,1,1.68-1.68Zm4.23,4.106a3.111,3.111,0,1,1,3.111-3.111A3.08,3.08,0,0,1,119.045,189.078Zm.373-3.484v-1.306a.378.378,0,1,0-.747,0v1.618a.229.229,0,0,0,.062.187.342.342,0,0,0,.311.187h1.12a.378.378,0,1,0,0-.747h-.747Z" transform="translate(-109.713 -182.857)" fill="url(#linear-gradient)"/>
+</svg>

+ 10 - 0
src/assets/images/ventAI/chatModalBlue/1-14.svg

@@ -0,0 +1,10 @@
+<svg id="组_16757" data-name="组 16757" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="13.901" height="13.9" viewBox="0 0 13.901 13.9">
+  <defs>
+    <linearGradient id="linear-gradient" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox">
+      <stop offset="0" stop-color="#659ebc"/>
+      <stop offset="1" stop-color="#207aab"/>
+    </linearGradient>
+  </defs>
+  <path id="路径_58006" data-name="路径 58006" d="M464.881,147.213a.881.881,0,0,1-.881-.881V134.193a.881.881,0,0,1,1.762,0v12.139A.881.881,0,0,1,464.881,147.213Z" transform="translate(-457.931 -133.312)" fill="url(#linear-gradient)"/>
+  <path id="路径_58007" data-name="路径 58007" d="M146.332,465.762H134.193a.881.881,0,0,1,0-1.762h12.139a.881.881,0,1,1,0,1.762Z" transform="translate(-133.312 -457.931)" fill="url(#linear-gradient)"/>
+</svg>

+ 10 - 0
src/assets/images/ventAI/chatModalBlue/1-15.svg

@@ -0,0 +1,10 @@
+<svg id="组_16758" data-name="组 16758" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="13.901" height="13.9" viewBox="0 0 13.901 13.9">
+  <defs>
+    <linearGradient id="linear-gradient" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox">
+      <stop offset="0" stop-color="#bce8ff"/>
+      <stop offset="1" stop-color="#28a6ea"/>
+    </linearGradient>
+  </defs>
+  <path id="路径_58006" data-name="路径 58006" d="M464.881,147.213a.881.881,0,0,1-.881-.881V134.193a.881.881,0,0,1,1.762,0v12.139A.881.881,0,0,1,464.881,147.213Z" transform="translate(-457.931 -133.312)" fill="url(#linear-gradient)"/>
+  <path id="路径_58007" data-name="路径 58007" d="M146.332,465.762H134.193a.881.881,0,0,1,0-1.762h12.139a.881.881,0,1,1,0,1.762Z" transform="translate(-133.312 -457.931)" fill="url(#linear-gradient)"/>
+</svg>

BIN
src/assets/images/ventAI/chatModalBlue/1-2.png


BIN
src/assets/images/ventAI/chatModalBlue/1-3.png


BIN
src/assets/images/ventAI/chatModalBlue/1-4.png


BIN
src/assets/images/ventAI/chatModalBlue/1-5.png


BIN
src/assets/images/ventAI/chatModalBlue/1-6.png


BIN
src/assets/images/ventAI/chatModalBlue/1-7.png


+ 3 - 0
src/assets/images/ventAI/chatModalBlue/2-10.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16.836" height="18.704" viewBox="0 0 16.836 18.704">
+  <path id="路径_58030" data-name="路径 58030" d="M143.9,102.337a.935.935,0,1,1,0,1.871H128.936a.935.935,0,1,1,0-1.871H143.9ZM136.419,85.5a.935.935,0,0,1,.935.935v11.3l4.48-4.48a.935.935,0,0,1,1.4,1.234l-.079.088-6.081,6.085a.935.935,0,0,1-1.235.079l-.088-.078L129.678,94.6a.935.935,0,0,1,1.233-1.4l.088.078,4.484,4.475V86.439A.935.935,0,0,1,136.419,85.5Z" transform="translate(-128 -85.504)" fill="#1889ed"/>
+</svg>

+ 3 - 0
src/assets/images/ventAI/chatModalBlue/2-11.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="18.843" height="18.705" viewBox="0 0 18.843 18.705">
+  <path id="路径_58031" data-name="路径 58031" d="M65.722,76.638H70.4l-5.141,5.139a.974.974,0,0,0,1.378,1.378l5.221-5.221v4.639a.975.975,0,0,0,1.95,0V75.661a.974.974,0,0,0-.975-.974h-7.11a.976.976,0,0,0,0,1.952ZM75.5,73.7h7.111a.974.974,0,0,0,0-1.949H77.939l5.141-5.141A.974.974,0,0,0,81.7,65.234L76.48,70.454V65.817a.975.975,0,1,0-1.95,0v6.911a.974.974,0,0,0,.975.974Z" transform="translate(-64.747 -64.842)" fill="#1889ed"/>
+</svg>

BIN
src/assets/images/ventAI/chatModalBlue/2-13.png


BIN
src/assets/images/ventAI/chatModalBlue/2-15.png


BIN
src/assets/images/ventAI/chatModalBlue/2-6.png


BIN
src/assets/images/ventAI/chatModalBlue/2-7.png


BIN
src/assets/images/ventAI/chatModalBlue/2-8.png


BIN
src/assets/images/ventAI/chatModalBlue/2-9.png


BIN
src/assets/images/ventAI/chatModalBlue/3-1.png


BIN
src/assets/images/ventAI/chatModalBlue/3-2.png


BIN
src/assets/images/ventAI/chatModalBlue/3-5.png


BIN
src/assets/images/ventAI/chatModalBlue/3-6.png


+ 7 - 0
src/assets/images/ventAI/chatModalBlue/3-7.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="41.266" height="48.044" viewBox="0 0 41.266 48.044">
+  <g id="组_16767" data-name="组 16767" transform="translate(-102.4 -34.133)">
+    <path id="路径_58014" data-name="路径 58014" d="M296.429,342.194H271.378a3.442,3.442,0,0,1-3.432-3.432V312.338a3.442,3.442,0,0,1,3.432-3.432h25.051a3.442,3.442,0,0,1,3.432,3.432v26.424A3.442,3.442,0,0,1,296.429,342.194Z" transform="translate(-157.225 -260.96)" fill="#1e6f9f" opacity="0.3"/>
+    <path id="路径_58015" data-name="路径 58015" d="M138.947,82.177H107.033a4.614,4.614,0,0,1-4.633-4.633V38.766a4.614,4.614,0,0,1,4.633-4.633h32a4.614,4.614,0,0,1,4.633,4.633V77.458a4.7,4.7,0,0,1-4.719,4.719Zm-31.915-45.47a2.107,2.107,0,0,0-2.145,2.059V77.458a2.088,2.088,0,0,0,2.059,2.059h32a2.088,2.088,0,0,0,2.059-2.059V38.766a1.981,1.981,0,0,0-2.059-2.059H107.033Z" transform="translate(0)" fill="#1e6f9f"/>
+    <path id="路径_58016" data-name="路径 58016" d="M311.82,233.17H294.834a1.287,1.287,0,0,1,0-2.574H311.82a1.287,1.287,0,0,1,0,2.574Zm-8.493,6.863h-8.493a1.287,1.287,0,0,1,0-2.574h8.493a1.287,1.287,0,0,1,0,2.574Zm6.778-15.614H296.549a3.04,3.04,0,0,1-3-3v-8.493a3.04,3.04,0,0,1,3-3H310.1a3.04,3.04,0,0,1,3,3v8.493A3.04,3.04,0,0,1,310.1,224.419Zm-13.555-11.925a.405.405,0,0,0-.429.429v8.493a.405.405,0,0,0,.429.429H310.1a.405.405,0,0,0,.429-.429v-8.493a.405.405,0,0,0-.429-.429Z" transform="translate(-181.538 -166.95)" fill="#1e6f9f"/>
+  </g>
+</svg>

BIN
src/assets/images/ventAI/chatModalBlue/3-8.png


BIN
src/assets/images/ventAI/chatModalBlue/4-1.png


+ 1 - 0
src/assets/images/ventAI/chatModalBlue/4-2.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1784266963109" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10842" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M532.207 718.134l0 0c3.079-1.777 6.39-3.079 9.112-5.802 0.477-0.594 0.827-1.064 1.303-1.539 0.594-0.709 1.539-0.946 2.128-1.54l319.434-331.383c8.519-8.757 12.781-20.121 12.781-31.483 0-11.836-4.733-23.79-13.964-32.666-18.107-17.398-46.871-16.924-64.265 1.183l-288.071 298.956-283.922-300.139c-17.28-18.225-46.04-19.057-64.265-1.777-18.226 17.281-19.057 46.04-1.777 64.265l316.355 334.462c13.844 14.676 35.149 17.399 52.429 9.113 0.945-0.35 1.777-1.182 2.722-1.653l0 0zM532.207 718.134z" fill="#ffffff" p-id="10843"></path></svg>

+ 1 - 0
src/assets/images/ventAI/chatModalBlue/4-3.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1784266918614" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8777" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M833.33 767.96h-91.9c-21.73 0-39.34-17.6-39.34-39.34s17.62-39.34 39.34-39.34h91.9c8.82 0 15.98-7.18 15.98-15.98V193.8c0-8.8-7.17-15.98-15.98-15.98H353.84c-8.82 0-15.98 7.18-15.98 15.98v90.86c0 21.75-17.62 39.34-39.34 39.34s-39.34-17.6-39.34-39.34V193.8c0-52.21 42.47-94.67 94.67-94.67h479.49c52.19 0 94.67 42.45 94.67 94.67v479.49c-0.01 52.21-42.49 94.67-94.68 94.67z" fill="#ffffff" p-id="8778"></path><path d="M675.96 925.33H196.47c-52.19 0-94.67-42.45-94.67-94.67V351.17c0-52.21 42.47-94.67 94.67-94.67h479.49c52.19 0 94.67 42.45 94.67 94.67v479.49c-0.01 52.22-42.48 94.67-94.67 94.67zM196.47 335.19c-8.82 0-15.98 7.18-15.98 15.98v479.49c0 8.8 7.17 15.98 15.98 15.98h479.49c8.82 0 15.98-7.18 15.98-15.98V351.17c0-8.8-7.17-15.98-15.98-15.98H196.47z" fill="#ffffff" p-id="8779"></path></svg>

+ 1 - 0
src/assets/images/ventAI/chatModalBlue/4-4.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1784266863166" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7525" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M807.538939 256.459755a20.897959 20.897959 0 0 1 0 29.549714L571.099429 522.44898l236.43951 236.43951a20.897959 20.897959 0 0 1 0 29.549714l-29.549715 29.549714a20.897959 20.897959 0 0 1-29.549714 0L512 581.548408 275.56049 817.987918a20.897959 20.897959 0 0 1-29.549714 0l-29.549715-29.549714a20.897959 20.897959 0 0 1 0-29.549714L452.900571 522.44898 216.461061 286.009469a20.897959 20.897959 0 0 1 0-29.549714l29.549715-29.549714a20.897959 20.897959 0 0 1 29.549714 0L512 463.349551l236.43951-236.43951a20.897959 20.897959 0 0 1 29.549714 0l29.549715 29.549714z" fill="#ffffff" p-id="7526"></path></svg>

+ 34 - 0
src/design/themify/deepblue.less

@@ -63,4 +63,38 @@ html[data-theme='deepblue'] {
   --vent-warn-tab-bg: #0f376c;
   --vent-warn-tab-border: #107eec;
   --vent-warn-tab-bg-actived: #0963c1;
+
+  // AI助手悬浮按钮图片
+  --img-ai-float: url('/src/assets/images/ventAI/chatModalBlue/4-1.png');
+
+  // 聊天弹窗图片
+  --img-chat-header-icon: url('/src/assets/images/ventAI/chatModalBlue/2-5.png');
+  --img-chat-selector-bg: url('/src/assets/images/ventAI/chatModalBlue/2-6.png');
+  --img-chat-upload-bg: url('/src/assets/images/ventAI/chatModalBlue/2-7.png');
+  --img-chat-upload-icon: url('/src/assets/images/ventAI/chatModalBlue/2-10.svg');
+  --img-chat-download-icon: url('/src/assets/images/ventAI/chatModalBlue/2-8.svg');
+  --img-chat-modal-title: url('/src/assets/images/ventAI/chatModalBlue/1-1.png');
+  --img-chat-panel-header: url('/src/assets/images/ventAI/chatModalBlue/3-5.png');
+  --img-chat-close-icon: url('/src/assets/images/ventAI/chatModalBlue/2-11.svg');
+  --img-chat-thinking-bg: url('/src/assets/images/ventAI/chatModalBlue/3-6.png');
+  --img-chat-task-bg: url('/src/assets/images/ventAI/chatModalBlue/2-3.png');
+  --img-chat-options-active-bg: url('/src/assets/images/ventAI/chatModalBlue/1-3.png');
+  --img-chat-options-bg: url('/src/assets/images/ventAI/chatModalBlue/1-4.png');
+  --img-chat-task-panel-bg: url('/src/assets/images/ventAI/chatModalBlue/1-5.png');
+  --img-chat-options-search-icon: url('/src/assets/images/ventAI/chatModalBlue/1-10.svg');
+  --img-chat-options-search-active-icon: url('/src/assets/images/ventAI/chatModalBlue/1-11.svg');
+  --img-chat-options-history-icon: url('/src/assets/images/ventAI/chatModalBlue/1-12.svg');
+  --img-chat-options-history-active-icon: url('/src/assets/images/ventAI/chatModalBlue/1-13.svg');
+  --img-chat-options-add-icon: url('/src/assets/images/ventAI/chatModalBlue/1-14.svg');
+  --img-chat-options-add-active-icon: url('/src/assets/images/ventAI/chatModalBlue/1-15.svg');
+  --img-chat-task-add-icon: url('/src/assets/images/ventAI/chatModalBlue/2-4.svg');
+  --img-chat-ai-avatar: url('/src/assets/images/ventAI/chatModalBlue/2-9.png');
+  --img-chat-thinking-avatar: url('/src/assets/images/ventAI/chatModalBlue/3-1.png');
+  --img-chat-agent-card-bg: url('/src/assets/images/ventAI/chatModalBlue/3-2.png');
+  --img-chat-agent-card-header: url('/src/assets/images/ventAI/chatModalBlue/3-3.png');
+  --img-chat-collapse-bg: url('/src/assets/images/ventAI/chatModalBlue/2-15.png');
+  --img-chat-attach-icon: url('/src/assets/images/ventAI/chatModalBlue/3-7.svg');
+  --img-chat-send-icon: url('/src/assets/images/ventAI/chatModalBlue/2-6.png');
+  --img-chat-scroll-bottom: url('/src/assets/images/ventAI/chatModalBlue/3-8.png');
+  --img-chat-input-bg: url('/src/assets/images/ventAI/chatModalBlue/2-13.png');
 }

+ 39 - 0
src/design/themify/default.less

@@ -1,4 +1,8 @@
 html {
+  @font-face {
+    font-family: 'douyuFont';
+    src: url('@/assets/font/douyuFont.otf');
+  }
   // html[data-theme='vent1'] {
   --vent-primary-color: #1580cc;
 
@@ -65,4 +69,39 @@ html {
 
   --vent-balancePress-fan-bg-default: linear-gradient(to right, #071d3c22, #035ea266);
   --vent-balancePress-fan-bg-select: linear-gradient(to right, #21a6ce99, #052d3d99);
+
+  --vet-font-family: 'douyuFont';
+  // AI助手悬浮按钮图片
+  --img-ai-float: url('/src/assets/images/ventAI/chatModalBlue/4-1.png');
+
+  // 聊天弹窗图片
+  --img-chat-modal-bg: url('/src/assets/images/ventAI/chatModalBlue/1-1.png');
+  --img-chat-close-bg: url('/src/assets/images/ventAI/chatModalBlue/1-2.png');
+  --img-chat-options-active-bg: url('/src/assets/images/ventAI/chatModalBlue/1-3.png');
+  --img-chat-options-bg: url('/src/assets/images/ventAI/chatModalBlue/1-4.png');
+  --img-chat-task-panel-bg: url('/src/assets/images/ventAI/chatModalBlue/1-5.png');
+  --img-chat-panel-bg: url('/src/assets/images/ventAI/chatModalBlue/1-6.png');
+  --img-chat-selector-bg: url('/src/assets/images/ventAI/chatModalBlue/1-7.png');
+  --img-chat-upload-bg: url('/src/assets/images/ventAI/chatModalBlue/2-7.png');
+  --img-chat-upload-icon: url('/src/assets/images/ventAI/chatModalBlue/2-10.svg');
+  --img-chat-download-icon: url('/src/assets/images/ventAI/chatModalBlue/2-8.png');
+  --img-chat-panel-header: url('/src/assets/images/ventAI/chatModalBlue/3-5.png');
+  --img-chat-close-icon: url('/src/assets/images/ventAI/chatModalBlue/2-11.svg');
+  --img-chat-thinking-bg: url('/src/assets/images/ventAI/chatModalBlue/3-6.png');
+  --img-chat-options-search-icon: url('/src/assets/images/ventAI/chatModalBlue/1-10.svg');
+  --img-chat-options-search-active-icon: url('/src/assets/images/ventAI/chatModalBlue/1-11.svg');
+  --img-chat-options-history-icon: url('/src/assets/images/ventAI/chatModalBlue/1-12.svg');
+  --img-chat-options-history-active-icon: url('/src/assets/images/ventAI/chatModalBlue/1-13.svg');
+  --img-chat-options-add-icon: url('/src/assets/images/ventAI/chatModalBlue/1-14.svg');
+  --img-chat-options-add-active-icon: url('/src/assets/images/ventAI/chatModalBlue/1-15.svg');
+  --img-chat-task-add-icon: url('/src/assets/images/ventAI/chatModalBlue/2-4.svg');
+  --img-chat-ai-avatar: url('/src/assets/images/ventAI/chatModalBlue/2-9.png');
+  --img-chat-thinking-avatar: url('/src/assets/images/ventAI/chatModalBlue/3-1.png');
+  --img-chat-agent-card-bg: url('/src/assets/images/ventAI/chatModalBlue/3-2.png');
+  --img-chat-agent-card-header: url('/src/assets/images/ventAI/chatModalBlue/3-3.png');
+  --img-chat-collapse-bg: url('/src/assets/images/ventAI/chatModalBlue/2-15.png');
+  --img-chat-attach-icon: url('/src/assets/images/ventAI/chatModalBlue/3-7.svg');
+  --img-chat-send-icon: url('/src/assets/images/ventAI/chatModalBlue/2-6.png');
+  --img-chat-scroll-bottom: url('/src/assets/images/ventAI/chatModalBlue/3-8.png');
+  --img-chat-input-bg: url('/src/assets/images/ventAI/chatModalBlue/2-13.png');
 }

+ 121 - 1
src/layouts/default/index.vue

@@ -12,11 +12,19 @@
         <LayoutFooter />
       </Layout>
     </Layout>
+
+    <!-- AI智能助手悬浮按钮(支持拖动,固定右侧) -->
+    <div class="ai-assistant-float" :style="{ right: aiBtnPosition.right, bottom: aiBtnPosition.bottom }" @mousedown="handleDragStart">
+      <div class="ai-float-icon" @click="openAiAssistant"></div>
+    </div>
+
+    <!-- AI助手弹窗 -->
+    <AiAssistantModal :visible="aiAssistantVisible" @close="aiAssistantVisible = false" />
   </Layout>
 </template>
 
 <script lang="ts">
-  import { defineComponent, computed, unref } from 'vue';
+  import { defineComponent, computed, unref, ref } from 'vue';
   import { Layout } from 'ant-design-vue';
   import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
 
@@ -24,10 +32,12 @@
   import LayoutContent from './content/index.vue';
   import LayoutSideBar from './sider/index.vue';
   import LayoutMultipleHeader from './header/MultipleHeader.vue';
+  import AiAssistantModal from '/@/views/ventAI/manageAssistent/components/AiAssistantModal.vue';
 
   import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
   import { useDesign } from '/@/hooks/web/useDesign';
   import { useLockPage } from '/@/hooks/web/useLockPage';
+  import { useAppStore } from '/@/store/modules/app';
 
   import { useAppInject } from '/@/hooks/web/useAppInject';
   import { antPrefixCls } from '/@/settings/designSetting';
@@ -42,6 +52,7 @@
       LayoutSideBar,
       LayoutMultipleHeader,
       Layout,
+      AiAssistantModal,
     },
     setup() {
       const { prefixCls } = useDesign('default-layout');
@@ -60,6 +71,85 @@
         return cls;
       });
 
+      // AI助手相关
+      const aiAssistantVisible = ref(false);
+      const appStore = useAppStore();
+
+      const openAiAssistant = () => {
+        aiAssistantVisible.value = true;
+      };
+
+      // 拖动辅助函数
+      const BTN_SIZE = 80;
+      function getLayoutHeight() {
+        const el = document.getElementById('adaptive-container');
+        return el ? el.clientHeight : window.innerHeight;
+      }
+      function getScale() {
+        return appStore.getHeightScale || 1;
+      }
+      function viewportToLayout(viewportVal: number) {
+        return viewportVal / getScale();
+      }
+
+      // 拖动状态和按钮位置(固定right,仅修改bottom)
+      const isDragging = ref(false);
+      const aiBtnPosition = ref({
+        right: '30px',
+        bottom: '30px',
+      });
+      let startY = 0;
+      let startBottomVp = 0;
+
+      // 获取按钮在视口中的bottom值
+      function getBtnBottomVp() {
+        const el = document.querySelector('.ai-assistant-float');
+        if (!el) {
+          const layoutBottom = parseFloat(aiBtnPosition.value.bottom);
+          return getLayoutHeight() - layoutBottom - BTN_SIZE;
+        }
+        const rect = el.getBoundingClientRect();
+        return window.innerHeight - rect.bottom;
+      }
+
+      // 开始拖动
+      const DRAG_THRESHOLD = 5;
+      const handleDragStart = (e: MouseEvent) => {
+        e.preventDefault();
+        const startX = e.clientX;
+        startY = e.clientY;
+        startBottomVp = getBtnBottomVp();
+        let dragged = false;
+        isDragging.value = false;
+
+        const handleDragMove = (e: MouseEvent) => {
+          e.preventDefault();
+          const deltaY = startY - e.clientY;
+          if (!dragged && (Math.abs(e.clientX - startX) > DRAG_THRESHOLD || Math.abs(deltaY) > DRAG_THRESHOLD)) {
+            dragged = true;
+            isDragging.value = true;
+          }
+          if (dragged) {
+            const newBottomVp = startBottomVp + deltaY;
+            const layoutBottom = viewportToLayout(newBottomVp);
+            const layoutH = getLayoutHeight();
+            const clamped = Math.max(0, Math.min(layoutH - BTN_SIZE, layoutBottom));
+            aiBtnPosition.value.bottom = `${clamped}px`;
+          }
+        };
+
+        const handleDragEnd = () => {
+          if (dragged) {
+            isDragging.value = false;
+          }
+          document.removeEventListener('mousemove', handleDragMove);
+          document.removeEventListener('mouseup', handleDragEnd);
+        };
+
+        document.addEventListener('mousemove', handleDragMove);
+        document.addEventListener('mouseup', handleDragEnd);
+      };
+
       return {
         getShowSidebar,
         prefixCls,
@@ -67,6 +157,10 @@
         getIsMixSidebar,
         layoutClass,
         lockEvents,
+        aiAssistantVisible,
+        aiBtnPosition,
+        openAiAssistant,
+        handleDragStart,
       };
     },
   });
@@ -81,6 +175,7 @@
     min-height: 100%;
     background-color: @content-bg;
     flex-direction: column;
+    position: relative;
 
     > .@{ventSpace}-layout {
       min-height: 100%;
@@ -92,4 +187,29 @@
       margin-left: 1px;
     }
   }
+
+  // AI助手悬浮按钮(支持拖动,固定右侧)
+  .ai-assistant-float {
+    position: fixed;
+    z-index: 1000;
+    cursor: move;
+    user-select: none;
+
+    .ai-float-icon {
+      width: 80px;
+      height: 80px;
+      background-image: var(--img-ai-float);
+      background-repeat: no-repeat;
+      background-size: contain;
+      background-position: center;
+      cursor: pointer;
+      transition: all 0.3s;
+      filter: drop-shadow(0 4px 12px rgba(10, 132, 255, 0.3));
+
+      &:hover {
+        transform: scale(1.1);
+        filter: drop-shadow(0 6px 16px rgba(10, 132, 255, 0.4));
+      }
+    }
+  }
 </style>

+ 105 - 0
src/views/ventAI/manageAssistent/api.ts

@@ -0,0 +1,105 @@
+import { defHttp } from '/@/utils/http/axios';
+
+enum Api {
+  unified = '/ventAI/api/chat',
+  getHistoryList = '/ventAI/api/sessions',
+  getDetail = '/ventAI/api/chat/history/',
+  deleteSession = '/ventAI/api/sessions/',
+}
+
+/**
+ * 历史会话接口
+ */
+export const getHistoryList = (params) => defHttp.get({ url: Api.getHistoryList, params }, { joinParamsToUrl: true, isTransformResponse: false });
+
+/**
+ * 获取历史会话详情
+ * @param session_id - 会话ID
+ */
+export const getDetail = (session_id: string) => defHttp.get({ url: Api.getDetail + session_id }, { isTransformResponse: false });
+
+/**
+ * 删除会话
+ * @param session_id - 会话ID
+ */
+export const deleteSession = (session_id: string) => defHttp.delete({ url: Api.deleteSession + session_id }, { isTransformResponse: false });
+
+/**
+ * 统一对话接口(SSE流式响应)
+ * @param params.message - 用户输入的文本,必填
+ * @param params.session_id - 会话唯一标识ID,不传参时服务端自动生成全新会话ID
+ * @param params.file - 上传的PDF文件,可选
+ * @param onChunk - 流式数据回调函数
+ * @returns Promise<{ session_id: string }>
+ */
+export const unifiedStream = async (
+  params: { message: string; session_id?: string; file?: File },
+  onChunk: (chunk: string) => void
+): Promise<{ session_id: string }> => {
+  try {
+    const formData = new FormData();
+    formData.append('message', params.message);
+    if (params.session_id) {
+      formData.append('session_id', params.session_id);
+    }
+    if (params.file) {
+      formData.append('file', params.file);
+    }
+
+    const response = await fetch(Api.unified, {
+      method: 'POST',
+      body: formData,
+    });
+
+    if (!response.ok) {
+      throw new Error(`HTTP错误: ${response.status}`);
+    }
+
+    const reader = response.body!.getReader();
+    const decoder = new TextDecoder();
+    let buffer = '';
+    let sessionId = '';
+    let isDone = false;
+
+    // eslint-disable-next-line no-constant-condition
+    while (true) {
+      const { done, value } = await reader.read();
+      if (done || isDone) break;
+
+      buffer += decoder.decode(value, { stream: true });
+      const lines = buffer.split('\n');
+      buffer = lines.pop() || '';
+
+      for (const line of lines) {
+        if (line.trim() === '') continue;
+
+        if (line.startsWith('data: ')) {
+          const dataStr = line.replace(/^data: /, '');
+
+          try {
+            const data = JSON.parse(dataStr);
+
+            onChunk(line);
+
+            if (data.type === 'done' || data.type === 'error') {
+              if (data.session_id) {
+                sessionId = data.session_id;
+              }
+              isDone = true;
+              break;
+            }
+          } catch (e) {
+            console.warn('SSE数据解析失败:', dataStr);
+          }
+        }
+      }
+
+      if (isDone) break;
+    }
+
+    return { session_id: sessionId };
+  } catch (error) {
+    console.error('统一对话流式请求失败:', error);
+    throw error;
+  }
+};

+ 1119 - 0
src/views/ventAI/manageAssistent/components/AiAssistantModal.vue

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

+ 224 - 0
src/views/ventAI/manageAssistent/components/chatModal/ChatInputArea.vue

@@ -0,0 +1,224 @@
+<template>
+  <div class="chat-input-area">
+    <div v-if="pendingFile" class="pending-file-display">
+      <div class="pending-file-card">
+        <div class="file-icon-pending"></div>
+        <div class="file-info-pending">
+          <div class="file-name-pending">{{ pendingFile.name }}</div>
+          <div class="file-size-pending">{{ formatFileSize(pendingFile.size) }}</div>
+        </div>
+        <div class="remove-pending" @click="emit('remove-pending-file')">
+          <CloseOutlined />
+        </div>
+      </div>
+    </div>
+
+    <a-textarea
+      :value="modelValue"
+      @update:value="emit('update:modelValue', $event)"
+      :rows="3"
+      class="chat-input"
+      placeholder="请输入你的问题"
+      :auto-size="{ minRows: 3, maxRows: 6 }"
+      @keydown.enter.prevent="handleEnterKey"
+    />
+    <div class="input-actions">
+      <div class="action-left">
+        <a-tooltip title="从本地上传">
+          <div class="btn-upload" @click="emit('file-upload')">
+            <div class="plus-icon"></div>
+          </div>
+        </a-tooltip>
+        <input ref="fileInputRef" type="file" style="display: none" @change="emit('file-change', $event)" accept=".pdf" />
+      </div>
+      <div class="action-right">
+        <div class="btn-send" :class="{ loading, disabled: !modelValue.trim() }" @click="handleSend">
+          <div class="send-icon"></div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref } from 'vue';
+  import { CloseOutlined } from '@ant-design/icons-vue';
+  import { formatFileSize } from './utils';
+  import type { AttachedFile } from './types';
+
+  const props = defineProps<{
+    modelValue: string;
+    pendingFile: AttachedFile | null;
+    loading: boolean;
+  }>();
+
+  const emit = defineEmits<{
+    (e: 'update:modelValue', value: string): void;
+    (e: 'send'): void;
+    (e: 'file-upload'): void;
+    (e: 'file-change', event: Event): void;
+    (e: 'remove-pending-file'): void;
+  }>();
+
+  const fileInputRef = ref<HTMLInputElement>();
+
+  const handleEnterKey = (e: KeyboardEvent) => {
+    if (!e.shiftKey && props.modelValue.trim()) {
+      emit('send');
+    }
+  };
+
+  const handleSend = () => {
+    if (props.modelValue.trim() && !props.loading) {
+      emit('send');
+    }
+  };
+
+  defineExpose({ fileInputRef });
+</script>
+
+<style scoped lang="less">
+  .chat-input-area {
+    padding: 5px 10px;
+    flex-shrink: 0;
+    background-color: rgba(10, 132, 255, 0.05);
+    border: 1px solid rgba(63, 80, 106, 0.5);
+    border-radius: 4px;
+    margin: 5px 10px;
+
+    .pending-file-display {
+      margin-bottom: 5px;
+
+      .pending-file-card {
+        display: flex;
+        align-items: center;
+        gap: 10px;
+        padding: 10px 12px;
+        background: rgba(10, 132, 255, 0.06);
+        border: 1px solid rgba(63, 80, 106, 0.5);
+        border-radius: 6px;
+        transition: all 0.3s;
+
+        &:hover {
+          background: rgba(10, 132, 255, 0.1);
+        }
+
+        .file-icon-pending {
+          width: 32px;
+          height: 32px;
+          background-image: var(--img-chat-attach-icon);
+          background-repeat: no-repeat;
+          background-size: 100% 100%;
+          flex-shrink: 0;
+        }
+
+        .file-info-pending {
+          flex: 1;
+          min-width: 0;
+
+          .file-name-pending {
+            font-size: 12px;
+            font-weight: 400;
+            line-height: 1.4;
+            color: #e0e6ed;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            margin-bottom: 2px;
+          }
+
+          .file-size-pending {
+            font-size: 11px;
+            font-weight: 400;
+            line-height: 1.3;
+            color: #8b949e;
+          }
+        }
+
+        .remove-pending {
+          cursor: pointer;
+          color: #8b949e;
+          font-size: 14px;
+          padding: 4px;
+          border-radius: 4px;
+          transition: all 0.3s;
+
+          &:hover {
+            color: #ff4d4f;
+            background: rgba(220, 38, 38, 0.1);
+          }
+        }
+      }
+    }
+
+    :deep(.chat-input) {
+      color: #e0e6ed;
+      background-color: transparent !important;
+      border: none !important;
+
+      &::placeholder {
+        color: #8b949e !important;
+        opacity: 1 !important;
+      }
+      &:focus,
+      &:hover,
+      &:active {
+        border: none !important;
+        box-shadow: none !important;
+        outline: none !important;
+      }
+    }
+    .input-actions {
+      display: flex;
+      justify-content: end;
+      align-items: center;
+      margin-top: 12px;
+
+      .action-left,
+      .action-right {
+        display: flex;
+        gap: 8px;
+        .btn-upload {
+          width: 30px;
+          height: 30px;
+          border-radius: 6px;
+          margin-right: 10px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+        }
+        .plus-icon {
+          width: 20px;
+          height: 20px;
+          background-image: var(--img-chat-input-bg);
+          background-repeat: no-repeat;
+          background-size: 100% 100%;
+          cursor: pointer;
+        }
+        .btn-send {
+          width: 32px;
+          height: 32px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          cursor: pointer;
+          opacity: 1;
+          transition: opacity 0.2s;
+
+          &.disabled {
+            opacity: 0.4;
+            cursor: not-allowed;
+          }
+
+          .send-icon {
+            width: 24px;
+            height: 24px;
+            background-image: var(--img-chat-send-icon);
+            background-repeat: no-repeat;
+            background-size: 100% 100%;
+          }
+        }
+      }
+    }
+  }
+</style>

+ 1050 - 0
src/views/ventAI/manageAssistent/components/chatModal/ChatMessages.vue

@@ -0,0 +1,1050 @@
+<template>
+  <div class="chat-messages-container">
+    <div class="chat-messages" ref="messagesRef">
+      <div v-for="(message, index) in messages" :key="index" :class="['message-wrapper', message.type]">
+        <!-- AI消息 -->
+        <div v-if="message.type === 'ai'" class="ai-message">
+          <div class="message-agent">
+            <div class="agent-icon"></div>
+          </div>
+          <div class="message-body">
+            <!-- 思考过程折叠卡片 -->
+            <div v-if="message.thinkingSteps && message.thinkingSteps.length > 0" class="thinking-card">
+              <div class="thinking-header" @click="toggleThinking(index)">
+                <div class="thinking-header-left">
+                  <div class="thinking-icon"></div>
+                  <span class="thinking-title">思考过程</span>
+                  <span class="thinking-count">{{ getAgentCount(message.thinkingSteps) }} 个子任务</span>
+                  <span v-if="getDurationText(message)" class="thinking-duration">{{ getDurationText(message) }}</span>
+                </div>
+                <div :class="['thinking-arrow', { expanded: expandedThinking[index] }]">
+                  <img :src="arrowIcon" width="12" height="12" />
+                </div>
+              </div>
+              <div v-show="expandedThinking[index]" class="thinking-body">
+                <div v-for="(group, groupIdx) in groupStepsByAgent(message.thinkingSteps)" :key="groupIdx" class="agent-group">
+                  <div class="agent-group-header" @click="toggleAgent(index + '-' + group.agent)">
+                    <div class="agent-group-header-left">
+                      <div class="agent-group-icon"></div>
+                      <span class="agent-group-name">{{ group.agent }}</span>
+                      <span class="agent-group-count">{{ group.steps.length }} 步</span>
+                    </div>
+                    <div :class="['thinking-arrow', { expanded: expandedAgents[index + '-' + group.agent] }]">
+                      <img :src="arrowIcon" width="10" height="10" />
+                    </div>
+                  </div>
+                  <div v-show="expandedAgents[index + '-' + group.agent]" class="agent-group-body">
+                    <div v-for="(step, stepIdx) in group.steps" :key="stepIdx" class="thinking-step">
+                      <!-- agent_start -->
+                      <div v-if="step.type === 'agent_start'" class="step-item step-agent-start">
+                        <div class="step-icon agent-icon-start"></div>
+                        <div class="step-content">
+                          <span class="step-desc">{{ step.message }}</span>
+                        </div>
+                      </div>
+                      <!-- tool_call -->
+                      <div v-else-if="step.type === 'tool_call'" class="step-item step-tool-call">
+                        <div class="step-icon tool-icon"></div>
+                        <div class="step-content">
+                          <span class="step-label">调用工具:</span>
+                          <span class="step-tool-name">{{ step.tool }}</span>
+                          <span v-if="step.message" class="step-desc">{{ step.message }}</span>
+                        </div>
+                      </div>
+                      <!-- executing -->
+                      <div v-else-if="step.type === 'executing'" class="step-item step-executing">
+                        <div class="step-icon executing-icon"></div>
+                        <div class="step-content">
+                          <span class="step-desc">{{ step.message || '正在执行...' }}</span>
+                          <span v-if="step.tool" class="step-tool-name">{{ step.tool }}</span>
+                        </div>
+                      </div>
+                      <!-- updated_todo_list -->
+                      <div v-else-if="step.type === 'updated_todo_list'" class="step-item step-todo">
+                        <div class="step-icon todo-icon"></div>
+                        <div class="step-content">
+                          <div class="todo-list">
+                            <div v-for="(todo, todoIdx) in step.todos" :key="todoIdx" class="todo-item">
+                              <span :class="['todo-status', todo.status]">
+                                {{ todo.status === 'completed' ? '✓' : todo.status === 'in_progress' ? '●' : '○' }}
+                              </span>
+                              <span class="todo-text">{{ todo.content }}</span>
+                            </div>
+                          </div>
+                        </div>
+                      </div>
+                      <!-- tool_result -->
+                      <div v-else-if="step.type === 'tool_result'" class="step-item step-tool-result">
+                        <div class="step-icon result-icon"></div>
+                        <div class="step-content">
+                          <span class="step-label">工具结果:</span>
+                          <span class="step-tool-name">{{ step.tool }}</span>
+                          <span class="step-desc">{{ step.message }}</span>
+                        </div>
+                      </div>
+                      <!-- agent_done -->
+                      <div v-else-if="step.type === 'agent_done'" class="step-item step-agent-done">
+                        <div class="step-icon done-icon"></div>
+                        <div class="step-content">
+                          <span class="step-desc">{{ step.message }}</span>
+                        </div>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+
+            <div v-if="message.content" class="message-text" v-html="renderMarkdown(message.content)"></div>
+            <div v-if="message.wordDownloadUrl" class="word-download-area">
+              <a-button type="link" @click="emit('download-word', message.wordDownloadUrl)"> 下载报告文件 (.docx) </a-button>
+            </div>
+            <div v-if="message.isLoading" class="loading-dots">
+              <span class="dot"></span>
+              <span class="dot"></span>
+              <span class="dot"></span>
+            </div>
+          </div>
+          <!-- AI消息操作栏 -->
+          <div class="message-actions">
+            <div class="action-btn" @click="copyMessageContent(message.content)" title="复制">
+              <img :src="copyIcon" width="14" height="14" />
+            </div>
+            <span v-if="message.createdAt" class="action-time">{{ formatFullTime(message.createdAt) }}</span>
+          </div>
+        </div>
+
+        <!-- 用户消息 -->
+        <div v-else class="user-message">
+          <div class="message-body">
+            <div class="message-text" v-html="renderMarkdown(message.content)"></div>
+            <div v-if="message.attachedFile" class="attached-file-card" @click="emit('file-preview', message.attachedFile)">
+              <div class="file-icon-card"></div>
+              <div class="file-info-card">
+                <div class="file-name-card">{{ message.attachedFile.name }}</div>
+                <div class="file-size-card">{{ formatFileSize(message.attachedFile.size) }}</div>
+              </div>
+            </div>
+          </div>
+          <!-- 用户消息操作栏 -->
+          <div class="message-actions">
+            <div class="action-btn" @click="copyMessageContent(message.content)" title="复制">
+              <img :src="copyIcon" width="14" height="14" />
+            </div>
+            <span v-if="message.createdAt" class="action-time">{{ formatFullTime(message.createdAt) }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 回到底部按钮 -->
+    <div v-if="!isAtBottom" class="scroll-to-bottom" @click="scrollToBottom">
+      <div class="scroll-to-bottom-icon"></div>
+    </div>
+
+    <!-- 表格预览弹窗 -->
+    <div v-if="showPreviewModal" class="table-preview-overlay" @click.self="closePreview">
+      <div class="table-preview-modal">
+        <div class="table-preview-header">
+          <span class="table-preview-title">表格预览</span>
+          <div class="table-preview-close" @click="closePreview">
+            <img :src="closeIcon" width="16" height="16" />
+          </div>
+        </div>
+        <div class="table-preview-body" v-html="previewTableHtml"></div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
+  import dayjs from 'dayjs';
+  import { renderMarkdown, formatFileSize, tableHtmlToMarkdown, tableHtmlToCsv } from './utils';
+  import type { Message, AttachedFile, ThinkingStep } from './types';
+  import arrowIcon from '@/assets/images/ventAI/chatModalBlue/4-2.svg';
+  import copyIcon from '@/assets/images/ventAI/chatModalBlue/4-3.svg';
+  import closeIcon from '@/assets/images/ventAI/chatModalBlue/4-4.svg';
+
+  // 全局计时器 - 每秒刷新一次,用于实时显示思考耗时
+  const nowTs = ref(Date.now());
+  let timerInterval: ReturnType<typeof setInterval> | null = null;
+
+  const getDurationText = (message: Message): string => {
+    // 优先使用后端返回的 duration_ms
+    if (message.durationMs) {
+      const totalSec = Math.round(message.durationMs / 1000);
+      const min = Math.floor(totalSec / 60);
+      const sec = totalSec % 60;
+      return `已工作 ${min}分${sec.toString().padStart(2, '0')}秒`;
+    }
+    // 前端实时计时
+    if (message.generateStartTime && message.isLoading) {
+      const elapsed = Math.floor((nowTs.value - message.generateStartTime) / 1000);
+      const min = Math.floor(elapsed / 60);
+      const sec = elapsed % 60;
+      return `已工作 ${min}分${sec.toString().padStart(2, '0')}秒`;
+    }
+    return '';
+  };
+
+  defineProps<{
+    messages: Message[];
+  }>();
+
+  const emit = defineEmits<{
+    (e: 'file-preview', file: AttachedFile): void;
+    (e: 'download-word', url: string): void;
+  }>();
+
+  const messagesRef = ref<HTMLElement>();
+  const expandedThinking = reactive<Record<number, boolean>>({});
+  const expandedAgents = reactive<Record<string, boolean>>({});
+  const isAtBottom = ref(true);
+
+  // 表格预览状态
+  const previewTableHtml = ref('');
+  const showPreviewModal = ref(false);
+
+  const handleTableAction = (e: Event) => {
+    const target = (e.target as HTMLElement).closest('.table-action-btn') as HTMLElement;
+    if (!target) return;
+
+    const action = target.dataset.action;
+    const encodedTable = target.dataset.table;
+    if (!action || !encodedTable) return;
+
+    const tableHtml = decodeURIComponent(encodedTable);
+
+    switch (action) {
+      case 'copy': {
+        const markdown = tableHtmlToMarkdown(tableHtml);
+        navigator.clipboard.writeText(markdown).catch(() => {
+          const ta = document.createElement('textarea');
+          ta.value = markdown;
+          ta.style.position = 'fixed';
+          ta.style.opacity = '0';
+          document.body.appendChild(ta);
+          ta.select();
+          document.execCommand('copy');
+          document.body.removeChild(ta);
+        });
+        break;
+      }
+      case 'csv': {
+        const csv = tableHtmlToCsv(tableHtml);
+        const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
+        const url = URL.createObjectURL(blob);
+        const a = document.createElement('a');
+        a.href = url;
+        a.download = `table_${Date.now()}.csv`;
+        a.click();
+        URL.revokeObjectURL(url);
+        break;
+      }
+      case 'preview': {
+        previewTableHtml.value = tableHtml;
+        showPreviewModal.value = true;
+        break;
+      }
+    }
+  };
+
+  const closePreview = () => {
+    showPreviewModal.value = false;
+    previewTableHtml.value = '';
+  };
+
+  const checkScrollBottom = () => {
+    const el = messagesRef.value;
+    if (!el) return;
+    const threshold = 50;
+    isAtBottom.value = el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
+  };
+
+  const scrollToBottom = () => {
+    const el = messagesRef.value;
+    if (el) {
+      el.scrollTop = el.scrollHeight;
+    }
+  };
+
+  onMounted(() => {
+    messagesRef.value?.addEventListener('click', handleTableAction);
+    messagesRef.value?.addEventListener('scroll', checkScrollBottom);
+    timerInterval = setInterval(() => {
+      nowTs.value = Date.now();
+    }, 1000);
+  });
+
+  onBeforeUnmount(() => {
+    messagesRef.value?.removeEventListener('click', handleTableAction);
+    messagesRef.value?.removeEventListener('scroll', checkScrollBottom);
+    if (timerInterval) {
+      clearInterval(timerInterval);
+      timerInterval = null;
+    }
+  });
+
+  const toggleThinking = (index: number) => {
+    expandedThinking[index] = !expandedThinking[index];
+  };
+
+  const toggleAgent = (key: string) => {
+    expandedAgents[key] = !expandedAgents[key];
+  };
+
+  interface AgentGroup {
+    agent: string;
+    steps: ThinkingStep[];
+  }
+
+  const groupStepsByAgent = (steps: ThinkingStep[]): AgentGroup[] => {
+    const groupMap = new Map<string, ThinkingStep[]>();
+    for (const step of steps) {
+      const agentName = step.agent || '系统';
+      if (!groupMap.has(agentName)) {
+        groupMap.set(agentName, []);
+      }
+      groupMap.get(agentName)!.push(step);
+    }
+    const result: AgentGroup[] = [];
+    for (const [agent, agentSteps] of groupMap) {
+      result.push({ agent, steps: agentSteps });
+    }
+    return result;
+  };
+
+  const getAgentCount = (steps: ThinkingStep[]): number => {
+    const agents = new Set(steps.map((s) => s.agent || '系统'));
+    return agents.size;
+  };
+
+  const formatFullTime = (createdAt: string) => {
+    return dayjs(createdAt).format('YYYY-MM-DD HH:mm:ss');
+  };
+
+  const copyMessageContent = async (content: string) => {
+    if (!content) return;
+    try {
+      await navigator.clipboard.writeText(content);
+    } catch {
+      const textarea = document.createElement('textarea');
+      textarea.value = content;
+      textarea.style.position = 'fixed';
+      textarea.style.opacity = '0';
+      document.body.appendChild(textarea);
+      textarea.select();
+      document.execCommand('copy');
+      document.body.removeChild(textarea);
+    }
+  };
+
+  defineExpose({ messagesRef, isAtBottom });
+</script>
+
+<style scoped lang="less">
+  .chat-messages-container {
+    flex: 1;
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+  }
+
+  .chat-messages {
+    flex: 1;
+    overflow-y: auto;
+    padding: 20px;
+
+    .message-wrapper {
+      margin-bottom: 20px;
+
+      .ai-message {
+        display: flex;
+        flex-wrap: wrap;
+        .message-agent {
+          display: flex;
+          margin-bottom: 8px;
+          opacity: 0;
+
+          .agent-icon {
+            width: 40px;
+            height: 40px;
+            background-image: var(--img-chat-ai-avatar);
+            background-repeat: no-repeat;
+            background-size: 100% 100%;
+          }
+        }
+
+        .message-body {
+          padding: 12px;
+          border-radius: 6px;
+          width: calc(100% - 50px);
+
+          /* 思考过程卡片 */
+          .thinking-card {
+            margin-bottom: 12px;
+            border: 1px solid rgba(63, 80, 106, 0.4);
+            border-radius: 8px;
+            background: rgba(10, 132, 255, 0.04);
+            overflow: hidden;
+
+            .thinking-header {
+              display: flex;
+              align-items: center;
+              justify-content: space-between;
+              padding: 10px 14px;
+              cursor: pointer;
+              user-select: none;
+              transition: background 0.2s;
+
+              &:hover {
+                background: rgba(10, 132, 255, 0.08);
+              }
+
+              .thinking-header-left {
+                display: flex;
+                align-items: center;
+                gap: 8px;
+              }
+
+              .thinking-icon {
+                width: 16px;
+                height: 16px;
+                background-image: var(--img-chat-thinking-icon, none);
+                background-repeat: no-repeat;
+                background-size: 100% 100%;
+                /* fallback: 用一个 brain/lightbulb emoji 风格的 SVG */
+                background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M8 1.5a4.5 4.5 0 0 1 3.5 7.35V11a1 1 0 0 1-1 1H5.5a1 1 0 0 1-1-1v-2.15A4.5 4.5 0 0 1 8 1.5Z' stroke='%230a84ff' stroke-width='1.2'/%3E%3Cpath d='M6 13.5h4M6.5 15h3' stroke='%230a84ff' stroke-width='1.2' stroke-linecap='round'/%3E%3C/svg%3E");
+              }
+
+              .thinking-title {
+                font-size: 13px;
+                font-weight: 500;
+                color: #0a84ff;
+              }
+
+              .thinking-count {
+                font-size: 12px;
+                color: #8b949e;
+                background: rgba(10, 132, 255, 0.1);
+                padding: 1px 6px;
+                border-radius: 10px;
+              }
+
+              .thinking-duration {
+                font-size: 12px;
+                color: #8b949e;
+                margin-left: 4px;
+              }
+            }
+
+            .thinking-arrow {
+              color: #8b949e;
+              display: flex;
+              align-items: center;
+              transition: transform 0.2s;
+
+              &.expanded {
+                transform: rotate(180deg);
+              }
+            }
+
+            .thinking-body {
+              border-top: 1px solid rgba(63, 80, 106, 0.3);
+              padding: 8px 14px;
+              max-height: 400px;
+              overflow-y: auto;
+
+              .agent-group {
+                margin-bottom: 8px;
+                border: 1px solid rgba(63, 80, 106, 0.25);
+                border-radius: 6px;
+                overflow: hidden;
+
+                &:last-child {
+                  margin-bottom: 0;
+                }
+
+                .agent-group-header {
+                  display: flex;
+                  align-items: center;
+                  justify-content: space-between;
+                  padding: 8px 12px;
+                  cursor: pointer;
+                  user-select: none;
+                  background: rgba(10, 132, 255, 0.06);
+                  transition: background 0.2s;
+
+                  &:hover {
+                    background: rgba(10, 132, 255, 0.1);
+                  }
+
+                  .agent-group-header-left {
+                    display: flex;
+                    align-items: center;
+                    gap: 8px;
+                  }
+
+                  .agent-group-icon {
+                    width: 14px;
+                    height: 14px;
+                    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14' fill='none'%3E%3Ccircle cx='7' cy='5' r='2.5' stroke='%230a84ff' stroke-width='1'/%3E%3Cpath d='M2.5 12.5c0-2.49 2.01-4.5 4.5-4.5s4.5 2.01 4.5 4.5' stroke='%230a84ff' stroke-width='1' stroke-linecap='round'/%3E%3C/svg%3E");
+                    background-repeat: no-repeat;
+                    background-size: 100% 100%;
+                    flex-shrink: 0;
+                  }
+
+                  .agent-group-name {
+                    font-size: 13px;
+                    font-weight: 500;
+                    color: #0a84ff;
+                  }
+
+                  .agent-group-count {
+                    font-size: 11px;
+                    color: #8b949e;
+                    background: rgba(10, 132, 255, 0.08);
+                    padding: 1px 5px;
+                    border-radius: 8px;
+                  }
+                }
+
+                .agent-group-body {
+                  padding: 4px 12px;
+                }
+              }
+
+              .thinking-step {
+                .step-item {
+                  display: flex;
+                  align-items: flex-start;
+                  gap: 8px;
+                  padding: 6px 0;
+                  border-bottom: 1px solid rgba(63, 80, 106, 0.15);
+
+                  &:last-child {
+                    border-bottom: none;
+                  }
+
+                  .step-icon {
+                    width: 18px;
+                    height: 18px;
+                    flex-shrink: 0;
+                    margin-top: 1px;
+                    background-repeat: no-repeat;
+                    background-size: 100% 100%;
+                  }
+
+                  .step-content {
+                    flex: 1;
+                    font-size: 13px;
+                    line-height: 1.5;
+                    color: #b0b8c4;
+
+                    .step-agent-name {
+                      color: #0a84ff;
+                      font-weight: 500;
+                    }
+
+                    .step-tool-name {
+                      color: #79c0ff;
+                      font-family: monospace;
+                      font-size: 12px;
+                      background: rgba(10, 132, 255, 0.1);
+                      padding: 1px 5px;
+                      border-radius: 3px;
+                    }
+
+                    .step-label {
+                      color: #8b949e;
+                    }
+
+                    .step-desc {
+                      color: #b0b8c4;
+                    }
+                  }
+                }
+
+                /* agent_start */
+                .step-agent-start .step-icon {
+                  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 18' fill='none'%3E%3Ccircle cx='9' cy='6' r='3' stroke='%230a84ff' stroke-width='1.2'/%3E%3Cpath d='M3 16c0-3.31 2.69-6 6-6s6 2.69 6 6' stroke='%230a84ff' stroke-width='1.2' stroke-linecap='round'/%3E%3C/svg%3E");
+                }
+
+                /* tool_call */
+                .step-tool-call .step-icon {
+                  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 18' fill='none'%3E%3Cpath d='M10.5 2.5l5 5-8 8H2.5v-5.5l8-8Z' stroke='%23d29922' stroke-width='1.2' stroke-linejoin='round'/%3E%3Cpath d='M7 7l4 4' stroke='%23d29922' stroke-width='1.2'/%3E%3C/svg%3E");
+                }
+
+                /* executing */
+                .step-executing .step-icon {
+                  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 18' fill='none'%3E%3Ccircle cx='9' cy='9' r='7' stroke='%230a84ff' stroke-width='1.2' stroke-dasharray='4 2'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 9 9' to='360 9 9' dur='2s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/svg%3E");
+                }
+
+                /* updated_todo_list */
+                .step-todo .step-icon {
+                  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 18' fill='none'%3E%3Crect x='2' y='2' width='14' height='14' rx='2' stroke='%233fb950' stroke-width='1.2'/%3E%3Cpath d='M5 9l3 3 5-6' stroke='%233fb950' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
+
+                  .todo-list {
+                    .todo-item {
+                      display: flex;
+                      align-items: flex-start;
+                      gap: 6px;
+                      padding: 2px 0;
+
+                      .todo-status {
+                        flex-shrink: 0;
+                        width: 16px;
+                        text-align: center;
+                        font-size: 12px;
+
+                        &.completed {
+                          color: #3fb950;
+                        }
+                        &.in_progress {
+                          color: #0a84ff;
+                        }
+                        &.pending {
+                          color: #8b949e;
+                        }
+                      }
+
+                      .todo-text {
+                        font-size: 13px;
+                        color: #b0b8c4;
+                      }
+                    }
+                  }
+                }
+
+                /* tool_result */
+                .step-tool-result .step-icon {
+                  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 18' fill='none'%3E%3Ccircle cx='9' cy='9' r='7' stroke='%233fb950' stroke-width='1.2'/%3E%3Cpath d='M6 9l2 2 4-4' stroke='%233fb950' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
+                }
+
+                /* agent_done */
+                .step-agent-done .step-icon {
+                  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 18' fill='none'%3E%3Ccircle cx='9' cy='9' r='7' stroke='%233fb950' stroke-width='1.2'/%3E%3Cpath d='M5.5 9l2.5 2.5 4.5-5' stroke='%233fb950' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
+                }
+              }
+            }
+          }
+
+          .message-text {
+            font-size: 14px;
+            font-weight: 400;
+            line-height: 1.6;
+            color: #e0e6ed;
+
+            :deep(h1),
+            :deep(h2),
+            :deep(h3),
+            :deep(h4),
+            :deep(h5),
+            :deep(h6) {
+              color: #fff;
+            }
+            :deep(p) {
+              margin: 0;
+            }
+            :deep(code) {
+              background: rgba(10, 132, 255, 0.1);
+              padding: 1px 4px;
+              border-radius: 3px;
+              font-size: 12px;
+            }
+            :deep(pre) {
+              background: rgba(10, 132, 255, 0.1);
+              padding: 8px;
+              border-radius: 4px;
+              overflow-x: auto;
+              margin: 6px 0;
+              white-space: pre-wrap;
+            }
+            :deep(pre code) {
+              background: transparent;
+              padding: 0;
+            }
+            :deep(ul),
+            :deep(ol) {
+              padding-left: 18px;
+              margin: 4px 0;
+            }
+            :deep(blockquote) {
+              border-left: 3px solid rgba(10, 132, 255, 0.3);
+              padding-left: 10px;
+              margin: 6px 0;
+              color: #8b949e;
+            }
+            :deep(table) {
+              border-collapse: collapse;
+              margin: 6px 0;
+            }
+            :deep(th),
+            :deep(td) {
+              border: 1px solid rgba(63, 80, 106, 0.5);
+              padding: 4px 8px;
+            }
+            :deep(th) {
+              background: rgba(10, 132, 255, 0.1);
+            }
+            :deep(strong) {
+              font-weight: 600;
+            }
+
+            :deep(.markdown-table-wrapper) {
+              position: relative;
+              margin: 6px 0;
+
+              .table-actions {
+                position: absolute;
+                top: 4px;
+                right: 4px;
+                display: flex;
+                gap: 2px;
+                opacity: 0;
+                transition: opacity 0.2s;
+                z-index: 1;
+              }
+
+              &:hover .table-actions {
+                opacity: 1;
+              }
+
+              .table-action-btn {
+                width: 26px;
+                height: 26px;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                border-radius: 4px;
+                color: #8b949e;
+                cursor: pointer;
+                background: rgba(22, 27, 34, 0.8);
+                backdrop-filter: blur(4px);
+                transition: all 0.2s;
+
+                &:hover {
+                  color: #e0e6ed;
+                  background: rgba(10, 132, 255, 0.3);
+                }
+              }
+            }
+          }
+        }
+        .loading-dots {
+          display: flex;
+          align-items: center;
+          gap: 6px;
+          padding: 4px 0;
+
+          .dot {
+            width: 8px;
+            height: 8px;
+            border-radius: 50%;
+            background: rgba(10, 132, 255, 0.4);
+            animation: dot-bounce 1.4s ease-in-out infinite both;
+
+            &:nth-child(1) {
+              animation-delay: 0s;
+            }
+            &:nth-child(2) {
+              animation-delay: 0.2s;
+            }
+            &:nth-child(3) {
+              animation-delay: 0.4s;
+            }
+          }
+        }
+
+        @keyframes dot-bounce {
+          0%,
+          80%,
+          100% {
+            transform: scale(0.6);
+            opacity: 0.4;
+          }
+          40% {
+            transform: scale(1);
+            opacity: 1;
+          }
+        }
+      }
+
+      .user-message {
+        display: flex;
+        flex-wrap: wrap;
+        justify-content: flex-end;
+        align-items: flex-start;
+
+        .message-agent {
+          display: flex;
+          margin-bottom: 8px;
+          margin-left: 5px;
+
+          .user-icon {
+            width: 40px;
+            height: 40px;
+            background-image: var(--img-chat-collapse-bg);
+            background-repeat: no-repeat;
+            background-size: 100% 100%;
+          }
+        }
+
+        .message-body {
+          background: rgba(10, 132, 255, 0.15);
+          padding: 12px;
+          border-radius: 6px 0 6px 6px;
+          max-width: 700px;
+
+          .message-text {
+            font-size: 14px;
+            font-weight: 400;
+            line-height: 1.6;
+            color: #e0e6ed;
+
+            :deep(h1, h2, h3, h4, h5, h6) {
+              color: #fff;
+            }
+            :deep(p) {
+              margin: 0;
+            }
+            :deep(code) {
+              background: rgba(0, 0, 0, 0.2);
+              padding: 1px 4px;
+              border-radius: 3px;
+              font-size: 12px;
+            }
+            :deep(pre) {
+              background: rgba(0, 0, 0, 0.2);
+              padding: 8px;
+              border-radius: 4px;
+              overflow-x: auto;
+              margin: 6px 0;
+              white-space: pre-wrap;
+            }
+            :deep(pre code) {
+              background: transparent;
+              padding: 0;
+            }
+          }
+
+          .attached-file-card {
+            margin-top: 10px;
+            background: rgba(255, 255, 255, 0.08);
+            border-radius: 6px;
+            display: flex;
+            align-items: center;
+            gap: 10px;
+            padding: 8px;
+            cursor: pointer;
+            transition: all 0.3s;
+
+            &:hover {
+              background: rgba(255, 255, 255, 0.15);
+            }
+
+            .file-icon-card {
+              width: 32px;
+              height: 32px;
+              background-image: var(--img-chat-attach-icon);
+              background-repeat: no-repeat;
+              background-size: 100% 100%;
+              flex-shrink: 0;
+            }
+
+            .file-info-card {
+              flex: 1;
+              min-width: 0;
+
+              .file-name-card {
+                font-size: 12px;
+                font-weight: 400;
+                line-height: 1.4;
+                color: #e0e6ed;
+                overflow: hidden;
+                text-overflow: ellipsis;
+                white-space: nowrap;
+                margin-bottom: 2px;
+              }
+
+              .file-size-card {
+                font-size: 11px;
+                font-weight: 400;
+                line-height: 1.3;
+                color: #8b949e;
+              }
+            }
+          }
+        }
+      }
+
+      .ai-message,
+      .user-message {
+        .message-actions {
+          display: flex;
+          align-items: center;
+          gap: 8px;
+          width: 100%;
+          opacity: 0;
+          transition: opacity 0.2s;
+
+          .action-btn {
+            width: 26px;
+            height: 26px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            border-radius: 4px;
+            color: #8b949e;
+            cursor: pointer;
+            transition: all 0.2s;
+
+            &:hover {
+              color: #e0e6ed;
+              background: rgba(10, 132, 255, 0.15);
+            }
+          }
+
+          .action-time {
+            font-size: 12px;
+            color: #8b949e;
+          }
+        }
+
+        &:hover .message-actions {
+          opacity: 1;
+        }
+      }
+
+      .ai-message .message-actions {
+        padding: 4px 0 0 50px;
+      }
+
+      .user-message .message-actions {
+        padding: 4px 0 0 0;
+        justify-content: flex-end;
+      }
+    }
+  }
+
+  /* 表格预览弹窗 */
+  .table-preview-overlay {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background: rgba(0, 0, 0, 0.6);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    z-index: 1000;
+  }
+
+  .table-preview-modal {
+    background: #1a1f2e;
+    border: 1px solid rgba(63, 80, 106, 0.5);
+    border-radius: 8px;
+    width: 90%;
+    max-width: 1200px;
+    max-height: 700px;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+
+    .table-preview-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 12px 16px;
+      border-bottom: 1px solid rgba(63, 80, 106, 0.3);
+      flex-shrink: 0;
+
+      .table-preview-title {
+        font-size: 15px;
+        font-weight: 500;
+        color: #e0e6ed;
+      }
+
+      .table-preview-close {
+        width: 32px;
+        height: 32px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        border-radius: 4px;
+        color: #8b949e;
+        cursor: pointer;
+        transition: all 0.2s;
+
+        &:hover {
+          color: #e0e6ed;
+          background: rgba(10, 132, 255, 0.15);
+        }
+      }
+    }
+
+    :deep(.table-preview-body) {
+      padding: 20px;
+      overflow: auto;
+      flex: 1;
+
+      table {
+        width: 100%;
+        border-collapse: collapse;
+      }
+
+      th,
+      td {
+        border: 1px solid #3f506a;
+        padding: 10px 14px;
+        text-align: left;
+        color: #e0e6ed;
+        font-size: 14px;
+      }
+
+      th {
+        background: #1e2538;
+        font-weight: 600;
+      }
+
+      tbody tr {
+        &:nth-child(even) td {
+          background: rgba(10, 132, 255, 0.04);
+        }
+
+        &:hover td {
+          background: rgba(10, 132, 255, 0.1);
+        }
+      }
+    }
+  }
+
+  .scroll-to-bottom {
+    position: absolute;
+    bottom: 20px;
+    left: 50%;
+    transform: translateX(-50%);
+    z-index: 10;
+
+    .scroll-to-bottom-icon {
+      width: 40px;
+      height: 40px;
+      border-radius: 50%;
+      background: rgba(255, 255, 255, 0.15);
+      backdrop-filter: blur(4px);
+      background-image: var(--img-chat-scroll-bottom);
+      background-repeat: no-repeat;
+      background-size: 20px 20px;
+      background-position: center;
+      cursor: pointer;
+      transition: all 0.2s;
+
+      &:hover {
+        background-color: rgba(255, 255, 255, 0.25);
+      }
+    }
+  }
+</style>

+ 133 - 0
src/views/ventAI/manageAssistent/components/chatModal/FileListPanel.vue

@@ -0,0 +1,133 @@
+<template>
+  <div v-if="visible" class="file-list-panel">
+    <div class="file-list-header">
+      <span>当前对话附件</span>
+      <CloseOutlined class="close-icon" @click="emit('close')" />
+    </div>
+    <div class="file-list-content">
+      <div v-for="file in files" :key="file.id" class="file-list-item" @click="emit('file-click', file)">
+        <div class="file-icon-small"></div>
+        <div class="file-info-small">
+          <div class="file-name-small">{{ file.name }}</div>
+          <div class="file-size-small">{{ formatFileSize(file.size) }}</div>
+        </div>
+      </div>
+      <div v-if="files.length === 0" class="empty-file-list">暂无附件</div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { CloseOutlined } from '@ant-design/icons-vue';
+  import { formatFileSize } from './utils';
+  import type { AttachedFile } from './types';
+
+  defineProps<{
+    visible: boolean;
+    files: AttachedFile[];
+  }>();
+
+  const emit = defineEmits<{
+    (e: 'close'): void;
+    (e: 'file-click', file: AttachedFile): void;
+  }>();
+</script>
+
+<style scoped lang="less">
+  .file-list-panel {
+    position: absolute;
+    top: 60px;
+    right: 20px;
+    width: 280px;
+    max-height: 400px;
+    background: rgba(10, 132, 255, 0.08);
+    border: 1px solid rgba(63, 80, 106, 0.5);
+    border-radius: 4px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+    z-index: 1000;
+    display: flex;
+    flex-direction: column;
+
+    .file-list-header {
+      padding: 12px 16px;
+      border-bottom: 1px solid rgba(63, 80, 106, 0.5);
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      font-size: 13px;
+      font-weight: 500;
+      line-height: 1.4;
+      color: #e0e6ed;
+
+      .close-icon {
+        cursor: pointer;
+        font-size: 14px;
+        color: #8b949e;
+
+        &:hover {
+          color: #e0e6ed;
+        }
+      }
+    }
+
+    .file-list-content {
+      flex: 1;
+      overflow-y: auto;
+      padding: 8px;
+
+      .file-list-item {
+        display: flex;
+        align-items: center;
+        gap: 10px;
+        padding: 10px;
+        cursor: pointer;
+        border-radius: 4px;
+        transition: all 0.3s;
+        margin-bottom: 4px;
+
+        &:hover {
+          background: rgba(10, 132, 255, 0.1);
+        }
+
+        .file-icon-small {
+          width: 32px;
+          height: 32px;
+          background-image: var(--img-chat-attach-icon);
+          background-repeat: no-repeat;
+          background-size: 100% 100%;
+          flex-shrink: 0;
+        }
+
+        .file-info-small {
+          flex: 1;
+          min-width: 0;
+
+          .file-name-small {
+            font-size: 12px;
+            font-weight: 400;
+            line-height: 1.4;
+            color: #e0e6ed;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            margin-bottom: 2px;
+          }
+
+          .file-size-small {
+            font-size: 11px;
+            font-weight: 400;
+            line-height: 1.3;
+            color: #8b949e;
+          }
+        }
+      }
+
+      .empty-file-list {
+        padding: 20px;
+        text-align: center;
+        color: #8b949e;
+        font-size: 12px;
+      }
+    }
+  }
+</style>

+ 141 - 0
src/views/ventAI/manageAssistent/components/chatModal/FilePreviewPanel.vue

@@ -0,0 +1,141 @@
+<template>
+  <div class="file-preview-panel">
+    <div class="panel-header">
+      <span class="panel-title">{{ file.name }}</span>
+      <div class="close-btn" @click="emit('close')">
+        <div class="btn-close-icon"></div>
+      </div>
+    </div>
+    <div class="panel-content file-preview-content">
+      <div v-if="isImageFile(file.name)" class="image-preview">
+        <img :src="file.preview" alt="预览图片" />
+      </div>
+      <div v-else-if="isPdfFile(file.name)" class="pdf-preview">
+        <iframe v-if="file.preview" :src="file.preview" frameborder="0"></iframe>
+        <div v-else class="pdf-no-preview">无法加载PDF预览</div>
+      </div>
+      <div v-else class="text-preview">
+        <pre>{{ file.content || '文件内容加载中...' }}</pre>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { isImageFile, isPdfFile } from './utils';
+  import type { AttachedFile } from './types';
+
+  defineProps<{
+    file: AttachedFile;
+  }>();
+
+  const emit = defineEmits<{
+    (e: 'close'): void;
+  }>();
+</script>
+
+<style scoped lang="less">
+  .file-preview-panel {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+  }
+
+  .panel-header {
+    padding: 10px 10px 5px 10px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    flex-shrink: 0;
+
+    .panel-title {
+      font-size: 15px;
+      font-weight: 600;
+      line-height: 1.4;
+      color: #e0e6ed;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      flex: 1;
+      margin-right: 10px;
+    }
+
+    .close-btn {
+      cursor: pointer;
+      width: 36px;
+      height: 36px;
+      background-image: var(--img-chat-upload-bg);
+      background-repeat: no-repeat;
+      background-size: 100% 100%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      flex-shrink: 0;
+      transition: all 0.3s;
+      margin-bottom: auto;
+
+      .btn-close-icon {
+        width: 20px;
+        height: 20px;
+        background-image: var(--img-chat-close-icon);
+        background-repeat: no-repeat;
+        background-size: 100% 100%;
+      }
+    }
+  }
+
+  .panel-content {
+    flex: 1;
+    overflow-y: auto;
+    padding: 16px;
+  }
+
+  .file-preview-content {
+    height: 100%;
+
+    .image-preview {
+      width: 100%;
+      height: 100%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      img {
+        width: 100%;
+        height: 100%;
+        object-fit: contain;
+        border-radius: 4px;
+      }
+    }
+
+    .pdf-preview {
+      width: 100%;
+      height: 100%;
+
+      iframe {
+        width: 100%;
+        height: 100%;
+        border: none;
+      }
+
+      .pdf-no-preview {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        height: 100%;
+        color: #8b949e;
+        font-size: 14px;
+      }
+    }
+
+    .text-preview {
+      pre {
+        white-space: pre-wrap;
+        word-wrap: break-word;
+        font-family: monospace;
+        font-size: 13px;
+        color: #e0e6ed;
+      }
+    }
+  }
+</style>

+ 290 - 0
src/views/ventAI/manageAssistent/components/chatModal/TaskListPanel.vue

@@ -0,0 +1,290 @@
+<template>
+  <div class="task-list-panel">
+    <!-- 搜索模式 -->
+    <div v-if="activeOption === 'search'" class="search-header">
+      <a-input v-model:value="searchKeyword" placeholder="搜索任务名称..." allow-clear size="small" class="search-input" />
+    </div>
+    <!-- 历史记录模式 -->
+    <div v-else class="task-header">
+      <span class="task-title">历史记录</span>
+    </div>
+
+    <div class="task-list">
+      <div
+        v-for="(task, index) in filteredTaskList"
+        :key="index"
+        :class="['task-item', { active: currentTaskId === task.id }]"
+        @click="emit('switch-task', task.id)"
+      >
+        <span class="task-name">{{ task.name }}</span>
+        <div class="task-delete" @click.stop="emit('delete-task', task, index)">
+          <DeleteOutlined />
+        </div>
+      </div>
+      <div v-if="activeOption === 'search' && searchKeyword && filteredTaskList.length === 0" class="empty-hint"> 未找到匹配的任务 </div>
+    </div>
+
+    <div class="options-area">
+      <div
+        v-for="item in optionItems"
+        :key="item.type"
+        :class="['options-area-item', `options-${item.type}`, { active: activeOption === item.type }]"
+        @click="handleOptionClick(item.type)"
+      >
+        <div class="options-img">
+          <div class="options-icon"></div>
+        </div>
+        <a-button size="small" type="primary">{{ item.label }}</a-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, computed } from 'vue';
+  import { DeleteOutlined } from '@ant-design/icons-vue';
+  import type { Task } from './types';
+
+  const props = defineProps<{
+    taskList: Task[];
+    currentTaskId: string;
+  }>();
+
+  const emit = defineEmits<{
+    (e: 'switch-task', taskId: string): void;
+    (e: 'create-task'): void;
+    (e: 'delete-task', task: Task, index: number): void;
+  }>();
+
+  type OptionType = 'search' | 'history' | 'add';
+
+  const activeOption = ref<OptionType>('history');
+  const searchKeyword = ref('');
+
+  const optionItems = [
+    { type: 'search' as OptionType, label: '内容搜索' },
+    { type: 'history' as OptionType, label: '历史记录' },
+    { type: 'add' as OptionType, label: '新增任务' },
+  ];
+
+  const filteredTaskList = computed(() => {
+    if (activeOption.value !== 'search' || !searchKeyword.value.trim()) {
+      return props.taskList;
+    }
+    const keyword = searchKeyword.value.trim().toLowerCase();
+    return props.taskList.filter((task) => task.name.toLowerCase().includes(keyword));
+  });
+
+  const handleAddTask = () => {
+    emit('create-task');
+  };
+
+  const handleOptionClick = (type: OptionType) => {
+    activeOption.value = type;
+    if (type === 'add') {
+      emit('create-task');
+    } else {
+      searchKeyword.value = '';
+    }
+  };
+</script>
+
+<style scoped lang="less">
+  .task-list-panel {
+    width: 260px;
+    display: flex;
+    flex-direction: column;
+    background-image: var(--img-chat-task-panel-bg);
+    background-repeat: no-repeat;
+    background-size: 100% 100%;
+    border-radius: 8px;
+    flex-shrink: 0;
+    padding: 10px;
+
+    .search-header {
+      padding-bottom: 10px;
+
+      .search-input {
+        height: 32px;
+        background: rgba(10, 132, 255, 0.1);
+        border: 1px solid rgba(63, 80, 106, 0.5);
+        color: #e0e6ed;
+
+        :deep(.zxm-input) {
+          background: transparent;
+          color: #e0e6ed;
+        }
+      }
+    }
+
+    .task-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding-bottom: 10px;
+
+      .task-title {
+        font-size: 15px;
+        line-height: 1.4;
+        color: #0a84ff;
+      }
+      .task-add-btn {
+        background-color: #0a84ff;
+
+        &:hover {
+          background-color: #0077e6;
+        }
+      }
+    }
+
+    .task-list {
+      flex: 1;
+      overflow-y: auto;
+
+      .task-item {
+        display: flex;
+        align-items: center;
+        gap: 10px;
+        padding: 4px;
+        cursor: pointer;
+        transition: all 0.3s;
+        margin-bottom: 4px;
+        color: #e0e6ed;
+
+        &:hover {
+          background: rgba(10, 132, 255, 0.1);
+          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+        }
+
+        &.active {
+          background: rgba(10, 132, 255, 0.1);
+          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+          color: #e0e6ed;
+        }
+
+        .task-icon {
+          width: 16px;
+          height: 16px;
+          flex-shrink: 0;
+          background-image: var(--img-chat-task-add-icon);
+          background-repeat: no-repeat;
+          background-size: 100% 100%;
+        }
+
+        .task-name {
+          font-size: 14px;
+          font-weight: 400;
+          line-height: 1.4;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+          flex: 1;
+        }
+
+        .task-delete {
+          flex-shrink: 0;
+          width: 28px;
+          height: 28px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          border-radius: 4px;
+          color: #8b949e;
+          font-size: 14px;
+          visibility: hidden;
+          transition: all 0.2s;
+
+          &:hover {
+            color: #ff4d4f;
+            background: rgba(220, 38, 38, 0.1);
+          }
+        }
+
+        &:hover .task-delete {
+          visibility: visible;
+        }
+      }
+
+      .empty-hint {
+        text-align: center;
+        color: #8b949e;
+        font-size: 13px;
+        padding: 20px 0;
+      }
+    }
+
+    .options-area {
+      display: flex;
+      gap: 6px;
+      padding-top: 8px;
+      border-top: 1px solid rgba(63, 80, 106, 0.3);
+
+      .options-area-item {
+        flex: 1;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        gap: 6px;
+        cursor: pointer;
+        border-radius: 6px;
+        transition: all 0.3s;
+
+        .options-img {
+          width: 60px;
+          height: 60px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          background-image: var(--img-chat-options-bg);
+          background-repeat: no-repeat;
+          background-size: 45px 45px;
+          background-position: center;
+        }
+
+        &.active .options-img {
+          background-image: var(--img-chat-options-active-bg);
+          background-size: 60px 60px;
+        }
+
+        .options-icon {
+          width: 16px;
+          height: 16px;
+          background-repeat: no-repeat;
+          background-size: 100% 100%;
+          background-position: center;
+        }
+
+        &.options-search .options-icon {
+          background-image: var(--img-chat-options-search-icon);
+        }
+        &.options-search.active .options-icon {
+          background-image: var(--img-chat-options-search-active-icon);
+        }
+
+        &.options-history .options-icon {
+          background-image: var(--img-chat-options-history-icon);
+        }
+        &.options-history.active .options-icon {
+          background-image: var(--img-chat-options-history-active-icon);
+        }
+
+        &.options-add .options-icon {
+          background-image: var(--img-chat-options-add-icon);
+        }
+        &.options-add.active .options-icon {
+          background-image: var(--img-chat-options-add-active-icon);
+        }
+
+        .zxm-btn {
+          font-size: 14px;
+          padding: 2px 6px;
+          height: auto;
+          background: transparent !important;
+          border: none;
+          color: #0a84ff;
+          box-shadow: none;
+        }
+      }
+    }
+  }
+</style>

+ 397 - 0
src/views/ventAI/manageAssistent/components/chatModal/ThinkingPanel.vue

@@ -0,0 +1,397 @@
+<template>
+  <div class="thinking-panel">
+    <div class="panel-header">
+      <div class="panel-left">
+        <div class="agent-avatar-small"></div>
+        <div class="panel-title-group">
+          <div class="panel-title">{{ currentDisplayAgent?.agentName || '智能分析' }}</div>
+        </div>
+      </div>
+      <div class="close-btn" @click="emit('close')">
+        <div class="btn-close-icon"></div>
+      </div>
+    </div>
+
+    <div class="thinking-section">
+      <div class="section-content thinking-content" ref="thinkingContentRef">
+        <div v-if="!thinkingSections.length" class="empty-thinking">暂无思考过程</div>
+        <div v-for="(section, idx) in thinkingSections" :key="idx" :class="['timeline-item', `timeline-${section.type}`]">
+          <div class="timeline-marker">
+            <div class="timeline-dot"></div>
+            <div v-if="idx < thinkingSections.length - 1" class="timeline-line"></div>
+          </div>
+          <div class="timeline-body">
+            <div class="timeline-header">
+              <span class="timeline-label">{{ section.label }}</span>
+            </div>
+            <div class="timeline-text" v-html="renderMarkdown(section.content)"></div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="agent-horizontal-list" v-if="selectedAgentParentMsg?.agentAnswers?.length">
+      <div
+        v-for="agent in selectedAgentParentMsg!.agentAnswers"
+        :key="agent.id"
+        :class="['agent-chip', { active: currentDisplayAgent?.id === agent.id }]"
+        @click="emit('select-agent', agent)"
+      >
+        <div class="chip-body">
+          <div class="chip-icon"></div>
+          <div class="chip-name">
+            {{ agent.agentName || '智能分析' }}
+          </div>
+        </div>
+        <div class="chip-status">
+          <span v-if="agent.status === 'thinking'">正在连接</span>
+          <span v-else>已完成</span>
+        </div>
+        <div class="chip-border"></div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref } from 'vue';
+  import { renderMarkdown } from './utils';
+  import type { AgentAnswer, Message, ThinkingSection } from './types';
+
+  defineProps<{
+    currentDisplayAgent: AgentAnswer | null;
+    selectedAgentParentMsg: Message | null;
+    thinkingSections: ThinkingSection[];
+  }>();
+
+  const emit = defineEmits<{
+    (e: 'select-agent', answer: AgentAnswer): void;
+    (e: 'close'): void;
+  }>();
+
+  const thinkingContentRef = ref<HTMLElement | null>(null);
+
+  defineExpose({ thinkingContentRef });
+</script>
+
+<style scoped lang="less">
+  .thinking-panel {
+    height: 100%;
+    width: 100%;
+    display: flex;
+    flex-direction: column;
+    padding: 12px 16px;
+
+    .panel-header {
+      display: flex;
+      padding: 0;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 6px;
+      flex-shrink: 0;
+
+      .panel-left {
+        display: flex;
+        align-items: center;
+        gap: 12px;
+      }
+
+      .agent-avatar-small {
+        width: 35px;
+        height: 36px;
+        border-radius: 8px;
+        background-image: var(--img-chat-panel-header);
+        background-repeat: no-repeat;
+        background-size: 100% 100%;
+        flex-shrink: 0;
+      }
+
+      .panel-title-group {
+        display: flex;
+        flex-direction: column;
+        line-height: 1;
+      }
+
+      .panel-title {
+        font-size: 15px;
+        font-weight: 600;
+        line-height: 1.4;
+        color: #0a84ff;
+      }
+      .close-btn {
+        cursor: pointer;
+        width: 36px;
+        height: 36px;
+        background-image: var(--img-chat-upload-bg);
+        background-repeat: no-repeat;
+        background-size: 100% 100%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        flex-shrink: 0;
+        transition: all 0.3s;
+        margin-bottom: auto;
+
+        .btn-close-icon {
+          width: 20px;
+          height: 20px;
+          background-image: var(--img-chat-close-icon);
+          background-repeat: no-repeat;
+          background-size: 100% 100%;
+        }
+      }
+    }
+
+    .thinking-section {
+      flex: 1;
+      height: 0;
+      display: flex;
+      flex-direction: column;
+      border: 1px solid #0a84ff;
+      border-radius: 4px 4px 0 0;
+
+      .thinking-content {
+        flex: 1;
+        background: rgba(10, 132, 255, 0.05);
+        border: 1px solid rgba(63, 80, 106, 0.5);
+        border-radius: 8px;
+        padding: 20px 16px;
+        overflow-y: auto;
+        font-size: 12px;
+        font-weight: 400;
+        color: #e0e6ed;
+        line-height: 1.6;
+
+        .empty-thinking {
+          color: #8b949e;
+          font-style: italic;
+          text-align: center;
+          padding: 32px 0;
+        }
+
+        .timeline-item {
+          display: flex;
+          gap: 0;
+          position: relative;
+
+          .timeline-marker {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            width: 28px;
+            flex-shrink: 0;
+            position: relative;
+          }
+
+          .timeline-dot {
+            width: 12px;
+            height: 12px;
+            border-radius: 50%;
+            border: 2px solid #fff;
+            flex-shrink: 0;
+            margin-top: 6px;
+            position: relative;
+            z-index: 1;
+          }
+
+          .timeline-line {
+            flex: 1;
+            width: 0;
+            min-height: 16px;
+            margin: 4px 0;
+            border-left: 2px dashed #8b949e;
+          }
+
+          &.timeline-select {
+            .timeline-dot {
+              background: #0a84ff;
+              box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.15);
+            }
+            .timeline-header {
+              color: #e0e6ed;
+            }
+          }
+          &.timeline-params {
+            display: none;
+            .timeline-dot {
+              background: #faad14;
+              box-shadow: 0 0 0 3px rgba(223, 91, 12, 0.15);
+            }
+            .timeline-header {
+              color: #e0e6ed;
+            }
+          }
+          &.timeline-result {
+            .timeline-dot {
+              background: #52c41a;
+              box-shadow: 0 0 0 3px rgba(46, 125, 50, 0.15);
+            }
+          }
+
+          .timeline-body {
+            flex: 1;
+            min-width: 0;
+            padding-bottom: 16px;
+            padding-left: 12px;
+          }
+
+          .timeline-header {
+            font-size: 13px;
+            font-weight: 500;
+            margin-bottom: 8px;
+            line-height: 1.4;
+          }
+
+          .timeline-text {
+            font-size: 14px;
+            font-weight: 400;
+            color: #e0e6ed;
+            line-height: 1.6;
+
+            :deep(p) {
+              margin: 0 0 6px;
+            }
+            :deep(code) {
+              background: rgba(10, 132, 255, 0.1);
+              padding: 2px 6px;
+              border-radius: 3px;
+              font-family: monospace;
+              font-size: 12px;
+            }
+            :deep(pre) {
+              background: rgba(10, 132, 255, 0.1);
+              padding: 10px;
+              border-radius: 4px;
+              overflow-x: auto;
+              margin: 8px 0;
+            }
+            :deep(pre code) {
+              background: transparent;
+              padding: 0;
+            }
+            :deep(ul),
+            :deep(ol) {
+              padding-left: 20px;
+              margin: 4px 0;
+            }
+            :deep(li) {
+              margin-bottom: 2px;
+            }
+            :deep(h1),
+            :deep(h2),
+            :deep(h3),
+            :deep(h4) {
+              margin: 8px 0 4px;
+              font-weight: 600;
+            }
+            :deep(blockquote) {
+              border-left: 3px solid rgba(63, 80, 106, 0.5);
+              padding-left: 12px;
+              margin: 8px 0;
+              color: #8b949e;
+            }
+            :deep(table) {
+              border-collapse: collapse;
+              width: 100%;
+              margin: 8px 0;
+            }
+            :deep(th),
+            :deep(td) {
+              border: 1px solid rgba(63, 80, 106, 0.5);
+              padding: 6px 10px;
+              text-align: left;
+            }
+            :deep(th) {
+              background: rgba(10, 132, 255, 0.1);
+              font-weight: 600;
+            }
+            :deep(strong) {
+              font-weight: 600;
+            }
+          }
+        }
+      }
+    }
+
+    .agent-horizontal-list {
+      display: flex;
+      gap: 10px;
+      padding: 8px 0;
+      overflow-x: auto;
+      align-items: center;
+      flex-shrink: 0;
+      border-radius: 0 0 4px 4px;
+      border-top: none;
+      background-color: rgba(10, 132, 255, 0.05);
+    }
+
+    .agent-chip {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      padding: 8px 12px 8px 20px;
+      background: rgba(10, 132, 255, 0.08);
+      border-radius: 10px;
+      border: 1px solid rgba(63, 80, 106, 0.5);
+      min-width: 140px;
+      cursor: pointer;
+      transition: all 0.2s;
+      position: relative;
+
+      .chip-body {
+        display: flex;
+        align-items: start;
+        gap: 8px;
+        flex-direction: column;
+      }
+
+      .chip-border {
+        position: absolute;
+        left: 10px;
+        top: 16px;
+        width: 5px;
+        height: 35px;
+        border: 2px solid #0a84ff;
+        pointer-events: none;
+        border-right: none;
+      }
+      .chip-icon {
+        width: 24px;
+        height: 24px;
+        background-image: var(--img-chat-thinking-bg);
+        background-repeat: no-repeat;
+        background-size: 100% 100%;
+      }
+      .chip-name {
+        font-size: 13px;
+        font-weight: 500;
+        line-height: 1.4;
+        color: #e0e6ed;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        width: 100px;
+      }
+
+      .chip-status {
+        font-size: 11px;
+        font-weight: 400;
+        line-height: 1.3;
+        color: #8b949e;
+        position: absolute;
+        top: 10px;
+        right: 10px;
+      }
+
+      &.active {
+        background-color: rgba(10, 132, 255, 0.12);
+        box-shadow: 0 6px 18px rgba(10, 132, 255, 0.06);
+        border-color: #0a84ff;
+
+        .chip-status {
+          color: #0a84ff;
+        }
+      }
+    }
+  }
+</style>

+ 64 - 0
src/views/ventAI/manageAssistent/components/chatModal/types.ts

@@ -0,0 +1,64 @@
+export interface AttachedFile {
+  id: string;
+  name: string;
+  size: number;
+  type: string;
+  preview?: string;
+  content?: string;
+  uploadTime: string;
+  messageId?: string;
+  originalFile?: File;
+}
+
+export interface AgentAnswer {
+  id: string;
+  agentName: string;
+  toolName: string;
+  thinking: string;
+  result: string;
+  status: 'thinking' | 'completed';
+  timestamp: number;
+}
+
+export interface ThinkingStep {
+  type: 'agent_start' | 'agent_done' | 'tool_call' | 'tool_result' | 'executing' | 'updated_todo_list' | 'token';
+  agent?: string;
+  tool?: string;
+  message?: string;
+  todos?: Array<{ content: string; status: string }>;
+  content?: string;
+  timestamp: number;
+}
+
+export interface Message {
+  type: 'user' | 'ai';
+  content: string;
+  time: string;
+  createdAt?: string;
+  isTerminal?: boolean;
+  isLoading?: boolean;
+  attachedFile?: AttachedFile;
+  agentAnswers?: AgentAnswer[];
+  thinkingSteps?: ThinkingStep[];
+  sessionId?: string;
+  wordDownloadUrl?: string;
+  durationMs?: number;
+  generateStartTime?: number;
+}
+
+export interface Task {
+  id: string;
+  name: string;
+  sessionId: string;
+  messages: Message[];
+  attachedFiles: AttachedFile[];
+  wordUrl?: string;
+}
+
+export type SectionType = 'select' | 'params' | 'result';
+
+export interface ThinkingSection {
+  label: string;
+  content: string;
+  type: SectionType;
+}

+ 90 - 0
src/views/ventAI/manageAssistent/components/chatModal/utils.ts

@@ -0,0 +1,90 @@
+import { marked } from 'marked';
+
+export const renderMarkdown = (text: string): string => {
+  if (!text) return '';
+  const processed = text.replace(/<br\s*\/?>/gi, '  \n');
+  const html = marked.parse(processed) as string;
+  return wrapTables(html);
+};
+
+const wrapTables = (html: string): string => {
+  return html.replace(/<table>([\s\S]*?)<\/table>/g, (match) => {
+    const encodedTable = encodeURIComponent(match);
+    return `<div class="markdown-table-wrapper">
+      <div class="table-actions">
+        <span class="table-action-btn" data-action="copy" data-table="${encodedTable}" title="复制Markdown">
+          <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="4.5" y="4.5" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.2"/><path d="M9.5 4.5V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v5.5a1 1 0 0 0 1 1h1.5" stroke="currentColor" stroke-width="1.2"/></svg>
+        </span>
+        <span class="table-action-btn" data-action="csv" data-table="${encodedTable}" title="下载CSV">
+          <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 2v7M4 7l3 3 3-3M3 12h8" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
+        </span>
+        <span class="table-action-btn" data-action="preview" data-table="${encodedTable}" title="预览表格">
+          <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1" y="1" width="12" height="12" rx="1.5" stroke="currentColor" stroke-width="1.2"/><path d="M1 5h12M1 9h12M5 1v12M9 1v12" stroke="currentColor" stroke-width="1.2"/></svg>
+        </span>
+      </div>
+      ${match}
+    </div>`;
+  });
+};
+
+export const tableHtmlToMarkdown = (tableHtml: string): string => {
+  const parser = new DOMParser();
+  const doc = parser.parseFromString(tableHtml, 'text/html');
+  const table = doc.querySelector('table');
+  if (!table) return tableHtml;
+
+  const rows: string[][] = [];
+  table.querySelectorAll('tr').forEach((tr) => {
+    const cells: string[] = [];
+    tr.querySelectorAll('th, td').forEach((cell) => {
+      cells.push(cell.textContent?.trim() || '');
+    });
+    rows.push(cells);
+  });
+
+  if (rows.length === 0) return tableHtml;
+
+  const colCount = Math.max(...rows.map((r) => r.length));
+  const normalized = rows.map((r) => {
+    while (r.length < colCount) r.push('');
+    return r;
+  });
+
+  const lines: string[] = [];
+  lines.push('| ' + normalized[0].join(' | ') + ' |');
+  lines.push('| ' + normalized[0].map(() => '---').join(' | ') + ' |');
+  for (let i = 1; i < normalized.length; i++) {
+    lines.push('| ' + normalized[i].join(' | ') + ' |');
+  }
+  return lines.join('\n');
+};
+
+export const tableHtmlToCsv = (tableHtml: string): string => {
+  const parser = new DOMParser();
+  const doc = parser.parseFromString(tableHtml, 'text/html');
+  const table = doc.querySelector('table');
+  if (!table) return '';
+
+  const rows: string[] = [];
+  table.querySelectorAll('tr').forEach((tr) => {
+    const cells: string[] = [];
+    tr.querySelectorAll('th, td').forEach((cell) => {
+      const text = (cell.textContent?.trim() || '').replace(/"/g, '""');
+      cells.push(`"${text}"`);
+    });
+    rows.push(cells.join(','));
+  });
+
+  return '' + rows.join('\n');
+};
+
+export const formatFileSize = (bytes: number): string => {
+  if (bytes < 1024) return bytes + ' B';
+  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
+  return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
+};
+
+export const isImageFile = (filename: string): boolean =>
+  /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(filename);
+
+export const isPdfFile = (filename: string): boolean => /\.pdf$/i.test(filename);