348 lines
8.6 KiB
Vue
348 lines
8.6 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="tool-call-display" :class="{ 'is-collapsed': !isExpanded }">
|
|||
|
|
<!-- Header Slot -->
|
|||
|
|
<div class="tool-header" @click="toggleExpand">
|
|||
|
|
<!-- Slot for completely overriding header (not recommended based on new requirement but kept for backward compat if needed, or remove if strict) -->
|
|||
|
|
<!-- Actually, the requirement says "tool call 的 header 也要有 slot", but "ICON 保留".
|
|||
|
|
So we should probably not use a single "header" slot that replaces everything.
|
|||
|
|
Instead, we structure it: Icon + Content + ExpandIcon.
|
|||
|
|
-->
|
|||
|
|
|
|||
|
|
<!-- Fixed Status Icon -->
|
|||
|
|
<span v-if="toolCall.status === 'success' || toolCall.tool_call_result">
|
|||
|
|
<CircleCheckBig size="16" class="tool-loader tool-success" />
|
|||
|
|
</span>
|
|||
|
|
<span v-else-if="toolCall.status === 'error'">
|
|||
|
|
<CircleCheckBig size="16" class="tool-loader tool-error" />
|
|||
|
|
</span>
|
|||
|
|
<span v-else>
|
|||
|
|
<Loader size="16" class="tool-loader rotate tool-loading" />
|
|||
|
|
</span>
|
|||
|
|
|
|||
|
|
<!-- Content Area with Slots -->
|
|||
|
|
<div class="tool-header-content">
|
|||
|
|
<slot
|
|||
|
|
name="header-success"
|
|||
|
|
v-if="toolCall.status === 'success' || toolCall.tool_call_result"
|
|||
|
|
:tool-name="toolName"
|
|||
|
|
:result-content="resultContent"
|
|||
|
|
>
|
|||
|
|
工具 <span class="tool-name">{{ toolName }}</span> 执行完成
|
|||
|
|
</slot>
|
|||
|
|
|
|||
|
|
<slot
|
|||
|
|
name="header-error"
|
|||
|
|
v-else-if="toolCall.status === 'error'"
|
|||
|
|
:tool-name="toolName"
|
|||
|
|
:error-message="toolCall.error_message"
|
|||
|
|
>
|
|||
|
|
工具 <span class="tool-name">{{ toolName }}</span> 执行失败
|
|||
|
|
<span v-if="toolCall.error_message">({{ toolCall.error_message }})</span>
|
|||
|
|
</slot>
|
|||
|
|
|
|||
|
|
<slot
|
|||
|
|
name="header-running"
|
|||
|
|
v-else
|
|||
|
|
:tool-name="toolName"
|
|||
|
|
>
|
|||
|
|
正在调用工具: <span class="tool-name">{{ toolName }}</span>
|
|||
|
|
</slot>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Fixed Expand Icon -->
|
|||
|
|
<span class="tool-expand-icon">
|
|||
|
|
<ChevronsDownUp v-if="isExpanded" size="14" />
|
|||
|
|
<ChevronsUpDown v-else size="14" />
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Content Area -->
|
|||
|
|
<div class="tool-content" v-show="isExpanded">
|
|||
|
|
<!-- Params Slot -->
|
|||
|
|
<div class="tool-params" v-if="hasParams && !hideParams">
|
|||
|
|
<slot name="params" :tool-call="toolCall" :args="formattedArgs">
|
|||
|
|
<div class="tool-params-content">
|
|||
|
|
<strong>参数: </strong>
|
|||
|
|
<span>{{ formattedArgs }}</span>
|
|||
|
|
</div>
|
|||
|
|
</slot>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Result Slot -->
|
|||
|
|
<div class="tool-result" v-if="hasResult">
|
|||
|
|
<slot name="result" :tool-call="toolCall" :result-content="resultContent">
|
|||
|
|
<div class="tool-result-content" :data-tool-call-id="toolCall.id">
|
|||
|
|
<!-- Default rendering -->
|
|||
|
|
<div class="tool-result-renderer">
|
|||
|
|
<div class="default-result">
|
|||
|
|
<div class="default-content">
|
|||
|
|
<pre>{{ formatResultData(parsedResultData) }}</pre>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</slot>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { ref, computed } from 'vue';
|
|||
|
|
import { Loader, CircleCheckBig, ChevronsUpDown, ChevronsDownUp } from 'lucide-vue-next';
|
|||
|
|
import { useAgentStore } from '@/stores/agent';
|
|||
|
|
import { storeToRefs } from 'pinia';
|
|||
|
|
|
|||
|
|
const props = defineProps({
|
|||
|
|
toolCall: {
|
|||
|
|
type: Object,
|
|||
|
|
required: true
|
|||
|
|
},
|
|||
|
|
defaultExpanded: {
|
|||
|
|
type: Boolean,
|
|||
|
|
default: false
|
|||
|
|
},
|
|||
|
|
hideParams: {
|
|||
|
|
type: Boolean,
|
|||
|
|
default: false
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const agentStore = useAgentStore();
|
|||
|
|
const { availableTools } = storeToRefs(agentStore);
|
|||
|
|
|
|||
|
|
const isExpanded = ref(props.defaultExpanded);
|
|||
|
|
|
|||
|
|
const toggleExpand = () => {
|
|||
|
|
isExpanded.value = !isExpanded.value;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Tool Name Logic
|
|||
|
|
const toolName = computed(() => {
|
|||
|
|
const toolId = props.toolCall.name || props.toolCall.function?.name;
|
|||
|
|
const toolsList = availableTools.value ? Object.values(availableTools.value) : [];
|
|||
|
|
const tool = toolsList.find(t => t.id === toolId);
|
|||
|
|
return tool ? tool.name : toolId;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Args Logic
|
|||
|
|
const formattedArgs = computed(() => {
|
|||
|
|
const args = props.toolCall.args ? props.toolCall.args : props.toolCall.function?.arguments;
|
|||
|
|
if (!args) return '';
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
if (typeof args === 'string' && args.trim().startsWith('{')) {
|
|||
|
|
const parsed = JSON.parse(args);
|
|||
|
|
return JSON.stringify(parsed, null, 2);
|
|||
|
|
} else if (typeof args === 'object' && args !== null) {
|
|||
|
|
return JSON.stringify(args, null, 2);
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
// ignore
|
|||
|
|
}
|
|||
|
|
return args;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const hasParams = computed(() => {
|
|||
|
|
const argsStr = String(props.toolCall.args || props.toolCall.function?.arguments || '');
|
|||
|
|
return argsStr.length > 2;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Result Logic
|
|||
|
|
const resultContent = computed(() => {
|
|||
|
|
return props.toolCall.tool_call_result?.content;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const hasResult = computed(() => {
|
|||
|
|
return !!resultContent.value;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Default Result Rendering Logic
|
|||
|
|
const parsedResultData = computed(() => {
|
|||
|
|
const content = resultContent.value;
|
|||
|
|
if (typeof content === 'string') {
|
|||
|
|
try {
|
|||
|
|
return JSON.parse(content);
|
|||
|
|
} catch (error) {
|
|||
|
|
return content;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return content;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const formatResultData = (data) => {
|
|||
|
|
if (typeof data === 'object') {
|
|||
|
|
return JSON.stringify(data, null, 2);
|
|||
|
|
}
|
|||
|
|
return String(data);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Auto expand if loading
|
|||
|
|
// Note: In the original code, expansion was managed by parent.
|
|||
|
|
// Here we might want to default to expanded if it's loading?
|
|||
|
|
// Original: :class="{ 'is-collapsed': !expandedToolCalls.has(toolCall.id) }"
|
|||
|
|
// And expandedToolCalls defaults to empty set.
|
|||
|
|
// User didn't specify default behavior, but usually we want to see what's happening.
|
|||
|
|
// Let's keep it simple for now, defaulting to closed unless specified.
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="less" scoped>
|
|||
|
|
.tool-call-display {
|
|||
|
|
background-color: var(--gray-25);
|
|||
|
|
outline: 1px solid var(--gray-150);
|
|||
|
|
border-radius: 8px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
transition: all 0.2s ease;
|
|||
|
|
margin-bottom: 10px;
|
|||
|
|
|
|||
|
|
&:last-child {
|
|||
|
|
margin-bottom: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tool-header {
|
|||
|
|
padding: 8px 12px;
|
|||
|
|
font-size: 14px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
color: var(--gray-800);
|
|||
|
|
border-bottom: 1px solid var(--gray-100);
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 8px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
user-select: none;
|
|||
|
|
position: relative;
|
|||
|
|
transition: color 0.2s ease;
|
|||
|
|
|
|||
|
|
.anticon {
|
|||
|
|
color: var(--main-color);
|
|||
|
|
font-size: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tool-name {
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: var(--main-700);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
span {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tool-loader {
|
|||
|
|
margin-top: 2px;
|
|||
|
|
color: var(--main-700);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tool-loader.rotate {
|
|||
|
|
animation: rotate 2s linear infinite;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tool-loader.tool-success {
|
|||
|
|
color: var(--color-success-500);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tool-loader.tool-error {
|
|||
|
|
color: var(--color-error-500);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tool-loader.tool-loading {
|
|||
|
|
color: var(--color-info-500);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tool-expand-icon {
|
|||
|
|
margin-left: auto;
|
|||
|
|
color: var(--gray-400);
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tool-header-content {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
flex: 1;
|
|||
|
|
overflow: hidden;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tool-content {
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
|
|||
|
|
.tool-params {
|
|||
|
|
padding: 8px 12px;
|
|||
|
|
background-color: var(--gray-25);
|
|||
|
|
border-bottom: 1px solid var(--gray-150);
|
|||
|
|
|
|||
|
|
.tool-params-content {
|
|||
|
|
margin: 0;
|
|||
|
|
font-size: 12px;
|
|||
|
|
overflow-x: auto;
|
|||
|
|
color: var(--gray-700);
|
|||
|
|
line-height: 1.5;
|
|||
|
|
|
|||
|
|
pre {
|
|||
|
|
margin: 0;
|
|||
|
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tool-result {
|
|||
|
|
padding: 0;
|
|||
|
|
background-color: transparent;
|
|||
|
|
|
|||
|
|
.tool-result-content {
|
|||
|
|
padding: 0;
|
|||
|
|
background-color: transparent;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&.is-collapsed {
|
|||
|
|
.tool-header {
|
|||
|
|
border-bottom: none;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes rotate {
|
|||
|
|
from {
|
|||
|
|
transform: rotate(0deg);
|
|||
|
|
}
|
|||
|
|
to {
|
|||
|
|
transform: rotate(360deg);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Default Renderer Styles */
|
|||
|
|
.tool-result-renderer {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
|
|||
|
|
.default-result {
|
|||
|
|
background: var(--gray-0);
|
|||
|
|
border-radius: 8px;
|
|||
|
|
|
|||
|
|
.default-content {
|
|||
|
|
background: var(--gray-0);
|
|||
|
|
padding: 12px;
|
|||
|
|
|
|||
|
|
pre {
|
|||
|
|
margin: 0;
|
|||
|
|
font-size: 12px;
|
|||
|
|
line-height: 1.4;
|
|||
|
|
color: var(--gray-700);
|
|||
|
|
white-space: pre-wrap;
|
|||
|
|
word-break: break-word;
|
|||
|
|
max-height: 300px;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
background: var(--gray-50);
|
|||
|
|
padding: 10px;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|