feat(todo-tools): 添加待办事项结果渲染组件并优化状态图标显示
添加 TodoListResult 组件用于渲染待办事项结果 使用 ant-design 图标替换原有 SVG 图标以保持一致性 优化待办事项状态显示样式
This commit is contained in:
parent
012e46a761
commit
e8a5844af7
@ -39,17 +39,13 @@
|
||||
:key="index"
|
||||
class="todo-item"
|
||||
>
|
||||
<span class="todo-status" :class="todo.status">
|
||||
<svg v-if="todo.status === 'completed'" viewBox="0 0 24 24" fill="currentColor" class="icon">
|
||||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
|
||||
</svg>
|
||||
<svg v-else-if="todo.status === 'in_progress'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="icon spinning">
|
||||
<circle cx="12" cy="12" r="10" stroke-dasharray="31.416" stroke-dashoffset="31.416" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="icon">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
</svg>
|
||||
</span>
|
||||
<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>
|
||||
@ -110,6 +106,13 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Download } from 'lucide-vue-next';
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
SyncOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
QuestionCircleOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@ -367,59 +370,19 @@ const emitRefresh = () => {
|
||||
|
||||
.todo-status {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 2px;
|
||||
border-radius: 50%;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
|
||||
.icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.spinning {
|
||||
animation: spin 1.5s linear infinite;
|
||||
stroke-dasharray: 31.416;
|
||||
stroke-dashoffset: 0;
|
||||
animation: spin 1.5s linear infinite;
|
||||
}
|
||||
|
||||
&.completed {
|
||||
background: var(--color-success-50);
|
||||
color: var(--color-success-700);
|
||||
|
||||
.icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
&.in_progress {
|
||||
background: var(--color-warning-50);
|
||||
color: var(--color-warning-700);
|
||||
}
|
||||
|
||||
&.pending {
|
||||
background: var(--gray-100);
|
||||
color: var(--gray-500);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
50% {
|
||||
stroke-dashoffset: 15.708;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
stroke-dashoffset: 0;
|
||||
font-size: 16px;
|
||||
|
||||
&.completed { color: #52c41a; }
|
||||
&.in-progress { color: #1890ff; }
|
||||
&.pending { color: #faad14; }
|
||||
&.cancelled { color: #ff4d4f; }
|
||||
&.unknown { color: var(--gray-400); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
118
web/src/components/ToolCallingResult/TodoListResult.vue
Normal file
118
web/src/components/ToolCallingResult/TodoListResult.vue
Normal file
@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div class="todo-list-result">
|
||||
<!-- <div class="todo-header">
|
||||
<h4><UnorderedListOutlined /> 待办事项</h4>
|
||||
</div> -->
|
||||
<div class="todo-content">
|
||||
<div v-for="(item, index) in data" :key="index" class="todo-item">
|
||||
<div class="todo-status">
|
||||
<CheckCircleOutlined v-if="item.status === 'completed'" class="icon completed" />
|
||||
<SyncOutlined v-else-if="item.status === 'in_progress'" class="icon in-progress" spin />
|
||||
<ClockCircleOutlined v-else-if="item.status === 'pending'" class="icon pending" />
|
||||
<CloseCircleOutlined v-else-if="item.status === 'cancelled'" class="icon cancelled" />
|
||||
<QuestionCircleOutlined v-else class="icon unknown" />
|
||||
</div>
|
||||
<div class="todo-text" :class="{ completed: item.status === 'completed', cancelled: item.status === 'cancelled' }">
|
||||
{{ item.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
UnorderedListOutlined,
|
||||
CheckCircleOutlined,
|
||||
SyncOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
QuestionCircleOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.todo-list-result {
|
||||
background: var(--gray-0);
|
||||
border-radius: 8px;
|
||||
// border: 1px solid var(--gray-200);
|
||||
overflow: hidden;
|
||||
|
||||
.todo-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--gray-100);
|
||||
background: var(--gray-25);
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--main-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.todo-content {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.todo-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--gray-50);
|
||||
border: 1px solid var(--gray-100);
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--gray-100);
|
||||
}
|
||||
|
||||
.todo-status {
|
||||
padding-top: 2px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
|
||||
&.completed { color: #52c41a; }
|
||||
&.in-progress { color: #1890ff; }
|
||||
&.pending { color: #faad14; }
|
||||
&.cancelled { color: #ff4d4f; }
|
||||
&.unknown { color: var(--gray-400); }
|
||||
}
|
||||
}
|
||||
|
||||
.todo-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--gray-800);
|
||||
word-break: break-word;
|
||||
|
||||
&.completed {
|
||||
text-decoration: line-through;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
&.cancelled {
|
||||
text-decoration: line-through;
|
||||
color: var(--gray-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -19,6 +19,12 @@
|
||||
ref="graphResultRef"
|
||||
/>
|
||||
|
||||
<!-- 待办事项结果 -->
|
||||
<TodoListResult
|
||||
v-else-if="isTodoListResult"
|
||||
:data="todoListData"
|
||||
/>
|
||||
|
||||
<!-- 计算器结果 -->
|
||||
<CalculatorResult
|
||||
v-else-if="isCalculatorResult"
|
||||
@ -49,6 +55,7 @@ import WebSearchResult from './WebSearchResult.vue'
|
||||
import KnowledgeBaseResult from './KnowledgeBaseResult.vue'
|
||||
import KnowledgeGraphResult from './KnowledgeGraphResult.vue'
|
||||
import CalculatorResult from './CalculatorResult.vue'
|
||||
import TodoListResult from './TodoListResult.vue'
|
||||
import { useAgentStore } from '@/stores/agent';
|
||||
|
||||
const agentStore = useAgentStore()
|
||||
@ -81,6 +88,60 @@ const parsedData = computed(() => {
|
||||
return props.resultContent
|
||||
})
|
||||
|
||||
const todoListData = computed(() => {
|
||||
if (props.toolName !== 'write_todos') return []
|
||||
|
||||
const raw = props.resultContent
|
||||
|
||||
// 1. Try from parsedData (JSON object)
|
||||
const data = parsedData.value
|
||||
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 raw === 'string') {
|
||||
let str = raw
|
||||
if (str.startsWith('Updated todo list to ')) {
|
||||
str = str.replace('Updated todo list to ', '')
|
||||
}
|
||||
|
||||
// Try regex parsing for Python-like string
|
||||
const items = []
|
||||
// Matches {'content': '...', 'status': '...'} with escaped quotes support
|
||||
// content might contain escaped quotes
|
||||
const contentRegex = /'content':\s*'((?:[^'\\]|\\.)*)'/
|
||||
const statusRegex = /'status':\s*'((?:[^'\\]|\\.)*)'/
|
||||
|
||||
// Split by "}, {" roughly, or just look for objects
|
||||
// Since it is a list of dicts, we can match individual dicts
|
||||
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 isTodoListResult = computed(() => {
|
||||
return props.toolName === 'write_todos' && todoListData.value.length > 0
|
||||
})
|
||||
|
||||
// 判断是否为网页搜索结果
|
||||
const isWebSearchResult = computed(() => {
|
||||
const toolNameLower = props.toolName.toLowerCase()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user