feat(tool-calling): 添加任务工具支持并改进工具显示样式
- 新增 TaskTool 组件用于显示任务分配工具结果 - 为 BaseToolCall 添加通用 header slot 支持自定义标题 - 改进 TodoListTool 和 WebSearchTool 的显示样式 - 调整浮动侧边栏的 z-index 防止遮挡 - 优化 AgentChatComponent 的 header-right slot 支持响应式隐藏文本
This commit is contained in:
parent
84128b831a
commit
2b315f19ab
@ -68,7 +68,7 @@
|
||||
<span v-if="hasAgentStateContent" class="text">状态</span>
|
||||
</div>
|
||||
</AgentPopover>
|
||||
<slot name="header-right"></slot>
|
||||
<slot name="header-right" :is-medium-container="isMediumContainer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1043,7 +1043,7 @@ watch(conversations, () => {
|
||||
|
||||
.floating-sidebar {
|
||||
position: absolute !important;
|
||||
z-index: 101;
|
||||
z-index: 1001;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
@ -21,32 +21,40 @@
|
||||
|
||||
<!-- 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>
|
||||
<!-- Generic Header Slot (Overrides specific slots if provided) -->
|
||||
<template v-if="$slots.header">
|
||||
<slot name="header" :tool-call="toolCall" :tool-name="toolName"></slot>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
<!-- Specific State Slots (Fallback) -->
|
||||
<template v-else>
|
||||
<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-running"
|
||||
v-else
|
||||
:tool-name="toolName"
|
||||
>
|
||||
正在调用工具: <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>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Fixed Expand Icon -->
|
||||
@ -264,6 +272,21 @@ const formatResultData = (data) => {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
:deep(.keywords) {
|
||||
color: var(--main-700);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.tag) {
|
||||
font-size: 12px;
|
||||
color: var(--gray-600);
|
||||
background-color: var(--gray-100);
|
||||
padding: 0px 4px;
|
||||
border-radius: 4px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -21,6 +21,9 @@
|
||||
<!-- 图片 -->
|
||||
<ImageTool v-else-if="isImageResult" :tool-call="toolCall" />
|
||||
|
||||
<!-- 任务分配 -->
|
||||
<TaskTool v-else-if="isTaskResult" :tool-call="toolCall" />
|
||||
|
||||
<!-- 默认展示 -->
|
||||
<BaseToolCall v-else :tool-call="toolCall" />
|
||||
</template>
|
||||
@ -36,6 +39,7 @@ import KnowledgeGraphTool from './tools/KnowledgeGraphTool.vue';
|
||||
import CalculatorTool from './tools/CalculatorTool.vue';
|
||||
import TodoListTool from './tools/TodoListTool.vue';
|
||||
import ImageTool from './tools/ImageTool.vue';
|
||||
import TaskTool from './tools/TaskTool.vue';
|
||||
|
||||
const props = defineProps({
|
||||
toolCall: {
|
||||
@ -63,10 +67,19 @@ const parseData = (content) => {
|
||||
// 识别逻辑
|
||||
const isWebSearchResult = computed(() => {
|
||||
const name = toolName.value.toLowerCase();
|
||||
const isWebSearchTool = name.includes('search') || name.includes('tavily') || name.includes('web');
|
||||
if (!isWebSearchTool) return false;
|
||||
const data = parseData(props.toolCall.tool_call_result?.content);
|
||||
return data && typeof data === 'object' && 'results' in data && Array.isArray(data.results);
|
||||
return name.includes('search') || name.includes('tavily') || name.includes('web');
|
||||
});
|
||||
|
||||
const isTaskResult = computed(() => {
|
||||
let args = props.toolCall.args || props.toolCall.function?.arguments;
|
||||
if (typeof args === 'string') {
|
||||
try {
|
||||
args = JSON.parse(args);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return args && typeof args === 'object' && 'subagent_type' in args;
|
||||
});
|
||||
|
||||
const isKnowledgeBaseResult = computed(() => {
|
||||
|
||||
107
web/src/components/ToolCallingResult/tools/TaskTool.vue
Normal file
107
web/src/components/ToolCallingResult/tools/TaskTool.vue
Normal file
@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<BaseToolCall :tool-call="toolCall" :hide-params="true">
|
||||
<template #header>
|
||||
<div class="task-header">
|
||||
<span class="subagent">{{ subagentType }}</span>
|
||||
<span class="separator" v-if="shortDescription">|</span>
|
||||
<span class="description" v-if="shortDescription">{{ shortDescription }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #result="{ resultContent }"> <div class="task-result">
|
||||
<MdPreview
|
||||
:modelValue="String(resultContent)"
|
||||
:theme="theme"
|
||||
previewTheme="github"
|
||||
class="md-preview-wrapper"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</BaseToolCall>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import BaseToolCall from '../BaseToolCall.vue';
|
||||
import { MdPreview } from 'md-editor-v3';
|
||||
import 'md-editor-v3/lib/preview.css';
|
||||
import { useThemeStore } from '@/stores/theme';
|
||||
|
||||
const props = defineProps({
|
||||
toolCall: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
const theme = computed(() => themeStore.isDark ? 'dark' : 'light');
|
||||
|
||||
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 subagentType = computed(() => parsedArgs.value.subagent_type || 'Unknown Agent');
|
||||
const description = computed(() => parsedArgs.value.description || '');
|
||||
|
||||
const shortDescription = computed(() => {
|
||||
const desc = description.value;
|
||||
if (!desc) return '';
|
||||
return desc.length > 50 ? desc.slice(0, 50) + '...' : desc;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.task-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.subagent {
|
||||
font-weight: 600;
|
||||
color: var(--main-700);
|
||||
white-space: nowrap;
|
||||
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 {
|
||||
padding: 12px;
|
||||
background: var(--gray-0);
|
||||
border-radius: 8px;
|
||||
|
||||
:deep(.md-editor-preview-wrapper) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:deep(.md-editor-preview) {
|
||||
font-size: 14px;
|
||||
color: var(--gray-800);
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,9 +1,7 @@
|
||||
<template>
|
||||
<BaseToolCall :tool-call="toolCall" hide-params>
|
||||
<template #header-success="{ resultContent }">
|
||||
<span v-if="todoListData(resultContent).length > 0">
|
||||
{{ getHeaderDisplayContent(resultContent) }}
|
||||
</span>
|
||||
<span v-if="todoListData(resultContent).length > 0" v-html="getHeaderDisplayContent(resultContent)"></span>
|
||||
<span v-else>
|
||||
待办事项工具执行完成
|
||||
</span>
|
||||
@ -106,15 +104,15 @@ const getHeaderDisplayContent = (content) => {
|
||||
|
||||
// 1. 显示 in_progress
|
||||
const inProgress = list.find(item => item.status === 'in_progress');
|
||||
if (inProgress) return `进行中: ${inProgress.content}`;
|
||||
if (inProgress) return `进行中:<span class="keywords">${inProgress.content}</span>`;
|
||||
|
||||
// 2. 显示第一个 pending
|
||||
const pending = list.find(item => item.status === 'pending');
|
||||
if (pending) return `待处理: ${pending.content}`;
|
||||
if (pending) return `待处理:<span class="keywords">${pending.content}</span>`;
|
||||
|
||||
// 3. 显示最后一个 (fallback)
|
||||
const last = list[list.length - 1];
|
||||
return `更新: ${last.content}`;
|
||||
return `更新:<span class="keywords">${last.content}</span>`;
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
<template>
|
||||
<BaseToolCall :tool-call="toolCall">
|
||||
<BaseToolCall :tool-call="toolCall" :hide-params="true">
|
||||
<template #header>
|
||||
查询:
|
||||
<span class="keywords">"{{ query }}"</span>
|
||||
</template>
|
||||
<template #result="{ resultContent }">
|
||||
<div class="web-search-result">
|
||||
<div class="search-meta">
|
||||
<span class="query-text">查询: {{ parsedData(resultContent).query }}</span>
|
||||
<span class="response-time">响应时间: {{ parsedData(resultContent).response_time }}s</span>
|
||||
</div>
|
||||
|
||||
<div class="search-results">
|
||||
<div
|
||||
v-for="(result, index) in parsedData(resultContent).results"
|
||||
@ -45,8 +44,8 @@
|
||||
|
||||
<script setup>
|
||||
import BaseToolCall from '../BaseToolCall.vue';
|
||||
import { GlobalOutlined } from '@ant-design/icons-vue'
|
||||
import { parseToShanghai } from '@/utils/time'
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
toolCall: {
|
||||
@ -68,6 +67,23 @@ const parseData = (content) => {
|
||||
|
||||
const parsedData = (content) => parseData(content);
|
||||
|
||||
const query = computed(() => {
|
||||
// First try to get it from result
|
||||
const result = parsedData(props.toolCall.tool_call_result?.content);
|
||||
if (result?.query) return result.query;
|
||||
|
||||
// Fallback to args
|
||||
const args = props.toolCall.args || props.toolCall.function?.arguments;
|
||||
if (!args) return '';
|
||||
if (typeof args === 'object') return args.query || args.q || '';
|
||||
try {
|
||||
const parsed = JSON.parse(args);
|
||||
return parsed.query || parsed.q || '';
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return ''
|
||||
const parsed = parseToShanghai(dateString)
|
||||
@ -181,4 +197,4 @@ const formatDate = (dateString) => {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -44,10 +44,10 @@
|
||||
@open-agent-modal="openAgentModal"
|
||||
@close-config-sidebar="() => chatUIStore.isConfigSidebarOpen = false"
|
||||
>
|
||||
<template #header-right>
|
||||
<template #header-right="{ isMediumContainer }">
|
||||
<div type="button" class="agent-nav-btn" @click="toggleConf">
|
||||
<Settings2 size="18" class="nav-btn-icon"/>
|
||||
<span class="text">配置</span>
|
||||
<span class="text" :class="{ 'hide-text': isMediumContainer }">配置</span>
|
||||
</div>
|
||||
<div v-if="selectedAgentId" ref="moreButtonRef" type="button" class="agent-nav-btn" @click="toggleMoreMenu">
|
||||
<Ellipsis size="18" class="nav-btn-icon"/>
|
||||
@ -1009,4 +1009,8 @@ const handlePreview = () => {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.hide-text {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user