From afc8c0aa1f9231196fd55c4d1d99b7d02f482ed2 Mon Sep 17 00:00:00 2001 From: GCWing Date: Mon, 23 Mar 2026 17:39:23 +0800 Subject: [PATCH] refactor(web-ui): streamline agents UI and refresh shell theming Remove legacy agent gallery and team composer components in favor of a simpler agents scene and create-agent flow. Update navigation, splash screen, and scene viewport styling; align Git and settings side nav with shared tokens. Refresh dark and slate theme presets, component tokens, and notification styles; adjust profile nursery and template pages plus i18n strings. --- .../GalleryLayout/GalleryPageHeader.tsx | 6 +- .../src/app/components/NavPanel/MainNav.tsx | 16 +- .../src/app/components/NavPanel/NavPanel.scss | 23 +- .../workspaces/WorkspaceListSection.scss | 2 +- .../components/SplashScreen/SplashScreen.scss | 54 +- .../components/SplashScreen/SplashScreen.tsx | 9 +- src/web-ui/src/app/scenes/SceneViewport.scss | 58 +- src/web-ui/src/app/scenes/SceneViewport.tsx | 36 +- .../src/app/scenes/agents/AgentsScene.tsx | 246 +-- .../src/app/scenes/agents/agentsIcons.ts | 65 +- .../src/app/scenes/agents/agentsStore.ts | 384 +---- .../agents/components/AgentGallery.scss | 333 ---- .../scenes/agents/components/AgentGallery.tsx | 303 ---- .../agents/components/AgentTeamCard.scss | 331 ---- .../agents/components/AgentTeamCard.tsx | 137 -- .../agents/components/AgentTeamComposer.scss | 473 ------ .../agents/components/AgentTeamComposer.tsx | 456 ------ .../agents/components/AgentTeamTabBar.scss | 322 ---- .../agents/components/AgentTeamTabBar.tsx | 218 --- .../agents/components/CapabilityBar.scss | 81 - .../agents/components/CapabilityBar.tsx | 67 - .../agents/components/CreateAgentPage.tsx | 16 +- src/web-ui/src/app/scenes/git/GitNav.scss | 2 +- .../app/scenes/my-agent/InsightsScene.scss | 40 +- .../src/app/scenes/my-agent/InsightsScene.tsx | 30 +- .../src/app/scenes/my-agent/MyAgentNav.scss | 4 +- .../profile/views/AssistantConfigPage.tsx | 1427 ++++++++++++----- .../scenes/profile/views/NurseryGallery.tsx | 3 +- .../app/scenes/profile/views/NurseryView.scss | 1221 +++++++++++--- .../profile/views/TemplateConfigPage.tsx | 524 ++++-- .../src/app/scenes/settings/SettingsNav.scss | 8 +- .../src/app/scenes/settings/settingsConfig.ts | 2 +- .../components/IconButton/IconButton.scss | 12 +- .../src/component-library/styles/tokens.scss | 10 +- .../config/components/AIModelConfig.tsx | 2 +- .../config/components/AIRulesMemoryConfig.tsx | 4 +- .../config/components/McpToolsConfig.tsx | 2 +- .../theme/presets/dark-theme.ts | 12 +- .../theme/presets/slate-theme.ts | 12 +- src/web-ui/src/locales/en-US/common.json | 8 +- .../src/locales/en-US/scenes/agents.json | 131 +- .../src/locales/en-US/scenes/profile.json | 29 +- src/web-ui/src/locales/zh-CN/common.json | 8 +- .../src/locales/zh-CN/scenes/agents.json | 135 +- .../src/locales/zh-CN/scenes/profile.json | 29 +- .../components/LoadingNotification.scss | 2 +- .../components/NotificationCenter.scss | 10 +- .../components/NotificationItem.scss | 5 +- .../components/ProgressNotification.scss | 6 +- 49 files changed, 2753 insertions(+), 4561 deletions(-) delete mode 100644 src/web-ui/src/app/scenes/agents/components/AgentGallery.scss delete mode 100644 src/web-ui/src/app/scenes/agents/components/AgentGallery.tsx delete mode 100644 src/web-ui/src/app/scenes/agents/components/AgentTeamCard.scss delete mode 100644 src/web-ui/src/app/scenes/agents/components/AgentTeamCard.tsx delete mode 100644 src/web-ui/src/app/scenes/agents/components/AgentTeamComposer.scss delete mode 100644 src/web-ui/src/app/scenes/agents/components/AgentTeamComposer.tsx delete mode 100644 src/web-ui/src/app/scenes/agents/components/AgentTeamTabBar.scss delete mode 100644 src/web-ui/src/app/scenes/agents/components/AgentTeamTabBar.tsx delete mode 100644 src/web-ui/src/app/scenes/agents/components/CapabilityBar.scss delete mode 100644 src/web-ui/src/app/scenes/agents/components/CapabilityBar.tsx diff --git a/src/web-ui/src/app/components/GalleryLayout/GalleryPageHeader.tsx b/src/web-ui/src/app/components/GalleryLayout/GalleryPageHeader.tsx index e23c02db..25ae3b26 100644 --- a/src/web-ui/src/app/components/GalleryLayout/GalleryPageHeader.tsx +++ b/src/web-ui/src/app/components/GalleryLayout/GalleryPageHeader.tsx @@ -1,10 +1,11 @@ import React from 'react'; interface GalleryPageHeaderProps { - title: string; + title: React.ReactNode; subtitle?: React.ReactNode; actions?: React.ReactNode; extraContent?: React.ReactNode; + className?: string; } const GalleryPageHeader: React.FC = ({ @@ -12,8 +13,9 @@ const GalleryPageHeader: React.FC = ({ subtitle, actions, extraContent, + className, }) => ( -
+

{title}

{subtitle ?
{subtitle}
: null} diff --git a/src/web-ui/src/app/components/NavPanel/MainNav.tsx b/src/web-ui/src/app/components/NavPanel/MainNav.tsx index 161ae0b1..309d2a93 100644 --- a/src/web-ui/src/app/components/NavPanel/MainNav.tsx +++ b/src/web-ui/src/app/components/NavPanel/MainNav.tsx @@ -471,7 +471,7 @@ const MainNav: React.FC = ({ }, [resolvedAssistantWorkspace, setSelectedAssistantWorkspaceId]); const handleOpenProModeSession = useCallback(async () => { - // 找到项目工作区(非 assistant 类型) + // Pick a project workspace (non-assistant) const projectWorkspaces = openedWorkspacesList.filter( w => w.workspaceKind !== WorkspaceKind.Assistant ); @@ -481,7 +481,7 @@ const MainNav: React.FC = ({ ? currentWorkspace : projectWorkspaces[0] ?? null; - // 若当前激活的是 assistant workspace,先切回项目工作区 + // If assistant workspace is active, switch to a project workspace first if (targetWorkspace && currentWorkspace?.id !== targetWorkspace.id) { await setActiveWorkspace(targetWorkspace.id).catch(() => {}); } @@ -509,7 +509,7 @@ const MainNav: React.FC = ({ } } - // 没有已有会话,显式传入 workspacePath 创建 Code 会话,避免被 assistant workspace 覆盖 + // No session yet: pass workspacePath so Code session creation is not overridden by assistant workspace openScene('session'); switchLeftPanelTab('sessions'); await flowChatManager.createChatSession({ workspacePath: workspacePath || undefined }, 'agentic'); @@ -548,20 +548,20 @@ const MainNav: React.FC = ({ } } - // 没有已有会话,新建 Claw 会话 + // No session yet: create a Claw session await handleCreateSession('Claw'); }, [currentWorkspace, defaultAssistantWorkspace, handleCreateSession, isAssistantWorkspaceActive, openScene, setActiveWorkspace, switchLeftPanelTab]); const handleToggleNavDisplayMode = useCallback(() => { - // 防止动画进行中重复触发 + // Ignore repeat clicks while the transition runs if (modeSwitchTimerRef.current !== null) return; setIsModeSwitching(true); - // 点击时同步计算目标模式,避免 timeout 闭包中读取到过期值 + // Resolve target mode synchronously on click so timeouts do not close over a stale value const nextMode: NavDisplayMode = navDisplayMode === 'pro' ? 'assistant' : 'pro'; - // 200ms(clip-path 收缩到最小圆点):只切换 nav 显示状态,不触发任何场景/会话操作 + // 200ms (clip-path at smallest dot): only flip nav display; no scene/session changes yet if (modeSwitchSwapTimerRef.current !== null) { window.clearTimeout(modeSwitchSwapTimerRef.current); } @@ -571,7 +571,7 @@ const MainNav: React.FC = ({ modeSwitchSwapTimerRef.current = null; }, 200); - // 480ms(动画完全结束):再切场景和会话,避免 tab 文字在动画期间闪动 + // 480ms (animation finished): then switch scene/session so tab labels do not flicker mid-animation modeSwitchTimerRef.current = window.setTimeout(() => { setIsModeSwitching(false); modeSwitchTimerRef.current = null; diff --git a/src/web-ui/src/app/components/NavPanel/NavPanel.scss b/src/web-ui/src/app/components/NavPanel/NavPanel.scss index a6e30153..96393bae 100644 --- a/src/web-ui/src/app/components/NavPanel/NavPanel.scss +++ b/src/web-ui/src/app/components/NavPanel/NavPanel.scss @@ -160,7 +160,7 @@ $_section-header-height: 24px; } &.is-switching { - // 整体 clip-path 涟漪收缩 → 展开,linear 因为各段 easing 写在关键帧内部 + // Full clip-path ripple: shrink then expand; linear outer timing because easing is set on keyframes animation: bitfun-nav-mode-switch 0.48s linear; .bitfun-nav-panel__mode-switch-logo { @@ -498,7 +498,7 @@ $_section-header-height: 24px; border-radius: 2px; } - // 与按钮涟漪同步:列表内容淡出后换组,再淡入 + // Synced with button ripple: fade list out, swap group, fade in &.is-mode-switching { animation: bitfun-nav-sections-mode-switch 0.48s linear; } @@ -1180,9 +1180,9 @@ $_section-header-height: 24px; } -// ── 形变涟漪:按钮 clip-path 收缩成圆点,再弹开回矩形 ────────────────────── -// 各关键帧内用 animation-timing-function 分段控制 easing: -// 收缩段用 ease-in(快速压缩);展开段用 spring curve(弹性回弹) +// ── Morph ripple: button clip-path shrinks to a dot, then springs back to a rectangle ── +// Split easing via animation-timing-function on each leg: +// shrink uses ease-in (fast squeeze); expand uses a springy cubic-bezier @keyframes bitfun-nav-mode-switch { 0% { clip-path: circle(120% at 50% 50%); @@ -1192,7 +1192,7 @@ $_section-header-height: 24px; clip-path: circle(26px at 50% 50%); animation-timing-function: linear; } - // 内容在此帧附近(200ms)悄然替换,圆点完全遮住跳变 + // Content swaps around this hold (~200ms); the dot masks the jump 58% { clip-path: circle(26px at 50% 50%); animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); @@ -1202,7 +1202,7 @@ $_section-header-height: 24px; } } -// Logo:缩小旋转消失,旋转反向放大出现 +// Logo: shrink + rotate out, reverse-rotate in while scaling up @keyframes bitfun-nav-mode-switch-logo { 0% { transform: scale(1) rotate(0deg); @@ -1225,7 +1225,7 @@ $_section-header-height: 24px; } } -// 文字区:向下淡出,从上淡入 +// Copy block: fade out downward, fade in from above @keyframes bitfun-nav-mode-switch-copy { 0% { opacity: 1; @@ -1256,7 +1256,7 @@ $_section-header-height: 24px; // Footer: Notification + More-options menu // ────────────────────────────────────────────── -// sections 与按钮涟漪联动:内容淡出后从对侧滑入 +// Sections tied to button ripple: fade out then slide in from the opposite side @keyframes bitfun-nav-sections-mode-switch { 0% { opacity: 1; @@ -1326,6 +1326,11 @@ $_section-header-height: 24px; flex-shrink: 0; } +// Footer notification: yellow only when there are unread messages (BellDot) +.bitfun-nav-panel__footer .bitfun-notification-btn .bitfun-notification-btn__icon--has-message { + color: #ca8a04; +} + .bitfun-nav-panel__footer-btn--icon { display: flex; align-items: center; diff --git a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss index 34174806..4bf9aa38 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss +++ b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss @@ -571,7 +571,7 @@ } } -// ── 助理模式 item(独立样式,不与专业模式共享)────────────────────────────── +// ── Assistant-mode row item (standalone styles, not shared with pro mode) ──────────── .bitfun-nav-panel { &__assistant-item { position: relative; diff --git a/src/web-ui/src/app/components/SplashScreen/SplashScreen.scss b/src/web-ui/src/app/components/SplashScreen/SplashScreen.scss index 6bb8b5e9..6919e640 100644 --- a/src/web-ui/src/app/components/SplashScreen/SplashScreen.scss +++ b/src/web-ui/src/app/components/SplashScreen/SplashScreen.scss @@ -20,7 +20,7 @@ pointer-events: none; // ── Exit state ───────────────────────────────────────────────────────────── - // Whole container fades together — logo, dots, and backdrop all in sync. + // Whole container fades together — logo and backdrop in sync. &--exiting { animation: splash-bg-exit 0.5s ease-in-out both; @@ -28,10 +28,6 @@ .splash-screen__logo-wrap { animation: none; // Stop idle pulse; let container opacity drive the fade } - - .splash-screen__dots { - animation: none; - } } } @@ -43,49 +39,34 @@ display: flex; flex-direction: column; align-items: center; - gap: 28px; } // ── Logo ────────────────────────────────────────────────────────────────────── .splash-screen__logo-wrap { - animation: splash-logo-idle 2.6s ease-in-out infinite; + animation: splash-logo-idle 3.2s ease-in-out infinite; } .splash-screen__logo { display: block; - width: 56px; - height: 56px; + width: 112px; + height: 112px; border-radius: $size-radius-lg; user-select: none; } -// ── Loading dots ────────────────────────────────────────────────────────────── - -.splash-screen__dots { - display: flex; - align-items: center; - gap: 8px; -} - -.splash-screen__dot { - display: block; - width: 6px; - height: 6px; - border-radius: 50%; - background: var(--color-text-muted); - - &:nth-child(1) { animation: splash-dot 1.4s ease-in-out 0.0s infinite; } - &:nth-child(2) { animation: splash-dot 1.4s ease-in-out 0.2s infinite; } - &:nth-child(3) { animation: splash-dot 1.4s ease-in-out 0.4s infinite; } -} - // ── Keyframes ───────────────────────────────────────────────────────────────── -// Idle logo: gentle breathing pulse +// Idle logo: soft opacity pulse with a slight breathing scale @keyframes splash-logo-idle { - 0%, 100% { opacity: 0.78; transform: scale(1); } - 50% { opacity: 1; transform: scale(1.04); } + 0%, 100% { + opacity: 0.38; + transform: scale(0.98); + } + 50% { + opacity: 0.98; + transform: scale(1); + } } // Exit: whole container fades to transparent @@ -94,17 +75,10 @@ 100% { opacity: 0; } } -// Dot loading pulse without vertical movement -@keyframes splash-dot { - 0%, 100% { opacity: 0.3; transform: scale(0.92); } - 50% { opacity: 0.9; transform: scale(1.16); } -} - // ── Reduced motion ──────────────────────────────────────────────────────────── @media (prefers-reduced-motion: reduce) { - .splash-screen__logo-wrap, - .splash-screen__dot { + .splash-screen__logo-wrap { animation: none; } diff --git a/src/web-ui/src/app/components/SplashScreen/SplashScreen.tsx b/src/web-ui/src/app/components/SplashScreen/SplashScreen.tsx index 647c4837..ea06ba39 100644 --- a/src/web-ui/src/app/components/SplashScreen/SplashScreen.tsx +++ b/src/web-ui/src/app/components/SplashScreen/SplashScreen.tsx @@ -1,7 +1,7 @@ /** * SplashScreen — full-screen loading overlay shown on app start. * - * Idle: logo breathes softly; loading dots bounce. + * Idle: logo larger, soft fade in/out. * Exiting: logo scales up and fades; backdrop dissolves. */ @@ -30,7 +30,6 @@ const SplashScreen: React.FC = ({ isExiting, onExited }) => { className={`splash-screen${isExiting ? ' splash-screen--exiting' : ''}`} aria-hidden="true" > - {/* Center: logo + loading dots */}
= ({ isExiting, onExited }) => { draggable={false} />
- -
); diff --git a/src/web-ui/src/app/scenes/SceneViewport.scss b/src/web-ui/src/app/scenes/SceneViewport.scss index 78f86b16..24341371 100644 --- a/src/web-ui/src/app/scenes/SceneViewport.scss +++ b/src/web-ui/src/app/scenes/SceneViewport.scss @@ -10,15 +10,41 @@ overflow: hidden; min-height: 0; border-radius: $size-radius-base; - border: 1px solid var(--border-subtle); + border: none; background: var(--color-bg-scene); - transition: - border-color 240ms cubic-bezier(0.25, 1, 0.5, 1), - box-shadow 240ms cubic-bezier(0.25, 1, 0.5, 1); + + &__clip { + position: absolute; + inset: 0; + overflow: hidden; + border-radius: inherit; + } + + // Divider hover / resize: inner glow only (::after paints above scene content). + &__clip::after { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + z-index: $z-decoration; + border-radius: inherit; + opacity: 0; + transition: opacity 240ms cubic-bezier(0.25, 1, 0.5, 1); + box-shadow: inset 0 0 72px -18px rgba(96, 165, 250, 0.14); + } + + body.bitfun-divider-hovered &__clip::after, + body.bitfun-is-resizing-nav &__clip::after { + opacity: 1; + } + + body.bitfun-is-resizing-nav &__clip::after { + box-shadow: inset 0 0 64px -16px rgba(96, 165, 250, 0.16); + } // ── Welcome overlay (app start) ────────────────────── - &--welcome { + &__clip--welcome { display: flex; align-items: center; justify-content: center; @@ -26,7 +52,7 @@ // ── Empty state (all tabs closed) ───────────────────── - &--empty { + &__clip--empty { display: flex; align-items: center; justify-content: center; @@ -53,26 +79,8 @@ } } -// ── Drag handle hover / active glow effect ────────────────────────────── - -body.bitfun-divider-hovered .bitfun-scene-viewport { - border-color: var(--border-accent-strong); - box-shadow: - inset 6px 0 32px -4px rgba(96, 165, 250, 0.22), - inset 0 0 60px -16px rgba(96, 165, 250, 0.10), - -3px 0 20px -4px rgba(96, 165, 250, 0.30); -} - -body.bitfun-is-resizing-nav .bitfun-scene-viewport { - border-color: var(--border-accent-strong); - box-shadow: - inset 4px 0 24px -4px rgba(96, 165, 250, 0.20), - inset 0 0 48px -16px rgba(96, 165, 250, 0.08), - -2px 0 18px -4px rgba(96, 165, 250, 0.28); -} - @media (prefers-reduced-motion: reduce) { - .bitfun-scene-viewport { + .bitfun-scene-viewport__clip::after { transition: none; } } diff --git a/src/web-ui/src/app/scenes/SceneViewport.tsx b/src/web-ui/src/app/scenes/SceneViewport.tsx index 61c2749f..cb4d4e49 100644 --- a/src/web-ui/src/app/scenes/SceneViewport.tsx +++ b/src/web-ui/src/app/scenes/SceneViewport.tsx @@ -42,28 +42,32 @@ const SceneViewport: React.FC = ({ workspacePath, isEntering // All tabs closed — show empty state if (openTabs.length === 0) { return ( -
-

{t('welcomeScene.emptyHint')}

+
+
+

{t('welcomeScene.emptyHint')}

+
); } return (
- - {openTabs.map(tab => ( -
- {renderScene(tab.id, workspacePath, isEntering)} -
- ))} -
+
+ + {openTabs.map(tab => ( +
+ {renderScene(tab.id, workspacePath, isEntering)} +
+ ))} +
+
); }; diff --git a/src/web-ui/src/app/scenes/agents/AgentsScene.tsx b/src/web-ui/src/app/scenes/agents/AgentsScene.tsx index 74718f63..3d6fe643 100644 --- a/src/web-ui/src/app/scenes/agents/AgentsScene.tsx +++ b/src/web-ui/src/app/scenes/agents/AgentsScene.tsx @@ -1,13 +1,11 @@ import React, { useCallback, useMemo } from 'react'; import { - ArrowLeft, Bot, Cpu, Plus, Puzzle, RefreshCw, Search as SearchIcon, - Users, Wrench, } from 'lucide-react'; import { useTranslation } from 'react-i18next'; @@ -23,67 +21,25 @@ import { } from '@/app/components'; import AgentCard from './components/AgentCard'; import CoreAgentCard, { type CoreAgentMeta } from './components/CoreAgentCard'; -import AgentTeamCard from './components/AgentTeamCard'; -import AgentTeamTabBar from './components/AgentTeamTabBar'; -import AgentGallery from './components/AgentGallery'; -import AgentTeamComposer from './components/AgentTeamComposer'; -import CapabilityBar from './components/CapabilityBar'; import CreateAgentPage from './components/CreateAgentPage'; import { - CAPABILITY_CATEGORIES, - MOCK_AGENT_TEAMS, - computeAgentTeamCapabilities, type AgentWithCapabilities, useAgentsStore, } from './agentsStore'; import { useAgentsList } from './hooks/useAgentsList'; -import { AGENT_ICON_MAP, CAPABILITY_ACCENT, AGENT_TEAM_ICON_MAP, getAgentTeamAccent } from './agentsIcons'; +import { AGENT_ICON_MAP, CAPABILITY_ACCENT } from './agentsIcons'; import { getCardGradient } from '@/shared/utils/cardGradients'; import { getAgentBadge } from './utils'; import './AgentsView.scss'; import './AgentsScene.scss'; -const EXAMPLE_TEAM_IDS = new Set(MOCK_AGENT_TEAMS.map((team) => team.id)); - const HIDDEN_AGENT_IDS = new Set(['Claw']); const CORE_AGENT_IDS = new Set(['agentic', 'Cowork']); -const AgentTeamEditorView: React.FC = () => { - const { t } = useTranslation('scenes/agents'); - const { openHome } = useAgentsStore(); - - return ( -
-
- -
- - - -
- - -
- -
-
- - -
- ); -}; - const AgentsHomeView: React.FC = () => { const { t } = useTranslation('scenes/agents'); const { - agentTeams, agentSoloEnabled, searchQuery, agentFilterLevel, @@ -92,12 +48,9 @@ const AgentsHomeView: React.FC = () => { setAgentFilterLevel, setAgentFilterType, setAgentSoloEnabled, - openAgentTeamEditor, openCreateAgent, - addAgentTeam, } = useAgentsStore(); const [selectedAgentId, setSelectedAgentId] = React.useState(null); - const [selectedTeamId, setSelectedTeamId] = React.useState(null); const [toolsEditing, setToolsEditing] = React.useState(false); const [skillsEditing, setSkillsEditing] = React.useState(false); const [pendingTools, setPendingTools] = React.useState(null); @@ -137,12 +90,6 @@ const AgentsHomeView: React.FC = () => { }, }), [t]); - const filteredTeams = useMemo(() => agentTeams.filter((team) => { - if (!searchQuery) return true; - const query = searchQuery.toLowerCase(); - return team.name.toLowerCase().includes(query) || team.description.toLowerCase().includes(query); - }), [agentTeams, searchQuery]); - const coreAgents = useMemo(() => allAgents.filter((agent) => CORE_AGENT_IDS.has(agent.id)), [allAgents]); const visibleAgents = useMemo( @@ -150,19 +97,6 @@ const AgentsHomeView: React.FC = () => { [filteredAgents], ); - const handleCreateTeam = useCallback(() => { - const id = `agent-team-${Date.now()}`; - addAgentTeam({ - id, - name: t('teamsZone.newTeamName'), - icon: 'users', - description: '', - strategy: 'collaborative', - shareContext: true, - }); - openAgentTeamEditor(id); - }, [addAgentTeam, openAgentTeamEditor, t]); - const scrollToZone = useCallback((targetId: string) => { document.getElementById(targetId)?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, []); @@ -195,27 +129,6 @@ const AgentsHomeView: React.FC = () => { : (selectedAgent?.defaultTools ?? []); const selectedAgentSkills = selectedAgentModeConfig?.available_skills ?? []; const selectedAgentSkillItems = availableSkills.filter((skill) => selectedAgentSkills.includes(skill.name)); - const selectedTeam = useMemo( - () => agentTeams.find((team) => team.id === selectedTeamId) ?? null, - [agentTeams, selectedTeamId], - ); - const selectedAgentTeamMembers = useMemo( - () => selectedTeam - ? selectedTeam.members - .map((member) => allAgents.find((agent) => agent.id === member.agentId)) - .filter((agent): agent is AgentWithCapabilities => Boolean(agent)) - : [], - [allAgents, selectedTeam], - ); - const selectedTeamTopCaps = useMemo(() => { - if (!selectedTeam) return []; - const caps = computeAgentTeamCapabilities(selectedTeam, allAgents); - return CAPABILITY_CATEGORIES - .filter((category) => caps[category] > 0) - .sort((a, b) => caps[b] - caps[a]) - .slice(0, 3); - }, [allAgents, selectedTeam]); - const resetEditState = useCallback(() => { setToolsEditing(false); setSkillsEditing(false); @@ -226,7 +139,6 @@ const AgentsHomeView: React.FC = () => { }, []); const openAgentDetails = useCallback((agent: AgentWithCapabilities) => { - setSelectedTeamId(null); setSelectedAgentId(agent.id); resetEditState(); }, [resetEditState]); @@ -236,12 +148,6 @@ const AgentsHomeView: React.FC = () => { resetEditState(); }, [resetEditState]); - const openTeamDetails = useCallback((teamId: string) => { - setSelectedAgentId(null); - resetEditState(); - setSelectedTeamId(teamId); - }, [resetEditState]); - return ( { > {t('nav.agents')} -
)} actions={( @@ -423,55 +322,6 @@ const AgentsHomeView: React.FC = () => { ) : null} - - - - {filteredTeams.length} - - )} - > - {filteredTeams.length === 0 ? ( - } - message={agentTeams.length === 0 ? t('teamsZone.empty.noTeams') : t('teamsZone.empty.noMatch')} - /> - ) : ( - - {filteredTeams.map((team, index) => { - const caps = computeAgentTeamCapabilities(team, allAgents); - const topCaps = CAPABILITY_CATEGORIES - .filter((category) => caps[category] > 0) - .sort((a, b) => caps[b] - caps[a]) - .slice(0, 3); - - return ( - openTeamDetails(currentTeam.id)} - topCapabilities={topCaps} - /> - ); - })} - - )} -
{ ) : null} - - setSelectedTeamId(null)} - icon={selectedTeam ? React.createElement( - AGENT_TEAM_ICON_MAP[(selectedTeam.icon ?? 'users') as keyof typeof AGENT_TEAM_ICON_MAP] ?? Users, - { size: 24, strokeWidth: 1.7 }, - ) : } - iconGradient={selectedTeam ? `linear-gradient(135deg, ${getAgentTeamAccent(selectedTeam.id)}33 0%, ${getAgentTeamAccent(selectedTeam.id)}14 100%)` : undefined} - title={selectedTeam?.name ?? ''} - badges={selectedTeam ? ( - <> - {EXAMPLE_TEAM_IDS.has(selectedTeam.id) ? {t('teamCard.badges.example')} : null} - - {selectedTeam.strategy === 'collaborative' - ? t('composer.strategy.collaborative') - : selectedTeam.strategy === 'sequential' - ? t('composer.strategy.sequential') - : t('composer.strategy.free')} - - {selectedTeam.shareContext ? ( - {t('teamCard.badges.sharedContext')} - ) : null} - - ) : null} - description={selectedTeam?.description} - meta={selectedTeam ? {t('home.members', { count: selectedTeam.members.length })} : null} - actions={selectedTeam ? ( - - ) : null} - > - {selectedAgentTeamMembers.length > 0 ? ( -
-
{t('teamCard.sections.members')}
-
- {selectedAgentTeamMembers.map((agent) => { - const member = selectedTeam?.members.find((item) => item.agentId === agent.id); - const roleLabel = - member?.role === 'leader' - ? t('composer.role.leader') - : member?.role === 'reviewer' - ? t('composer.role.reviewer') - : t('composer.role.member'); - const AgentIcon = AGENT_ICON_MAP[(agent.iconKey ?? 'bot') as keyof typeof AGENT_ICON_MAP] ?? Bot; - - return ( - - - {agent.name} - {roleLabel} - - ); - })} -
-
- ) : null} - - {selectedTeamTopCaps.length > 0 ? ( -
-
{t('teamCard.sections.capabilities')}
-
- {selectedTeamTopCaps.map((cap) => ( - - {cap} - - ))} -
-
- ) : null} -
); }; @@ -873,14 +637,6 @@ const AgentsHomeView: React.FC = () => { const AgentsScene: React.FC = () => { const { page } = useAgentsStore(); - if (page === 'editor') { - return ( -
- -
- ); - } - if (page === 'createAgent') { return (
diff --git a/src/web-ui/src/app/scenes/agents/agentsIcons.ts b/src/web-ui/src/app/scenes/agents/agentsIcons.ts index 2a9dc6be..548cfffe 100644 --- a/src/web-ui/src/app/scenes/agents/agentsIcons.ts +++ b/src/web-ui/src/app/scenes/agents/agentsIcons.ts @@ -4,20 +4,16 @@ */ import { Code2, - BarChart2, - LayoutTemplate, - Rocket, FlaskConical, Bug, FileText, Globe, + BarChart2, PenLine, Server, Eye, Layers, Bot, - Users, - Briefcase, Cpu, Terminal, Microscope, @@ -30,38 +26,23 @@ export type AgentIconKey = | 'globe' | 'barchart' | 'layers' | 'penline' | 'server' | 'bot' | 'terminal' | 'microscope' | 'cpu'; -export type AgentTeamIconKey = - | 'code' | 'chart' | 'layout' | 'rocket' - | 'users' | 'briefcase' | 'layers'; - export const AGENT_ICON_MAP: Record> = { - code2: Code2, - eye: Eye, - flask: FlaskConical, - bug: Bug, - filetext: FileText, - globe: Globe, - barchart: BarChart2, - layers: Layers, - penline: PenLine, - server: Server, - bot: Bot, - terminal: Terminal, - microscope: Microscope, - cpu: Cpu, + code2: Code2, + eye: Eye, + flask: FlaskConical, + bug: Bug, + filetext: FileText, + globe: Globe, + barchart: BarChart2, + layers: Layers, + penline: PenLine, + server: Server, + bot: Bot, + terminal: Terminal, + microscope: Microscope, + cpu: Cpu, }; -export const AGENT_TEAM_ICON_MAP: Record> = { - code: Code2, - chart: BarChart2, - layout: LayoutTemplate, - rocket: Rocket, - users: Users, - briefcase: Briefcase, - layers: Layers, -}; - -// Accent color per agent capability (used as CSS color values) export const CAPABILITY_ACCENT: Record = { 编码: '#60a5fa', 文档: '#6eb88c', @@ -70,19 +51,3 @@ export const CAPABILITY_ACCENT: Record = { 创意: '#e879a0', 运维: '#5ea3a3', }; - -// Each agent team has a deterministic accent derived from its id. -const AGENT_TEAM_ACCENTS = [ - '#60a5fa', // blue - '#6eb88c', // green - '#8b5cf6', // purple - '#c9944d', // amber - '#e879a0', // pink - '#5ea3a3', // teal -]; - -export function getAgentTeamAccent(id: string): string { - let hash = 0; - for (let i = 0; i < id.length; i++) hash = (hash * 31 + id.charCodeAt(i)) >>> 0; - return AGENT_TEAM_ACCENTS[hash % AGENT_TEAM_ACCENTS.length]; -} diff --git a/src/web-ui/src/app/scenes/agents/agentsStore.ts b/src/web-ui/src/app/scenes/agents/agentsStore.ts index 0637d711..bf77e605 100644 --- a/src/web-ui/src/app/scenes/agents/agentsStore.ts +++ b/src/web-ui/src/app/scenes/agents/agentsStore.ts @@ -1,52 +1,27 @@ /** - * Agents scene state management + mock data + * Agents scene state management */ import { create } from 'zustand'; import type { SubagentInfo } from '@/infrastructure/api/service-api/SubagentAPI'; -// ─── Types ──────────────────────────────────────────────────────────────────── - -export type MemberRole = 'leader' | 'member' | 'reviewer'; -export type AgentTeamStrategy = 'sequential' | 'collaborative' | 'free'; -export type AgentTeamViewMode = 'formation' | 'list'; - export const CAPABILITY_CATEGORIES = ['编码', '文档', '分析', '测试', '创意', '运维'] as const; export type CapabilityCategory = (typeof CAPABILITY_CATEGORIES)[number]; -/** 'mode' = 主 Agent 模式(如 Agentic/Plan/Debug),'subagent' = 子 Agent */ +/** 'mode' = primary agent mode (e.g. Agentic/Plan/Debug); 'subagent' = sub-agent */ export type AgentKind = 'mode' | 'subagent'; export interface AgentCapability { category: CapabilityCategory; - level: number; // 1-5 + level: number; } export interface AgentWithCapabilities extends SubagentInfo { capabilities: AgentCapability[]; iconKey?: string; - /** 区分 Agent 模式与 Sub-Agent */ + /** Distinguishes primary agent mode from sub-agent */ agentKind?: AgentKind; } -export interface AgentTeamMember { - agentId: string; - role: MemberRole; - modelOverride?: string; - order: number; -} - -export interface AgentTeam { - id: string; - name: string; - icon: string; - description: string; - members: AgentTeamMember[]; - strategy: AgentTeamStrategy; - shareContext: boolean; -} - -// ─── Capability colors ──────────────────────────────────────────────────────── - export const CAPABILITY_COLORS: Record = { 编码: '#60a5fa', 文档: '#6eb88c', @@ -56,293 +31,11 @@ export const CAPABILITY_COLORS: Record = { 运维: '#5ea3a3', }; -// ─── Mock agents ────────────────────────────────────────────────────────────── - -export const MOCK_AGENTS: AgentWithCapabilities[] = [ - { - id: 'mock-code-architect', - name: 'CodeArchitect', - description: '负责系统架构设计、代码结构规划与技术选型,擅长识别设计模式和架构反模式', - isReadonly: true, - toolCount: 14, - defaultTools: ['read_file', 'write_file', 'search_code', 'run_command'], - enabled: true, - subagentSource: 'builtin', - model: 'primary', - iconKey: 'code2', - capabilities: [ - { category: '编码', level: 5 }, - { category: '分析', level: 4 }, - { category: '文档', level: 3 }, - ], - }, - { - id: 'mock-code-reviewer', - name: 'CodeReviewer', - description: '专注代码审查与质量评估,能发现潜在 Bug、安全漏洞及性能瓶颈', - isReadonly: true, - toolCount: 8, - defaultTools: ['read_file', 'search_code', 'list_files'], - enabled: true, - subagentSource: 'builtin', - model: 'primary', - iconKey: 'eye', - capabilities: [ - { category: '编码', level: 4 }, - { category: '测试', level: 3 }, - { category: '分析', level: 4 }, - ], - }, - { - id: 'mock-test-gen', - name: 'TestGenerator', - description: '自动生成单元测试、集成测试用例,提升代码覆盖率,支持多种测试框架', - isReadonly: true, - toolCount: 10, - defaultTools: ['read_file', 'write_file', 'run_command', 'search_code'], - enabled: true, - subagentSource: 'builtin', - model: 'fast', - iconKey: 'flask', - capabilities: [ - { category: '测试', level: 5 }, - { category: '编码', level: 3 }, - { category: '分析', level: 2 }, - ], - }, - { - id: 'mock-debugger', - name: 'Debugger', - description: '精准定位程序错误,分析堆栈跟踪,给出修复建议,支持多种运行时环境', - isReadonly: true, - toolCount: 12, - defaultTools: ['read_file', 'run_command', 'search_code', 'search_files'], - enabled: true, - subagentSource: 'builtin', - model: 'primary', - iconKey: 'bug', - capabilities: [ - { category: '编码', level: 5 }, - { category: '测试', level: 4 }, - { category: '分析', level: 3 }, - ], - }, - { - id: 'mock-documentor', - name: 'Documentor', - description: '自动生成项目文档、API 文档与注释,保持文档与代码同步更新', - isReadonly: true, - toolCount: 6, - defaultTools: ['read_file', 'write_file', 'list_files'], - enabled: true, - subagentSource: 'builtin', - model: 'fast', - iconKey: 'filetext', - capabilities: [ - { category: '文档', level: 5 }, - { category: '分析', level: 3 }, - { category: '创意', level: 2 }, - ], - }, - { - id: 'mock-researcher', - name: 'Researcher', - description: '深度信息检索与知识整合,擅长从海量资料中提炼关键洞察与数据支撑', - isReadonly: true, - toolCount: 9, - defaultTools: ['web_search', 'read_file', 'write_file'], - enabled: true, - subagentSource: 'builtin', - model: 'primary', - iconKey: 'globe', - capabilities: [ - { category: '分析', level: 5 }, - { category: '文档', level: 4 }, - { category: '创意', level: 2 }, - ], - }, - { - id: 'mock-data-analyst', - name: 'DataAnalyst', - description: '数据清洗、统计分析与可视化,擅长发现数据规律并生成分析报告', - isReadonly: true, - toolCount: 11, - defaultTools: ['read_file', 'run_command', 'write_file', 'web_search'], - enabled: true, - subagentSource: 'builtin', - model: 'primary', - iconKey: 'barchart', - capabilities: [ - { category: '分析', level: 5 }, - { category: '文档', level: 4 }, - { category: '编码', level: 2 }, - ], - }, - { - id: 'mock-content-planner', - name: 'ContentPlanner', - description: '内容结构规划与大纲设计,擅长将复杂主题转化为清晰易懂的内容框架', - isReadonly: true, - toolCount: 5, - defaultTools: ['read_file', 'write_file', 'web_search'], - enabled: true, - subagentSource: 'builtin', - model: 'fast', - iconKey: 'layers', - capabilities: [ - { category: '创意', level: 5 }, - { category: '文档', level: 4 }, - { category: '分析', level: 3 }, - ], - }, - { - id: 'mock-copywriter', - name: 'Copywriter', - description: '专业文案撰写与优化,擅长多种文体风格,能够精准传递品牌价值', - isReadonly: true, - toolCount: 4, - defaultTools: ['read_file', 'write_file'], - enabled: true, - subagentSource: 'builtin', - model: 'fast', - iconKey: 'penline', - capabilities: [ - { category: '创意', level: 4 }, - { category: '文档', level: 5 }, - { category: '分析', level: 2 }, - ], - }, - { - id: 'mock-ops-agent', - name: 'OpsAgent', - description: '自动化运维任务执行,监控系统状态,处理部署流程和环境配置', - isReadonly: true, - toolCount: 16, - defaultTools: ['run_command', 'read_file', 'write_file', 'search_files'], - enabled: false, - subagentSource: 'builtin', - model: 'fast', - iconKey: 'server', - capabilities: [ - { category: '运维', level: 5 }, - { category: '编码', level: 3 }, - { category: '分析', level: 2 }, - ], - }, -]; - -// ─── Mock agent teams with pre-seeded members ───────────────────────────────── - -export const MOCK_AGENT_TEAMS: AgentTeam[] = [ - { - id: 'agent-team-coding', - name: '编码团队', - icon: 'code', - description: '代码审查、重构与质量保障', - members: [ - { agentId: 'mock-code-architect', role: 'leader', order: 0 }, - { agentId: 'mock-code-reviewer', role: 'member', order: 1 }, - { agentId: 'mock-debugger', role: 'member', order: 2 }, - { agentId: 'mock-test-gen', role: 'reviewer', order: 3 }, - ], - strategy: 'collaborative', - shareContext: true, - }, - { - id: 'agent-team-research', - name: '调研团队', - icon: 'chart', - description: '信息搜集、数据分析与报告撰写', - members: [ - { agentId: 'mock-researcher', role: 'leader', order: 0 }, - { agentId: 'mock-data-analyst', role: 'member', order: 1 }, - { agentId: 'mock-documentor', role: 'reviewer', order: 2 }, - ], - strategy: 'sequential', - shareContext: true, - }, - { - id: 'agent-team-ppt', - name: 'PPT 制作', - icon: 'layout', - description: '内容策划、视觉设计与文案润色', - members: [ - { agentId: 'mock-content-planner', role: 'leader', order: 0 }, - { agentId: 'mock-copywriter', role: 'member', order: 1 }, - ], - strategy: 'collaborative', - shareContext: false, - }, -]; - -// ─── Agent team templates (for "use template" quick start) ──────────────────── - -export const AGENT_TEAM_TEMPLATES: Array<{ - id: string; - name: string; - icon: string; - description: string; - memberIds: string[]; -}> = [ - { - id: 'tpl-coding', - name: '编码团队', - icon: 'code', - description: '代码审查、重构与质量保障', - memberIds: ['mock-code-architect', 'mock-code-reviewer', 'mock-debugger', 'mock-test-gen'], - }, - { - id: 'tpl-research', - name: '调研团队', - icon: 'chart', - description: '信息搜集、数据分析与报告撰写', - memberIds: ['mock-researcher', 'mock-data-analyst', 'mock-documentor'], - }, - { - id: 'tpl-ppt', - name: 'PPT 制作', - icon: 'layout', - description: '内容策划、文案与视觉规划', - memberIds: ['mock-content-planner', 'mock-copywriter'], - }, - { - id: 'tpl-fullstack', - name: '全栈团队', - icon: 'rocket', - description: '全流程开发、测试与文档', - memberIds: ['mock-code-architect', 'mock-debugger', 'mock-test-gen', 'mock-documentor'], - }, -]; - -// ─── Helper: compute agent team capability coverage ──────────────────────────── - -export function computeAgentTeamCapabilities( - team: AgentTeam, - allAgents: AgentWithCapabilities[], -): Record { - const result: Record = { - 编码: 0, 文档: 0, 分析: 0, 测试: 0, 创意: 0, 运维: 0, - }; - for (const member of team.members) { - const agent = allAgents.find((a) => a.id === member.agentId); - if (!agent) continue; - for (const cap of agent.capabilities) { - result[cap.category] = Math.max(result[cap.category], cap.level); - } - } - return result; -} - -// ─── Scene page ─────────────────────────────────────────────────────────────── - -export type AgentsScenePage = 'home' | 'editor' | 'createAgent'; +export type AgentsScenePage = 'home' | 'createAgent'; export type AgentFilterLevel = 'all' | 'builtin' | 'user' | 'project'; export type AgentFilterType = 'all' | 'mode' | 'subagent'; -// ─── Store ──────────────────────────────────────────────────────────────────── - interface AgentsStoreState { - // Scene navigation page: AgentsScenePage; searchQuery: string; agentFilterLevel: AgentFilterLevel; @@ -352,23 +45,9 @@ interface AgentsStoreState { setAgentFilterLevel: (filter: AgentFilterLevel) => void; setAgentFilterType: (filter: AgentFilterType) => void; openHome: () => void; - openAgentTeamEditor: (teamId: string) => void; openCreateAgent: () => void; agentSoloEnabled: Record; setAgentSoloEnabled: (agentId: string, enabled: boolean) => void; - - agentTeams: AgentTeam[]; - activeAgentTeamId: string | null; - viewMode: AgentTeamViewMode; - - setActiveAgentTeam: (id: string | null) => void; - setViewMode: (mode: AgentTeamViewMode) => void; - addAgentTeam: (team: Omit) => void; - updateAgentTeam: (id: string, patch: Partial>) => void; - deleteAgentTeam: (id: string) => void; - addMember: (teamId: string, agentId: string, role?: MemberRole) => void; - removeMember: (teamId: string, agentId: string) => void; - updateMemberRole: (teamId: string, agentId: string, role: MemberRole) => void; } export const useAgentsStore = create((set) => ({ @@ -381,7 +60,6 @@ export const useAgentsStore = create((set) => ({ setAgentFilterLevel: (filter) => set({ agentFilterLevel: filter }), setAgentFilterType: (filter) => set({ agentFilterType: filter }), openHome: () => set({ page: 'home' }), - openAgentTeamEditor: (teamId) => set({ page: 'editor', activeAgentTeamId: teamId }), openCreateAgent: () => set({ page: 'createAgent' }), agentSoloEnabled: {}, setAgentSoloEnabled: (agentId, enabled) => @@ -391,56 +69,4 @@ export const useAgentsStore = create((set) => ({ [agentId]: enabled, }, })), - - agentTeams: MOCK_AGENT_TEAMS, - activeAgentTeamId: MOCK_AGENT_TEAMS[0].id, - viewMode: 'formation', - - setActiveAgentTeam: (id) => set({ activeAgentTeamId: id }), - setViewMode: (mode) => set({ viewMode: mode }), - - addAgentTeam: (team) => { - const newAgentTeam: AgentTeam = { ...team, members: [] }; - set((s) => ({ agentTeams: [...s.agentTeams, newAgentTeam], activeAgentTeamId: newAgentTeam.id })); - }, - - updateAgentTeam: (id, patch) => - set((s) => ({ - agentTeams: s.agentTeams.map((t) => (t.id === id ? { ...t, ...patch } : t)), - })), - - deleteAgentTeam: (id) => - set((s) => { - const next = s.agentTeams.filter((t) => t.id !== id); - const activeId = s.activeAgentTeamId === id ? (next[0]?.id ?? null) : s.activeAgentTeamId; - return { agentTeams: next, activeAgentTeamId: activeId }; - }), - - addMember: (teamId, agentId, role = 'member') => - set((s) => ({ - agentTeams: s.agentTeams.map((t) => { - if (t.id !== teamId) return t; - if (t.members.some((m) => m.agentId === agentId)) return t; - const newMember: AgentTeamMember = { agentId, role, order: t.members.length }; - return { ...t, members: [...t.members, newMember] }; - }), - })), - - removeMember: (teamId, agentId) => - set((s) => ({ - agentTeams: s.agentTeams.map((t) => - t.id === teamId - ? { ...t, members: t.members.filter((m) => m.agentId !== agentId) } - : t, - ), - })), - - updateMemberRole: (teamId, agentId, role) => - set((s) => ({ - agentTeams: s.agentTeams.map((t) => - t.id === teamId - ? { ...t, members: t.members.map((m) => (m.agentId === agentId ? { ...m, role } : m)) } - : t, - ), - })), })); diff --git a/src/web-ui/src/app/scenes/agents/components/AgentGallery.scss b/src/web-ui/src/app/scenes/agents/components/AgentGallery.scss deleted file mode 100644 index b396f5ab..00000000 --- a/src/web-ui/src/app/scenes/agents/components/AgentGallery.scss +++ /dev/null @@ -1,333 +0,0 @@ -@use '../../../../component-library/styles/tokens' as *; - -// ─── Gallery container ──────────────────────────────────────────────────────── -.ag { - display: flex; - flex-direction: column; - height: 100%; - overflow: hidden; - border-right: 1px solid var(--border-subtle); - - // ── Search ────────────────────────────────────────────────────────────── - &__search-bar { - position: relative; - flex-shrink: 0; - padding: $size-gap-3 $size-gap-4; - border-bottom: 1px solid var(--border-subtle); - } - - &__search-ico { - position: absolute; - left: calc(#{$size-gap-4} + 10px); - top: 50%; - transform: translateY(-50%); - color: var(--color-text-disabled); - pointer-events: none; - } - - &__search-input { - width: 100%; - padding: 7px $size-gap-3 7px 30px; - background: var(--element-bg-subtle); - border: 1px solid var(--border-subtle); - border-radius: $size-radius-sm; - color: var(--color-text-primary); - font-size: $font-size-xs; - outline: none; - box-sizing: border-box; - font-family: $font-family-sans; - transition: border-color $motion-fast $easing-standard; - - &::placeholder { color: var(--color-text-disabled); } - &:focus { border-color: var(--border-medium); } - } - - // ── Filters ───────────────────────────────────────────────────────────── - &__filters { - flex-shrink: 0; - display: flex; - flex-wrap: wrap; - gap: 5px; - padding: $size-gap-2 $size-gap-4; - border-bottom: 1px solid var(--border-subtle); - } - - &__pill { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 3px 9px; - border: 1px solid var(--border-subtle); - border-radius: 3px; - background: transparent; - color: var(--color-text-muted); - font-size: $font-size-xs; - cursor: pointer; - transition: all $motion-fast $easing-standard; - font-family: $font-family-sans; - line-height: 1.4; - - &:hover:not(.is-active) { - background: var(--element-bg-subtle); - color: var(--color-text-secondary); - } - - &.is-active { - background: var(--element-bg-subtle); - } - } - - &__pill-n { - font-size: 10px; - opacity: 0.55; - margin-left: 1px; - } - - // ── List ──────────────────────────────────────────────────────────────── - &__list { - flex: 1; - overflow-y: auto; - padding: $size-gap-3; - display: flex; - flex-direction: column; - gap: $size-gap-3; - - &::-webkit-scrollbar { width: 3px; } - &::-webkit-scrollbar-track { background: transparent; } - &::-webkit-scrollbar-thumb { background: var(--border-subtle); border-radius: 2px; } - } - - &__empty { - display: flex; - align-items: center; - justify-content: center; - padding: 40px 0; - color: var(--color-text-disabled); - font-size: $font-size-xs; - } - - // ── Card ───────────────────────────────────────────────────────────────── - &__footer { - flex-shrink: 0; - padding: 7px $size-gap-4; - font-size: $font-size-xs; - color: var(--color-text-disabled); - border-top: 1px solid var(--border-subtle); - text-align: center; - letter-spacing: 0.2px; - } -} - -// ─── Agent card ─────────────────────────────────────────────────────────────── -.ag-card { - flex-shrink: 0; - border-radius: $size-radius-sm; - border: 1px solid var(--border-subtle); - background: var(--element-bg-subtle); - transition: border-color $motion-fast $easing-standard, background $motion-fast $easing-standard; - - &:hover { - background: var(--element-bg-base); - border-color: var(--border-base); - } - - &.is-member { - background: color-mix(in srgb, var(--color-primary) 5%, transparent); - border-color: color-mix(in srgb, var(--color-primary) 22%, transparent); - } - - &.is-disabled { opacity: 0.45; } - - // ── Summary row ───────────────────────────────────────────────────────── - &__row { - display: flex; - align-items: flex-start; - gap: $size-gap-3; - padding: $size-gap-4; - cursor: pointer; - } - - &__icon { - flex-shrink: 0; - margin-top: 2px; - width: 32px; - height: 32px; - border-radius: $size-radius-sm; - border: 1px solid; - display: flex; - align-items: center; - justify-content: center; - } - - // ── Meta block ────────────────────────────────────────────────────────── - &__meta { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: $size-gap-2; - } - - &__name { - font-size: $font-size-sm; - font-weight: $font-weight-semibold; - color: var(--color-text-primary); - line-height: 1.4; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - &__desc { - font-size: $font-size-xs; - color: var(--color-text-muted); - line-height: 1.6; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - word-break: break-word; - } - - &__name-row { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 5px; - } - - &__badge { - display: inline-block; - font-size: $font-size-xs; - padding: 2px 7px; - border: 1px solid var(--border-subtle); - border-radius: 3px; - color: var(--color-text-muted); - line-height: 1.4; - flex-shrink: 0; - - &--dim { color: var(--color-text-disabled); border-color: transparent; } - } - - // ── Right controls ──────────────────────────────────────────────────── - &__actions { - flex-shrink: 0; - margin-top: 2px; - } - - &__add { - display: flex; - align-items: center; - justify-content: center; - width: 26px; - height: 26px; - border: 1px solid var(--border-subtle); - border-radius: $size-radius-sm; - background: transparent; - color: var(--color-text-muted); - cursor: pointer; - transition: all $motion-fast $easing-standard; - - &:hover { border-color: var(--color-primary); color: var(--color-primary); } - - &.is-added { - border-color: var(--color-success); - color: var(--color-success); - background: color-mix(in srgb, var(--color-success) 8%, transparent); - } - } - - &__chevron { - flex-shrink: 0; - margin-top: 6px; - color: var(--color-text-disabled); - display: flex; - align-items: center; - } - - // ── Expanded detail ──────────────────────────────────────────────────── - &__detail { - padding: $size-gap-4; - display: flex; - flex-direction: column; - gap: $size-gap-3; - border-top: 1px solid var(--border-subtle); - background: color-mix(in srgb, var(--element-bg-subtle) 60%, transparent); - animation: ag-expand $motion-fast $easing-decelerate; - } - - &__detail-meta { - display: flex; - flex-wrap: wrap; - gap: $size-gap-1 $size-gap-4; - font-size: $font-size-xs; - color: var(--color-text-muted); - } - - &__add-full { - align-self: flex-end; - padding: 5px 14px; - border: 1px solid var(--border-subtle); - border-radius: 3px; - background: transparent; - color: var(--color-text-muted); - font-size: $font-size-xs; - cursor: pointer; - transition: all $motion-fast $easing-standard; - font-family: $font-family-sans; - - &:hover { border-color: var(--color-primary); color: var(--color-primary); } - - &.is-added { - border-color: var(--color-success); - color: var(--color-success); - } - } -} - -// ─── Capability bars ────────────────────────────────────────────────────────── -.ag-cap-bars { - display: flex; - flex-direction: column; - gap: 6px; - padding-top: $size-gap-3; -} - -.ag-cap-bar { - display: flex; - align-items: center; - gap: $size-gap-2; -} - -.ag-cap-label { - font-size: 10px; - color: var(--color-text-muted); - width: 26px; - flex-shrink: 0; -} - -.ag-cap-track { - display: flex; - gap: 2px; -} - -.ag-cap-seg { - width: 10px; - height: 3px; - border-radius: 1px; - background: var(--element-bg-medium); - transition: background $motion-fast $easing-standard; -} - -.ag-cap-level { - font-size: 10px; - color: var(--color-text-disabled); - width: 22px; - text-align: right; - flex-shrink: 0; -} - -@keyframes ag-expand { - from { opacity: 0; } - to { opacity: 1; } -} diff --git a/src/web-ui/src/app/scenes/agents/components/AgentGallery.tsx b/src/web-ui/src/app/scenes/agents/components/AgentGallery.tsx deleted file mode 100644 index c4028c7b..00000000 --- a/src/web-ui/src/app/scenes/agents/components/AgentGallery.tsx +++ /dev/null @@ -1,303 +0,0 @@ -import React, { useState, useCallback, useEffect } from 'react'; -import { Search, ChevronDown, ChevronUp, Plus, Check, Bot, Cpu } from 'lucide-react'; -import { useTranslation } from 'react-i18next'; -import { Badge } from '@/component-library'; -import { agentAPI } from '@/infrastructure/api/service-api/AgentAPI'; -import { SubagentAPI } from '@/infrastructure/api/service-api/SubagentAPI'; -import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext'; -import { - useAgentsStore, - CAPABILITY_CATEGORIES, - CAPABILITY_COLORS, - type AgentWithCapabilities, - type CapabilityCategory, -} from '../agentsStore'; -import { AGENT_ICON_MAP } from '../agentsIcons'; -import { enrichCapabilities, getAgentBadge } from '../utils'; -import './AgentGallery.scss'; - -// ─── Agent icon ─────────────────────────────────────────────────────────────── - -const AgentIcon: React.FC<{ iconKey?: string; primaryCap?: string; size?: number }> = ({ - iconKey, - primaryCap, - size = 14, -}) => { - const color = primaryCap ? CAPABILITY_COLORS[primaryCap as CapabilityCategory] : 'var(--color-text-muted)'; - const key = (iconKey ?? 'bot') as keyof typeof AGENT_ICON_MAP; - const IconComp = AGENT_ICON_MAP[key] ?? Bot; - return ; -}; - -// ─── Capability bars ────────────────────────────────────────────────────────── - -const CapBars: React.FC<{ caps: AgentWithCapabilities['capabilities'] }> = ({ caps }) => ( -
- {caps.map((c) => ( -
- {c.category} -
- {Array.from({ length: 5 }, (_, i) => ( - - ))} -
- {c.level}/5 -
- ))} -
-); - -// ─── Agent card ─────────────────────────────────────────────────────────────── - -interface AgentCardProps { - agent: AgentWithCapabilities; - isMember: boolean; - onAdd: () => void; - onRemove: () => void; -} - -const AgentCard: React.FC = ({ agent, isMember, onAdd, onRemove }) => { - const { t } = useTranslation('scenes/agents'); - const [expanded, setExpanded] = useState(false); - const primaryCap = agent.capabilities[0]?.category; - const badge = getAgentBadge(t, agent.agentKind, agent.subagentSource); - - return ( -
- {/* ── Summary row ── */} -
setExpanded((v) => !v)}> - {/* Icon cell */} -
- -
- - {/* Meta */} -
- {agent.name} - {agent.description} -
- {!agent.enabled && 已禁用} - {/* Agent kind badge */} - - {agent.agentKind === 'mode' ? : } - {badge.label} - - {/* Capability chips */} - {agent.capabilities.slice(0, 2).map((c) => ( - - {c.category} - - ))} -
-
- - {/* Actions */} -
e.stopPropagation()}> - -
- - - {expanded ? : } - -
- - {/* ── Expanded detail ── */} - {expanded && ( -
- -
- {t('gallery.toolCount', '{{count}} 个工具', { count: agent.toolCount })} - {agent.model && {t('gallery.modelLabel', '模型')} · {agent.model}} - - {agent.agentKind === 'mode' ? : } - {badge.label} - -
- -
- )} -
- ); -}; - -// ─── Gallery ────────────────────────────────────────────────────────────────── - -const AgentGallery: React.FC = () => { - const { t } = useTranslation('scenes/agents'); - const { agentTeams, activeAgentTeamId, addMember, removeMember } = useAgentsStore(); - const { workspacePath } = useCurrentWorkspace(); - const [agents, setAgents] = useState([]); - const [query, setQuery] = useState(''); - const [activeCategories, setActiveCategories] = useState>(new Set()); - const [showMembersOnly, setShowMembersOnly] = useState(false); - - const activeTeam = agentTeams.find((t) => t.id === activeAgentTeamId); - const memberIds = new Set(activeTeam?.members.map((m) => m.agentId) ?? []); - - useEffect(() => { - let cancelled = false; - - Promise.all([ - agentAPI.getAvailableModes().catch(() => []), - SubagentAPI.listSubagents({ workspacePath: workspacePath || undefined }).catch(() => []), - ]).then(([modes, subagents]) => { - if (cancelled) return; - - const modeAgents: AgentWithCapabilities[] = modes.map((m) => - enrichCapabilities({ - id: m.id, - name: m.name, - description: m.description, - isReadonly: m.isReadonly, - toolCount: m.toolCount, - defaultTools: m.defaultTools ?? [], - enabled: m.enabled, - capabilities: [], - agentKind: 'mode', - }) - ); - - const subAgents: AgentWithCapabilities[] = subagents.map((s) => - enrichCapabilities({ - ...s, - capabilities: [], - agentKind: 'subagent', - }) - ); - - setAgents([...modeAgents, ...subAgents]); - }); - - return () => { cancelled = true; }; - }, [workspacePath]); - - const toggleCategory = useCallback((cat: CapabilityCategory) => { - setActiveCategories((prev) => { - const next = new Set(prev); - if (next.has(cat)) next.delete(cat); - else next.add(cat); - return next; - }); - }, []); - - const filtered = agents.filter((a) => { - if (showMembersOnly && !memberIds.has(a.id)) return false; - if (query) { - const q = query.toLowerCase(); - if (!a.name.toLowerCase().includes(q) && !a.description.toLowerCase().includes(q)) return false; - } - if (activeCategories.size > 0) { - const agentCats = new Set(a.capabilities.map((c) => c.category)); - if (![...activeCategories].some((c) => agentCats.has(c))) return false; - } - return true; - }); - - const categoryCounts = CAPABILITY_CATEGORIES.reduce>((acc, cat) => { - acc[cat] = agents.filter((a) => a.capabilities.some((c) => c.category === cat)).length; - return acc; - }, {}); - - return ( -
- {/* ── Search bar ── */} -
- - setQuery(e.target.value)} - /> -
- - {/* ── Filter pills ── */} -
- - {CAPABILITY_CATEGORIES.map((cat) => ( - - ))} -
- - {/* ── List ── */} -
- {filtered.length === 0 ? ( -
{t('gallery.empty')}
- ) : ( - filtered.map((agent) => ( - activeAgentTeamId && addMember(activeAgentTeamId, agent.id)} - onRemove={() => activeAgentTeamId && removeMember(activeAgentTeamId, agent.id)} - /> - )) - )} -
- - {/* ── Footer ── */} -
- {t('gallery.footer', { - shown: filtered.length, - total: agents.length, - enabled: agents.filter((a) => a.enabled).length, - })} -
-
- ); -}; - -export default AgentGallery; diff --git a/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.scss b/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.scss deleted file mode 100644 index 8d25c2a4..00000000 --- a/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.scss +++ /dev/null @@ -1,331 +0,0 @@ -@use '../../../../component-library/styles/tokens' as *; - -.agent-team-card { - width: 360px; - height: 200px; - border-radius: 15px; - background: var(--element-bg-soft); - display: flex; - flex-direction: column; - position: relative; - overflow: hidden; - cursor: pointer; - animation: agent-team-card-in 0.22s $easing-decelerate both; - animation-delay: calc(var(--card-index, 0) * 35ms); - transition: - transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), - box-shadow 0.35s ease; - - // Top gradient overlay - shows on hover, covers entire card except footer - &::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 40px; - background: var(--agent-team-card-gradient); - opacity: 0; - transition: opacity 0.35s ease; - pointer-events: none; - z-index: 0; - } - - &:hover { - transform: translateY(-4px) scale(1.02); - box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2); - - &::before { - opacity: 0.4; - } - } - - &:focus-visible { - outline: 2px solid var(--color-accent-500); - outline-offset: 2px; - } - - // ── Header with icon ── - &__header { - display: flex; - align-items: center; - gap: $size-gap-3; - padding: $size-gap-3; - padding-bottom: 0; - position: relative; - z-index: 1; - } - - &__icon-area { - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - border-radius: 10px; - background: rgba(255, 255, 255, 0.12); - backdrop-filter: blur(8px); - } - - &__icon { - color: var(--agent-team-card-accent); - } - - &__header-info { - flex: 1; - min-width: 0; - display: flex; - align-items: center; - justify-content: space-between; - gap: $size-gap-2; - } - - &__name { - font-size: 1.2em; - font-weight: 900; - color: var(--color-text-primary); - line-height: $line-height-base; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - &__actions { - display: flex; - align-items: center; - } - - &__icon-btn { - width: 26px; - height: 26px; - display: inline-flex; - align-items: center; - justify-content: center; - border: none; - border-radius: $size-radius-sm; - background: rgba(255, 255, 255, 0.1); - color: var(--color-text-secondary); - cursor: pointer; - transition: - background $motion-fast $easing-standard, - color $motion-fast $easing-standard; - - &:hover { - background: rgba(255, 255, 255, 0.25); - color: var(--color-text-primary); - } - } - - // ── Body ── - &__body { - flex: 1; - padding: $size-gap-2 $size-gap-3; - display: flex; - flex-direction: column; - gap: 4px; - overflow: hidden; - position: relative; - z-index: 1; - } - - &__desc { - margin: 0; - font-size: 0.85em; - font-weight: 300; - color: rgba(var(--color-text-secondary), 0.85); - line-height: $line-height-relaxed; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - word-break: break-word; - } - - &__meta { - display: flex; - align-items: center; - gap: $size-gap-1; - flex-wrap: wrap; - margin-top: auto; - padding-top: $size-gap-2; - color: var(--color-text-muted); - font-size: $font-size-xs; - line-height: $line-height-base; - } - - &__meta-item { - white-space: nowrap; - } - - &__avatars { - display: inline-flex; - - > * + * { - margin-left: -4px; - } - } - - &__avatar { - width: 18px; - height: 18px; - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 50%; - border: 1px solid var(--border-subtle); - background: var(--element-bg-base); - color: var(--color-text-muted); - flex-shrink: 0; - - &--more { - font-size: 9px; - font-weight: $font-weight-semibold; - } - } - - &__cap-chips { - display: inline-flex; - align-items: center; - gap: 4px; - flex-wrap: wrap; - } - - &__cap-chip { - display: inline-flex; - align-items: center; - padding: 2px 8px; - border-radius: $size-radius-full; - border: 1px solid; - background: rgba(255, 255, 255, 0.04); - font-size: 10px; - font-weight: $font-weight-medium; - white-space: nowrap; - } - - &__state-badges { - display: inline-flex; - align-items: center; - gap: 4px; - flex-wrap: wrap; - margin-left: auto; - padding: $size-gap-2 $size-gap-3; - } - - // ── Footer for badges ── - &__footer { - display: flex; - align-items: center; - width: 100%; - border-radius: 0 0 15px 15px; - overflow: hidden; - position: relative; - z-index: 1; - - // Bottom gradient blur background matching card color - &::after { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: var(--agent-team-card-gradient); - opacity: 0.5; - transition: opacity 0.35s ease; - pointer-events: none; - } - } - - &:hover &__footer::after { - opacity: 1; - } -} - -// ── Responsive ── -@media (max-width: 720px) { - .agent-team-card { - width: 100%; - min-height: 180px; - - &__header { - flex-direction: column; - } - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// Detail modal content styles (rendered inside GalleryDetailModal) -// These classes are used in AgentsScene.tsx for the team detail popup -// ───────────────────────────────────────────────────────────────────────────── - -.agent-team-card { - &__section { - display: flex; - flex-direction: column; - gap: $size-gap-2; - } - - &__section-title { - font-size: $font-size-xs; - font-weight: $font-weight-semibold; - color: var(--color-text-secondary); - text-transform: uppercase; - letter-spacing: 0.5px; - } - - &__member-list { - display: flex; - flex-wrap: wrap; - gap: $size-gap-1; - } - - &__member-chip { - display: inline-flex; - align-items: center; - gap: $size-gap-1; - padding: 4px 10px; - border-radius: $size-radius-full; - border: 1px solid var(--border-subtle); - background: var(--element-bg-subtle); - font-size: 11px; - color: var(--color-text-secondary); - white-space: nowrap; - } - - &__member-name { - font-weight: $font-weight-medium; - color: var(--color-text-primary); - } - - &__member-role { - font-size: 10px; - color: var(--color-text-muted); - padding-left: 2px; - - &::before { - content: "·"; - margin-right: 2px; - } - } -} - -// ── Animations ── -@keyframes agent-team-card-in { - from { - opacity: 0; - transform: translateY(10px) scale(0.98); - } - - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -@media (prefers-reduced-motion: reduce) { - .agent-team-card { - animation: none; - transition: none; - } -} diff --git a/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.tsx b/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.tsx deleted file mode 100644 index a006a83c..00000000 --- a/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import React from 'react'; -import { Bot, Pencil, Users } from 'lucide-react'; -import { useTranslation } from 'react-i18next'; -import { Badge } from '@/component-library'; -import type { AgentTeam, AgentWithCapabilities } from '../agentsStore'; -import { AGENT_ICON_MAP, CAPABILITY_ACCENT, AGENT_TEAM_ICON_MAP, getAgentTeamAccent } from '../agentsIcons'; -import './AgentTeamCard.scss'; - -interface AgentTeamCardProps { - team: AgentTeam; - allAgents: AgentWithCapabilities[]; - index?: number; - isExample?: boolean; - onEdit: (teamId: string) => void; - onOpenDetails: (team: AgentTeam) => void; - topCapabilities: string[]; -} - -const AgentTeamCard: React.FC = ({ - team, - allAgents, - index = 0, - isExample = false, - onEdit, - onOpenDetails, - topCapabilities, -}) => { - const { t } = useTranslation('scenes/agents'); - const Icon = AGENT_TEAM_ICON_MAP[team.icon as keyof typeof AGENT_TEAM_ICON_MAP] ?? Users; - const accent = getAgentTeamAccent(team.id); - const memberAgents = team.members - .map((member) => allAgents.find((agent) => agent.id === member.agentId)) - .filter(Boolean) as AgentWithCapabilities[]; - - const strategyLabel = - team.strategy === 'collaborative' - ? t('composer.strategy.collaborative') - : team.strategy === 'sequential' - ? t('composer.strategy.sequential') - : t('composer.strategy.free'); - - const openDetails = () => onOpenDetails(team); - - return ( -
e.key === 'Enter' && openDetails()} - aria-label={team.name} - > - {/* Header: icon + name */} -
-
-
- -
-
-
- {team.name} -
e.stopPropagation()}> - -
-
-
- - {/* Body: description + meta */} -
-

