AgentDetailPanel.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. <template>
  2. <div v-if="detailData" class="agent-detail-panel">
  3. <div v-if="!hideHeader" class="agent-detail-header">
  4. <div class="agent-detail-header-left">
  5. <div class="agent-detail-icon"></div>
  6. <span class="agent-detail-title">{{ detailData.agent }}</span>
  7. </div>
  8. <div class="agent-detail-close" @click="emit('close')">
  9. <div class="close-icon"></div>
  10. </div>
  11. </div>
  12. <div class="agent-detail-body">
  13. <!-- 思考内容 -->
  14. <div v-if="detailData.thinkingContent" class="agent-detail-section">
  15. <div class="agent-detail-section-header" @click="thinkingCollapsed = !thinkingCollapsed">
  16. <span class="agent-detail-section-title">思考内容</span>
  17. <div :class="['section-arrow', { collapsed: thinkingCollapsed }]">
  18. <div class="arrow-icon"></div>
  19. </div>
  20. </div>
  21. <Transition name="collapse">
  22. <div v-show="!thinkingCollapsed" class="agent-detail-section-body thinking" v-html="renderMarkdown(detailData.thinkingContent)"></div>
  23. </Transition>
  24. </div>
  25. <!-- 执行步骤 -->
  26. <div v-if="detailData.steps.length" class="agent-detail-section">
  27. <div class="agent-detail-section-header" @click="stepsCollapsed = !stepsCollapsed">
  28. <span class="agent-detail-section-title">执行步骤</span>
  29. <span class="agent-detail-section-count">{{ detailData.steps.length }}</span>
  30. <div :class="['section-arrow', { collapsed: stepsCollapsed }]">
  31. <div class="arrow-icon"></div>
  32. </div>
  33. </div>
  34. <Transition name="collapse">
  35. <div v-show="!stepsCollapsed" class="agent-detail-section-body">
  36. <div v-for="(step, sIdx) in detailData.steps" :key="sIdx" class="detail-step">
  37. <!-- agent_start -->
  38. <div v-if="step.type === 'agent_start'" class="detail-step-item">
  39. <div class="step-dot agent-start"></div>
  40. <div class="step-text">{{ step.message }}</div>
  41. </div>
  42. <!-- tool_call -->
  43. <div v-else-if="step.type === 'tool_call'" class="detail-step-item">
  44. <div class="step-dot tool-call"></div>
  45. <div class="step-text">
  46. <span class="step-label">调用工具:</span>
  47. <span class="step-tool">{{ step.tool }}</span>
  48. <span v-if="step.message" class="step-msg">{{ step.message }}</span>
  49. </div>
  50. </div>
  51. <!-- executing -->
  52. <div v-else-if="step.type === 'executing'" class="detail-step-item">
  53. <div class="step-dot executing"></div>
  54. <div class="step-text">
  55. {{ step.message || '正在执行...' }}
  56. <span v-if="step.tool" class="step-tool">{{ step.tool }}</span>
  57. </div>
  58. </div>
  59. <!-- tool_result -->
  60. <div v-else-if="step.type === 'tool_result'" class="detail-step-item">
  61. <div class="step-dot tool-result"></div>
  62. <div class="step-text">
  63. <span class="step-label">结果:</span>
  64. <span class="step-tool">{{ step.tool }}</span>
  65. <span class="step-msg">{{ step.message }}</span>
  66. </div>
  67. </div>
  68. <!-- agent_done -->
  69. <div v-else-if="step.type === 'agent_done'" class="detail-step-item">
  70. <div class="step-dot agent-done"></div>
  71. <div class="step-text">{{ step.message }}</div>
  72. </div>
  73. </div>
  74. </div>
  75. </Transition>
  76. </div>
  77. <!-- 输出内容 -->
  78. <div v-if="detailData.content" class="agent-detail-section">
  79. <div class="agent-detail-section-header" @click="outputCollapsed = !outputCollapsed">
  80. <span class="agent-detail-section-title">输出内容</span>
  81. <div :class="['section-arrow', { collapsed: outputCollapsed }]">
  82. <div class="arrow-icon"></div>
  83. </div>
  84. </div>
  85. <Transition name="collapse">
  86. <div v-show="!outputCollapsed" class="agent-detail-section-body output" v-html="renderMarkdown(detailData.content)"></div>
  87. </Transition>
  88. </div>
  89. </div>
  90. </div>
  91. </template>
  92. <script setup lang="ts">
  93. import { ref, computed } from 'vue';
  94. import { renderMarkdown } from './utils';
  95. import type { Message, ThinkingStep } from './types';
  96. const props = defineProps<{
  97. messageIndex: number;
  98. agentName: string;
  99. agentId?: string;
  100. messages: Message[];
  101. hideHeader?: boolean;
  102. }>();
  103. const emit = defineEmits<{
  104. (e: 'close'): void;
  105. }>();
  106. const thinkingCollapsed = ref(false);
  107. const stepsCollapsed = ref(false);
  108. const outputCollapsed = ref(false);
  109. const detailData = computed(() => {
  110. const msg = props.messages[props.messageIndex];
  111. if (!msg?.thinkingSteps) return null;
  112. const agentName = props.agentName;
  113. const agentId = props.agentId;
  114. let thinkingContent = '';
  115. let content = '';
  116. const steps: ThinkingStep[] = [];
  117. for (const step of msg.thinkingSteps) {
  118. const match = agentId ? step.agentId === agentId : (step.agent || '系统') === agentName;
  119. if (match) {
  120. if (step.thinkingContent) thinkingContent += step.thinkingContent;
  121. if (step.content) content += step.content;
  122. steps.push(step);
  123. }
  124. }
  125. return { agent: agentName, thinkingContent, content, steps };
  126. });
  127. </script>
  128. <style scoped lang="less">
  129. .agent-detail-panel {
  130. height: 100%;
  131. display: flex;
  132. flex-direction: column;
  133. overflow: hidden;
  134. .agent-detail-header {
  135. display: flex;
  136. align-items: center;
  137. justify-content: space-between;
  138. padding: 10px 16px;
  139. border-bottom: 1px solid rgba(63, 80, 106, 0.3);
  140. flex-shrink: 0;
  141. .agent-detail-header-left {
  142. display: flex;
  143. align-items: center;
  144. gap: 10px;
  145. min-width: 0;
  146. }
  147. .agent-detail-icon {
  148. width: 20px;
  149. height: 20px;
  150. background-image: var(--img-chat-agent-icon);
  151. background-repeat: no-repeat;
  152. background-size: 100% 100%;
  153. flex-shrink: 0;
  154. }
  155. .agent-detail-title {
  156. font-size: 15px;
  157. font-weight: 600;
  158. color: #e0e6ed;
  159. overflow: hidden;
  160. text-overflow: ellipsis;
  161. white-space: nowrap;
  162. }
  163. .agent-detail-close {
  164. width: 32px;
  165. height: 32px;
  166. display: flex;
  167. align-items: center;
  168. justify-content: center;
  169. border-radius: 4px;
  170. cursor: pointer;
  171. transition: all 0.2s;
  172. flex-shrink: 0;
  173. &:hover {
  174. background: rgba(10, 132, 255, 0.15);
  175. }
  176. .close-icon {
  177. width: 16px;
  178. height: 16px;
  179. background-image: var(--img-chat-close-icon-btn);
  180. background-repeat: no-repeat;
  181. background-size: 100% 100%;
  182. }
  183. }
  184. }
  185. .agent-detail-body {
  186. flex: 1;
  187. overflow-y: auto;
  188. padding: 12px 16px;
  189. .agent-detail-section {
  190. margin-bottom: 8px;
  191. border: 1px solid rgba(63, 80, 106, 0.25);
  192. border-radius: 8px;
  193. overflow: hidden;
  194. background: rgba(10, 13, 23, 0.3);
  195. &:last-child {
  196. margin-bottom: 0;
  197. }
  198. .agent-detail-section-header {
  199. display: flex;
  200. align-items: center;
  201. gap: 8px;
  202. padding: 10px 14px;
  203. cursor: pointer;
  204. user-select: none;
  205. transition: background 0.2s;
  206. &:hover {
  207. background: rgba(10, 132, 255, 0.06);
  208. }
  209. .agent-detail-section-title {
  210. font-size: 13px;
  211. font-weight: 600;
  212. color: #8b949e;
  213. }
  214. .agent-detail-section-count {
  215. font-size: 11px;
  216. color: #8b949e;
  217. background: rgba(10, 132, 255, 0.1);
  218. padding: 1px 6px;
  219. border-radius: 10px;
  220. }
  221. .section-arrow {
  222. margin-left: auto;
  223. display: flex;
  224. align-items: center;
  225. transition: transform 0.25s ease;
  226. &.collapsed {
  227. transform: rotate(-90deg);
  228. }
  229. .arrow-icon {
  230. width: 10px;
  231. height: 10px;
  232. background-image: var(--img-chat-arrow-icon);
  233. background-repeat: no-repeat;
  234. background-size: 100% 100%;
  235. }
  236. }
  237. }
  238. .agent-detail-section-body {
  239. padding: 0 14px 12px;
  240. font-size: 13px;
  241. line-height: 1.6;
  242. color: #e0e6ed;
  243. word-break: break-word;
  244. &.thinking {
  245. white-space: pre-wrap;
  246. color: #b0b8c4;
  247. max-height: 300px;
  248. overflow-y: auto;
  249. }
  250. &.output {
  251. max-height: 500px;
  252. overflow-y: auto;
  253. }
  254. :deep(h1),
  255. :deep(h2),
  256. :deep(h3),
  257. :deep(h4),
  258. :deep(h5),
  259. :deep(h6) {
  260. color: #fff;
  261. }
  262. :deep(p) {
  263. margin: 0;
  264. }
  265. :deep(code) {
  266. background: rgba(10, 132, 255, 0.1);
  267. padding: 1px 4px;
  268. border-radius: 3px;
  269. font-size: 12px;
  270. }
  271. :deep(pre) {
  272. background: rgba(10, 132, 255, 0.1);
  273. padding: 8px;
  274. border-radius: 4px;
  275. overflow-x: auto;
  276. margin: 6px 0;
  277. white-space: pre-wrap;
  278. }
  279. :deep(pre code) {
  280. background: transparent;
  281. padding: 0;
  282. }
  283. /* 步骤时间线 */
  284. .detail-step {
  285. position: relative;
  286. padding-left: 20px;
  287. &::before {
  288. content: '';
  289. position: absolute;
  290. left: 5px;
  291. top: 16px;
  292. bottom: 0;
  293. width: 1px;
  294. background: rgba(63, 80, 106, 0.3);
  295. }
  296. &:last-child::before {
  297. display: none;
  298. }
  299. .detail-step-item {
  300. display: flex;
  301. align-items: flex-start;
  302. gap: 10px;
  303. padding: 8px 0;
  304. .step-dot {
  305. width: 10px;
  306. height: 10px;
  307. border-radius: 50%;
  308. flex-shrink: 0;
  309. margin-top: 4px;
  310. position: relative;
  311. z-index: 1;
  312. &.agent-start {
  313. background: #0a84ff;
  314. box-shadow: 0 0 6px rgba(10, 132, 255, 0.4);
  315. }
  316. &.tool-call {
  317. background: #79c0ff;
  318. }
  319. &.executing {
  320. background: #d29922;
  321. animation: pulse-dot 1.5s ease-in-out infinite;
  322. }
  323. &.tool-result {
  324. background: #3fb950;
  325. }
  326. &.agent-done {
  327. background: #8b949e;
  328. }
  329. }
  330. .step-text {
  331. flex: 1;
  332. font-size: 12px;
  333. line-height: 1.5;
  334. color: #b0b8c4;
  335. .step-label {
  336. color: #8b949e;
  337. }
  338. .step-tool {
  339. color: #79c0ff;
  340. font-family: monospace;
  341. font-size: 11px;
  342. background: rgba(10, 132, 255, 0.1);
  343. padding: 1px 5px;
  344. border-radius: 3px;
  345. }
  346. .step-msg {
  347. color: #b0b8c4;
  348. }
  349. }
  350. }
  351. }
  352. }
  353. }
  354. }
  355. }
  356. .collapse-enter-active,
  357. .collapse-leave-active {
  358. transition: all 0.25s ease;
  359. overflow: hidden;
  360. }
  361. .collapse-enter-from,
  362. .collapse-leave-to {
  363. opacity: 0;
  364. max-height: 0;
  365. padding-top: 0;
  366. padding-bottom: 0;
  367. margin-top: 0;
  368. margin-bottom: 0;
  369. }
  370. .collapse-enter-to,
  371. .collapse-leave-from {
  372. opacity: 1;
  373. max-height: 500px;
  374. }
  375. @keyframes pulse-dot {
  376. 0%,
  377. 100% {
  378. opacity: 1;
  379. }
  380. 50% {
  381. opacity: 0.4;
  382. }
  383. }
  384. </style>