mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 19:23:42 +08:00
204 lines
5.8 KiB
Vue
204 lines
5.8 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, watch } from 'vue'
|
|
import { Loading, Select, CloseBold, ArrowDown, Document, Setting, Connection } from '@element-plus/icons-vue'
|
|
import { useToolLabel } from '@/composables/useToolLabel'
|
|
import type { MessageSegment } from '@/types'
|
|
|
|
const props = defineProps<{
|
|
segment: MessageSegment
|
|
}>()
|
|
|
|
const { getToolLabel } = useToolLabel()
|
|
|
|
const expanded = ref(props.segment.status === 'running')
|
|
|
|
// running → completed 时自动折叠
|
|
watch(() => props.segment.status, (val) => {
|
|
if (val !== 'running') expanded.value = false
|
|
})
|
|
|
|
/** Delegation segments are identified by the → prefix injected in useChat.ts. */
|
|
const isDelegation = computed(() => (props.segment.toolName || '').startsWith('→'))
|
|
|
|
const displayName = computed(() => {
|
|
const raw = props.segment.toolName || ''
|
|
// Strip the → prefix for delegation segments so getToolLabel works cleanly,
|
|
// then prepend it back as a visual indicator.
|
|
if (isDelegation.value) return `→ ${getToolLabel(raw.slice(1).trim())}`
|
|
return getToolLabel(raw)
|
|
})
|
|
|
|
const truncatedArgs = computed(() => {
|
|
const args = props.segment.toolArgs || ''
|
|
if (!args) return ''
|
|
try {
|
|
const parsed = JSON.parse(args)
|
|
const vals = Object.values(parsed).filter(v => typeof v === 'string') as string[]
|
|
const s = vals.join(', ')
|
|
return s.length > 80 ? s.slice(0, 80) + '...' : s
|
|
} catch {
|
|
// Multi-line delegation progress — show first line only in collapsed view
|
|
const firstLine = args.split('\n')[0].trim()
|
|
return firstLine.length > 80 ? firstLine.slice(0, 80) + '...' : firstLine
|
|
}
|
|
})
|
|
|
|
const resultPreview = computed(() => {
|
|
const r = props.segment.toolResult || ''
|
|
return r.length <= 600 ? r : r.slice(0, 600) + '\n... [truncated]'
|
|
})
|
|
|
|
const isRead = computed(() => {
|
|
const n = props.segment.toolName || ''
|
|
return n.includes('read') || n.includes('Read')
|
|
})
|
|
|
|
const isSuccess = computed(() => props.segment.status === 'completed' && props.segment.toolSuccess !== false)
|
|
const isError = computed(() => props.segment.status === 'error' || props.segment.toolSuccess === false)
|
|
const isRunning = computed(() => props.segment.status === 'running')
|
|
</script>
|
|
|
|
<template>
|
|
<div class="seg-tool" :class="{ 'is-running': isRunning, 'is-success': isSuccess, 'is-error': isError }">
|
|
<div class="seg-tool__header" @click="segment.toolResult ? (expanded = !expanded) : null">
|
|
<span class="seg-tool__status">
|
|
<el-icon v-if="isRunning" class="is-loading" :size="13"><Loading /></el-icon>
|
|
<el-icon v-else-if="isSuccess" :size="13"><Select /></el-icon>
|
|
<el-icon v-else :size="13"><CloseBold /></el-icon>
|
|
</span>
|
|
<span class="seg-tool__type-icon">
|
|
<el-icon v-if="isDelegation" :size="12"><Connection /></el-icon>
|
|
<el-icon v-else-if="isRead" :size="12"><Document /></el-icon>
|
|
<el-icon v-else :size="12"><Setting /></el-icon>
|
|
</span>
|
|
<span class="seg-tool__name">{{ displayName }}</span>
|
|
<span v-if="truncatedArgs" class="seg-tool__args">{{ truncatedArgs }}</span>
|
|
<el-icon
|
|
v-if="segment.toolResult"
|
|
class="seg-tool__arrow"
|
|
:class="{ 'is-open': expanded }"
|
|
:size="11"
|
|
><ArrowDown /></el-icon>
|
|
</div>
|
|
<Transition name="seg-slide">
|
|
<div v-if="expanded && segment.toolResult" class="seg-tool__body">
|
|
<pre>{{ resultPreview }}</pre>
|
|
</div>
|
|
</Transition>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.seg-tool {
|
|
margin: 2px 0;
|
|
border-left: 3px solid var(--mc-border-light);
|
|
border-radius: 0 var(--mc-radius-sm, 6px) var(--mc-radius-sm, 6px) 0;
|
|
transition: all 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
|
}
|
|
.seg-tool:hover {
|
|
background: var(--mc-bg-muted);
|
|
transform: translateX(2px);
|
|
box-shadow: 0 2px 8px rgba(217, 109, 70, 0.08);
|
|
}
|
|
.seg-tool.is-running {
|
|
border-left-color: var(--mc-primary);
|
|
background: var(--mc-primary-bg);
|
|
}
|
|
.seg-tool.is-success {
|
|
border-left-color: var(--mc-success);
|
|
}
|
|
.seg-tool.is-error {
|
|
border-left-color: var(--mc-danger);
|
|
}
|
|
|
|
.seg-tool__header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 5px 10px;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
color: var(--mc-text-secondary);
|
|
user-select: none;
|
|
transition: color 0.15s;
|
|
}
|
|
.seg-tool__header:hover {
|
|
color: var(--mc-text-primary);
|
|
}
|
|
|
|
.seg-tool__status {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-shrink: 0;
|
|
}
|
|
.is-success .seg-tool__status { color: var(--mc-success); }
|
|
.is-error .seg-tool__status { color: var(--mc-danger); }
|
|
.is-running .seg-tool__status { color: var(--mc-primary); }
|
|
|
|
.seg-tool__type-icon {
|
|
display: flex;
|
|
align-items: center;
|
|
color: var(--mc-text-tertiary);
|
|
}
|
|
|
|
.seg-tool__name {
|
|
font-weight: 500;
|
|
color: var(--mc-text-primary);
|
|
white-space: nowrap;
|
|
max-width: 200px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.seg-tool__args {
|
|
flex: 1;
|
|
min-width: 0;
|
|
font-family: var(--mc-font-mono, 'SF Mono', 'Menlo', 'Consolas', monospace);
|
|
font-size: 12px;
|
|
color: var(--mc-text-tertiary);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
background: rgba(217, 109, 70, 0.06);
|
|
padding: 1px 5px;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.seg-tool__arrow {
|
|
flex-shrink: 0;
|
|
color: var(--mc-text-tertiary);
|
|
transition: transform 0.2s;
|
|
margin-left: auto;
|
|
}
|
|
.seg-tool__arrow.is-open {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
.seg-tool__body {
|
|
padding: 0 10px 6px 22px;
|
|
}
|
|
.seg-tool__body pre {
|
|
margin: 0;
|
|
padding: 8px 10px;
|
|
background: var(--mc-bg-sunken);
|
|
border-radius: 4px;
|
|
border: 1px solid var(--mc-border-light);
|
|
font-family: 'SF Mono', 'Menlo', 'Consolas', monospace;
|
|
font-size: 12px;
|
|
line-height: 1.5;
|
|
color: var(--mc-text-secondary);
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.seg-slide-enter-active, .seg-slide-leave-active {
|
|
transition: all 0.2s ease;
|
|
}
|
|
.seg-slide-enter-from, .seg-slide-leave-to {
|
|
opacity: 0;
|
|
transform: translateY(-4px);
|
|
}
|
|
</style>
|