{team.description?.trim() || '—'}

- -
-
- {memberAgents.slice(0, 4).map((agent) => { - const AgentIcon = AGENT_ICON_MAP[(agent.iconKey ?? 'bot') as keyof typeof AGENT_ICON_MAP] ?? Bot; - return ( - - - - ); - })} - {team.members.length > 4 ? ( - - +{team.members.length - 4} - - ) : null} -
- - {t('home.members', { count: team.members.length })} - - {topCapabilities.length > 0 ? ( -
- {topCapabilities.map((cap) => ( - - {cap} - - ))} -
- ) : null} -
-
- - {/* Footer: badges */} -
-
- {isExample ? {t('teamCard.badges.example', '示例')} : null} - {strategyLabel} - {team.shareContext ? ( - {t('teamCard.badges.sharedContext', '共享上下文')} - ) : null} -
-
-
- ); -}; - -export default AgentTeamCard; diff --git a/src/web-ui/src/app/scenes/agents/components/AgentTeamComposer.scss b/src/web-ui/src/app/scenes/agents/components/AgentTeamComposer.scss deleted file mode 100644 index 2fdc14cf..00000000 --- a/src/web-ui/src/app/scenes/agents/components/AgentTeamComposer.scss +++ /dev/null @@ -1,473 +0,0 @@ -@use '../../../../component-library/styles/tokens' as *; - -// ─── Composer shell ─────────────────────────────────────────────────────────── -.tc { - display: flex; - flex-direction: column; - height: 100%; - overflow: hidden; - - &--empty { - align-items: center; - justify-content: center; - color: var(--color-text-disabled); - font-size: $font-size-sm; - } - - // ── Compact bar (replaces header + toolbar) ──────────────────────────── - &__bar { - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: space-between; - gap: $size-gap-4; - padding: 8px $size-gap-4; - border-bottom: 1px solid var(--border-subtle); - min-height: 0; - } - - &__bar-left { - display: flex; - align-items: center; - gap: $size-gap-2; - min-width: 0; - flex-shrink: 1; - } - - &__name { - font-size: $font-size-sm; - font-weight: $font-weight-semibold; - color: var(--color-text-primary); - cursor: pointer; - border-bottom: 1px dashed transparent; - transition: border-color $motion-fast $easing-standard; - white-space: nowrap; - - &:hover { border-bottom-color: var(--border-medium); } - } - - &__name-input { - font-size: $font-size-sm; - font-weight: $font-weight-semibold; - color: var(--color-text-primary); - background: transparent; - border: none; - border-bottom: 1px solid var(--color-primary); - outline: none; - font-family: $font-family-sans; - padding: 0; - width: 120px; - } - - &__sep { - color: var(--color-text-disabled); - font-size: $font-size-xs; - flex-shrink: 0; - } - - &__meta { - font-size: $font-size-xs; - color: var(--color-text-muted); - white-space: nowrap; - flex-shrink: 0; - } - - &__bar-right { - display: flex; - align-items: center; - gap: $size-gap-3; - flex-shrink: 0; - } - - &__bar-sep { - width: 1px; - height: 14px; - background: var(--border-subtle); - flex-shrink: 0; - } - - // ── View toggle ──────────────────────────────────────────────────────── - &__toggle { - display: flex; - gap: 1px; - background: var(--element-bg-subtle); - border: 1px solid var(--border-subtle); - border-radius: 4px; - padding: 2px; - } - - &__toggle-btn { - display: inline-flex; - align-items: center; - gap: 3px; - padding: 3px 8px; - border: none; - border-radius: 3px; - background: transparent; - color: var(--color-text-muted); - font-size: $font-size-xs; - cursor: pointer; - transition: all $motion-fast $easing-standard; - font-family: $font-family-sans; - - &.is-on { - background: var(--element-bg-elevated); - color: var(--color-text-primary); - } - - &:not(.is-on):hover { color: var(--color-text-secondary); } - } - - // ── Role legend ──────────────────────────────────────────────────────── - &__legend { - display: flex; - gap: $size-gap-3; - } - - &__legend-item { - display: inline-flex; - align-items: center; - gap: 4px; - font-size: $font-size-xs; - color: var(--color-text-muted); - } - - &__legend-dot { - width: 5px; - height: 5px; - border-radius: 50%; - flex-shrink: 0; - } - - // ── Body ──────────────────────────────────────────────────────────────── - &__body { - flex: 1; - overflow: visible; - position: relative; - } -} - -// ─── Formation ──────────────────────────────────────────────────────────────── -.tcf { - position: relative; - width: 100%; - height: 100%; - - &--empty { - display: flex; - align-items: center; - justify-content: center; - } - - &__empty-msg { - display: flex; - flex-direction: column; - align-items: center; - gap: $size-gap-2; - text-align: center; - } - - &__empty-ico { - display: flex; - align-items: center; - justify-content: center; - width: 48px; - height: 48px; - border: 1px solid var(--border-subtle); - border-radius: $size-radius-base; - color: var(--color-text-disabled); - margin-bottom: $size-gap-2; - } - - &__empty-msg p { - margin: 0; - font-size: $font-size-sm; - color: var(--color-text-muted); - } - - &__empty-sub { - font-size: $font-size-xs !important; - color: var(--color-text-disabled) !important; - } - - &__svg { - position: absolute; - inset: 0; - pointer-events: none; - } - - // ── Node ──────────────────────────────────────────────────────────────── - &__node { - position: absolute; - pointer-events: all; - - &:hover .tcf__node-del { opacity: 1; } - } - - &__node-card { - width: 100%; - background: var(--color-bg-elevated); - border: 1px solid var(--border-subtle); - border-top-width: 2px; - border-radius: $size-radius-base; - padding: $size-gap-2 $size-gap-3; - box-sizing: border-box; - display: flex; - flex-direction: column; - gap: $size-gap-2; - position: relative; - transition: border-color $motion-fast $easing-standard; - - &:hover { border-color: var(--border-medium); } - } - - // Row 1: icon + name + delete - &__node-head { - display: flex; - align-items: center; - gap: $size-gap-2; - min-width: 0; - } - - &__node-name { - flex: 1; - min-width: 0; - font-size: $font-size-sm; - font-weight: $font-weight-semibold; - color: var(--color-text-primary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - line-height: 1.3; - } - - &__node-del { - flex-shrink: 0; - width: 16px; - height: 16px; - display: flex; - align-items: center; - justify-content: center; - border: none; - border-radius: 2px; - background: transparent; - color: var(--color-text-disabled); - cursor: pointer; - opacity: 0; - transition: all $motion-fast $easing-standard; - - &:hover { background: var(--color-error-bg); color: var(--color-error); } - } - - &__node-foot { - display: flex; - align-items: baseline; - gap: $size-gap-2; - flex-wrap: nowrap; - font-size: $font-size-xs; - line-height: 1; - } - - &__role-wrap { position: relative; flex-shrink: 0; } - - &__role-btn { - display: inline-flex; - align-items: baseline; - gap: 2px; - padding: 1px 5px; - border: none; - border-radius: 2px; - font-size: inherit; - line-height: inherit; - font-weight: $font-weight-semibold; - cursor: pointer; - font-family: $font-family-sans; - background: transparent; - transition: opacity $motion-fast $easing-standard; - - &:hover { opacity: 0.7; } - } - - &__role-menu { - position: absolute; - top: calc(100% + 2px); - left: 0; - background: var(--color-bg-elevated); - border: 1px solid var(--border-base); - border-radius: $size-radius-sm; - z-index: $z-dropdown; - overflow: hidden; - min-width: 64px; - animation: tc-in $motion-fast $easing-decelerate; - } - - &__role-opt { - display: block; - width: 100%; - padding: 5px $size-gap-3; - background: transparent; - border: none; - color: var(--color-text-muted); - font-size: $font-size-xs; - cursor: pointer; - text-align: left; - font-family: $font-family-sans; - transition: background $motion-fast $easing-standard; - - &:hover { background: var(--element-bg-medium); } - &.is-active { font-weight: $font-weight-semibold; } - } - - &__role-bd { position: fixed; inset: 0; z-index: calc(#{$z-dropdown} - 1); } - - &__node-cap { - font-size: inherit; - line-height: inherit; - font-weight: $font-weight-medium; - white-space: nowrap; - } - - &__node-model { - font-size: inherit; - line-height: inherit; - color: var(--color-text-disabled); - white-space: nowrap; - margin-left: auto; - } -} - -// ─── List view ──────────────────────────────────────────────────────────────── -.tcl { - height: 100%; - overflow-y: auto; - padding: $size-gap-4 $size-gap-6; - - &::-webkit-scrollbar { width: 3px; } - &::-webkit-scrollbar-track { background: transparent; } - &::-webkit-scrollbar-thumb { background: var(--border-subtle); border-radius: 2px; } - - &--empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100%; - gap: $size-gap-3; - color: var(--color-text-disabled); - font-size: $font-size-sm; - p { margin: 0; } - } - - &__table { - width: 100%; - border-collapse: collapse; - } - - &__th { - padding: $size-gap-2 $size-gap-3; - text-align: left; - font-size: $font-size-xs; - font-weight: $font-weight-semibold; - color: var(--color-text-muted); - text-transform: uppercase; - letter-spacing: 0.5px; - border-bottom: 1px solid var(--border-subtle); - white-space: nowrap; - } - - &__tr { - &:hover .tcl__td { background: var(--element-bg-subtle); } - &:last-child .tcl__td { border-bottom: none; } - } - - &__td { - padding: $size-gap-3; - border-bottom: 1px solid color-mix(in srgb, var(--border-subtle) 50%, transparent); - vertical-align: middle; - font-size: $font-size-sm; - transition: background $motion-fast $easing-standard; - } - - &__seq { - color: var(--color-text-disabled); - font-size: $font-size-xs; - width: 28px; - } - - &__agent { - display: flex; - align-items: center; - gap: $size-gap-3; - } - - &__agent-icon { - flex-shrink: 0; - width: 26px; - height: 26px; - border-radius: $size-radius-sm; - border: 1px solid; - display: flex; - align-items: center; - justify-content: center; - } - - &__agent-info { - display: flex; - flex-direction: column; - gap: 2px; - min-width: 0; - } - - &__agent-name { - font-size: $font-size-sm; - font-weight: $font-weight-medium; - color: var(--color-text-primary); - } - - &__agent-desc { - font-size: $font-size-xs; - color: var(--color-text-muted); - } - - &__role { - background: var(--element-bg-subtle); - border: 1px solid var(--border-subtle); - border-radius: 3px; - padding: 3px $size-gap-2; - font-size: $font-size-xs; - font-weight: $font-weight-semibold; - cursor: pointer; - outline: none; - font-family: $font-family-sans; - transition: border-color $motion-fast $easing-standard; - - &:hover { border-color: var(--border-medium); } - option { background: var(--color-bg-elevated); color: var(--color-text-primary); } - } - - &__muted { - font-size: $font-size-xs; - color: var(--color-text-muted); - } - - &__del { - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - border: none; - border-radius: 3px; - background: transparent; - color: var(--color-text-disabled); - cursor: pointer; - transition: all $motion-fast $easing-standard; - - &:hover { background: var(--color-error-bg); color: var(--color-error); } - } -} - -@keyframes tc-in { - from { opacity: 0; transform: translateY(-3px); } - to { opacity: 1; transform: translateY(0); } -} diff --git a/src/web-ui/src/app/scenes/agents/components/AgentTeamComposer.tsx b/src/web-ui/src/app/scenes/agents/components/AgentTeamComposer.tsx deleted file mode 100644 index ad8357dd..00000000 --- a/src/web-ui/src/app/scenes/agents/components/AgentTeamComposer.tsx +++ /dev/null @@ -1,456 +0,0 @@ -import React, { useState, useRef, useLayoutEffect, useCallback } from 'react'; -import { LayoutGrid, List, Trash2, ChevronDown, Bot } from 'lucide-react'; -import { useTranslation } from 'react-i18next'; -import { - useAgentsStore, - MOCK_AGENTS, - CAPABILITY_COLORS, - type AgentTeam, - type AgentTeamMember, - type MemberRole, - type AgentWithCapabilities, - type CapabilityCategory, -} from '../agentsStore'; -import { AGENT_ICON_MAP } from '../agentsIcons'; -import './AgentTeamComposer.scss'; - -// ─── Constants ──────────────────────────────────────────────────────────────── - -const ROLE_COLORS: Record = { - leader: '#60a5fa', - member: '#6eb88c', - reviewer: '#c9944d', -}; - -// ─── Helpers ───────────────────────────────────────────────────────────────── - -function getAgent(id: string): AgentWithCapabilities | undefined { - return MOCK_AGENTS.find((a) => a.id === id); -} - -const AgentIconSmall: React.FC<{ agent?: AgentWithCapabilities }> = ({ agent }) => { - const primaryCap = agent?.capabilities[0]?.category; - const color = primaryCap - ? CAPABILITY_COLORS[primaryCap as CapabilityCategory] - : 'var(--color-text-muted)'; - const key = (agent?.iconKey ?? 'bot') as keyof typeof AGENT_ICON_MAP; - const IconComp = AGENT_ICON_MAP[key] ?? Bot; - return ; -}; - -// ─── Formation layout ───────────────────────────────────────────────────────── - -interface NodePos { x: number; y: number; memberId: string } - -function layoutNodes(members: AgentTeamMember[]): NodePos[] { - const leaders = members.filter((m) => m.role === 'leader'); - const middles = members.filter((m) => m.role === 'member'); - const reviewers = members.filter((m) => m.role === 'reviewer'); - const positions: NodePos[] = []; - - const placeRow = (group: AgentTeamMember[], y: number) => { - const n = group.length; - group.forEach((m, i) => { - const x = n === 1 ? 50 : 15 + (70 / Math.max(n - 1, 1)) * i; - positions.push({ x, y, memberId: m.agentId }); - }); - }; - - const rows = [leaders, middles, reviewers].filter((r) => r.length > 0); - const ys = rows.length === 1 ? [50] : rows.length === 2 ? [28, 72] : [18, 50, 82]; - rows.forEach((row, i) => placeRow(row, ys[i])); - return positions; -} - -function buildEdges(members: AgentTeamMember[]): Array<[string, string]> { - const l = members.filter((m) => m.role === 'leader').map((m) => m.agentId); - const m = members.filter((m) => m.role === 'member').map((m) => m.agentId); - const r = members.filter((m) => m.role === 'reviewer').map((m) => m.agentId); - const edges: Array<[string, string]> = []; - - if (l.length && m.length) l.forEach((a) => m.forEach((b) => edges.push([a, b]))); - else if (l.length && r.length) l.forEach((a) => r.forEach((b) => edges.push([a, b]))); - - if (m.length && r.length) m.forEach((a) => r.forEach((b) => edges.push([a, b]))); - - if (!l.length && !r.length && m.length > 1) { - for (let i = 0; i < m.length - 1; i++) edges.push([m[i], m[i + 1]]); - } - return edges; -} - -// ─── Formation node ─────────────────────────────────────────────────────────── - -const NODE_W = 176; -const NODE_H = 72; - -interface NodeProps { - member: AgentTeamMember; - pos: NodePos; - cw: number; - ch: number; - onRoleChange: (r: MemberRole) => void; - onRemove: () => void; -} - -const FormationNode: React.FC = ({ member, pos, cw, ch, onRoleChange, onRemove }) => { - const { t } = useTranslation('scenes/agents'); - const [roleOpen, setRoleOpen] = useState(false); - const agent = getAgent(member.agentId); - const x = (pos.x / 100) * cw - NODE_W / 2; - const y = (pos.y / 100) * ch - NODE_H / 2; - const roleColor = ROLE_COLORS[member.role]; - const primaryCap = agent?.capabilities[0]?.category; - const roleLabels: Record = { - leader: t('composer.role.leader'), - member: t('composer.role.member'), - reviewer: t('composer.role.reviewer'), - }; - - return ( -
-
- {/* Row 1: name + role + delete */} -
- - {agent?.name ?? member.agentId} - -
- - {/* Row 2: role selector + capability */} -
-
- - {roleOpen && ( - <> -
- {(Object.keys(roleLabels) as MemberRole[]).map((r) => ( - - ))} -
-
setRoleOpen(false)} /> - - )} -
- {primaryCap && ( - - {primaryCap} - - )} - {agent?.model && ( - {agent.model} - )} -
-
-
- ); -}; - -// ─── Formation View ─────────────────────────────────────────────────────────── - -const FormationView: React.FC<{ team: AgentTeam }> = ({ team }) => { - const { t } = useTranslation('scenes/agents'); - const { removeMember, updateMemberRole } = useAgentsStore(); - const ref = useRef(null); - const [size, setSize] = useState({ w: 600, h: 320 }); - - useLayoutEffect(() => { - const el = ref.current; - if (!el) return; - const ob = new ResizeObserver(() => setSize({ w: el.clientWidth, h: el.clientHeight })); - ob.observe(el); - setSize({ w: el.clientWidth, h: el.clientHeight }); - return () => ob.disconnect(); - }, []); - - if (team.members.length === 0) { - return ( -
-
- -

