feat(tool-calling): 统一工具调用结果的头部样式并添加写文件工具

- 为WebSearchTool、KnowledgeGraphTool、TodoListTool和TaskTool添加统一的sep-header样式
- 新增WriteFileTool组件用于显示写文件工具调用结果
- 在ToolCallRenderer中集成WriteFileTool组件
- 在BaseToolCall中定义统一的sep-header样式
This commit is contained in:
Wenjie Zhang 2025-12-28 01:00:12 +08:00
parent 2b315f19ab
commit 0d87c1755e
8 changed files with 168 additions and 41 deletions

View File

@ -273,12 +273,43 @@ const formatResultData = (data) => {
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
:deep(.sep-header) {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
width: 100%;
overflow: hidden;
}
:deep(.keywords) { :deep(.keywords) {
color: var(--main-700); color: var(--main-700);
font-weight: 600; font-weight: 600;
font-size: 14px; font-size: 14px;
} }
:deep(.note) {
font-weight: 600;
color: var(--main-700);
white-space: nowrap;
flex-shrink: 0;
}
:deep(.separator) {
color: var(--gray-300);
flex-shrink: 0;
}
:deep(.description) {
color: var(--gray-600);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
flex: 1;
}
:deep(.tag) { :deep(.tag) {
font-size: 12px; font-size: 12px;
color: var(--gray-600); color: var(--gray-600);

View File

@ -24,6 +24,9 @@
<!-- 任务分配 --> <!-- 任务分配 -->
<TaskTool v-else-if="isTaskResult" :tool-call="toolCall" /> <TaskTool v-else-if="isTaskResult" :tool-call="toolCall" />
<!-- 写文件 -->
<WriteFileTool v-else-if="isWriteFileResult" :tool-call="toolCall" />
<!-- 默认展示 --> <!-- 默认展示 -->
<BaseToolCall v-else :tool-call="toolCall" /> <BaseToolCall v-else :tool-call="toolCall" />
</template> </template>
@ -40,6 +43,7 @@ import CalculatorTool from './tools/CalculatorTool.vue';
import TodoListTool from './tools/TodoListTool.vue'; import TodoListTool from './tools/TodoListTool.vue';
import ImageTool from './tools/ImageTool.vue'; import ImageTool from './tools/ImageTool.vue';
import TaskTool from './tools/TaskTool.vue'; import TaskTool from './tools/TaskTool.vue';
import WriteFileTool from './tools/WriteFileTool.vue';
const props = defineProps({ const props = defineProps({
toolCall: { toolCall: {
@ -121,6 +125,10 @@ const isImageResult = computed(() => {
return data && typeof data === 'string' && data.startsWith('http'); return data && typeof data === 'string' && data.startsWith('http');
}); });
const isWriteFileResult = computed(() => {
return toolName.value === 'write_file';
});
// //
const graphToolCallRef = ref(null); const graphToolCallRef = ref(null);
const refreshGraph = () => { const refreshGraph = () => {

View File

@ -8,4 +8,5 @@ export { default as KnowledgeBaseTool } from './tools/KnowledgeBaseTool.vue'
export { default as KnowledgeGraphTool } from './tools/KnowledgeGraphTool.vue' export { default as KnowledgeGraphTool } from './tools/KnowledgeGraphTool.vue'
export { default as CalculatorTool } from './tools/CalculatorTool.vue' export { default as CalculatorTool } from './tools/CalculatorTool.vue'
export { default as TodoListTool } from './tools/TodoListTool.vue' export { default as TodoListTool } from './tools/TodoListTool.vue'
export { default as ImageTool } from './tools/ImageTool.vue' export { default as ImageTool } from './tools/ImageTool.vue'
export { default as WriteFileTool } from './tools/WriteFileTool.vue'

View File

@ -1,5 +1,12 @@
<template> <template>
<BaseToolCall :tool-call="toolCall"> <BaseToolCall :tool-call="toolCall" :hide-params="true">
<template #header>
<div class="sep-header">
<span class="note">knowledgegraph</span>
<span class="separator" v-if="query">|</span>
<span class="description">{{ query }}</span>
</div>
</template>
<template #result="{ resultContent }"> <template #result="{ resultContent }">
<div class="knowledge-graph-result"> <div class="knowledge-graph-result">
<div class="result-summary"> <div class="result-summary">
@ -57,6 +64,24 @@ const graphContainerRef = ref(null)
const isVisible = ref(false) const isVisible = ref(false)
const isRefreshing = ref(false) const isRefreshing = ref(false)
const query = computed(() => {
const args = props.toolCall.args || props.toolCall.function?.arguments;
if (!args) return '';
let parsedArgs = args;
if (typeof args === 'string') {
try {
parsedArgs = JSON.parse(args);
} catch (e) {
return '';
}
}
// Try common keys for KG queries
if (typeof parsedArgs === 'object') {
return parsedArgs.query || parsedArgs.keywords || parsedArgs.q || parsedArgs.entities || '';
}
return '';
});
// //
const graphData = computed(() => { const graphData = computed(() => {
const data = parseData(props.toolCall.tool_call_result?.content); const data = parseData(props.toolCall.tool_call_result?.content);

View File

@ -1,7 +1,7 @@
<template> <template>
<BaseToolCall :tool-call="toolCall" :hide-params="true"> <BaseToolCall :tool-call="toolCall" :hide-params="true">
<template #header> <template #header>
<div class="task-header"> <div class="sep-header">
<span class="subagent">{{ subagentType }}</span> <span class="subagent">{{ subagentType }}</span>
<span class="separator" v-if="shortDescription">|</span> <span class="separator" v-if="shortDescription">|</span>
<span class="description" v-if="shortDescription">{{ shortDescription }}</span> <span class="description" v-if="shortDescription">{{ shortDescription }}</span>
@ -59,7 +59,7 @@ const shortDescription = computed(() => {
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.task-header { .sep-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
@ -74,19 +74,6 @@ const shortDescription = computed(() => {
flex-shrink: 0; flex-shrink: 0;
} }
.separator {
color: var(--gray-300);
flex-shrink: 0;
}
.description {
color: var(--gray-600);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
flex: 1;
}
} }
.task-result { .task-result {

View File

@ -1,10 +1,11 @@
<template> <template>
<BaseToolCall :tool-call="toolCall" hide-params> <BaseToolCall :tool-call="toolCall" hide-params>
<template #header-success="{ resultContent }"> <template #header>
<span v-if="todoListData(resultContent).length > 0" v-html="getHeaderDisplayContent(resultContent)"></span> <div class="sep-header">
<span v-else> <span class="note">todo</span>
待办事项工具执行完成 <span class="separator" v-if="query">|</span>
</span> <span class="description">{{ query }}</span>
</div>
</template> </template>
<template #result="{ resultContent }"> <template #result="{ resultContent }">
<div class="todo-list-result"> <div class="todo-list-result">
@ -33,6 +34,7 @@
</template> </template>
<script setup> <script setup>
import { computed } from 'vue';
import BaseToolCall from '../BaseToolCall.vue'; import BaseToolCall from '../BaseToolCall.vue';
import { import {
CheckCircleOutlined, CheckCircleOutlined,
@ -49,6 +51,43 @@ const props = defineProps({
} }
}); });
const query = computed(() => {
// 1. Try to get status from result content (Priority)
const content = props.toolCall.tool_call_result?.content;
if (content) {
const list = todoListData(content);
if (list && list.length > 0) {
// 1. In Progress
const inProgress = list.find(item => item.status === 'in_progress');
if (inProgress) return `进行中: ${inProgress.content}`;
// 2. Pending
const pending = list.find(item => item.status === 'pending');
if (pending) return `待处理: ${pending.content}`;
// 3. Last item fallback
const last = list[list.length - 1];
return `更新: ${last.content}`;
}
}
// 2. Fallback to args
const args = props.toolCall.args || props.toolCall.function?.arguments;
if (!args) return '';
let parsedArgs = args;
if (typeof args === 'string') {
try {
parsedArgs = JSON.parse(args);
} catch (e) {
return '';
}
}
if (typeof parsedArgs === 'object') {
return parsedArgs.content || parsedArgs.action || parsedArgs.todo || '';
}
return '';
});
const parseData = (content) => { const parseData = (content) => {
if (typeof content === 'string') { if (typeof content === 'string') {
try { try {
@ -97,23 +136,6 @@ const todoListData = (content) => {
} }
return []; return [];
}; };
const getHeaderDisplayContent = (content) => {
const list = todoListData(content);
if (!list || list.length === 0) return '';
// 1. in_progress
const inProgress = list.find(item => item.status === 'in_progress');
if (inProgress) return `进行中:<span class="keywords">${inProgress.content}</span>`;
// 2. pending
const pending = list.find(item => item.status === 'pending');
if (pending) return `待处理:<span class="keywords">${pending.content}</span>`;
// 3. (fallback)
const last = list[list.length - 1];
return `更新:<span class="keywords">${last.content}</span>`;
};
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -1,8 +1,11 @@
<template> <template>
<BaseToolCall :tool-call="toolCall" :hide-params="true"> <BaseToolCall :tool-call="toolCall" :hide-params="true">
<template #header> <template #header>
查询 <div class="sep-header">
<span class="keywords">"{{ query }}"</span> <span class="note">websearch</span>
<span class="separator" v-if="query">|</span>
<span class="description">"{{ query }}"</span>
</div>
</template> </template>
<template #result="{ resultContent }"> <template #result="{ resultContent }">
<div class="web-search-result"> <div class="web-search-result">

View File

@ -0,0 +1,50 @@
<template>
<BaseToolCall :tool-call="toolCall">
<template #header>
<div class="sep-header">
<span class="note">write_file</span>
<span class="separator" v-if="filePath">|</span>
<span class="description">{{ filePath }} <span v-if="lineCount"> ({{ lineCount }} lines)</span></span>
</div>
</template>
<template #result>
</template>
</BaseToolCall>
</template>
<script setup>
import { computed } from 'vue';
import BaseToolCall from '../BaseToolCall.vue';
const props = defineProps({
toolCall: {
type: Object,
required: true
}
});
const parsedArgs = computed(() => {
const args = props.toolCall.args || props.toolCall.function?.arguments;
if (!args) return {};
if (typeof args === 'object') return args;
try {
return JSON.parse(args);
} catch (e) {
return {};
}
});
const filePath = computed(() => {
const path = parsedArgs.value.file_path || '';
return path.startsWith('/') ? path.slice(1) : path;
});
const content = computed(() => parsedArgs.value.content || '');
const lineCount = computed(() => {
if (!content.value) return 0;
return String(content.value).split('\n').length;
});
</script>
<style lang="less" scoped>
</style>