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>
|
<span v-if="hasAgentStateContent" class="text">状态</span>
|
||||||
</div>
|
</div>
|
||||||
</AgentPopover>
|
</AgentPopover>
|
||||||
<slot name="header-right"></slot>
|
<slot name="header-right" :is-medium-container="isMediumContainer"></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1043,7 +1043,7 @@ watch(conversations, () => {
|
|||||||
|
|
||||||
.floating-sidebar {
|
.floating-sidebar {
|
||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
z-index: 101;
|
z-index: 1001;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
@ -21,32 +21,40 @@
|
|||||||
|
|
||||||
<!-- Content Area with Slots -->
|
<!-- Content Area with Slots -->
|
||||||
<div class="tool-header-content">
|
<div class="tool-header-content">
|
||||||
<slot
|
<!-- Generic Header Slot (Overrides specific slots if provided) -->
|
||||||
name="header-success"
|
<template v-if="$slots.header">
|
||||||
v-if="toolCall.status === 'success' || toolCall.tool_call_result"
|
<slot name="header" :tool-call="toolCall" :tool-name="toolName"></slot>
|
||||||
:tool-name="toolName"
|
</template>
|
||||||
:result-content="resultContent"
|
|
||||||
>
|
|
||||||
工具 <span class="tool-name">{{ toolName }}</span> 执行完成
|
|
||||||
</slot>
|
|
||||||
|
|
||||||
<slot
|
<!-- Specific State Slots (Fallback) -->
|
||||||
name="header-error"
|
<template v-else>
|
||||||
v-else-if="toolCall.status === 'error'"
|
<slot
|
||||||
:tool-name="toolName"
|
name="header-success"
|
||||||
:error-message="toolCall.error_message"
|
v-if="toolCall.status === 'success' || toolCall.tool_call_result"
|
||||||
>
|
:tool-name="toolName"
|
||||||
工具 <span class="tool-name">{{ toolName }}</span> 执行失败
|
:result-content="resultContent"
|
||||||
<span v-if="toolCall.error_message">({{ toolCall.error_message }})</span>
|
>
|
||||||
</slot>
|
工具 <span class="tool-name">{{ toolName }}</span> 执行完成
|
||||||
|
</slot>
|
||||||
|
|
||||||
<slot
|
<slot
|
||||||
name="header-running"
|
name="header-error"
|
||||||
v-else
|
v-else-if="toolCall.status === 'error'"
|
||||||
:tool-name="toolName"
|
:tool-name="toolName"
|
||||||
>
|
:error-message="toolCall.error_message"
|
||||||
正在调用工具: <span class="tool-name">{{ toolName }}</span>
|
>
|
||||||
</slot>
|
工具 <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>
|
</div>
|
||||||
|
|
||||||
<!-- Fixed Expand Icon -->
|
<!-- Fixed Expand Icon -->
|
||||||
@ -264,6 +272,21 @@ const formatResultData = (data) => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
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" />
|
<ImageTool v-else-if="isImageResult" :tool-call="toolCall" />
|
||||||
|
|
||||||
|
<!-- 任务分配 -->
|
||||||
|
<TaskTool v-else-if="isTaskResult" :tool-call="toolCall" />
|
||||||
|
|
||||||
<!-- 默认展示 -->
|
<!-- 默认展示 -->
|
||||||
<BaseToolCall v-else :tool-call="toolCall" />
|
<BaseToolCall v-else :tool-call="toolCall" />
|
||||||
</template>
|
</template>
|
||||||
@ -36,6 +39,7 @@ import KnowledgeGraphTool from './tools/KnowledgeGraphTool.vue';
|
|||||||
import CalculatorTool from './tools/CalculatorTool.vue';
|
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';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
toolCall: {
|
toolCall: {
|
||||||
@ -63,10 +67,19 @@ const parseData = (content) => {
|
|||||||
// 识别逻辑
|
// 识别逻辑
|
||||||
const isWebSearchResult = computed(() => {
|
const isWebSearchResult = computed(() => {
|
||||||
const name = toolName.value.toLowerCase();
|
const name = toolName.value.toLowerCase();
|
||||||
const isWebSearchTool = name.includes('search') || name.includes('tavily') || name.includes('web');
|
return 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);
|
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(() => {
|
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>
|
<template>
|
||||||
<BaseToolCall :tool-call="toolCall" hide-params>
|
<BaseToolCall :tool-call="toolCall" hide-params>
|
||||||
<template #header-success="{ resultContent }">
|
<template #header-success="{ resultContent }">
|
||||||
<span v-if="todoListData(resultContent).length > 0">
|
<span v-if="todoListData(resultContent).length > 0" v-html="getHeaderDisplayContent(resultContent)"></span>
|
||||||
{{ getHeaderDisplayContent(resultContent) }}
|
|
||||||
</span>
|
|
||||||
<span v-else>
|
<span v-else>
|
||||||
待办事项工具执行完成
|
待办事项工具执行完成
|
||||||
</span>
|
</span>
|
||||||
@ -106,15 +104,15 @@ const getHeaderDisplayContent = (content) => {
|
|||||||
|
|
||||||
// 1. 显示 in_progress
|
// 1. 显示 in_progress
|
||||||
const inProgress = list.find(item => item.status === '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
|
// 2. 显示第一个 pending
|
||||||
const pending = list.find(item => item.status === '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)
|
// 3. 显示最后一个 (fallback)
|
||||||
const last = list[list.length - 1];
|
const last = list[list.length - 1];
|
||||||
return `更新: ${last.content}`;
|
return `更新:<span class="keywords">${last.content}</span>`;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<BaseToolCall :tool-call="toolCall">
|
<BaseToolCall :tool-call="toolCall" :hide-params="true">
|
||||||
|
<template #header>
|
||||||
|
查询:
|
||||||
|
<span class="keywords">"{{ query }}"</span>
|
||||||
|
</template>
|
||||||
<template #result="{ resultContent }">
|
<template #result="{ resultContent }">
|
||||||
<div class="web-search-result">
|
<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 class="search-results">
|
||||||
<div
|
<div
|
||||||
v-for="(result, index) in parsedData(resultContent).results"
|
v-for="(result, index) in parsedData(resultContent).results"
|
||||||
@ -45,8 +44,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import BaseToolCall from '../BaseToolCall.vue';
|
import BaseToolCall from '../BaseToolCall.vue';
|
||||||
import { GlobalOutlined } from '@ant-design/icons-vue'
|
|
||||||
import { parseToShanghai } from '@/utils/time'
|
import { parseToShanghai } from '@/utils/time'
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
toolCall: {
|
toolCall: {
|
||||||
@ -68,6 +67,23 @@ const parseData = (content) => {
|
|||||||
|
|
||||||
const parsedData = (content) => 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) => {
|
const formatDate = (dateString) => {
|
||||||
if (!dateString) return ''
|
if (!dateString) return ''
|
||||||
const parsed = parseToShanghai(dateString)
|
const parsed = parseToShanghai(dateString)
|
||||||
@ -181,4 +197,4 @@ const formatDate = (dateString) => {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -44,10 +44,10 @@
|
|||||||
@open-agent-modal="openAgentModal"
|
@open-agent-modal="openAgentModal"
|
||||||
@close-config-sidebar="() => chatUIStore.isConfigSidebarOpen = false"
|
@close-config-sidebar="() => chatUIStore.isConfigSidebarOpen = false"
|
||||||
>
|
>
|
||||||
<template #header-right>
|
<template #header-right="{ isMediumContainer }">
|
||||||
<div type="button" class="agent-nav-btn" @click="toggleConf">
|
<div type="button" class="agent-nav-btn" @click="toggleConf">
|
||||||
<Settings2 size="18" class="nav-btn-icon"/>
|
<Settings2 size="18" class="nav-btn-icon"/>
|
||||||
<span class="text">配置</span>
|
<span class="text" :class="{ 'hide-text': isMediumContainer }">配置</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedAgentId" ref="moreButtonRef" type="button" class="agent-nav-btn" @click="toggleMoreMenu">
|
<div v-if="selectedAgentId" ref="moreButtonRef" type="button" class="agent-nav-btn" @click="toggleMoreMenu">
|
||||||
<Ellipsis size="18" class="nav-btn-icon"/>
|
<Ellipsis size="18" class="nav-btn-icon"/>
|
||||||
@ -1009,4 +1009,8 @@ const handlePreview = () => {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hide-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user