{t('formation.empty')}

-

{t('formation.emptySub')}

-
-
- ); - } - - const positions = layoutNodes(team.members); - const edges = buildEdges(team.members); - - const getCenter = (agentId: string) => { - const p = positions.find((pos) => pos.memberId === agentId); - return p ? { x: (p.x / 100) * size.w, y: (p.y / 100) * size.h } : { x: 0, y: 0 }; - }; - - return ( -
- {/* SVG edges */} - - - - - - - {edges.map(([a, b], i) => { - const from = getCenter(a); - const to = getCenter(b); - const cy = (from.y + to.y) / 2; - return ( - - ); - })} - - - {/* Nodes */} - {team.members.map((member) => { - const pos = positions.find((p) => p.memberId === member.agentId); - if (!pos) return null; - return ( - updateMemberRole(team.id, member.agentId, r)} - onRemove={() => removeMember(team.id, member.agentId)} - /> - ); - })} -
- ); -}; - -// ─── List View ──────────────────────────────────────────────────────────────── - -const ListView: React.FC<{ team: AgentTeam }> = ({ team }) => { - const { t } = useTranslation('scenes/agents'); - const { removeMember, updateMemberRole } = useAgentsStore(); - const roleLabels: Record = { - leader: t('composer.role.leader'), - member: t('composer.role.member'), - reviewer: t('composer.role.reviewer'), - }; - - if (team.members.length === 0) { - return ( -
- -

