feat(todo-tools): 添加待办事项结果渲染组件并优化状态图标显示
添加 TodoListResult 组件用于渲染待办事项结果 使用 ant-design 图标替换原有 SVG 图标以保持一致性 优化待办事项状态显示样式
This commit is contained in:
parent
012e46a761
commit
e8a5844af7
@ -39,17 +39,13 @@
|
|||||||
:key="index"
|
:key="index"
|
||||||
class="todo-item"
|
class="todo-item"
|
||||||
>
|
>
|
||||||
<span class="todo-status" :class="todo.status">
|
<div class="todo-status">
|
||||||
<svg v-if="todo.status === 'completed'" viewBox="0 0 24 24" fill="currentColor" class="icon">
|
<CheckCircleOutlined v-if="todo.status === 'completed'" class="icon completed" />
|
||||||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
|
<SyncOutlined v-else-if="todo.status === 'in_progress'" class="icon in-progress" spin />
|
||||||
</svg>
|
<ClockCircleOutlined v-else-if="todo.status === 'pending'" class="icon pending" />
|
||||||
<svg v-else-if="todo.status === 'in_progress'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="icon spinning">
|
<CloseCircleOutlined v-else-if="todo.status === 'cancelled'" class="icon cancelled" />
|
||||||
<circle cx="12" cy="12" r="10" stroke-dasharray="31.416" stroke-dashoffset="31.416" stroke-linecap="round"/>
|
<QuestionCircleOutlined v-else class="icon unknown" />
|
||||||
</svg>
|
</div>
|
||||||
<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>
|
|
||||||
<span class="todo-text">{{ todo.content }}</span>
|
<span class="todo-text">{{ todo.content }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -110,6 +106,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { Download } from 'lucide-vue-next';
|
import { Download } from 'lucide-vue-next';
|
||||||
|
import {
|
||||||
|
CheckCircleOutlined,
|
||||||
|
SyncOutlined,
|
||||||
|
ClockCircleOutlined,
|
||||||
|
CloseCircleOutlined,
|
||||||
|
QuestionCircleOutlined
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: {
|
visible: {
|
||||||
@ -367,59 +370,19 @@ const emitRefresh = () => {
|
|||||||
|
|
||||||
.todo-status {
|
.todo-status {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
border-radius: 50%;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
width: 14px;
|
font-size: 16px;
|
||||||
height: 14px;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinning {
|
&.completed { color: #52c41a; }
|
||||||
animation: spin 1.5s linear infinite;
|
&.in-progress { color: #1890ff; }
|
||||||
stroke-dasharray: 31.416;
|
&.pending { color: #faad14; }
|
||||||
stroke-dashoffset: 0;
|
&.cancelled { color: #ff4d4f; }
|
||||||
animation: spin 1.5s linear infinite;
|
&.unknown { color: var(--gray-400); }
|
||||||
}
|
|
||||||
|
|
||||||
&.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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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"
|
ref="graphResultRef"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 待办事项结果 -->
|
||||||
|
<TodoListResult
|
||||||
|
v-else-if="isTodoListResult"
|
||||||
|
:data="todoListData"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 计算器结果 -->
|
<!-- 计算器结果 -->
|
||||||
<CalculatorResult
|
<CalculatorResult
|
||||||
v-else-if="isCalculatorResult"
|
v-else-if="isCalculatorResult"
|
||||||
@ -49,6 +55,7 @@ import WebSearchResult from './WebSearchResult.vue'
|
|||||||
import KnowledgeBaseResult from './KnowledgeBaseResult.vue'
|
import KnowledgeBaseResult from './KnowledgeBaseResult.vue'
|
||||||
import KnowledgeGraphResult from './KnowledgeGraphResult.vue'
|
import KnowledgeGraphResult from './KnowledgeGraphResult.vue'
|
||||||
import CalculatorResult from './CalculatorResult.vue'
|
import CalculatorResult from './CalculatorResult.vue'
|
||||||
|
import TodoListResult from './TodoListResult.vue'
|
||||||
import { useAgentStore } from '@/stores/agent';
|
import { useAgentStore } from '@/stores/agent';
|
||||||
|
|
||||||
const agentStore = useAgentStore()
|
const agentStore = useAgentStore()
|
||||||
@ -81,6 +88,60 @@ const parsedData = computed(() => {
|
|||||||
return props.resultContent
|
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 isWebSearchResult = computed(() => {
|
||||||
const toolNameLower = props.toolName.toLowerCase()
|
const toolNameLower = props.toolName.toLowerCase()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user