| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
- <script setup>
- /**
- * v1.1:区域 ③ —— 工具调用
- * - 整体外框样式与"思考过程"区块(.nrc-thinking)一致
- * - header:"工具调用" + "(N)" 计数,与"思考过程 / XX 字"结构对齐
- * - currentTool:当前执行中的工具(始终展开)
- * - history:已完成的工具,默认折叠(仅显示 header),点击 header 展开看详情
- * - 节点 running 时(nodeRunning=true):历史强制展开,便于实时观察
- * - 节点完成时(nodeRunning=false):自动收起历史
- * - 历史完整显示(不再截断为最近 5 条)
- */
- import { ref, computed, watch } from 'vue'
- const props = defineProps({
- currentTool: { type: Object, default: null }, // {name, argsSummary, result}
- history: { type: Array, default: () => [] },
- nodeRunning: { type: Boolean, default: false },
- })
- // 历史整体折叠状态:默认 false(仅显示切换器)
- const historyExpanded = ref(false)
- // 每个工具卡片的独立展开状态:key=工具名+索引,value=true/false(默认 false 即收起)
- const cardExpanded = ref({})
- const historyCount = computed(() => props.history.length)
- // 节点 running 时强制展开历史;变为非 running 时自动收起
- watch(() => props.nodeRunning, (running) => {
- historyExpanded.value = running
- }, { immediate: true })
- function toggleCard(key) {
- cardExpanded.value[key] = !cardExpanded.value[key]
- }
- function isCardExpanded(key) {
- return cardExpanded.value[key] === true
- }
- // 历史变化时重置卡片展开状态,避免错位
- watch(() => props.history, () => {
- cardExpanded.value = {}
- }, { deep: false })
- </script>
- <template>
- <!-- v-if 守卫:无任何工具调用数据时不渲染外框,避免出现空白圆角矩形 -->
- <div
- v-if="historyCount > 0 || currentTool"
- class="tool-calls-wrap"
- :class="{ live: nodeRunning }"
- >
- <!-- header:与"思考过程"结构对齐 -->
- <div
- v-if="historyCount > 0 || currentTool"
- class="tool-calls-header"
- :class="{ clickable: historyCount > 0 }"
- @click="historyCount > 0 && (historyExpanded = !historyExpanded)"
- >
- <span class="tool-calls-tag">工具调用</span>
- <span v-if="nodeRunning && currentTool" class="tool-calls-live">执行中…</span>
- <span v-else class="tool-calls-count">({{ historyCount + (currentTool ? 1 : 0) }})</span>
- <span
- v-if="historyCount > 0"
- class="tool-calls-expand"
- :class="{ expanded: historyExpanded }"
- >▶</span>
- </div>
- <div v-if="historyExpanded || currentTool" class="tool-calls-body">
- <!-- 当前执行中的工具:始终展开 -->
- <div v-if="currentTool" class="tool-card current">
- <div class="header">
- <span class="spinner"></span>
- <span class="name">{{ currentTool.name }}</span>
- <span class="status">执行中</span>
- </div>
- <div v-if="currentTool.argsSummary" class="args">{{ currentTool.argsSummary }}</div>
- </div>
- <!-- 已完成的工具调用列表(全部展示) -->
- <template v-if="historyExpanded">
- <div
- v-for="(t, i) in history"
- :key="i"
- class="tool-card history"
- >
- <div class="header clickable" @click.stop="toggleCard(`h-${i}`)">
- <span class="toggle-arrow small" :class="{ expanded: isCardExpanded(`h-${i}`) }">▶</span>
- <span class="name">{{ t.name }}</span>
- <span class="status done">已完成</span>
- </div>
- <div v-if="isCardExpanded(`h-${i}`)">
- <div v-if="t.argsSummary" class="args">{{ t.argsSummary }}</div>
- <div v-if="t.result" class="result">{{ t.result }}</div>
- </div>
- </div>
- </template>
- </div>
- </div>
- </template>
- <style scoped>
- /* v1.1:工具调用区外框 —— 与 .nrc-thinking 完全一致的视觉语言 */
- .tool-calls-wrap {
- margin: 6px 10px;
- border-radius: 6px;
- background: rgba(168, 85, 247, 0.05);
- border: 1px solid rgba(168, 85, 247, 0.2);
- overflow: hidden;
- font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
- }
- .tool-calls-wrap.live {
- border-color: rgba(168, 85, 247, 0.5);
- box-shadow: 0 0 0 1px rgba(168, 85, 247, 0.15);
- }
- /* header —— 与 .nrc-thinking-header 对齐 */
- .tool-calls-header {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 5px 10px;
- font-size: 11px;
- background: rgba(168, 85, 247, 0.08);
- }
- .tool-calls-header.clickable {
- cursor: pointer;
- user-select: none;
- }
- /* "🔧 工具调用" —— 与 .nrc-thinking-tag 一致:紫色加粗 */
- .tool-calls-tag {
- color: #c084fc;
- font-weight: 600;
- }
- /* "执行中…" 实时提示 —— 与 .nrc-thinking-live 一致:粉色脉冲 */
- .tool-calls-live {
- color: #ec4899;
- font-size: 10px;
- animation: tc-pulse 1.2s ease-in-out infinite;
- }
- /* "(N)" 计数 —— 与 .nrc-thinking-static 一致:灰色小字 */
- .tool-calls-count {
- color: #888;
- font-size: 10px;
- }
- /* 展开箭头 —— 与 .nrc-thinking-expand 一致 */
- .tool-calls-expand {
- margin-left: auto;
- font-size: 10px;
- color: var(--text-tertiary, #666);
- transition: transform 0.2s;
- flex-shrink: 0;
- }
- .tool-calls-expand.expanded {
- transform: rotate(90deg);
- }
- @keyframes tc-pulse {
- 0%, 100% { opacity: 0.55; }
- 50% { opacity: 1; }
- }
- /* body —— 工具卡片列表 */
- .tool-calls-body {
- padding: 6px 8px;
- }
- .tool-card {
- border: 1px solid rgba(168, 85, 247, 0.2);
- border-radius: 4px;
- background: rgba(168, 85, 247, 0.04);
- padding: 5px 8px;
- margin-bottom: 4px;
- font-size: 11px;
- }
- .tool-card:last-child {
- margin-bottom: 0;
- }
- .tool-card.current {
- border-color: rgba(168, 85, 247, 0.6);
- background: rgba(168, 85, 247, 0.1);
- box-shadow: 0 0 0 1px rgba(168, 85, 247, 0.15);
- }
- .header {
- display: flex;
- align-items: center;
- gap: 6px;
- }
- .header.clickable {
- cursor: pointer;
- user-select: none;
- }
- .header.clickable:hover .name {
- color: #c084fc;
- }
- .name {
- font-weight: 600;
- color: var(--text-primary, #e0e0e0);
- }
- .status {
- margin-left: auto;
- color: #c084fc;
- font-size: 10px;
- }
- .status.done {
- color: var(--text-tertiary, #666);
- }
- .args, .result {
- margin-top: 3px;
- white-space: pre-wrap;
- word-break: break-all;
- max-height: 80px;
- overflow-y: auto;
- color: var(--text-tertiary, #666);
- font-size: 10px;
- padding-left: 4px;
- border-left: 1px solid rgba(168, 85, 247, 0.2);
- }
- .result {
- color: #10b981;
- border-left-color: rgba(16, 185, 129, 0.3);
- }
- .spinner {
- width: 8px;
- height: 8px;
- border: 1.5px solid rgba(168, 85, 247, 0.3);
- border-top-color: #c084fc;
- border-radius: 50%;
- animation: tc-spin 0.8s linear infinite;
- flex-shrink: 0;
- }
- @keyframes tc-spin {
- to { transform: rotate(360deg); }
- }
- /* 卡片内展开箭头 */
- .toggle-arrow {
- display: inline-block;
- font-size: 9px;
- transition: transform 0.2s;
- flex-shrink: 0;
- }
- .toggle-arrow.small {
- font-size: 8px;
- }
- .toggle-arrow.expanded {
- transform: rotate(90deg);
- }
- </style>
|