ideaco 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +33 -0
- package/.nvmrc +1 -0
- package/ARCHITECTURE.md +394 -0
- package/Dockerfile +50 -0
- package/LICENSE +29 -0
- package/README.md +206 -0
- package/bin/i18n.js +46 -0
- package/bin/ideaco.js +494 -0
- package/deploy.sh +15 -0
- package/docker-compose.yml +30 -0
- package/electron/main.cjs +986 -0
- package/electron/preload.cjs +14 -0
- package/electron/web-backends.cjs +854 -0
- package/jsconfig.json +8 -0
- package/next.config.mjs +34 -0
- package/package.json +134 -0
- package/postcss.config.mjs +6 -0
- package/public/demo/dashboard.png +0 -0
- package/public/demo/employee.png +0 -0
- package/public/demo/messages.png +0 -0
- package/public/demo/office.png +0 -0
- package/public/demo/requirement.png +0 -0
- package/public/logo.jpeg +0 -0
- package/public/logo.png +0 -0
- package/scripts/prepare-electron.js +67 -0
- package/scripts/release.js +76 -0
- package/src/app/api/agents/[agentId]/chat/route.js +70 -0
- package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
- package/src/app/api/agents/[agentId]/route.js +106 -0
- package/src/app/api/avatar/route.js +104 -0
- package/src/app/api/browse-dir/route.js +44 -0
- package/src/app/api/chat/route.js +265 -0
- package/src/app/api/company/factory-reset/route.js +43 -0
- package/src/app/api/company/route.js +82 -0
- package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
- package/src/app/api/departments/route.js +92 -0
- package/src/app/api/group-chat-loop/events/route.js +70 -0
- package/src/app/api/group-chat-loop/route.js +94 -0
- package/src/app/api/mailbox/route.js +100 -0
- package/src/app/api/messages/route.js +14 -0
- package/src/app/api/providers/[id]/configure/route.js +21 -0
- package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
- package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
- package/src/app/api/providers/route.js +11 -0
- package/src/app/api/requirements/route.js +242 -0
- package/src/app/api/secretary/route.js +65 -0
- package/src/app/api/system/cli-backends/route.js +91 -0
- package/src/app/api/system/cron/route.js +110 -0
- package/src/app/api/system/knowledge/route.js +104 -0
- package/src/app/api/system/plugins/route.js +40 -0
- package/src/app/api/system/skills/route.js +46 -0
- package/src/app/api/system/status/route.js +46 -0
- package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
- package/src/app/api/talent-market/[profileId]/route.js +17 -0
- package/src/app/api/talent-market/route.js +26 -0
- package/src/app/api/teams/route.js +773 -0
- package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
- package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
- package/src/app/globals.css +130 -0
- package/src/app/layout.jsx +40 -0
- package/src/app/page.jsx +97 -0
- package/src/components/AgentChatModal.jsx +164 -0
- package/src/components/AgentDetailModal.jsx +425 -0
- package/src/components/AgentSpyModal.jsx +481 -0
- package/src/components/AvatarGrid.jsx +29 -0
- package/src/components/BossProfileModal.jsx +162 -0
- package/src/components/CachedAvatar.jsx +77 -0
- package/src/components/ChatPanel.jsx +219 -0
- package/src/components/ChatShared.jsx +255 -0
- package/src/components/DepartmentDetail.jsx +842 -0
- package/src/components/DepartmentView.jsx +367 -0
- package/src/components/FileReference.jsx +260 -0
- package/src/components/FilesView.jsx +465 -0
- package/src/components/GroupChatView.jsx +799 -0
- package/src/components/Mailbox.jsx +926 -0
- package/src/components/MessagesView.jsx +112 -0
- package/src/components/OnboardingGuide.jsx +209 -0
- package/src/components/OrgTree.jsx +151 -0
- package/src/components/Overview.jsx +391 -0
- package/src/components/PixelOffice.jsx +2281 -0
- package/src/components/ProviderGrid.jsx +551 -0
- package/src/components/ProvidersBoard.jsx +16 -0
- package/src/components/RequirementDetail.jsx +1279 -0
- package/src/components/RequirementsBoard.jsx +187 -0
- package/src/components/SecretarySettings.jsx +295 -0
- package/src/components/SetupWizard.jsx +388 -0
- package/src/components/Sidebar.jsx +169 -0
- package/src/components/SystemMonitor.jsx +808 -0
- package/src/components/TalentMarket.jsx +183 -0
- package/src/components/TeamDetail.jsx +697 -0
- package/src/core/agent/base-agent.js +104 -0
- package/src/core/agent/chat-store.js +602 -0
- package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
- package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
- package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
- package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
- package/src/core/agent/cli-agent/backends/index.js +27 -0
- package/src/core/agent/cli-agent/backends/registry.js +580 -0
- package/src/core/agent/cli-agent/index.js +154 -0
- package/src/core/agent/index.js +60 -0
- package/src/core/agent/llm-agent/client.js +320 -0
- package/src/core/agent/llm-agent/index.js +97 -0
- package/src/core/agent/message-bus.js +211 -0
- package/src/core/agent/session.js +608 -0
- package/src/core/agent/tools.js +596 -0
- package/src/core/agent/web-agent/backends/base-backend.js +180 -0
- package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
- package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
- package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
- package/src/core/agent/web-agent/backends/index.js +91 -0
- package/src/core/agent/web-agent/index.js +278 -0
- package/src/core/agent/web-agent/web-client.js +407 -0
- package/src/core/employee/base-employee.js +1088 -0
- package/src/core/employee/index.js +35 -0
- package/src/core/employee/knowledge.js +327 -0
- package/src/core/employee/lifecycle.js +990 -0
- package/src/core/employee/memory/index.js +642 -0
- package/src/core/employee/memory/store.js +143 -0
- package/src/core/employee/performance.js +224 -0
- package/src/core/employee/secretary.js +625 -0
- package/src/core/employee/skills.js +398 -0
- package/src/core/index.js +38 -0
- package/src/core/organization/company.js +2600 -0
- package/src/core/organization/department.js +737 -0
- package/src/core/organization/group-chat-loop.js +264 -0
- package/src/core/organization/index.js +8 -0
- package/src/core/organization/persistence.js +111 -0
- package/src/core/organization/team.js +267 -0
- package/src/core/organization/workforce/hr.js +377 -0
- package/src/core/organization/workforce/providers.js +468 -0
- package/src/core/organization/workforce/role-archetypes.js +805 -0
- package/src/core/organization/workforce/talent-market.js +205 -0
- package/src/core/prompts.js +532 -0
- package/src/core/requirement.js +1789 -0
- package/src/core/system/audit.js +483 -0
- package/src/core/system/cron.js +449 -0
- package/src/core/system/index.js +7 -0
- package/src/core/system/plugin.js +2183 -0
- package/src/core/utils/json-parse.js +188 -0
- package/src/core/workspace.js +239 -0
- package/src/lib/api-i18n.js +211 -0
- package/src/lib/avatar.js +268 -0
- package/src/lib/client-store.js +1025 -0
- package/src/lib/config-validator.js +483 -0
- package/src/lib/format-time.js +22 -0
- package/src/lib/hooks.js +414 -0
- package/src/lib/i18n.js +134 -0
- package/src/lib/paths.js +23 -0
- package/src/lib/store.js +72 -0
- package/src/locales/de.js +393 -0
- package/src/locales/en.js +1054 -0
- package/src/locales/es.js +393 -0
- package/src/locales/fr.js +393 -0
- package/src/locales/ja.js +501 -0
- package/src/locales/ko.js +513 -0
- package/src/locales/zh.js +828 -0
- package/tailwind.config.mjs +11 -0
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { useStore } from '@/lib/client-store';
|
|
5
|
+
import AgentDetailModal from './AgentDetailModal';
|
|
6
|
+
import AgentChatModal from './AgentChatModal';
|
|
7
|
+
import RequirementDetail from './RequirementDetail';
|
|
8
|
+
import { useI18n } from '@/lib/i18n';
|
|
9
|
+
import CachedAvatar from './CachedAvatar';
|
|
10
|
+
|
|
11
|
+
export default function DepartmentDetail() {
|
|
12
|
+
const { t } = useI18n();
|
|
13
|
+
const {
|
|
14
|
+
company, loading, dismissAgent,
|
|
15
|
+
fetchDepartmentRequirements, createRequirement,
|
|
16
|
+
navigateToRequirement, navigateBackFromDepartment,
|
|
17
|
+
activeDepartmentId, planAdjustment, confirmAdjustment,
|
|
18
|
+
disbandDepartment, pendingPlan, setPendingPlan,
|
|
19
|
+
deleteRequirement, restartRequirement,
|
|
20
|
+
createTeam, fetchTeams, navigateToTeam,
|
|
21
|
+
} = useStore();
|
|
22
|
+
|
|
23
|
+
// Sub-modals
|
|
24
|
+
const [selectedAgent, setSelectedAgent] = useState(null);
|
|
25
|
+
const [chatAgent, setChatAgent] = useState(null);
|
|
26
|
+
const [activeReqId, setActiveReqId] = useState(null);
|
|
27
|
+
const [dismissTarget, setDismissTarget] = useState(null);
|
|
28
|
+
const [dismissReason, setDismissReason] = useState('');
|
|
29
|
+
const [showAdjust, setShowAdjust] = useState(false);
|
|
30
|
+
const [adjustGoal, setAdjustGoal] = useState('');
|
|
31
|
+
const [showDisband, setShowDisband] = useState(false);
|
|
32
|
+
const [disbandReason, setDisbandReason] = useState('');
|
|
33
|
+
|
|
34
|
+
// New requirement form (inline)
|
|
35
|
+
const [showNewReq, setShowNewReq] = useState(false);
|
|
36
|
+
const [newReqTitle, setNewReqTitle] = useState('');
|
|
37
|
+
const [newReqDesc, setNewReqDesc] = useState('');
|
|
38
|
+
const [newReqWorkspaceDir, setNewReqWorkspaceDir] = useState('');
|
|
39
|
+
|
|
40
|
+
const [deptRequirements, setDeptRequirements] = useState([]);
|
|
41
|
+
|
|
42
|
+
// Folder browser state
|
|
43
|
+
const [showFolderBrowser, setShowFolderBrowser] = useState(false);
|
|
44
|
+
const [browseDirs, setBrowseDirs] = useState([]);
|
|
45
|
+
const [browseCurrentPath, setBrowseCurrentPath] = useState('');
|
|
46
|
+
const [browseParentPath, setBrowseParentPath] = useState(null);
|
|
47
|
+
const [browseLoading, setBrowseLoading] = useState(false);
|
|
48
|
+
|
|
49
|
+
// Team creation form
|
|
50
|
+
const [showNewTeam, setShowNewTeam] = useState(false);
|
|
51
|
+
const [teamName, setTeamName] = useState('');
|
|
52
|
+
const [teamDesc, setTeamDesc] = useState('');
|
|
53
|
+
const [selectedMembers, setSelectedMembers] = useState([]);
|
|
54
|
+
const [selectedLeader, setSelectedLeader] = useState('');
|
|
55
|
+
const [deptTeams, setDeptTeams] = useState([]);
|
|
56
|
+
|
|
57
|
+
const fetchDirs = async (dirPath) => {
|
|
58
|
+
setBrowseLoading(true);
|
|
59
|
+
try {
|
|
60
|
+
const url = dirPath ? `/api/browse-dir?path=${encodeURIComponent(dirPath)}` : '/api/browse-dir';
|
|
61
|
+
const res = await fetch(url);
|
|
62
|
+
const data = await res.json();
|
|
63
|
+
if (data.error) return;
|
|
64
|
+
setBrowseDirs(data.dirs || []);
|
|
65
|
+
setBrowseCurrentPath(data.current || '');
|
|
66
|
+
setBrowseParentPath(data.parent || null);
|
|
67
|
+
} catch (e) { /* handled */ }
|
|
68
|
+
setBrowseLoading(false);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const dept = company?.departments?.find(d => d.id === activeDepartmentId);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (activeDepartmentId) {
|
|
75
|
+
fetchDepartmentRequirements(activeDepartmentId).then(setDeptRequirements);
|
|
76
|
+
fetchTeams(activeDepartmentId).then(teams => setDeptTeams(teams || []));
|
|
77
|
+
}
|
|
78
|
+
}, [activeDepartmentId]);
|
|
79
|
+
|
|
80
|
+
if (!dept) {
|
|
81
|
+
return (
|
|
82
|
+
<div className="p-6 text-center text-[var(--muted)]">
|
|
83
|
+
<p>{t('dept.empty')}</p>
|
|
84
|
+
<button className="btn-secondary mt-4" onClick={navigateBackFromDepartment}>{t('dept.detail.back')}</button>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const handleDismiss = async () => {
|
|
90
|
+
if (!dismissTarget) return;
|
|
91
|
+
try {
|
|
92
|
+
await dismissAgent(dismissTarget.deptId, dismissTarget.agentId, dismissReason || 'Boss decision');
|
|
93
|
+
setDismissTarget(null);
|
|
94
|
+
setDismissReason('');
|
|
95
|
+
} catch (e) { /* handled */ }
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const handleCreateRequirement = async () => {
|
|
99
|
+
if (!newReqTitle) return;
|
|
100
|
+
try {
|
|
101
|
+
const result = await createRequirement(activeDepartmentId, newReqTitle, newReqDesc, newReqWorkspaceDir || undefined);
|
|
102
|
+
setShowNewReq(false);
|
|
103
|
+
setNewReqTitle('');
|
|
104
|
+
setNewReqDesc('');
|
|
105
|
+
setNewReqWorkspaceDir('');
|
|
106
|
+
if (result?.id) {
|
|
107
|
+
navigateToRequirement(result.id);
|
|
108
|
+
}
|
|
109
|
+
fetchDepartmentRequirements(activeDepartmentId).then(setDeptRequirements);
|
|
110
|
+
} catch (e) { /* handled */ }
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handleCreateTeam = async () => {
|
|
114
|
+
if (!teamName || selectedMembers.length === 0 || !selectedLeader) return;
|
|
115
|
+
try {
|
|
116
|
+
const result = await createTeam(activeDepartmentId, teamName, selectedMembers, selectedLeader, teamDesc);
|
|
117
|
+
setShowNewTeam(false);
|
|
118
|
+
setTeamName('');
|
|
119
|
+
setTeamDesc('');
|
|
120
|
+
setSelectedMembers([]);
|
|
121
|
+
setSelectedLeader('');
|
|
122
|
+
if (result?.id) {
|
|
123
|
+
navigateToTeam(result.id);
|
|
124
|
+
}
|
|
125
|
+
fetchTeams(activeDepartmentId).then(teams => setDeptTeams(teams || []));
|
|
126
|
+
} catch (e) { /* handled */ }
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const handleAdjustPlan = async () => {
|
|
130
|
+
if (!adjustGoal) return;
|
|
131
|
+
try {
|
|
132
|
+
await planAdjustment(activeDepartmentId, adjustGoal);
|
|
133
|
+
} catch (e) { /* handled */ }
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const handleDisband = async () => {
|
|
137
|
+
try {
|
|
138
|
+
await disbandDepartment(activeDepartmentId, disbandReason || 'Organization restructuring');
|
|
139
|
+
setShowDisband(false);
|
|
140
|
+
setDisbandReason('');
|
|
141
|
+
navigateBackFromDepartment();
|
|
142
|
+
} catch (e) { /* handled */ }
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const deptReports = (company.progressReports || [])
|
|
146
|
+
.slice().reverse()
|
|
147
|
+
.filter(pr => pr.reports.some(r => r.department === dept.name))
|
|
148
|
+
.slice(0, 5);
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div className="h-full flex flex-col animate-fade-in">
|
|
152
|
+
{/* ===== Top navigation bar ===== */}
|
|
153
|
+
<div className="shrink-0 border-b border-[var(--border)] bg-[var(--card)]/50 backdrop-blur-sm px-6 py-4">
|
|
154
|
+
<div className="flex items-center justify-between">
|
|
155
|
+
<div className="flex items-center gap-4 min-w-0">
|
|
156
|
+
<button
|
|
157
|
+
onClick={navigateBackFromDepartment}
|
|
158
|
+
className="text-[var(--muted)] hover:text-[var(--foreground)] transition-colors text-sm flex items-center gap-1 shrink-0"
|
|
159
|
+
>
|
|
160
|
+
← {t('dept.detail.back')}
|
|
161
|
+
</button>
|
|
162
|
+
<div className="w-px h-6 bg-[var(--border)]" />
|
|
163
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
164
|
+
<div className="w-10 h-10 bg-gradient-to-br from-indigo-600 to-blue-700 rounded-lg flex items-center justify-center text-lg shrink-0">
|
|
165
|
+
🏢
|
|
166
|
+
</div>
|
|
167
|
+
<div className="min-w-0">
|
|
168
|
+
<h1 className="text-lg font-bold flex items-center gap-2 truncate">
|
|
169
|
+
{dept.name}
|
|
170
|
+
<span className={`text-xs px-2 py-0.5 rounded-full ${
|
|
171
|
+
dept.status === 'completed' ? 'bg-green-900/30 text-green-400' :
|
|
172
|
+
dept.status === 'active' ? 'bg-yellow-900/30 text-yellow-400' :
|
|
173
|
+
'bg-blue-900/30 text-blue-400'
|
|
174
|
+
}`}>
|
|
175
|
+
{dept.status}
|
|
176
|
+
</span>
|
|
177
|
+
</h1>
|
|
178
|
+
<p className="text-xs text-[var(--muted)] truncate">{dept.mission}</p>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
183
|
+
<button
|
|
184
|
+
className="btn-primary flex items-center gap-1.5"
|
|
185
|
+
onClick={() => { setShowNewReq(true); setNewReqTitle(''); setNewReqDesc(''); setNewReqWorkspaceDir(''); }}
|
|
186
|
+
>
|
|
187
|
+
{t('dept.newReq.btn')}
|
|
188
|
+
</button>
|
|
189
|
+
<button
|
|
190
|
+
className="text-xs bg-purple-900/20 text-purple-400 hover:bg-purple-900/40 px-3 py-1.5 rounded-lg transition-colors"
|
|
191
|
+
onClick={() => { setShowNewTeam(true); setTeamName(''); setTeamDesc(''); setSelectedMembers([]); setSelectedLeader(''); }}
|
|
192
|
+
>{t('team.newTeamBtn')}</button>
|
|
193
|
+
<button
|
|
194
|
+
className="text-xs bg-blue-900/20 text-blue-400 hover:bg-blue-900/40 px-3 py-1.5 rounded-lg transition-colors"
|
|
195
|
+
onClick={() => { setShowAdjust(true); setAdjustGoal(''); setPendingPlan(null); }}
|
|
196
|
+
>{t('dept.detail.adjustBtn')}</button>
|
|
197
|
+
<button
|
|
198
|
+
className="text-xs bg-red-900/20 text-red-400 hover:bg-red-900/40 px-3 py-1.5 rounded-lg transition-colors"
|
|
199
|
+
onClick={() => { setShowDisband(true); setDisbandReason(''); }}
|
|
200
|
+
>{t('dept.detail.disbandBtn')}</button>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
{/* Stats bar */}
|
|
204
|
+
<div className="flex items-center gap-4 mt-2 text-xs text-[var(--muted)]">
|
|
205
|
+
<span>👥 {t('dept.members', { n: dept.members.length })}</span>
|
|
206
|
+
<span>💰 ${(dept.tokenUsage?.totalCost || 0).toFixed(4)}</span>
|
|
207
|
+
<span>🔢 {(dept.tokenUsage?.totalTokens || 0).toLocaleString()} tokens</span>
|
|
208
|
+
<span>📋 {deptRequirements.length} {t('dept.detail.requirements')}</span>
|
|
209
|
+
<span>👥 {deptTeams.length} {t('team.teamsCount')}</span>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
{/* ===== Main content (scrollable) ===== */}
|
|
214
|
+
<div className="flex-1 overflow-auto p-6 space-y-6">
|
|
215
|
+
|
|
216
|
+
{/* New requirement form (expandable card) */}
|
|
217
|
+
{showNewReq && (
|
|
218
|
+
<div className="card border-[var(--accent)]/30 animate-fade-in space-y-4">
|
|
219
|
+
<div className="flex items-center justify-between">
|
|
220
|
+
<h3 className="text-base font-semibold">{t('dept.newReq.title')}</h3>
|
|
221
|
+
<button onClick={() => setShowNewReq(false)} className="text-[var(--muted)] hover:text-white text-lg">✕</button>
|
|
222
|
+
</div>
|
|
223
|
+
<div>
|
|
224
|
+
<label className="block text-sm mb-1 text-[var(--muted)]">{t('dept.newReq.nameLabel')}</label>
|
|
225
|
+
<input
|
|
226
|
+
className="input w-full"
|
|
227
|
+
placeholder={t('dept.newReq.namePlaceholder')}
|
|
228
|
+
value={newReqTitle}
|
|
229
|
+
onChange={e => setNewReqTitle(e.target.value)}
|
|
230
|
+
autoFocus
|
|
231
|
+
/>
|
|
232
|
+
</div>
|
|
233
|
+
<div>
|
|
234
|
+
<label className="block text-sm mb-1 text-[var(--muted)]">{t('dept.newReq.descLabel')}</label>
|
|
235
|
+
<textarea
|
|
236
|
+
className="input w-full h-20 resize-none"
|
|
237
|
+
placeholder={t('dept.newReq.descPlaceholder')}
|
|
238
|
+
value={newReqDesc}
|
|
239
|
+
onChange={e => setNewReqDesc(e.target.value)}
|
|
240
|
+
/>
|
|
241
|
+
</div>
|
|
242
|
+
<div>
|
|
243
|
+
<label className="block text-sm mb-1 text-[var(--muted)]">{t('dept.newReq.workspaceDirLabel')}</label>
|
|
244
|
+
<div className="flex gap-2">
|
|
245
|
+
<input
|
|
246
|
+
className="input w-full font-mono text-xs min-h-[36px]"
|
|
247
|
+
value={newReqWorkspaceDir}
|
|
248
|
+
onChange={e => setNewReqWorkspaceDir(e.target.value)}
|
|
249
|
+
placeholder={t('dept.newReq.workspaceDirPlaceholder')}
|
|
250
|
+
/>
|
|
251
|
+
<button
|
|
252
|
+
className="btn-secondary shrink-0 text-sm px-3"
|
|
253
|
+
onClick={() => { setShowFolderBrowser(true); fetchDirs(newReqWorkspaceDir || ''); }}
|
|
254
|
+
title={t('dept.newReq.browseTitle')}
|
|
255
|
+
>📁</button>
|
|
256
|
+
{newReqWorkspaceDir && (
|
|
257
|
+
<button
|
|
258
|
+
className="text-[var(--muted)] hover:text-red-400 text-sm px-1 shrink-0"
|
|
259
|
+
onClick={() => setNewReqWorkspaceDir('')}
|
|
260
|
+
title={t('common.delete')}
|
|
261
|
+
>✕</button>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
<p className="text-[10px] text-[var(--muted)] mt-1">{t('dept.newReq.workspaceDirHint')}</p>
|
|
265
|
+
</div>
|
|
266
|
+
<div className="flex justify-end gap-2">
|
|
267
|
+
<button className="btn-secondary" onClick={() => setShowNewReq(false)}>{t('common.cancel')}</button>
|
|
268
|
+
<button
|
|
269
|
+
className="btn-primary"
|
|
270
|
+
disabled={!newReqTitle || loading}
|
|
271
|
+
onClick={handleCreateRequirement}
|
|
272
|
+
>
|
|
273
|
+
{loading ? t('dept.newReq.creating') : t('dept.newReq.submitBtn')}
|
|
274
|
+
</button>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
|
|
279
|
+
{/* New team form (expandable card) */}
|
|
280
|
+
{showNewTeam && (
|
|
281
|
+
<div className="card border-purple-500/30 animate-fade-in space-y-4">
|
|
282
|
+
<div className="flex items-center justify-between">
|
|
283
|
+
<h3 className="text-base font-semibold">👥 {t('team.newTeamBtn')}</h3>
|
|
284
|
+
<button onClick={() => setShowNewTeam(false)} className="text-[var(--muted)] hover:text-white text-lg">✕</button>
|
|
285
|
+
</div>
|
|
286
|
+
<div>
|
|
287
|
+
<label className="block text-sm mb-1 text-[var(--muted)]">{t('team.nameLabel')}</label>
|
|
288
|
+
<input
|
|
289
|
+
className="input w-full"
|
|
290
|
+
placeholder={t('team.namePlaceholder')}
|
|
291
|
+
value={teamName}
|
|
292
|
+
onChange={e => setTeamName(e.target.value)}
|
|
293
|
+
autoFocus
|
|
294
|
+
/>
|
|
295
|
+
</div>
|
|
296
|
+
<div>
|
|
297
|
+
<label className="block text-sm mb-1 text-[var(--muted)]">{t('team.descLabel')}</label>
|
|
298
|
+
<textarea
|
|
299
|
+
className="input w-full h-16 resize-none"
|
|
300
|
+
placeholder={t('team.descPlaceholder')}
|
|
301
|
+
value={teamDesc}
|
|
302
|
+
onChange={e => setTeamDesc(e.target.value)}
|
|
303
|
+
/>
|
|
304
|
+
</div>
|
|
305
|
+
<div>
|
|
306
|
+
<label className="block text-sm mb-1 text-[var(--muted)]">{t('team.selectMembers')}</label>
|
|
307
|
+
<div className="flex flex-wrap gap-1.5 max-h-40 overflow-auto">
|
|
308
|
+
{dept.members.map(member => {
|
|
309
|
+
const selected = selectedMembers.includes(member.id);
|
|
310
|
+
return (
|
|
311
|
+
<label
|
|
312
|
+
key={member.id}
|
|
313
|
+
className={`inline-flex items-center gap-1.5 px-2 py-1 rounded-full cursor-pointer text-xs transition-all border ${
|
|
314
|
+
selected
|
|
315
|
+
? 'bg-purple-900/30 border-purple-500/50 text-purple-300'
|
|
316
|
+
: 'bg-white/5 border-transparent hover:bg-white/10 text-[var(--foreground)]'
|
|
317
|
+
}`}
|
|
318
|
+
>
|
|
319
|
+
<input
|
|
320
|
+
type="checkbox"
|
|
321
|
+
checked={selected}
|
|
322
|
+
onChange={e => {
|
|
323
|
+
if (e.target.checked) {
|
|
324
|
+
setSelectedMembers(prev => [...prev, member.id]);
|
|
325
|
+
} else {
|
|
326
|
+
setSelectedMembers(prev => prev.filter(id => id !== member.id));
|
|
327
|
+
if (selectedLeader === member.id) setSelectedLeader('');
|
|
328
|
+
}
|
|
329
|
+
}}
|
|
330
|
+
className="hidden"
|
|
331
|
+
/>
|
|
332
|
+
<CachedAvatar src={member.avatar} alt={member.name} className="w-5 h-5 rounded-full" />
|
|
333
|
+
<span>{member.name}</span>
|
|
334
|
+
{selected && <span className="text-purple-400">✓</span>}
|
|
335
|
+
</label>
|
|
336
|
+
);
|
|
337
|
+
})}
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
{selectedMembers.length > 0 && (
|
|
341
|
+
<div>
|
|
342
|
+
<label className="block text-sm mb-1 text-[var(--muted)]">{t('team.selectLeader')}</label>
|
|
343
|
+
<select
|
|
344
|
+
className="input w-full"
|
|
345
|
+
value={selectedLeader}
|
|
346
|
+
onChange={e => setSelectedLeader(e.target.value)}
|
|
347
|
+
>
|
|
348
|
+
<option value="">{t('team.selectLeaderPlaceholder')}</option>
|
|
349
|
+
{dept.members.filter(m => selectedMembers.includes(m.id)).map(m => (
|
|
350
|
+
<option key={m.id} value={m.id}>{m.name} - {m.role}</option>
|
|
351
|
+
))}
|
|
352
|
+
</select>
|
|
353
|
+
</div>
|
|
354
|
+
)}
|
|
355
|
+
<div className="flex justify-end gap-2">
|
|
356
|
+
<button className="btn-secondary" onClick={() => setShowNewTeam(false)}>{t('common.cancel')}</button>
|
|
357
|
+
<button
|
|
358
|
+
className="btn-primary"
|
|
359
|
+
disabled={!teamName || selectedMembers.length === 0 || !selectedLeader || loading}
|
|
360
|
+
onClick={handleCreateTeam}
|
|
361
|
+
>
|
|
362
|
+
{loading ? t('common.loading') : t('team.createBtn')}
|
|
363
|
+
</button>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
)}
|
|
367
|
+
|
|
368
|
+
{/* Requirements list */}
|
|
369
|
+
<div>
|
|
370
|
+
<div className="flex items-center justify-between mb-3">
|
|
371
|
+
<h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider">{t('dept.detail.requirements')}</h2>
|
|
372
|
+
{!showNewReq && (
|
|
373
|
+
<button
|
|
374
|
+
className="text-xs text-[var(--accent)] hover:underline"
|
|
375
|
+
onClick={() => { setShowNewReq(true); setNewReqTitle(''); setNewReqDesc(''); setNewReqWorkspaceDir(''); }}
|
|
376
|
+
>
|
|
377
|
+
+ {t('dept.newReq.btn')}
|
|
378
|
+
</button>
|
|
379
|
+
)}
|
|
380
|
+
</div>
|
|
381
|
+
{deptRequirements.length > 0 ? (
|
|
382
|
+
<div className="space-y-2">
|
|
383
|
+
{deptRequirements.map((req) => {
|
|
384
|
+
const statusCfg = {
|
|
385
|
+
pending: { label: t('requirements.status.pending'), color: 'text-gray-400', bg: 'bg-gray-900/30', icon: '⏳' },
|
|
386
|
+
planning: { label: t('requirements.status.planning'), color: 'text-blue-400', bg: 'bg-blue-900/30', icon: '📝' },
|
|
387
|
+
in_progress: { label: t('requirements.status.in_progress'), color: 'text-yellow-400', bg: 'bg-yellow-900/30', icon: '⚙️' },
|
|
388
|
+
pending_approval: { label: t('requirements.status.pending_approval'), color: 'text-orange-400', bg: 'bg-orange-900/30', icon: '🔍' },
|
|
389
|
+
completed: { label: t('requirements.stats.completed'), color: 'text-green-400', bg: 'bg-green-900/30', icon: '✅' },
|
|
390
|
+
failed: { label: t('requirements.status.failed'), color: 'text-red-400', bg: 'bg-red-900/30', icon: '❌' },
|
|
391
|
+
};
|
|
392
|
+
const st = statusCfg[req.status] || statusCfg.pending;
|
|
393
|
+
return (
|
|
394
|
+
<div
|
|
395
|
+
key={req.id}
|
|
396
|
+
className="card cursor-pointer hover:border-[var(--accent)]/30 transition-all"
|
|
397
|
+
onClick={() => navigateToRequirement(req.id)}
|
|
398
|
+
>
|
|
399
|
+
<div className="flex items-center justify-between">
|
|
400
|
+
<div className="flex items-center gap-2">
|
|
401
|
+
<span>{st.icon}</span>
|
|
402
|
+
<span className="text-sm font-medium">{req.title}</span>
|
|
403
|
+
</div>
|
|
404
|
+
<div className="flex items-center gap-2">
|
|
405
|
+
<span className={`text-[10px] px-1.5 py-0.5 rounded ${st.bg} ${st.color}`}>{st.label}</span>
|
|
406
|
+
{req.workflow && <span className="text-[10px] text-[var(--muted)]">📊 {req.workflow.completedCount || 0}/{req.workflow.nodeCount || 0}</span>}
|
|
407
|
+
{req.chatCount > 0 && <span className="text-[10px] text-[var(--muted)]">💬 {req.chatCount}</span>}
|
|
408
|
+
{req.outputCount > 0 && <span className="text-[10px] text-[var(--muted)]">📦 {req.outputCount}</span>}
|
|
409
|
+
<button
|
|
410
|
+
onClick={(e) => { e.stopPropagation(); restartRequirement(req.id); }}
|
|
411
|
+
className="text-[10px] px-1.5 py-0.5 rounded bg-blue-600/15 hover:bg-blue-600/25 text-blue-400 transition-colors"
|
|
412
|
+
title={t('reqDetail.live.restart')}
|
|
413
|
+
>🔄</button>
|
|
414
|
+
<button
|
|
415
|
+
onClick={(e) => { e.stopPropagation(); if (confirm(t('reqDetail.live.confirmDelete'))) deleteRequirement(req.id); }}
|
|
416
|
+
className="text-[10px] px-1.5 py-0.5 rounded bg-red-600/15 hover:bg-red-600/25 text-red-400 transition-colors"
|
|
417
|
+
title={t('reqDetail.live.deleteReq')}
|
|
418
|
+
>🗑️</button>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
);
|
|
423
|
+
})}
|
|
424
|
+
</div>
|
|
425
|
+
) : (
|
|
426
|
+
<div className="card text-center py-8 text-[var(--muted)]">
|
|
427
|
+
<div className="text-3xl mb-2">📋</div>
|
|
428
|
+
<p className="text-sm">{t('requirements.empty')}</p>
|
|
429
|
+
{!showNewReq && (
|
|
430
|
+
<button
|
|
431
|
+
className="btn-secondary mt-3 text-sm"
|
|
432
|
+
onClick={() => { setShowNewReq(true); setNewReqTitle(''); setNewReqDesc(''); setNewReqWorkspaceDir(''); }}
|
|
433
|
+
>
|
|
434
|
+
{t('dept.newReq.btn')}
|
|
435
|
+
</button>
|
|
436
|
+
)}
|
|
437
|
+
</div>
|
|
438
|
+
)}
|
|
439
|
+
</div>
|
|
440
|
+
|
|
441
|
+
{/* Teams list */}
|
|
442
|
+
<div>
|
|
443
|
+
<div className="flex items-center justify-between mb-3">
|
|
444
|
+
<h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider">👥 {t('team.teamsTitle')}</h2>
|
|
445
|
+
{!showNewTeam && (
|
|
446
|
+
<button
|
|
447
|
+
className="text-xs text-purple-400 hover:underline"
|
|
448
|
+
onClick={() => { setShowNewTeam(true); setTeamName(''); setTeamDesc(''); setSelectedMembers([]); setSelectedLeader(''); }}
|
|
449
|
+
>
|
|
450
|
+
+ {t('team.newTeamBtn')}
|
|
451
|
+
</button>
|
|
452
|
+
)}
|
|
453
|
+
</div>
|
|
454
|
+
{deptTeams.length > 0 ? (
|
|
455
|
+
<div className="space-y-2">
|
|
456
|
+
{deptTeams.map(team => {
|
|
457
|
+
const teamMembers = (team.memberIds || [])
|
|
458
|
+
.map(mid => dept.members.find(m => m.id === mid))
|
|
459
|
+
.filter(Boolean);
|
|
460
|
+
return (
|
|
461
|
+
<div
|
|
462
|
+
key={team.id}
|
|
463
|
+
className="card cursor-pointer hover:border-purple-500/30 transition-all"
|
|
464
|
+
onClick={() => navigateToTeam(team.id)}
|
|
465
|
+
>
|
|
466
|
+
<div className="flex items-center justify-between">
|
|
467
|
+
<div className="flex items-center gap-2">
|
|
468
|
+
<span className="text-lg">👥</span>
|
|
469
|
+
<span className="text-sm font-medium">{team.name}</span>
|
|
470
|
+
{team.leaderName && (
|
|
471
|
+
<span className="text-[10px] bg-yellow-900/30 text-yellow-400 px-1.5 py-0.5 rounded">👔 {team.leaderName}</span>
|
|
472
|
+
)}
|
|
473
|
+
</div>
|
|
474
|
+
<div className="flex items-center gap-2">
|
|
475
|
+
<div className="flex items-center -space-x-1.5">
|
|
476
|
+
{teamMembers.slice(0, 5).map(m => (
|
|
477
|
+
<CachedAvatar key={m.id} src={m.avatar} alt={m.name} className="w-5 h-5 rounded-full ring-1 ring-[var(--card)]" />
|
|
478
|
+
))}
|
|
479
|
+
{teamMembers.length > 5 && (
|
|
480
|
+
<span className="w-5 h-5 rounded-full bg-white/10 ring-1 ring-[var(--card)] flex items-center justify-center text-[8px] text-[var(--muted)]">+{teamMembers.length - 5}</span>
|
|
481
|
+
)}
|
|
482
|
+
</div>
|
|
483
|
+
<span className="text-[10px] text-[var(--muted)]">🔄 {team.sprintCount || 0}</span>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
{team.description && <p className="text-xs text-[var(--muted)] mt-1 truncate">{team.description}</p>}
|
|
487
|
+
</div>
|
|
488
|
+
);
|
|
489
|
+
})}
|
|
490
|
+
</div>
|
|
491
|
+
) : (
|
|
492
|
+
<div className="card text-center py-6 text-[var(--muted)]">
|
|
493
|
+
<div className="text-2xl mb-2">👥</div>
|
|
494
|
+
<p className="text-sm">{t('team.noTeams')}</p>
|
|
495
|
+
{!showNewTeam && (
|
|
496
|
+
<button
|
|
497
|
+
className="btn-secondary mt-3 text-sm"
|
|
498
|
+
onClick={() => { setShowNewTeam(true); setTeamName(''); setTeamDesc(''); setSelectedMembers([]); setSelectedLeader(''); }}
|
|
499
|
+
>
|
|
500
|
+
{t('team.newTeamBtn')}
|
|
501
|
+
</button>
|
|
502
|
+
)}
|
|
503
|
+
</div>
|
|
504
|
+
)}
|
|
505
|
+
</div>
|
|
506
|
+
|
|
507
|
+
{/* Member list */}
|
|
508
|
+
<div>
|
|
509
|
+
<h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider mb-3">{t('dept.detail.members')}</h2>
|
|
510
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
511
|
+
{dept.members.map((member) => (
|
|
512
|
+
<div
|
|
513
|
+
key={member.id}
|
|
514
|
+
className="card hover:border-[var(--accent)]/30 transition-all cursor-pointer group"
|
|
515
|
+
onClick={() => setSelectedAgent(member.id)}
|
|
516
|
+
>
|
|
517
|
+
<div className="flex items-start gap-3">
|
|
518
|
+
<div className="relative shrink-0">
|
|
519
|
+
<CachedAvatar src={member.avatar} alt={member.name} className="w-12 h-12 rounded-full bg-[var(--border)]" />
|
|
520
|
+
{member.avgScore >= 80 && (
|
|
521
|
+
<span className="absolute -top-1 -right-1 text-xs animate-pulse drop-shadow-lg">🌸</span>
|
|
522
|
+
)}
|
|
523
|
+
</div>
|
|
524
|
+
<div className="flex-1 min-w-0">
|
|
525
|
+
<div className="flex items-center gap-2">
|
|
526
|
+
<span className="font-medium text-sm truncate">{member.name}</span>
|
|
527
|
+
{dept.leader === member.id && (
|
|
528
|
+
<span className="text-[10px] bg-yellow-900/30 text-yellow-400 px-1.5 py-0.5 rounded">{t('dept.detail.leader')}</span>
|
|
529
|
+
)}
|
|
530
|
+
<span className={`status-dot ${member.status}`} />
|
|
531
|
+
</div>
|
|
532
|
+
<div className="text-xs text-[var(--muted)]">
|
|
533
|
+
{member.gender === 'female' ? '👩' : '👨'}{member.age ? ` ${t('display.ageYears', { n: member.age })}` : ''} · {member.role}
|
|
534
|
+
</div>
|
|
535
|
+
<div className="text-[10px] text-[var(--muted)] italic mt-1 truncate">"{member.signature}"</div>
|
|
536
|
+
</div>
|
|
537
|
+
<button
|
|
538
|
+
className="opacity-0 group-hover:opacity-100 text-blue-400 hover:text-blue-300 text-sm transition-opacity"
|
|
539
|
+
title={t('agentChat.chatBtn')}
|
|
540
|
+
onClick={(e) => {
|
|
541
|
+
e.stopPropagation();
|
|
542
|
+
setChatAgent({ id: member.id, name: member.name, avatar: member.avatar, role: member.role, signature: member.signature, department: dept.name });
|
|
543
|
+
}}
|
|
544
|
+
>
|
|
545
|
+
💬
|
|
546
|
+
</button>
|
|
547
|
+
<button
|
|
548
|
+
className="opacity-0 group-hover:opacity-100 text-red-400 hover:text-red-300 text-sm transition-opacity"
|
|
549
|
+
title={t('dept.dismiss.title')}
|
|
550
|
+
onClick={(e) => {
|
|
551
|
+
e.stopPropagation();
|
|
552
|
+
setDismissTarget({ deptId: dept.id, agentId: member.id, name: member.name });
|
|
553
|
+
}}
|
|
554
|
+
>
|
|
555
|
+
🔥
|
|
556
|
+
</button>
|
|
557
|
+
</div>
|
|
558
|
+
{/* Tags */}
|
|
559
|
+
<div className="flex items-center gap-2 mt-2 flex-wrap">
|
|
560
|
+
<span className="text-[10px] bg-blue-900/30 text-blue-400 px-1.5 py-0.5 rounded">{member.provider.name}</span>
|
|
561
|
+
{member.avgScore && (
|
|
562
|
+
<span className={`text-[10px] px-1.5 py-0.5 rounded ${
|
|
563
|
+
member.avgScore >= 80 ? 'bg-green-900/30 text-green-400' :
|
|
564
|
+
member.avgScore >= 60 ? 'bg-yellow-900/30 text-yellow-400' :
|
|
565
|
+
'bg-red-900/30 text-red-400'
|
|
566
|
+
}`}>
|
|
567
|
+
{t('dept.detail.performance', { score: member.avgScore })}
|
|
568
|
+
</span>
|
|
569
|
+
)}
|
|
570
|
+
<span className="text-[10px] bg-purple-900/30 text-purple-400 px-1.5 py-0.5 rounded">
|
|
571
|
+
{t('dept.detail.memory', { n: (member.memory?.shortTermCount || 0) + (member.memory?.longTermCount || 0) })}
|
|
572
|
+
</span>
|
|
573
|
+
{member.taskCount > 0 && (
|
|
574
|
+
<span className="text-[10px] bg-orange-900/30 text-orange-400 px-1.5 py-0.5 rounded">
|
|
575
|
+
{t('dept.detail.tasks', { n: member.taskCount })}
|
|
576
|
+
</span>
|
|
577
|
+
)}
|
|
578
|
+
{member.tokenUsage?.totalTokens > 0 && (
|
|
579
|
+
<span className="text-[10px] bg-green-900/30 text-green-400 px-1.5 py-0.5 rounded">
|
|
580
|
+
${(member.tokenUsage.totalCost || 0).toFixed(4)}
|
|
581
|
+
</span>
|
|
582
|
+
)}
|
|
583
|
+
</div>
|
|
584
|
+
{/* Skills */}
|
|
585
|
+
<div className="flex gap-1 mt-2 flex-wrap">
|
|
586
|
+
{member.skills.slice(0, 3).map((s, i) => (
|
|
587
|
+
<span key={i} className="text-[10px] text-[var(--muted)] bg-white/5 px-1.5 py-0.5 rounded">{s}</span>
|
|
588
|
+
))}
|
|
589
|
+
{member.skills.length > 3 && (
|
|
590
|
+
<span className="text-[10px] text-[var(--muted)]">+{member.skills.length - 3}</span>
|
|
591
|
+
)}
|
|
592
|
+
</div>
|
|
593
|
+
</div>
|
|
594
|
+
))}
|
|
595
|
+
</div>
|
|
596
|
+
</div>
|
|
597
|
+
|
|
598
|
+
{/* Project reports */}
|
|
599
|
+
{deptReports.length > 0 && (
|
|
600
|
+
<div>
|
|
601
|
+
<h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider mb-3">{t('dept.detail.reports')}</h2>
|
|
602
|
+
<div className="space-y-2">
|
|
603
|
+
{deptReports.map((pr, i) => {
|
|
604
|
+
const r = pr.reports.find(r => r.department === dept.name);
|
|
605
|
+
if (!r) return null;
|
|
606
|
+
return (
|
|
607
|
+
<div key={i} className="card text-sm flex items-center justify-between">
|
|
608
|
+
<div className="flex items-center gap-2">
|
|
609
|
+
<span className="text-[10px] text-[var(--muted)]">{new Date(pr.time).toLocaleString()}</span>
|
|
610
|
+
<span className={`text-xs px-1.5 py-0.5 rounded ${
|
|
611
|
+
r.status === 'completed' ? 'bg-green-900/30 text-green-400' : 'bg-blue-900/30 text-blue-400'
|
|
612
|
+
}`}>{r.status}</span>
|
|
613
|
+
</div>
|
|
614
|
+
<div className="flex items-center gap-3 text-xs text-[var(--muted)]">
|
|
615
|
+
<span>🤖 {r.memberCount}</span>
|
|
616
|
+
<span>📝 {r.completedTasks}</span>
|
|
617
|
+
{r.avgScore && <span className="text-yellow-400">⭐ {r.avgScore}</span>}
|
|
618
|
+
</div>
|
|
619
|
+
</div>
|
|
620
|
+
);
|
|
621
|
+
})}
|
|
622
|
+
</div>
|
|
623
|
+
</div>
|
|
624
|
+
)}
|
|
625
|
+
</div>
|
|
626
|
+
|
|
627
|
+
{/* ========== Sub-modals ========== */}
|
|
628
|
+
|
|
629
|
+
{/* Requirement detail modal */}
|
|
630
|
+
{activeReqId && (
|
|
631
|
+
<RequirementDetail
|
|
632
|
+
requirementId={activeReqId}
|
|
633
|
+
onClose={() => setActiveReqId(null)}
|
|
634
|
+
/>
|
|
635
|
+
)}
|
|
636
|
+
|
|
637
|
+
{/* Agent detail modal */}
|
|
638
|
+
{selectedAgent && (
|
|
639
|
+
<AgentDetailModal agentId={selectedAgent} onClose={() => setSelectedAgent(null)} />
|
|
640
|
+
)}
|
|
641
|
+
|
|
642
|
+
{/* Agent chat modal */}
|
|
643
|
+
{chatAgent && (
|
|
644
|
+
<AgentChatModal
|
|
645
|
+
agentId={chatAgent.id}
|
|
646
|
+
agentName={chatAgent.name}
|
|
647
|
+
agentAvatar={chatAgent.avatar}
|
|
648
|
+
agentRole={chatAgent.role}
|
|
649
|
+
agentSignature={chatAgent.signature}
|
|
650
|
+
agentDepartment={chatAgent.department}
|
|
651
|
+
onClose={() => setChatAgent(null)}
|
|
652
|
+
/>
|
|
653
|
+
)}
|
|
654
|
+
|
|
655
|
+
{/* Dismiss confirm modal */}
|
|
656
|
+
{dismissTarget && (
|
|
657
|
+
<div className="fixed inset-0 z-[60] bg-black/70 flex items-center justify-center !m-0" onClick={() => setDismissTarget(null)}>
|
|
658
|
+
<div className="card max-w-sm w-full mx-4 space-y-4" onClick={e => e.stopPropagation()}>
|
|
659
|
+
<h3 className="text-lg font-semibold text-red-400">{t('dept.dismiss.title')}</h3>
|
|
660
|
+
<p className="text-sm">{t('dept.dismiss.desc', { name: '' })}<strong>{dismissTarget.name}</strong></p>
|
|
661
|
+
<div>
|
|
662
|
+
<label className="block text-sm mb-1 text-[var(--muted)]">{t('dept.dismiss.reasonLabel')}</label>
|
|
663
|
+
<input className="input w-full" placeholder={t('dept.dismiss.reasonPlaceholder')} value={dismissReason} onChange={e => setDismissReason(e.target.value)} />
|
|
664
|
+
</div>
|
|
665
|
+
<div className="flex gap-2">
|
|
666
|
+
<button className="btn-secondary flex-1" onClick={() => setDismissTarget(null)}>{t('common.cancel')}</button>
|
|
667
|
+
<button className="btn-danger flex-1" onClick={handleDismiss}>{t('dept.dismiss.confirmBtn')}</button>
|
|
668
|
+
</div>
|
|
669
|
+
</div>
|
|
670
|
+
</div>
|
|
671
|
+
)}
|
|
672
|
+
|
|
673
|
+
{/* Disband department confirm modal */}
|
|
674
|
+
{showDisband && (
|
|
675
|
+
<div className="fixed inset-0 z-[60] bg-black/70 flex items-center justify-center !m-0" onClick={() => setShowDisband(false)}>
|
|
676
|
+
<div className="card max-w-sm w-full mx-4 space-y-4" onClick={e => e.stopPropagation()}>
|
|
677
|
+
<h3 className="text-lg font-semibold text-red-400">{t('dept.detail.disbandBtn')}</h3>
|
|
678
|
+
<p className="text-sm">
|
|
679
|
+
{t('dept.disband.desc', { name: '' })}<strong>{dept.name}</strong>
|
|
680
|
+
<br />{t('dept.disband.descSuffix')}
|
|
681
|
+
</p>
|
|
682
|
+
<div>
|
|
683
|
+
<label className="block text-sm mb-1 text-[var(--muted)]">{t('dept.disband.reasonLabel')}</label>
|
|
684
|
+
<input className="input w-full" placeholder={t('dept.disband.reasonPlaceholder')} value={disbandReason} onChange={e => setDisbandReason(e.target.value)} />
|
|
685
|
+
</div>
|
|
686
|
+
<div className="flex gap-2">
|
|
687
|
+
<button className="btn-secondary flex-1" onClick={() => setShowDisband(false)}>{t('common.cancel')}</button>
|
|
688
|
+
<button className="btn-danger flex-1" onClick={handleDisband} disabled={loading}>
|
|
689
|
+
{loading ? t('dept.disband.disbanding') : t('dept.disband.confirmBtn')}
|
|
690
|
+
</button>
|
|
691
|
+
</div>
|
|
692
|
+
</div>
|
|
693
|
+
</div>
|
|
694
|
+
)}
|
|
695
|
+
|
|
696
|
+
{/* Adjust workforce modal */}
|
|
697
|
+
{showAdjust && (
|
|
698
|
+
<div className="fixed inset-0 z-[60] bg-black/70 flex items-center justify-center !m-0" onClick={() => { setShowAdjust(false); setPendingPlan(null); }}>
|
|
699
|
+
<div className="card max-w-lg w-full mx-4 space-y-4 max-h-[80vh] overflow-auto" onClick={e => e.stopPropagation()}>
|
|
700
|
+
{!pendingPlan || pendingPlan.type !== 'adjustment' ? (
|
|
701
|
+
<>
|
|
702
|
+
<h3 className="text-lg font-semibold">{t('dept.detail.adjustBtn')}</h3>
|
|
703
|
+
<p className="text-sm text-[var(--muted)]">{t('dept.adjust.desc')}</p>
|
|
704
|
+
<div className="bg-[var(--background)] border border-[var(--border)] rounded-lg p-3">
|
|
705
|
+
<div className="text-xs text-[var(--muted)] mb-1">{t('dept.adjust.currentDept')}</div>
|
|
706
|
+
<div className="font-medium">{dept.name}</div>
|
|
707
|
+
<div className="text-xs text-[var(--muted)] mt-1">
|
|
708
|
+
{t('dept.adjust.currentMembers', { n: dept.members.length })}
|
|
709
|
+
</div>
|
|
710
|
+
</div>
|
|
711
|
+
<div>
|
|
712
|
+
<label className="block text-sm mb-1 text-[var(--muted)]">{t('dept.adjust.goalLabel')}</label>
|
|
713
|
+
<textarea
|
|
714
|
+
className="input w-full h-20 resize-none"
|
|
715
|
+
placeholder={t('dept.adjust.goalPlaceholder')}
|
|
716
|
+
value={adjustGoal}
|
|
717
|
+
onChange={e => setAdjustGoal(e.target.value)}
|
|
718
|
+
/>
|
|
719
|
+
</div>
|
|
720
|
+
<div className="flex gap-2">
|
|
721
|
+
<button className="btn-secondary flex-1" onClick={() => setShowAdjust(false)}>{t('common.cancel')}</button>
|
|
722
|
+
<button className="btn-primary flex-1" disabled={!adjustGoal || loading} onClick={handleAdjustPlan}>
|
|
723
|
+
{loading ? t('dept.adjust.planning') : t('dept.adjust.planBtn')}
|
|
724
|
+
</button>
|
|
725
|
+
</div>
|
|
726
|
+
</>
|
|
727
|
+
) : (
|
|
728
|
+
<>
|
|
729
|
+
<h3 className="text-lg font-semibold">{t('dept.adjust.reviewTitle')}</h3>
|
|
730
|
+
<p className="text-sm text-[var(--muted)]">
|
|
731
|
+
{t('dept.adjust.reviewDesc', { dept: pendingPlan.departmentName })}
|
|
732
|
+
</p>
|
|
733
|
+
{pendingPlan.reasoning && (
|
|
734
|
+
<div className="bg-blue-900/10 border border-blue-500/20 rounded-lg p-3">
|
|
735
|
+
<div className="text-xs font-medium text-blue-400 mb-1">{t('overview.planReview.analysis')}</div>
|
|
736
|
+
<div className="text-sm text-[var(--muted)]">{pendingPlan.reasoning}</div>
|
|
737
|
+
</div>
|
|
738
|
+
)}
|
|
739
|
+
{pendingPlan.fires?.length > 0 && (
|
|
740
|
+
<div className="bg-red-900/10 border border-red-500/20 rounded-lg p-3 space-y-2">
|
|
741
|
+
<div className="text-xs font-medium text-red-400 mb-1">{t('dept.adjust.firesTitle', { n: pendingPlan.fires.length })}</div>
|
|
742
|
+
{pendingPlan.fires.map((f, i) => (
|
|
743
|
+
<div key={i} className="flex items-center gap-2 text-sm">
|
|
744
|
+
<span className="text-red-400">✕</span>
|
|
745
|
+
<span className="font-medium">{f.name}</span>
|
|
746
|
+
<span className="text-xs text-[var(--muted)]">- {f.reason}</span>
|
|
747
|
+
</div>
|
|
748
|
+
))}
|
|
749
|
+
</div>
|
|
750
|
+
)}
|
|
751
|
+
{pendingPlan.hires?.length > 0 && (
|
|
752
|
+
<div className="bg-green-900/10 border border-green-500/20 rounded-lg p-3 space-y-2">
|
|
753
|
+
<div className="text-xs font-medium text-green-400 mb-1">{t('dept.adjust.hiresTitle', { n: pendingPlan.hires.length })}</div>
|
|
754
|
+
{pendingPlan.hires.map((h, i) => (
|
|
755
|
+
<div key={i} className="flex items-center gap-3 p-2 rounded-lg bg-white/5">
|
|
756
|
+
<span>🤖</span>
|
|
757
|
+
<div className="flex-1">
|
|
758
|
+
<div className="text-sm font-medium">{h.name}</div>
|
|
759
|
+
<div className="text-xs text-[var(--muted)]">{h.templateTitle || h.templateId}</div>
|
|
760
|
+
{h.providerName && <div className="text-[10px] text-purple-400/80 mt-0.5">⚡ {h.providerName}</div>}
|
|
761
|
+
{h.reason && <div className="text-[10px] text-blue-400/70 mt-0.5">💡 {h.reason}</div>}
|
|
762
|
+
</div>
|
|
763
|
+
</div>
|
|
764
|
+
))}
|
|
765
|
+
</div>
|
|
766
|
+
)}
|
|
767
|
+
{pendingPlan.fires?.length === 0 && pendingPlan.hires?.length === 0 && (
|
|
768
|
+
<div className="text-center text-[var(--muted)] py-4">{t('dept.adjust.noChanges')}</div>
|
|
769
|
+
)}
|
|
770
|
+
<div className="flex gap-2">
|
|
771
|
+
<button className="btn-secondary flex-1" onClick={() => setPendingPlan(null)}>{t('overview.planReview.rejectBtn')}</button>
|
|
772
|
+
<button
|
|
773
|
+
className="btn-primary flex-1"
|
|
774
|
+
disabled={loading || (pendingPlan.fires?.length === 0 && pendingPlan.hires?.length === 0)}
|
|
775
|
+
onClick={async () => { await confirmAdjustment(pendingPlan.planId); setShowAdjust(false); }}
|
|
776
|
+
>
|
|
777
|
+
{loading ? t('dept.adjust.executing') : t('dept.adjust.approveBtn')}
|
|
778
|
+
</button>
|
|
779
|
+
</div>
|
|
780
|
+
</>
|
|
781
|
+
)}
|
|
782
|
+
</div>
|
|
783
|
+
</div>
|
|
784
|
+
)}
|
|
785
|
+
|
|
786
|
+
{/* Folder browser modal */}
|
|
787
|
+
{showFolderBrowser && (
|
|
788
|
+
<div className="fixed inset-0 z-[70] bg-black/70 flex items-center justify-center !m-0" onClick={() => setShowFolderBrowser(false)}>
|
|
789
|
+
<div className="card max-w-lg w-full mx-4 max-h-[70vh] flex flex-col" onClick={e => e.stopPropagation()}>
|
|
790
|
+
<div className="flex items-center justify-between pb-3 border-b border-[var(--border)]">
|
|
791
|
+
<h3 className="text-base font-semibold">📁 {t('dept.newReq.browseTitle')}</h3>
|
|
792
|
+
<button onClick={() => setShowFolderBrowser(false)} className="text-[var(--muted)] hover:text-white text-lg">✕</button>
|
|
793
|
+
</div>
|
|
794
|
+
<div className="flex items-center gap-2 py-2 px-1 bg-[var(--background)] rounded-lg mt-3 mb-2">
|
|
795
|
+
<span className="text-xs text-[var(--muted)] shrink-0">📍</span>
|
|
796
|
+
<span className="text-xs font-mono text-[var(--foreground)] truncate">{browseCurrentPath}</span>
|
|
797
|
+
</div>
|
|
798
|
+
<div className="flex-1 overflow-auto space-y-0.5 min-h-[200px]">
|
|
799
|
+
{browseLoading ? (
|
|
800
|
+
<div className="text-center py-8 text-[var(--muted)] text-sm animate-pulse">{t('common.loading')}</div>
|
|
801
|
+
) : (
|
|
802
|
+
<>
|
|
803
|
+
{browseParentPath !== null && (
|
|
804
|
+
<div
|
|
805
|
+
className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-white/5 cursor-pointer transition-colors text-sm"
|
|
806
|
+
onClick={() => fetchDirs(browseParentPath)}
|
|
807
|
+
>
|
|
808
|
+
<span>📂</span>
|
|
809
|
+
<span className="text-[var(--muted)]">..</span>
|
|
810
|
+
</div>
|
|
811
|
+
)}
|
|
812
|
+
{browseDirs.length === 0 && !browseLoading && (
|
|
813
|
+
<div className="text-center py-6 text-xs text-[var(--muted)]">{t('dept.newReq.emptyDir')}</div>
|
|
814
|
+
)}
|
|
815
|
+
{browseDirs.map((dir) => (
|
|
816
|
+
<div
|
|
817
|
+
key={dir.path}
|
|
818
|
+
className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-white/5 cursor-pointer transition-colors text-sm group"
|
|
819
|
+
onClick={() => fetchDirs(dir.path)}
|
|
820
|
+
>
|
|
821
|
+
<span>📁</span>
|
|
822
|
+
<span className="flex-1 truncate">{dir.name}</span>
|
|
823
|
+
</div>
|
|
824
|
+
))}
|
|
825
|
+
</>
|
|
826
|
+
)}
|
|
827
|
+
</div>
|
|
828
|
+
<div className="flex gap-2 pt-3 border-t border-[var(--border)] mt-2">
|
|
829
|
+
<button className="btn-secondary flex-1" onClick={() => setShowFolderBrowser(false)}>{t('common.cancel')}</button>
|
|
830
|
+
<button
|
|
831
|
+
className="btn-primary flex-1"
|
|
832
|
+
onClick={() => { setNewReqWorkspaceDir(browseCurrentPath); setShowFolderBrowser(false); }}
|
|
833
|
+
>
|
|
834
|
+
{t('dept.newReq.selectDir')}
|
|
835
|
+
</button>
|
|
836
|
+
</div>
|
|
837
|
+
</div>
|
|
838
|
+
</div>
|
|
839
|
+
)}
|
|
840
|
+
</div>
|
|
841
|
+
);
|
|
842
|
+
}
|