Files
zclaw_openfang/desktop/src/components/TeamList.tsx
iven 48a430fc97 refactor(skills): add skill-adapter and refactor SkillMarket
- Add skill-adapter.ts to bridge configStore and UI skill formats
- Refactor SkillMarket to use new skill-adapter instead of skill-discovery
- Add health check state to connectionStore
- Update multiple components with improved typing
- Clean up test artifacts and add new test results
- Update README and add skill-market-mvp plan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 00:28:03 +08:00

295 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* TeamList - Sidebar Team List Component
*
* Displays a compact list of teams for the sidebar navigation.
*
* @module components/TeamList
*/
import { useEffect, useState } from 'react';
import { useTeamStore } from '../store/teamStore';
import { useAgentStore } from '../store/agentStore';
import { useChatStore } from '../store/chatStore';
import { Users, Plus, Activity, CheckCircle, AlertTriangle, X, Bot } from 'lucide-react';
import type { TeamMemberRole } from '../types/team';
interface TeamListProps {
onSelectTeam?: (teamId: string) => void;
selectedTeamId?: string;
}
export function TeamList({ onSelectTeam, selectedTeamId }: TeamListProps) {
const { teams, loadTeams, setActiveTeam, createTeam, isLoading } = useTeamStore();
const clones = useAgentStore((s) => s.clones);
const { agents } = useChatStore();
const [showCreateModal, setShowCreateModal] = useState(false);
const [teamName, setTeamName] = useState('');
const [teamDescription, setTeamDescription] = useState('');
const [teamPattern, setTeamPattern] = useState<'sequential' | 'parallel' | 'pipeline'>('sequential');
const [selectedAgents, setSelectedAgents] = useState<string[]>([]);
const [isCreating, setIsCreating] = useState(false);
useEffect(() => {
try {
loadTeams();
} catch (err) {
console.error('[TeamList] Failed to load teams:', err);
}
}, [loadTeams]);
const handleSelectTeam = (teamId: string) => {
const team = teams.find(t => t.id === teamId);
if (team) {
setActiveTeam(team);
onSelectTeam?.(teamId);
}
};
const handleCreateTeam = async () => {
if (!teamName.trim() || selectedAgents.length === 0) return;
setIsCreating(true);
try {
const roleAssignments: { agentId: string; role: TeamMemberRole }[] = selectedAgents.map((agentId, index) => ({
agentId,
role: (index === 0 ? 'orchestrator' : index === 1 ? 'reviewer' : 'worker') as TeamMemberRole,
}));
const team = await createTeam({
name: teamName.trim(),
description: teamDescription.trim() || undefined,
pattern: teamPattern,
memberAgents: roleAssignments,
});
if (team) {
setShowCreateModal(false);
setTeamName('');
setTeamDescription('');
setSelectedAgents([]);
setTeamPattern('sequential');
setActiveTeam(team);
onSelectTeam?.(team.id);
}
} finally {
setIsCreating(false);
}
};
const toggleAgentSelection = (agentId: string) => {
setSelectedAgents(prev =>
prev.includes(agentId)
? prev.filter(id => id !== agentId)
: [...prev, agentId]
);
};
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" />;
}
};
// Merge clones and agents for display - normalize to common type with defensive checks
const availableAgents: Array<{ id: string; name: string; role?: string }> =
(clones && clones.length > 0)
? clones.map(c => ({ id: c.id, name: c.name, role: c.role }))
: (agents && agents.length > 0)
? agents.map(a => ({
id: a.id,
name: a.name,
role: '默认助手',
}))
: [];
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">
</h3>
<button
onClick={() => setShowCreateModal(true)}
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-800 rounded transition-colors"
title="创建团队"
>
<Plus className="w-4 h-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200" />
</button>
</div>
</div>
{/* Create Team Modal */}
{showCreateModal && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-xl w-80 max-h-[90vh] overflow-y-auto">
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-gray-900 dark:text-white"></h3>
<button
onClick={() => setShowCreateModal(false)}
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
>
<X className="w-4 h-4 text-gray-400" />
</button>
</div>
</div>
<div className="p-4 space-y-4">
{/* Team Name */}
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
*
</label>
<input
type="text"
value={teamName}
onChange={(e) => setTeamName(e.target.value)}
placeholder="例如:开发团队 Alpha"
className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-gray-400"
/>
</div>
{/* Team Description */}
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
</label>
<textarea
value={teamDescription}
onChange={(e) => setTeamDescription(e.target.value)}
placeholder="这个团队将负责什么工作?"
rows={2}
className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-gray-400 resize-none"
/>
</div>
{/* Collaboration Pattern */}
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
</label>
<select
value={teamPattern}
onChange={(e) => setTeamPattern(e.target.value as typeof teamPattern)}
className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-gray-400"
>
<option value="sequential"></option>
<option value="parallel"></option>
<option value="pipeline">线</option>
</select>
</div>
{/* Agent Selection */}
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">
( {selectedAgents.length} ) *
</label>
<div className="space-y-2 max-h-40 overflow-y-auto">
{availableAgents.map((agent) => (
<button
key={agent.id}
onClick={() => toggleAgentSelection(agent.id)}
className={`w-full p-2 rounded-lg text-left text-sm transition-colors flex items-center gap-2 ${
selectedAgents.includes(agent.id)
? 'bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800'
: 'bg-gray-50 dark:bg-gray-700 border border-transparent hover:bg-gray-100 dark:hover:bg-gray-600'
}`}
>
<div className="w-6 h-6 rounded-full bg-gray-600 flex items-center justify-center text-white text-xs">
<Bot className="w-3 h-3" />
</div>
<span className="text-gray-900 dark:text-white truncate">{agent.name}</span>
{selectedAgents.includes(agent.id) && (
<CheckCircle className="w-4 h-4 text-blue-500 ml-auto" />
)}
</button>
))}
{availableAgents.length === 0 && (
<p className="text-xs text-gray-500 dark:text-gray-400 text-center py-2">
</p>
)}
</div>
</div>
</div>
{/* Footer */}
<div className="p-4 border-t border-gray-200 dark:border-gray-700 flex gap-2">
<button
onClick={() => setShowCreateModal(false)}
className="flex-1 px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
</button>
<button
onClick={handleCreateTeam}
disabled={!teamName.trim() || selectedAgents.length === 0 || isCreating}
className="flex-1 px-4 py-2 text-sm text-white bg-gray-700 dark:bg-gray-600 rounded-lg hover:bg-gray-800 dark:hover:bg-gray-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isCreating ? '创建中...' : '创建'}
</button>
</div>
</div>
</div>
)}
{/* Team List */}
<div className="flex-1 overflow-y-auto">
{isLoading ? (
<div className="p-4 text-center text-gray-400 text-sm">...</div>
) : !Array.isArray(teams) || 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">
</p>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
+
</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} </span>
</div>
</button>
))}
</div>
)}
</div>
</div>
);
}
export default TeamList;