feat(team): integrate Team components into main application
- Add Team tab to Sidebar with TeamList component - Update MainViewType to include 'team' view - Add TeamCollaborationView rendering in App.tsx - Add selectedTeamId and onSelectTeam props to Sidebar - Create TeamList component for sidebar team navigation UI Integration: - Team tab shows list of teams with status indicators - Selecting a team displays TeamCollaborationView - Empty state shows guidance for creating teams Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,9 @@ import { SettingsLayout } from './components/Settings/SettingsLayout';
|
|||||||
import { HandTaskPanel } from './components/HandTaskPanel';
|
import { HandTaskPanel } from './components/HandTaskPanel';
|
||||||
import { WorkflowList } from './components/WorkflowList';
|
import { WorkflowList } from './components/WorkflowList';
|
||||||
import { TriggersPanel } from './components/TriggersPanel';
|
import { TriggersPanel } from './components/TriggersPanel';
|
||||||
|
import { TeamCollaborationView } from './components/TeamCollaborationView';
|
||||||
import { useGatewayStore } from './store/gatewayStore';
|
import { useGatewayStore } from './store/gatewayStore';
|
||||||
|
import { useTeamStore } from './store/teamStore';
|
||||||
import { getStoredGatewayToken } from './lib/gateway-client';
|
import { getStoredGatewayToken } from './lib/gateway-client';
|
||||||
|
|
||||||
type View = 'main' | 'settings';
|
type View = 'main' | 'settings';
|
||||||
@@ -16,7 +18,9 @@ function App() {
|
|||||||
const [view, setView] = useState<View>('main');
|
const [view, setView] = useState<View>('main');
|
||||||
const [mainContentView, setMainContentView] = useState<MainViewType>('chat');
|
const [mainContentView, setMainContentView] = useState<MainViewType>('chat');
|
||||||
const [selectedHandId, setSelectedHandId] = useState<string | undefined>(undefined);
|
const [selectedHandId, setSelectedHandId] = useState<string | undefined>(undefined);
|
||||||
|
const [selectedTeamId, setSelectedTeamId] = useState<string | undefined>(undefined);
|
||||||
const { connect, connectionState } = useGatewayStore();
|
const { connect, connectionState } = useGatewayStore();
|
||||||
|
const { activeTeam, setActiveTeam, teams } = useTeamStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'ZCLAW';
|
document.title = 'ZCLAW';
|
||||||
@@ -38,6 +42,14 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSelectTeam = (teamId: string) => {
|
||||||
|
const team = teams.find(t => t.id === teamId);
|
||||||
|
if (team) {
|
||||||
|
setActiveTeam(team);
|
||||||
|
setSelectedTeamId(teamId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (view === 'settings') {
|
if (view === 'settings') {
|
||||||
return <SettingsLayout onBack={() => setView('main')} />;
|
return <SettingsLayout onBack={() => setView('main')} />;
|
||||||
}
|
}
|
||||||
@@ -50,6 +62,8 @@ function App() {
|
|||||||
onMainViewChange={handleMainViewChange}
|
onMainViewChange={handleMainViewChange}
|
||||||
selectedHandId={selectedHandId}
|
selectedHandId={selectedHandId}
|
||||||
onSelectHand={setSelectedHandId}
|
onSelectHand={setSelectedHandId}
|
||||||
|
selectedTeamId={selectedTeamId}
|
||||||
|
onSelectTeam={handleSelectTeam}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 中间区域 */}
|
{/* 中间区域 */}
|
||||||
@@ -76,6 +90,22 @@ function App() {
|
|||||||
<WorkflowList />
|
<WorkflowList />
|
||||||
<TriggersPanel />
|
<TriggersPanel />
|
||||||
</div>
|
</div>
|
||||||
|
) : mainContentView === 'team' ? (
|
||||||
|
activeTeam ? (
|
||||||
|
<TeamCollaborationView teamId={activeTeam.id} />
|
||||||
|
) : (
|
||||||
|
<div className="flex-1 flex items-center justify-center p-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-20 h-20 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<span className="text-4xl">👥</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-700 mb-2">选择或创建团队</h3>
|
||||||
|
<p className="text-sm text-gray-400 max-w-sm">
|
||||||
|
从左侧列表选择一个团队,或点击 + 创建新的多 Agent 协作团队。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<ChatArea />
|
<ChatArea />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,26 +3,37 @@ import { Settings } from 'lucide-react';
|
|||||||
import { CloneManager } from './CloneManager';
|
import { CloneManager } from './CloneManager';
|
||||||
import { HandList } from './HandList';
|
import { HandList } from './HandList';
|
||||||
import { TaskList } from './TaskList';
|
import { TaskList } from './TaskList';
|
||||||
|
import { TeamList } from './TeamList';
|
||||||
import { useGatewayStore } from '../store/gatewayStore';
|
import { useGatewayStore } from '../store/gatewayStore';
|
||||||
|
|
||||||
export type MainViewType = 'chat' | 'hands' | 'workflow';
|
export type MainViewType = 'chat' | 'hands' | 'workflow' | 'team';
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
onOpenSettings?: () => void;
|
onOpenSettings?: () => void;
|
||||||
onMainViewChange?: (view: MainViewType) => void;
|
onMainViewChange?: (view: MainViewType) => void;
|
||||||
selectedHandId?: string;
|
selectedHandId?: string;
|
||||||
onSelectHand?: (handId: string) => void;
|
onSelectHand?: (handId: string) => void;
|
||||||
|
selectedTeamId?: string;
|
||||||
|
onSelectTeam?: (teamId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tab = 'clones' | 'hands' | 'workflow';
|
type Tab = 'clones' | 'hands' | 'workflow' | 'team';
|
||||||
|
|
||||||
const TABS: { key: Tab; label: string; mainView?: MainViewType }[] = [
|
const TABS: { key: Tab; label: string; mainView?: MainViewType }[] = [
|
||||||
{ key: 'clones', label: '分身' },
|
{ key: 'clones', label: '分身' },
|
||||||
{ key: 'hands', label: 'HANDS', mainView: 'hands' },
|
{ key: 'hands', label: 'HANDS', mainView: 'hands' },
|
||||||
{ key: 'workflow', label: 'Workflow', mainView: 'workflow' },
|
{ key: 'workflow', label: 'Workflow', mainView: 'workflow' },
|
||||||
|
{ key: 'team', label: 'Team', mainView: 'team' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Sidebar({ onOpenSettings, onMainViewChange, selectedHandId, onSelectHand }: SidebarProps) {
|
export function Sidebar({
|
||||||
|
onOpenSettings,
|
||||||
|
onMainViewChange,
|
||||||
|
selectedHandId,
|
||||||
|
onSelectHand,
|
||||||
|
selectedTeamId,
|
||||||
|
onSelectTeam
|
||||||
|
}: SidebarProps) {
|
||||||
const [activeTab, setActiveTab] = useState<Tab>('clones');
|
const [activeTab, setActiveTab] = useState<Tab>('clones');
|
||||||
const userName = useGatewayStore((state) => state.quickConfig.userName) || '用户7141';
|
const userName = useGatewayStore((state) => state.quickConfig.userName) || '用户7141';
|
||||||
|
|
||||||
@@ -37,22 +48,21 @@ export function Sidebar({ onOpenSettings, onMainViewChange, selectedHandId, onSe
|
|||||||
|
|
||||||
const handleSelectHand = (handId: string) => {
|
const handleSelectHand = (handId: string) => {
|
||||||
onSelectHand?.(handId);
|
onSelectHand?.(handId);
|
||||||
// 切换到 hands 视图
|
|
||||||
setActiveTab('hands');
|
setActiveTab('hands');
|
||||||
onMainViewChange?.('hands');
|
onMainViewChange?.('hands');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="w-64 bg-gray-50 border-r border-gray-200 flex flex-col flex-shrink-0">
|
<aside className="w-64 bg-gray-50 dark:bg-gray-900 border-r border-gray-200 dark:border-gray-700 flex flex-col flex-shrink-0">
|
||||||
{/* 顶部标签 */}
|
{/* 顶部标签 */}
|
||||||
<div className="flex border-b border-gray-200 bg-white">
|
<div className="flex border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
|
||||||
{TABS.map(({ key, label }) => (
|
{TABS.map(({ key, label }) => (
|
||||||
<button
|
<button
|
||||||
key={key}
|
key={key}
|
||||||
className={`flex-1 py-3 px-4 text-xs font-medium transition-colors ${
|
className={`flex-1 py-3 px-4 text-xs font-medium transition-colors ${
|
||||||
activeTab === key
|
activeTab === key
|
||||||
? 'text-gray-900 border-b-2 border-gray-900'
|
? 'text-gray-900 dark:text-white border-b-2 border-blue-500'
|
||||||
: 'text-gray-500 hover:text-gray-700'
|
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleTabClick(key, TABS.find(t => t.key === key)?.mainView)}
|
onClick={() => handleTabClick(key, TABS.find(t => t.key === key)?.mainView)}
|
||||||
>
|
>
|
||||||
@@ -71,16 +81,22 @@ export function Sidebar({ onOpenSettings, onMainViewChange, selectedHandId, onSe
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{activeTab === 'workflow' && <TaskList />}
|
{activeTab === 'workflow' && <TaskList />}
|
||||||
|
{activeTab === 'team' && (
|
||||||
|
<TeamList
|
||||||
|
selectedTeamId={selectedTeamId}
|
||||||
|
onSelectTeam={onSelectTeam}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 底部用户 */}
|
{/* 底部用户 */}
|
||||||
<div className="p-3 border-t border-gray-200 bg-gray-50">
|
<div className="p-3 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-8 h-8 bg-gradient-to-br from-orange-400 to-red-500 rounded-full flex items-center justify-center text-white text-xs font-bold">
|
<div className="w-8 h-8 bg-gradient-to-br from-orange-400 to-red-500 rounded-full flex items-center justify-center text-white text-xs font-bold">
|
||||||
用
|
{userName?.charAt(0) || '用'}
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium text-gray-700">{userName}</span>
|
<span className="font-medium text-gray-700 dark:text-gray-300">{userName}</span>
|
||||||
<button className="ml-auto text-gray-400 hover:text-gray-600" onClick={onOpenSettings}>
|
<button className="ml-auto text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" onClick={onOpenSettings}>
|
||||||
<Settings className="w-4 h-4" />
|
<Settings className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
112
desktop/src/components/TeamList.tsx
Normal file
112
desktop/src/components/TeamList.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
/**
|
||||||
|
* TeamList - Sidebar Team List Component
|
||||||
|
*
|
||||||
|
* Displays a compact list of teams for the sidebar navigation.
|
||||||
|
*
|
||||||
|
* @module components/TeamList
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useTeamStore } from '../store/teamStore';
|
||||||
|
import { Users, Plus, Activity, CheckCircle, AlertTriangle } from 'lucide-react';
|
||||||
|
|
||||||
|
interface TeamListProps {
|
||||||
|
onSelectTeam?: (teamId: string) => void;
|
||||||
|
selectedTeamId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TeamList({ onSelectTeam, selectedTeamId }: TeamListProps) {
|
||||||
|
const { teams, loadTeams, setActiveTeam, isLoading } = useTeamStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadTeams();
|
||||||
|
}, [loadTeams]);
|
||||||
|
|
||||||
|
const handleSelectTeam = (teamId: string) => {
|
||||||
|
const team = teams.find(t => t.id === teamId);
|
||||||
|
if (team) {
|
||||||
|
setActiveTeam(team);
|
||||||
|
onSelectTeam?.(teamId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusIcon = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'active':
|
||||||
|
return <Activity className="w-3 h-3 text-green-500" />;
|
||||||
|
case 'paused':
|
||||||
|
return <AlertTriangle className="w-3 h-3 text-yellow-500" />;
|
||||||
|
case 'completed':
|
||||||
|
return <CheckCircle className="w-3 h-3 text-blue-500" />;
|
||||||
|
default:
|
||||||
|
return <Activity className="w-3 h-3 text-gray-400" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-3 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
|
Teams
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-800 rounded"
|
||||||
|
title="Create Team"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 text-gray-400" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Team List */}
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="p-4 text-center text-gray-400 text-sm">Loading...</div>
|
||||||
|
) : teams.length === 0 ? (
|
||||||
|
<div className="p-4 text-center">
|
||||||
|
<Users className="w-8 h-8 mx-auto mb-2 text-gray-300 dark:text-gray-600" />
|
||||||
|
<p className="text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
No teams yet
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
||||||
|
Click + to create one
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-1 p-2">
|
||||||
|
{teams.map((team) => (
|
||||||
|
<button
|
||||||
|
key={team.id}
|
||||||
|
onClick={() => handleSelectTeam(team.id)}
|
||||||
|
className={`w-full p-2 rounded-lg text-left transition-colors ${
|
||||||
|
selectedTeamId === team.id
|
||||||
|
? 'bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800'
|
||||||
|
: 'hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getStatusIcon(team.status)}
|
||||||
|
<span className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||||
|
{team.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Users className="w-3 h-3" />
|
||||||
|
{team.members.length}
|
||||||
|
</span>
|
||||||
|
<span>·</span>
|
||||||
|
<span>{team.tasks.length} tasks</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TeamList;
|
||||||
Reference in New Issue
Block a user