feat: 优化 Agent 组件和模型选择器,调整 UI 细节

This commit is contained in:
Wenjie Zhang 2026-06-12 21:15:06 +08:00
parent 253cc0b149
commit 7f454e64e6
9 changed files with 163 additions and 75 deletions

View File

@ -22,6 +22,7 @@
- [ ] 子智能体的双向通信,缺少 ask_for_main_agent 的机制
- [ ] 子智能体与子智能体的通信机制
- [ ] 如何停掉一个子智能体、查看智能体的进度
- [ ] 优化 Agent `read_file` 工具:至少对齐 DeepAgents 的读取行为
- [ ] RAG 评估支持 Agent 模式 <Badge text="v0.7.1" />
- [ ] 添加 Agent 独立调用接口,方便后续评估使用
- [ ] 任务队列 <Badge text="v0.7.2" />

View File

@ -29,7 +29,7 @@
aria-controls="agent-state-panel"
@click.stop="toggleStatePanel"
>
<SquareCheck size="18" class="nav-btn-icon" />
<LayoutList size="18" class="nav-btn-icon" />
<span class="hide-text">状态</span>
</button>
<button
@ -157,6 +157,9 @@
@remove-attachment="handleAttachmentRemove"
>
<template #actions-left-extra>
<slot name="input-actions-left" :has-active-thread="!!currentChatId"></slot>
</template>
<template #actions-right-extra>
<div class="input-model-selector">
<ModelSelectorComponent
:model_spec="currentModelSpec"
@ -166,9 +169,6 @@
@select-model="handleModelSelect"
/>
</div>
<slot name="input-actions-left" :has-active-thread="!!currentChatId"></slot>
</template>
<template #actions-right-extra>
<slot name="input-actions-right" :has-active-thread="!!currentChatId"></slot>
</template>
</AgentInputArea>
@ -497,7 +497,7 @@ import {
onDeactivated
} from 'vue'
import { message } from 'ant-design-vue'
import { ChevronDown, FolderKanban, RefreshCw, SquareCheck } from 'lucide-vue-next'
import { ChevronDown, FolderKanban, RefreshCw, LayoutList } from 'lucide-vue-next'
import { formatFileSize } from '@/utils/file_utils'
import FileTypeIcon from '@/components/common/FileTypeIcon.vue'
import { generatePixelAvatar } from '@/utils/pixelAvatar'
@ -2757,7 +2757,8 @@ watch(currentChatId, (threadId, oldThreadId) => {
.side-panel--state.is-docked {
align-self: flex-start;
margin: 0 8px 8px 0;
margin: 8px 8px 8px 0;
max-height: calc(100% - 16px);
}
.side-panel--state.is-floating {

View File

@ -122,6 +122,7 @@ const v2Models = ref({})
const loadingV2Models = ref(false)
const dropdownOpen = ref(false)
const modelSearchKeyword = ref('')
let fetchV2ModelsPromise = null
const filteredV2Models = computed(() => {
const keyword = modelSearchKeyword.value.trim().toLowerCase()
@ -165,28 +166,42 @@ const getProviderDisplayName = (providerId, providerData = {}) => {
// v2
const fetchV2Models = async () => {
if (loadingV2Models.value) return
if (fetchV2ModelsPromise) return fetchV2ModelsPromise
loadingV2Models.value = true
try {
const response = await modelProviderApi.getV2Models('chat')
if (response.success) {
v2Models.value = response.data || {}
fetchV2ModelsPromise = (async () => {
try {
const response = await modelProviderApi.getV2Models('chat')
if (response.success) {
v2Models.value = response.data || {}
}
} catch (error) {
console.warn('Failed to load v2 models:', error)
} finally {
loadingV2Models.value = false
fetchV2ModelsPromise = null
}
} catch (error) {
console.warn('Failed to load v2 models:', error)
} finally {
loadingV2Models.value = false
}
})()
return fetchV2ModelsPromise
}
//
const handleOpenChange = (open) => {
//
const handleOpenChange = async (open) => {
if (props.disabled) {
dropdownOpen.value = false
return
}
dropdownOpen.value = open
if (open) fetchV2Models()
if (!open) {
dropdownOpen.value = false
return
}
await fetchV2Models()
if (!props.disabled) {
dropdownOpen.value = true
}
}
//

View File

@ -21,13 +21,46 @@
<User class="icon" />
<span class="user-greeting">{{ greeting }}</span>
</div>
<div class="task-center-entry" @click="openTaskCenter">
<a-badge :count="activeTaskCount" :overflow-count="99" class="task-center-badge">
<span class="task-center-button">
<div class="header-actions">
<a-tooltip title="系统设置">
<button
type="button"
class="header-action-button"
aria-label="系统设置"
@click="openSettings"
>
<Settings class="icon" />
</button>
</a-tooltip>
<a-tooltip :title="themeStore.isDark ? '切换到浅色模式' : '切换到深色模式'">
<button
type="button"
class="header-action-button"
aria-label="切换主题"
@click="toggleTheme"
>
<Sun v-if="themeStore.isDark" class="icon" />
<Moon v-else class="icon" />
</button>
</a-tooltip>
<a-tooltip title="任务中心">
<button
type="button"
class="header-action-button task-center-button"
:class="{ active: taskerStore.isDrawerOpen }"
aria-label="任务中心"
@click="openTaskCenter"
>
<ClipboardList class="icon" />
<span class="task-center-label">任务中心</span>
</span>
</a-badge>
<a-badge
:count="activeTaskCount"
:overflow-count="99"
class="task-center-badge"
size="small"
/>
</button>
</a-tooltip>
</div>
</div>
</div>
@ -35,11 +68,12 @@
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { ref, computed, inject, onMounted, onUnmounted } from 'vue'
import { useInfoStore } from '@/stores/info'
import { useUserStore } from '@/stores/user'
import { Clock, User, ClipboardList } from 'lucide-vue-next'
import { Clock, User, ClipboardList, Settings, Sun, Moon } from 'lucide-vue-next'
import { useTaskerStore } from '@/stores/tasker'
import { useThemeStore } from '@/stores/theme'
import { storeToRefs } from 'pinia'
import dayjs from '@/utils/time'
@ -47,7 +81,9 @@ import dayjs from '@/utils/time'
const infoStore = useInfoStore()
const userStore = useUserStore()
const taskerStore = useTaskerStore()
const themeStore = useThemeStore()
const { activeCount: activeCountRef } = storeToRefs(taskerStore)
const { openSettingsModal } = inject('settingsModal', {})
//
const currentTime = ref('')
@ -86,6 +122,14 @@ const openTaskCenter = () => {
taskerStore.openDrawer()
}
const openSettings = () => {
openSettingsModal?.(userStore.isAdmin ? 'base' : 'account')
}
const toggleTheme = () => {
themeStore.toggleTheme()
}
//
const updateTime = () => {
const now = dayjs().tz('Asia/Shanghai')
@ -159,57 +203,62 @@ onUnmounted(() => {
.status-right {
display: flex;
align-items: center;
gap: 18px;
gap: 16px;
font-size: 13px;
}
.task-center-entry {
.header-actions {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
gap: 4px;
}
.task-center-badge {
display: flex;
align-items: center;
}
.task-center-button {
.header-action-button {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 14px;
border-radius: 999px;
justify-content: center;
gap: 6px;
height: 32px;
min-width: 32px;
padding: 0 8px;
border: 1px solid transparent;
border-radius: 8px;
background-color: transparent;
color: var(--main-600, #2563eb);
color: var(--gray-600, #4b5563);
font-size: 13px;
font-weight: 500;
border: 1px solid rgba(37, 99, 235, 0.3);
cursor: pointer;
user-select: none;
transition:
background-color 0.2s ease,
border-color 0.2s ease,
color 0.2s ease;
&:hover,
&.active {
border-color: var(--gray-150, #e5e7eb);
background: var(--gray-0, #fff);
color: var(--gray-900, #111827);
}
.icon {
width: 16px;
height: 16px;
color: inherit;
}
}
.task-center-button .icon {
width: 15px;
height: 15px;
color: inherit;
.task-center-button {
padding-right: 10px;
}
.task-center-label {
letter-spacing: 0.2px;
}
.task-center-entry:hover .task-center-button {
background-color: rgba(37, 99, 235, 0.08);
color: var(--main-700, #1d4ed8);
border-color: rgba(37, 99, 235, 0.5);
line-height: 1;
}
.task-center-badge :deep(.ant-badge-count) {
background-color: var(--main-color, #1d4ed8);
box-shadow: 0 0 0 1px var(--gray-25, #f9fafb);
}
.time-info,
@ -255,7 +304,15 @@ onUnmounted(() => {
}
.status-right {
gap: 12px;
gap: 8px;
}
.header-actions {
gap: 2px;
}
.task-center-label {
display: none;
}
.time-info,

View File

@ -313,7 +313,7 @@ const formatResultData = (data) => {
}
.description {
color: var(--gray-500);
color: var(--gray-600);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@ -329,7 +329,7 @@ const formatResultData = (data) => {
:deep(.tag) {
font-size: 11px;
color: var(--gray-500);
background-color: var(--gray-50);
// background-color: var(--gray-50);
padding: 0px 4px;
border-radius: 4px;
margin-left: 8px;
@ -337,11 +337,11 @@ const formatResultData = (data) => {
&.success {
color: var(--color-success-500);
background-color: var(--color-success-50);
// background-color: var(--color-success-50);
}
&.error {
color: var(--color-error-500);
background-color: var(--color-error-50);
// background-color: var(--color-error-50);
}
}
}

View File

@ -9,7 +9,7 @@
@click="toggleToolCallsExpanded"
>
<span class="summary-leading">
<Wrench size="14" />
<Atom size="14" />
</span>
<span class="summary-content">
<span class="summary-title">{{ toolCallsSummaryTitle }}</span>
@ -40,7 +40,7 @@
<script setup>
import { computed, ref, watch, inject } from 'vue'
import { ChevronDown, ChevronRight, Wrench } from 'lucide-vue-next'
import { ChevronDown, ChevronRight, Atom } from 'lucide-vue-next'
import { ToolCallRenderer } from '@/components/ToolCallingResult'
import { getToolCallId, normalizeToolCalls } from '@/components/ToolCallingResult/toolRegistry'
@ -106,7 +106,7 @@ const getToolCallLabel = (toolCall) => {
const toolCallsSummaryTitle = computed(() => {
if (normalizedToolCalls.value.length === 1) {
return `使用了工具: ${getToolCallLabel(normalizedToolCalls.value[0])}`
return `调用: ${getToolCallLabel(normalizedToolCalls.value[0])}`
}
return `已调用 ${normalizedToolCalls.value.length} 个工具`
})
@ -211,7 +211,7 @@ const toggleToolCallsExpanded = () => {
margin-left: 4px;
font-size: 11px;
padding: 0px 4px;
background: var(--gray-25);
// background: var(--gray-25);
color: var(--gray-600);
border-radius: 4px;
white-space: nowrap;

View File

@ -56,14 +56,13 @@
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'rank'">
<div class="rank-display">
<span v-if="index < 3" class="rank-medal">
{{ index === 0 ? '🥇' : index === 1 ? '🥈' : '🥉' }}
</span>
<span v-else class="rank-number">{{ index + 1 }}</span>
<span class="rank-number" :class="{ featured: index < 3 }">{{ index + 1 }}</span>
</div>
</template>
<template v-if="column.key === 'agent_id'">
<a-tag color="blue">{{ resolveAgentName(record.agent_id) }}</a-tag>
<span class="agent-name" :title="resolveAgentName(record.agent_id)">
{{ resolveAgentName(record.agent_id) }}
</span>
</template>
<template v-if="column.key === 'satisfaction_rate'">
<a-statistic
@ -379,23 +378,38 @@ defineExpose({
align-items: center;
justify-content: center;
.rank-medal {
font-size: 20px;
}
.rank-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background-color: var(--gray-100);
background-color: var(--gray-50);
border-radius: 50%;
font-size: 12px;
font-weight: 600;
color: var(--gray-600);
border: 1px solid var(--gray-200);
border: 1px solid var(--gray-150);
}
.rank-number.featured {
background-color: var(--main-20);
border-color: var(--main-100);
color: var(--main-color);
}
}
.agent-name {
display: inline-block;
max-width: 100%;
color: var(--gray-900);
font-size: 13px;
font-weight: 500;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
}
// AgentStatsComponent

View File

@ -7,7 +7,7 @@
</div>
<div class="stat-content">
<div class="stat-value">{{ basicStats?.total_conversations || 0 }}</div>
<div class="stat-label">总对话数</div>
<div class="stat-label">累计会话</div>
<div class="stat-trend" v-if="basicStats?.conversation_trend">
<TrendingUp v-if="basicStats.conversation_trend > 0" class="trend-icon up" />
<TrendingDown v-else-if="basicStats.conversation_trend < 0" class="trend-icon down" />

View File

@ -8,7 +8,7 @@
:single-mode="false"
@thread-change="handleThreadChange"
>
<template #input-actions-right="{ hasActiveThread }">
<template #input-actions-left="{ hasActiveThread }">
<a-dropdown
v-if="selectedAgentId"
v-model:open="agentDropdownOpen"