ToolCalls.vue 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. <script setup>
  2. /**
  3. * v1.1:区域 ③ —— 工具调用
  4. * - 整体外框样式与"思考过程"区块(.nrc-thinking)一致
  5. * - header:"工具调用" + "(N)" 计数,与"思考过程 / XX 字"结构对齐
  6. * - currentTool:当前执行中的工具(始终展开)
  7. * - history:已完成的工具,默认折叠(仅显示 header),点击 header 展开看详情
  8. * - 节点 running 时(nodeRunning=true):历史强制展开,便于实时观察
  9. * - 节点完成时(nodeRunning=false):自动收起历史
  10. * - 历史完整显示(不再截断为最近 5 条)
  11. */
  12. import { ref, computed, watch } from 'vue'
  13. const props = defineProps({
  14. currentTool: { type: Object, default: null }, // {name, argsSummary, result}
  15. history: { type: Array, default: () => [] },
  16. nodeRunning: { type: Boolean, default: false },
  17. })
  18. // 历史整体折叠状态:默认 false(仅显示切换器)
  19. const historyExpanded = ref(false)
  20. // 每个工具卡片的独立展开状态:key=工具名+索引,value=true/false(默认 false 即收起)
  21. const cardExpanded = ref({})
  22. const historyCount = computed(() => props.history.length)
  23. // 节点 running 时强制展开历史;变为非 running 时自动收起
  24. watch(() => props.nodeRunning, (running) => {
  25. historyExpanded.value = running
  26. }, { immediate: true })
  27. function toggleCard(key) {
  28. cardExpanded.value[key] = !cardExpanded.value[key]
  29. }
  30. function isCardExpanded(key) {
  31. return cardExpanded.value[key] === true
  32. }
  33. // 历史变化时重置卡片展开状态,避免错位
  34. watch(() => props.history, () => {
  35. cardExpanded.value = {}
  36. }, { deep: false })
  37. </script>
  38. <template>
  39. <!-- v-if 守卫:无任何工具调用数据时不渲染外框,避免出现空白圆角矩形 -->
  40. <div
  41. v-if="historyCount > 0 || currentTool"
  42. class="tool-calls-wrap"
  43. :class="{ live: nodeRunning }"
  44. >
  45. <!-- header:与"思考过程"结构对齐 -->
  46. <div
  47. v-if="historyCount > 0 || currentTool"
  48. class="tool-calls-header"
  49. :class="{ clickable: historyCount > 0 }"
  50. @click="historyCount > 0 && (historyExpanded = !historyExpanded)"
  51. >
  52. <span class="tool-calls-tag">工具调用</span>
  53. <span v-if="nodeRunning && currentTool" class="tool-calls-live">执行中…</span>
  54. <span v-else class="tool-calls-count">({{ historyCount + (currentTool ? 1 : 0) }})</span>
  55. <span
  56. v-if="historyCount > 0"
  57. class="tool-calls-expand"
  58. :class="{ expanded: historyExpanded }"
  59. >▶</span>
  60. </div>
  61. <div v-if="historyExpanded || currentTool" class="tool-calls-body">
  62. <!-- 当前执行中的工具:始终展开 -->
  63. <div v-if="currentTool" class="tool-card current">
  64. <div class="header">
  65. <span class="spinner"></span>
  66. <span class="name">{{ currentTool.name }}</span>
  67. <span class="status">执行中</span>
  68. </div>
  69. <div v-if="currentTool.argsSummary" class="args">{{ currentTool.argsSummary }}</div>
  70. </div>
  71. <!-- 已完成的工具调用列表(全部展示) -->
  72. <template v-if="historyExpanded">
  73. <div
  74. v-for="(t, i) in history"
  75. :key="i"
  76. class="tool-card history"
  77. >
  78. <div class="header clickable" @click.stop="toggleCard(`h-${i}`)">
  79. <span class="toggle-arrow small" :class="{ expanded: isCardExpanded(`h-${i}`) }">▶</span>
  80. <span class="name">{{ t.name }}</span>
  81. <span class="status done">已完成</span>
  82. </div>
  83. <div v-if="isCardExpanded(`h-${i}`)">
  84. <div v-if="t.argsSummary" class="args">{{ t.argsSummary }}</div>
  85. <div v-if="t.result" class="result">{{ t.result }}</div>
  86. </div>
  87. </div>
  88. </template>
  89. </div>
  90. </div>
  91. </template>
  92. <style scoped>
  93. /* v1.1:工具调用区外框 —— 与 .nrc-thinking 完全一致的视觉语言 */
  94. .tool-calls-wrap {
  95. margin: 6px 10px;
  96. border-radius: 6px;
  97. background: rgba(168, 85, 247, 0.05);
  98. border: 1px solid rgba(168, 85, 247, 0.2);
  99. overflow: hidden;
  100. font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
  101. }
  102. .tool-calls-wrap.live {
  103. border-color: rgba(168, 85, 247, 0.5);
  104. box-shadow: 0 0 0 1px rgba(168, 85, 247, 0.15);
  105. }
  106. /* header —— 与 .nrc-thinking-header 对齐 */
  107. .tool-calls-header {
  108. display: flex;
  109. align-items: center;
  110. gap: 8px;
  111. padding: 5px 10px;
  112. font-size: 11px;
  113. background: rgba(168, 85, 247, 0.08);
  114. }
  115. .tool-calls-header.clickable {
  116. cursor: pointer;
  117. user-select: none;
  118. }
  119. /* "🔧 工具调用" —— 与 .nrc-thinking-tag 一致:紫色加粗 */
  120. .tool-calls-tag {
  121. color: #c084fc;
  122. font-weight: 600;
  123. }
  124. /* "执行中…" 实时提示 —— 与 .nrc-thinking-live 一致:粉色脉冲 */
  125. .tool-calls-live {
  126. color: #ec4899;
  127. font-size: 10px;
  128. animation: tc-pulse 1.2s ease-in-out infinite;
  129. }
  130. /* "(N)" 计数 —— 与 .nrc-thinking-static 一致:灰色小字 */
  131. .tool-calls-count {
  132. color: #888;
  133. font-size: 10px;
  134. }
  135. /* 展开箭头 —— 与 .nrc-thinking-expand 一致 */
  136. .tool-calls-expand {
  137. margin-left: auto;
  138. font-size: 10px;
  139. color: var(--text-tertiary, #666);
  140. transition: transform 0.2s;
  141. flex-shrink: 0;
  142. }
  143. .tool-calls-expand.expanded {
  144. transform: rotate(90deg);
  145. }
  146. @keyframes tc-pulse {
  147. 0%, 100% { opacity: 0.55; }
  148. 50% { opacity: 1; }
  149. }
  150. /* body —— 工具卡片列表 */
  151. .tool-calls-body {
  152. padding: 6px 8px;
  153. }
  154. .tool-card {
  155. border: 1px solid rgba(168, 85, 247, 0.2);
  156. border-radius: 4px;
  157. background: rgba(168, 85, 247, 0.04);
  158. padding: 5px 8px;
  159. margin-bottom: 4px;
  160. font-size: 11px;
  161. }
  162. .tool-card:last-child {
  163. margin-bottom: 0;
  164. }
  165. .tool-card.current {
  166. border-color: rgba(168, 85, 247, 0.6);
  167. background: rgba(168, 85, 247, 0.1);
  168. box-shadow: 0 0 0 1px rgba(168, 85, 247, 0.15);
  169. }
  170. .header {
  171. display: flex;
  172. align-items: center;
  173. gap: 6px;
  174. }
  175. .header.clickable {
  176. cursor: pointer;
  177. user-select: none;
  178. }
  179. .header.clickable:hover .name {
  180. color: #c084fc;
  181. }
  182. .name {
  183. font-weight: 600;
  184. color: var(--text-primary, #e0e0e0);
  185. }
  186. .status {
  187. margin-left: auto;
  188. color: #c084fc;
  189. font-size: 10px;
  190. }
  191. .status.done {
  192. color: var(--text-tertiary, #666);
  193. }
  194. .args, .result {
  195. margin-top: 3px;
  196. white-space: pre-wrap;
  197. word-break: break-all;
  198. max-height: 80px;
  199. overflow-y: auto;
  200. color: var(--text-tertiary, #666);
  201. font-size: 10px;
  202. padding-left: 4px;
  203. border-left: 1px solid rgba(168, 85, 247, 0.2);
  204. }
  205. .result {
  206. color: #10b981;
  207. border-left-color: rgba(16, 185, 129, 0.3);
  208. }
  209. .spinner {
  210. width: 8px;
  211. height: 8px;
  212. border: 1.5px solid rgba(168, 85, 247, 0.3);
  213. border-top-color: #c084fc;
  214. border-radius: 50%;
  215. animation: tc-spin 0.8s linear infinite;
  216. flex-shrink: 0;
  217. }
  218. @keyframes tc-spin {
  219. to { transform: rotate(360deg); }
  220. }
  221. /* 卡片内展开箭头 */
  222. .toggle-arrow {
  223. display: inline-block;
  224. font-size: 9px;
  225. transition: transform 0.2s;
  226. flex-shrink: 0;
  227. }
  228. .toggle-arrow.small {
  229. font-size: 8px;
  230. }
  231. .toggle-arrow.expanded {
  232. transform: rotate(90deg);
  233. }
  234. </style>