{t('composer.emptyMembers', '暂无成员,从左侧 Agent 图鉴添加')}

-
- ); - } - - return ( -
- - - - - - - - - - - - {team.members.map((member, i) => { - const agent = getAgent(member.agentId); - const primaryCap = agent?.capabilities[0]?.category; - return ( - - - - - - - - - ); - })} - -
#{t('composer.columns.agent', 'Agent')}{t('composer.columns.role', '角色')}{t('composer.columns.tools', '工具')}{t('composer.columns.model', '模型')} -
{i + 1} -
- -
-
- {agent?.name ?? member.agentId} - - {agent?.description ? `${agent.description.slice(0, 28)}…` : ''} - -
-
- - {agent?.toolCount ?? '—'}{member.modelOverride ?? agent?.model ?? 'primary'} - -
-
- ); -}; - -// ─── Composer shell ─────────────────────────────────────────────────────────── - -const AgentTeamComposer: React.FC = () => { - const { t } = useTranslation('scenes/agents'); - const { agentTeams, activeAgentTeamId, viewMode, setViewMode, updateAgentTeam } = useAgentsStore(); - const [editingName, setEditingName] = useState(false); - const [nameVal, setNameVal] = useState(''); - const nameRef = useRef(null); - const roleLabels: Record = { - leader: t('composer.role.leader'), - member: t('composer.role.member'), - reviewer: t('composer.role.reviewer'), - }; - - const team = agentTeams.find((t) => t.id === activeAgentTeamId); - - const startEdit = useCallback(() => { - if (!team) return; - setNameVal(team.name); - setEditingName(true); - setTimeout(() => nameRef.current?.select(), 0); - }, [team]); - - const commitName = useCallback(() => { - if (team && nameVal.trim()) updateAgentTeam(team.id, { name: nameVal.trim() }); - setEditingName(false); - }, [team, nameVal, updateAgentTeam]); - - if (!team) { - return ( -
-

