ToolTrace.vue 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839
  1. <script setup lang="ts">
  2. import { computed, ref } from 'vue'
  3. import AppIcon from '@/components/common/AppIcon.vue'
  4. import type { UiToolCall } from './types'
  5. const props = defineProps<{ tool: UiToolCall }>()
  6. const open = ref(props.tool.status === 'error')
  7. const statusLabel = computed(() => ({ running: '运行中', success: '完成', error: '失败', pending: '等待' })[props.tool.status])
  8. const detail = computed(() => {
  9. const value = props.tool.output ?? props.tool.input
  10. if (typeof value === 'string') return value
  11. try { return JSON.stringify(value, null, 2) } catch { return String(value ?? '') }
  12. })
  13. </script>
  14. <template>
  15. <div class="tool-trace" :class="`tool-trace--${tool.status}`">
  16. <button type="button" @click="open = !open">
  17. <span class="tool-trace__icon"><AppIcon name="tools" :size="14" /></span>
  18. <span class="tool-trace__copy"><strong>{{ tool.name }}</strong><small>{{ statusLabel }}<template v-if="tool.durationMs"> · {{ tool.durationMs }} ms</template></small></span>
  19. <span v-if="tool.status === 'running'" class="tool-trace__pulse" />
  20. <AppIcon name="chevron-down" :size="13" :class="{ rotated: open }" />
  21. </button>
  22. <pre v-if="open && detail">{{ detail }}</pre>
  23. </div>
  24. </template>
  25. <style scoped>
  26. .tool-trace { margin: 7px 0; border: 1px solid var(--line); border-radius: 10px; background: var(--surface); overflow: hidden; }
  27. .tool-trace > button { display: flex; width: 100%; min-height: 42px; align-items: center; gap: 9px; padding: 6px 9px; border: 0; background: transparent; color: var(--text-secondary); cursor: pointer; text-align: left; }
  28. .tool-trace > button:hover { background: var(--surface-soft); }
  29. .tool-trace__icon { display: grid; place-items: center; width: 26px; height: 26px; border-radius: 7px; background: var(--surface-soft); }
  30. .tool-trace__copy { display: flex; min-width: 0; flex: 1; flex-direction: column; }.tool-trace__copy strong { color: var(--text-primary); font-size: 10px; font-weight: 580; }.tool-trace__copy small { margin-top: 2px; color: var(--text-muted); font-size: 9px; }
  31. .tool-trace__pulse { width: 5px; height: 5px; border-radius: 50%; background: var(--warning); animation: pulse 1s ease infinite; }
  32. .rotated { transform: rotate(180deg); }.app-icon { transition: transform 140ms ease; }
  33. pre { max-height: 260px; margin: 0; padding: 10px 12px; overflow: auto; border-top: 1px solid var(--line); background: var(--surface-soft); color: var(--text-secondary); font: 10px/1.55 var(--font-code); white-space: pre-wrap; overflow-wrap: anywhere; }
  34. .tool-trace--error { border-color: color-mix(in srgb, var(--danger) 30%, var(--line)); }
  35. @keyframes pulse { 50% { opacity: .3; } }
  36. </style>