重构工具调用结果展示组件,将原有单一组件拆分为多个专用工具组件,包括网页搜索、知识库、知识图谱、计算器、待办事项和图片工具。新增BaseToolCall基础组件提供统一布局和交互,ToolCallRenderer组件负责动态渲染不同类型工具结果。优化样式和交互体验,支持折叠展开和参数展示。 新增ImageTool、CalculatorTool、WebSearchTool、TodoListTool、KnowledgeGraphTool和KnowledgeBaseTool等专用工具组件,各组件独立处理对应类型的数据展示逻辑。删除旧版ToolResultRenderer组件,迁移至新架构。 同时更新AgentMessageComponent和AgentPopover组件,适配新的工具调用展示架构,改进文件预览弹窗的下载功能和Markdown支持。
182 lines
4.7 KiB
Vue
182 lines
4.7 KiB
Vue
<template>
|
|
<BaseToolCall :tool-call="toolCall" hide-params>
|
|
<template #header-success="{ resultContent }">
|
|
<span v-if="todoListData(resultContent).length > 0">
|
|
{{ getHeaderDisplayContent(resultContent) }}
|
|
</span>
|
|
<span v-else>
|
|
待办事项工具执行完成
|
|
</span>
|
|
</template>
|
|
<template #result="{ resultContent }">
|
|
<div class="todo-list-result">
|
|
<div class="todo-list">
|
|
<div
|
|
v-for="(todo, index) in todoListData(resultContent)"
|
|
:key="index"
|
|
class="todo-item"
|
|
>
|
|
<div class="todo-status">
|
|
<CheckCircleOutlined v-if="todo.status === 'completed'" class="icon completed" />
|
|
<SyncOutlined v-else-if="todo.status === 'in_progress'" class="icon in-progress" spin />
|
|
<ClockCircleOutlined v-else-if="todo.status === 'pending'" class="icon pending" />
|
|
<CloseCircleOutlined v-else-if="todo.status === 'cancelled'" class="icon cancelled" />
|
|
<QuestionCircleOutlined v-else class="icon unknown" />
|
|
</div>
|
|
<span class="todo-text">{{ todo.content }}</span>
|
|
</div>
|
|
</div>
|
|
<div v-if="todoListData(resultContent).length === 0" class="no-results">
|
|
<p>暂无待办事项</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</BaseToolCall>
|
|
</template>
|
|
|
|
<script setup>
|
|
import BaseToolCall from '../BaseToolCall.vue';
|
|
import {
|
|
CheckCircleOutlined,
|
|
SyncOutlined,
|
|
ClockCircleOutlined,
|
|
CloseCircleOutlined,
|
|
QuestionCircleOutlined
|
|
} from '@ant-design/icons-vue';
|
|
|
|
const props = defineProps({
|
|
toolCall: {
|
|
type: Object,
|
|
required: true
|
|
}
|
|
});
|
|
|
|
const parseData = (content) => {
|
|
if (typeof content === 'string') {
|
|
try {
|
|
return JSON.parse(content);
|
|
} catch (error) {
|
|
return content;
|
|
}
|
|
}
|
|
return content;
|
|
};
|
|
|
|
const todoListData = (content) => {
|
|
if (!content) return [];
|
|
const data = parseData(content);
|
|
|
|
// 1. Try from parsed data
|
|
if (data && typeof data === 'object') {
|
|
if (Array.isArray(data)) return data;
|
|
if (data.todos && Array.isArray(data.todos)) return data.todos;
|
|
}
|
|
|
|
// 2. Try parsing string if it matches specific pattern
|
|
if (typeof content === 'string') {
|
|
let str = content;
|
|
if (str.startsWith('Updated todo list to ')) {
|
|
str = str.replace('Updated todo list to ', '');
|
|
}
|
|
const items = [];
|
|
const contentRegex = /'content':\s*'((?:[^'\\]|\\.)*)'/;
|
|
const statusRegex = /'status':\s*'((?:[^'\\]|\\.)*)'/;
|
|
const dictRegex = /\{.*?\}/g;
|
|
const dictMatches = str.match(dictRegex);
|
|
if (dictMatches) {
|
|
for (const dictStr of dictMatches) {
|
|
const contentMatch = dictStr.match(contentRegex);
|
|
const statusMatch = dictStr.match(statusRegex);
|
|
if (contentMatch && statusMatch) {
|
|
items.push({
|
|
content: contentMatch[1].replace(/\\'/g, "'").replace(/\\\\/g, "\\"),
|
|
status: statusMatch[1]
|
|
});
|
|
}
|
|
}
|
|
}
|
|
if (items.length > 0) return items;
|
|
}
|
|
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 `进行中: ${inProgress.content}`;
|
|
|
|
// 2. 显示第一个 pending
|
|
const pending = list.find(item => item.status === 'pending');
|
|
if (pending) return `待处理: ${pending.content}`;
|
|
|
|
// 3. 显示最后一个 (fallback)
|
|
const last = list[list.length - 1];
|
|
return `更新: ${last.content}`;
|
|
};
|
|
</script>
|
|
|
|
<style lang="less" scoped>
|
|
.todo-list-result {
|
|
background: var(--gray-0);
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
|
|
.todo-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.todo-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
padding: 10px 12px;
|
|
background: var(--gray-25);
|
|
border-radius: 6px;
|
|
border: 1px solid var(--gray-150);
|
|
|
|
.todo-status {
|
|
flex-shrink: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-top: 2px;
|
|
|
|
.icon {
|
|
font-size: 16px;
|
|
|
|
&.completed { color: #52c41a; }
|
|
&.in-progress { color: #1890ff; }
|
|
&.pending { color: #faad14; }
|
|
&.cancelled { color: #ff4d4f; }
|
|
&.unknown { color: var(--gray-400); }
|
|
}
|
|
}
|
|
|
|
.todo-text {
|
|
flex: 1;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
color: var(--gray-1000);
|
|
word-break: break-word;
|
|
|
|
.todo-item.completed & {
|
|
color: var(--gray-500);
|
|
text-decoration: line-through;
|
|
}
|
|
}
|
|
}
|
|
|
|
.no-results {
|
|
text-align: center;
|
|
color: var(--gray-500);
|
|
padding: 10px 0;
|
|
font-size: 13px;
|
|
}
|
|
}
|
|
</style>
|