{t('composer.emptyTeam')}

-
- ); - } - - return ( -
- {/* ── Compact header bar: name + meta + view toggle ── */} -
-
- {editingName ? ( - setNameVal(e.target.value)} - onBlur={commitName} - onKeyDown={(e) => { - if (e.key === 'Enter') commitName(); - if (e.key === 'Escape') setEditingName(false); - }} - autoFocus - /> - ) : ( - - {team.name} - - )} - · - {t('composer.memberCount', { count: team.members.length })} - - {team.strategy === 'collaborative' - ? t('composer.strategy.collaborative') - : team.strategy === 'sequential' - ? t('composer.strategy.sequential') - : t('composer.strategy.free')} - -
- -
- {/* Role legend */} -
- {(Object.keys(roleLabels) as MemberRole[]).map((r) => ( - - - {roleLabels[r]} - - ))} -
- - - - {/* View toggle */} -
- - -
-
-
- - {/* ── Body ── */} -
- {viewMode === 'formation' ? ( - - ) : ( - - )} -
-
- ); -}; - -export default AgentTeamComposer; diff --git a/src/web-ui/src/app/scenes/agents/components/AgentTeamTabBar.scss b/src/web-ui/src/app/scenes/agents/components/AgentTeamTabBar.scss deleted file mode 100644 index 5a05adf9..00000000 --- a/src/web-ui/src/app/scenes/agents/components/AgentTeamTabBar.scss +++ /dev/null @@ -1,322 +0,0 @@ -@use '../../../../component-library/styles/tokens' as *; - -.bt-tabbar { - position: relative; - flex-shrink: 0; - border-bottom: 1px solid var(--border-subtle); - - // ── Tab rail ──────────────────────────────────────────────────────────── - &__rail { - display: flex; - align-items: center; - padding: 0 $size-gap-4; - gap: 2px; - overflow-x: auto; - overflow-y: visible; - scrollbar-width: none; - &::-webkit-scrollbar { display: none; } - } - - &__sep { - flex-shrink: 0; - width: 1px; - height: 16px; - background: var(--border-subtle); - margin: 0 $size-gap-2; - } - - // ── Tab ───────────────────────────────────────────────────────────────── - &__tab { - display: inline-flex; - align-items: center; - gap: $size-gap-2; - padding: 9px $size-gap-3; - border: none; - background: transparent; - color: var(--color-text-muted); - font-size: $font-size-xs; - cursor: pointer; - border-bottom: 2px solid transparent; - transition: color $motion-fast $easing-standard, border-color $motion-fast $easing-standard; - border-radius: 0; - white-space: nowrap; - margin-bottom: -1px; - font-family: $font-family-sans; - - &:hover { color: var(--color-text-secondary); } - - &.is-active { - color: var(--color-text-primary); - border-bottom-color: var(--color-primary); - } - } - - &__agent-team-icon { - display: flex; - align-items: center; - flex-shrink: 0; - } - - &__tab-name { - font-weight: $font-weight-medium; - } - - &__tab-count { - font-size: 10px; - min-width: 16px; - height: 16px; - line-height: 16px; - text-align: center; - padding: 0 4px; - border-radius: 2px; - background: var(--element-bg-medium); - color: var(--color-text-muted); - - .bt-tabbar__tab.is-active & { - background: color-mix(in srgb, var(--color-primary) 15%, transparent); - color: var(--color-primary); - } - } - - &__tab-close { - display: inline-flex; - align-items: center; - justify-content: center; - width: 14px; - height: 14px; - border-radius: 2px; - color: var(--color-text-disabled); - opacity: 0; - transition: opacity $motion-fast $easing-standard, background $motion-fast $easing-standard; - cursor: pointer; - - &:hover { background: var(--element-bg-medium); color: var(--color-text-muted); } - } - - &__tab:hover &__tab-close { opacity: 1; } - - // ── New button ──────────────────────────────────────────────────────── - &__new { - display: inline-flex; - align-items: center; - gap: $size-gap-1; - padding: 5px 10px; - border: 1px solid var(--border-subtle); - border-radius: 3px; - background: transparent; - color: var(--color-text-muted); - font-size: $font-size-xs; - cursor: pointer; - transition: all $motion-fast $easing-standard; - font-family: $font-family-sans; - margin: 8px 0; - - &:hover, &.is-open { - color: var(--color-text-primary); - border-color: var(--border-medium); - background: var(--element-bg-subtle); - } - } - - // ── Panel ──────────────────────────────────────────────────────────────── - &__panel { - position: absolute; - top: calc(100% + 2px); - left: $size-gap-4; - width: 280px; - background: var(--color-bg-elevated); - border: 1px solid var(--border-base); - border-radius: $size-radius-base; - z-index: $z-dropdown; - overflow: hidden; - padding: $size-gap-4; - display: flex; - flex-direction: column; - gap: $size-gap-3; - animation: tb-in $motion-fast $easing-decelerate; - - &--wide { width: 360px; } - } - - // ── Icon options ───────────────────────────────────────────────────────── - &__icon-row { - display: flex; - gap: $size-gap-2; - flex-wrap: wrap; - } - - &__icon-opt { - width: 30px; - height: 30px; - display: flex; - align-items: center; - justify-content: center; - border: 1px solid var(--border-subtle); - border-radius: 4px; - background: transparent; - color: var(--color-text-muted); - cursor: pointer; - transition: all $motion-fast $easing-standard; - - &:hover { background: var(--element-bg-medium); color: var(--color-text-primary); } - &.is-sel { background: var(--element-bg-medium); border-color: var(--border-medium); } - } - - // ── Input field ─────────────────────────────────────────────────────────── - &__field { - width: 100%; - padding: 7px $size-gap-3; - background: var(--element-bg-subtle); - border: 1px solid var(--border-subtle); - border-radius: 4px; - color: var(--color-text-primary); - font-size: $font-size-xs; - outline: none; - box-sizing: border-box; - transition: border-color $motion-fast $easing-standard; - font-family: $font-family-sans; - - &::placeholder { color: var(--color-text-disabled); } - &:focus { border-color: var(--border-medium); } - } - - &__panel-row { - display: flex; - align-items: center; - gap: $size-gap-2; - } - - // ── Template panel ──────────────────────────────────────────────────────── - &__tpl-head { - display: flex; - align-items: center; - justify-content: space-between; - } - - &__tpl-title { - font-size: $font-size-xs; - font-weight: $font-weight-semibold; - color: var(--color-text-muted); - text-transform: uppercase; - letter-spacing: 0.6px; - } - - &__close-btn { - display: flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; - border: none; - background: transparent; - color: var(--color-text-muted); - cursor: pointer; - border-radius: 3px; - transition: background $motion-fast $easing-standard; - &:hover { background: var(--element-bg-medium); } - } - - &__tpl-grid { - display: flex; - flex-direction: column; - gap: 2px; - } - - &__tpl-card { - display: flex; - align-items: center; - gap: $size-gap-3; - padding: $size-gap-3; - background: transparent; - border: 1px solid transparent; - border-radius: $size-radius-sm; - text-align: left; - cursor: pointer; - transition: all $motion-fast $easing-standard; - font-family: $font-family-sans; - - &:hover { background: var(--element-bg-subtle); border-color: var(--border-subtle); } - } - - &__tpl-icon { - flex-shrink: 0; - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - border: 1px solid; - border-radius: $size-radius-sm; - background: var(--element-bg-subtle); - } - - &__tpl-info { - flex: 1; - display: flex; - flex-direction: column; - gap: 2px; - min-width: 0; - } - - &__tpl-name { - font-size: $font-size-sm; - font-weight: $font-weight-semibold; - color: var(--color-text-primary); - } - - &__tpl-desc { - font-size: $font-size-xs; - color: var(--color-text-muted); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - &__tpl-cnt { - flex-shrink: 0; - font-size: 10px; - color: var(--color-text-disabled); - background: var(--element-bg-medium); - padding: 2px 6px; - border-radius: 2px; - } - - // ── Action buttons ──────────────────────────────────────────────────────── - &__action { - padding: 5px 12px; - border-radius: 3px; - font-size: $font-size-xs; - font-weight: $font-weight-medium; - cursor: pointer; - border: 1px solid transparent; - transition: all $motion-fast $easing-standard; - font-family: $font-family-sans; - white-space: nowrap; - - &--primary { - background: var(--color-primary); - color: #fff; - &:hover:not(:disabled) { opacity: 0.88; } - &:disabled { opacity: 0.35; cursor: not-allowed; } - } - - &--ghost { - background: transparent; - color: var(--color-text-muted); - border-color: var(--border-subtle); - &:hover { background: var(--element-bg-subtle); color: var(--color-text-secondary); } - } - } - - // ── Backdrop ───────────────────────────────────────────────────────────── - &__backdrop { - position: fixed; - inset: 0; - z-index: calc(#{$z-dropdown} - 1); - } -} - -@keyframes tb-in { - from { opacity: 0; transform: translateY(-4px); } - to { opacity: 1; transform: translateY(0); } -} diff --git a/src/web-ui/src/app/scenes/agents/components/AgentTeamTabBar.tsx b/src/web-ui/src/app/scenes/agents/components/AgentTeamTabBar.tsx deleted file mode 100644 index 82515fce..00000000 --- a/src/web-ui/src/app/scenes/agents/components/AgentTeamTabBar.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import React, { useState } from 'react'; -import { Plus, X, Code2, BarChart2, LayoutTemplate, Rocket, Users, Briefcase, Layers, type LucideIcon } from 'lucide-react'; -import { useTranslation } from 'react-i18next'; -import { useAgentsStore, AGENT_TEAM_TEMPLATES } from '../agentsStore'; -import { AGENT_TEAM_ICON_MAP, getAgentTeamAccent } from '../agentsIcons'; -import './AgentTeamTabBar.scss'; - -// ─── Agent team icon renderer ───────────────────────────────────────────────── - -const AgentTeamIconBadge: React.FC<{ iconKey: string; teamId: string; size?: number }> = ({ - iconKey, - teamId, - size = 12, -}) => { - const accent = getAgentTeamAccent(teamId); - const key = iconKey as keyof typeof AGENT_TEAM_ICON_MAP; - const IconComp = AGENT_TEAM_ICON_MAP[key] ?? Users; - return ( - - - - ); -}; - -// ─── New agent team form ────────────────────────────────────────────────────── - -const ICON_OPTIONS: Array<{ key: string; Icon: LucideIcon }> = [ - { key: 'code', Icon: Code2 }, - { key: 'chart', Icon: BarChart2 }, - { key: 'layout', Icon: LayoutTemplate }, - { key: 'rocket', Icon: Rocket }, - { key: 'users', Icon: Users }, - { key: 'briefcase', Icon: Briefcase }, - { key: 'layers', Icon: Layers }, -]; - -interface NewTeamForm { name: string; icon: string; description: string } - -const AgentTeamTabBar: React.FC = () => { - const { t } = useTranslation('scenes/agents'); - const { agentTeams, activeAgentTeamId, setActiveAgentTeam, addAgentTeam, deleteAgentTeam } = useAgentsStore(); - const [panel, setPanel] = useState<'none' | 'create' | 'templates'>('none'); - const [form, setForm] = useState({ name: '', icon: 'rocket', description: '' }); - - const closePanel = () => setPanel('none'); - - const handleCreate = () => { - if (!form.name.trim()) return; - addAgentTeam({ id: `agent-team-${Date.now()}`, ...form, strategy: 'collaborative', shareContext: true }); - setForm({ name: '', icon: 'rocket', description: '' }); - closePanel(); - }; - - const handleUseTemplate = (tpl: typeof AGENT_TEAM_TEMPLATES[number]) => { - addAgentTeam({ - id: `agent-team-${Date.now()}`, - name: tpl.name, - icon: tpl.icon, - description: tpl.description, - strategy: 'collaborative', - shareContext: true, - }); - closePanel(); - }; - - const handleDelete = (e: React.MouseEvent, id: string) => { - e.stopPropagation(); - if (agentTeams.length <= 1) return; - deleteAgentTeam(id); - }; - - return ( -
-
- {/* ── Tabs ── */} - {agentTeams.map((team) => { - const isActive = team.id === activeAgentTeamId; - return ( - - ); - })} - - {/* ── Divider ── */} - - - {/* ── New team ── */} - -
- - {/* ── Create panel ── */} - {panel === 'create' && ( -
- {/* Icon selector */} -
- {ICON_OPTIONS.map(({ key, Icon }) => ( - - ))} -
- - setForm((f) => ({ ...f, name: e.target.value }))} - onKeyDown={(e) => e.key === 'Enter' && handleCreate()} - autoFocus - /> - setForm((f) => ({ ...f, description: e.target.value }))} - /> - -
- -
- - -
-
- )} - - {/* ── Templates panel ── */} - {panel === 'templates' && ( -
-
- {t('tabbar.templateTitle')} - -
-
- {AGENT_TEAM_TEMPLATES.map((tpl) => { - const key = tpl.icon as keyof typeof AGENT_TEAM_ICON_MAP; - const IconComp = AGENT_TEAM_ICON_MAP[key] ?? Users; - const accent = getAgentTeamAccent(`team-${tpl.id}`); - return ( - - ); - })} -
- -
- )} - - {(panel !== 'none') && ( -
- )} -
- ); -}; - -export default AgentTeamTabBar; diff --git a/src/web-ui/src/app/scenes/agents/components/CapabilityBar.scss b/src/web-ui/src/app/scenes/agents/components/CapabilityBar.scss deleted file mode 100644 index e30a6ae7..00000000 --- a/src/web-ui/src/app/scenes/agents/components/CapabilityBar.scss +++ /dev/null @@ -1,81 +0,0 @@ -@use '../../../../component-library/styles/tokens' as *; - -.cap-bar { - flex-shrink: 0; - display: flex; - align-items: center; - gap: $size-gap-4; - padding: 8px $size-gap-6; - border-top: 1px solid var(--border-subtle); - - &__label { - flex-shrink: 0; - font-size: $font-size-xs; - color: var(--color-text-disabled); - letter-spacing: 0.3px; - } - - &__items { - flex: 1; - display: flex; - align-items: center; - gap: $size-gap-5; - flex-wrap: wrap; - min-width: 0; - } - - &__item { - display: flex; - align-items: center; - gap: $size-gap-2; - flex-shrink: 0; - } - - &__cat { - font-size: $font-size-xs; - color: var(--color-text-muted); - width: 26px; - flex-shrink: 0; - } - - &__track { - width: 48px; - height: 3px; - background: var(--element-bg-medium); - border-radius: 2px; - overflow: hidden; - flex-shrink: 0; - } - - &__fill { - height: 100%; - background: var(--border-subtle); - border-radius: 2px; - transition: width 0.35s $easing-decelerate; - min-width: 0; - } - - &__lv { - font-size: 10px; - color: var(--color-text-disabled); - width: 14px; - text-align: right; - flex-shrink: 0; - font-weight: $font-weight-medium; - font-variant-numeric: tabular-nums; - } - - &__warn { - flex-shrink: 0; - display: inline-flex; - align-items: center; - gap: 4px; - font-size: $font-size-xs; - color: var(--color-warning); - padding: 2px 8px; - background: var(--color-warning-bg); - border: 1px solid var(--color-warning-border); - border-radius: 2px; - white-space: nowrap; - } -} diff --git a/src/web-ui/src/app/scenes/agents/components/CapabilityBar.tsx b/src/web-ui/src/app/scenes/agents/components/CapabilityBar.tsx deleted file mode 100644 index 9dcd5d8a..00000000 --- a/src/web-ui/src/app/scenes/agents/components/CapabilityBar.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import { AlertTriangle } from 'lucide-react'; -import { useTranslation } from 'react-i18next'; -import { - useAgentsStore, - MOCK_AGENTS, - CAPABILITY_CATEGORIES, - CAPABILITY_COLORS, - computeAgentTeamCapabilities, - type AgentWithCapabilities, - type CapabilityCategory, -} from '../agentsStore'; -import './CapabilityBar.scss'; - -const CapabilityBar: React.FC = () => { - const { t } = useTranslation('scenes/agents'); - const { agentTeams, activeAgentTeamId } = useAgentsStore(); - const team = agentTeams.find((t) => t.id === activeAgentTeamId); - if (!team) return null; - - const coverage = computeAgentTeamCapabilities(team, MOCK_AGENTS as AgentWithCapabilities[]); - const weak = CAPABILITY_CATEGORIES.filter((c) => coverage[c] === 0); - - return ( -
- {t('capability.coverage', '能力覆盖')} - -
- {CAPABILITY_CATEGORIES.map((cat) => { - const level = coverage[cat]; - const color = CAPABILITY_COLORS[cat as CapabilityCategory]; - const pct = Math.round((level / 5) * 100); - return ( -
0 ? `Lv${level}` : t('capability.none', '无覆盖')}`} - > - {cat} -
-
0 ? color : undefined }} - /> -
- 0 ? { color } : undefined} - > - {level > 0 ? level : '—'} - -
- ); - })} -
- - {weak.length > 0 && ( -
- - {t('capability.warning', { cats: weak.join('、') })} -
- )} -
- ); -}; - -export default CapabilityBar; diff --git a/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx b/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx index b6755162..b1a069b6 100644 --- a/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx +++ b/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx @@ -86,7 +86,7 @@ const CreateAgentPage: React.FC = () => { return (
- {/* 顶部导航栏 */} + {/* Top bar */}
- {/* 页面内容 */} + {/* Page body */}
@@ -103,7 +103,7 @@ const CreateAgentPage: React.FC = () => {
- {/* 名称 */} + {/* Name */}
{ {nameError && {nameError}}
- {/* 描述 */} + {/* Description */}
{ />
- {/* 级别 + 只读模式 同行 */} + {/* Level + read-only on one row */}
{(['user', 'project'] as SubagentLevel[]).map((lv) => { @@ -153,7 +153,7 @@ const CreateAgentPage: React.FC = () => {
- {/* 工具 */} + {/* Tools */} {toolNames.length > 0 && (
)} - {/* 系统提示词 */} + {/* System prompt */}