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,697 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
4
|
+
import { useStore } from '@/lib/client-store';
|
|
5
|
+
import AgentDetailModal from './AgentDetailModal';
|
|
6
|
+
import AgentChatModal from './AgentChatModal';
|
|
7
|
+
import { useI18n } from '@/lib/i18n';
|
|
8
|
+
import CachedAvatar from './CachedAvatar';
|
|
9
|
+
import FilesView from './FilesView';
|
|
10
|
+
import GroupChatView from './GroupChatView';
|
|
11
|
+
import ReactMarkdown from 'react-markdown';
|
|
12
|
+
import remarkGfm from 'remark-gfm';
|
|
13
|
+
|
|
14
|
+
export default function TeamDetail() {
|
|
15
|
+
const { t } = useI18n();
|
|
16
|
+
const {
|
|
17
|
+
company, loading, activeTeamId, activeSprintId, setActiveSprintId,
|
|
18
|
+
navigateBackFromTeam, fetchTeamDetail, updateTeam, deleteTeam,
|
|
19
|
+
createSprint, discussSprint, approveSprint, fetchSprintDetail,
|
|
20
|
+
sendSprintMessage, deleteSprint,
|
|
21
|
+
fetchWorkspaceFile, navigateToRequirement,
|
|
22
|
+
} = useStore();
|
|
23
|
+
|
|
24
|
+
const [team, setTeam] = useState(null);
|
|
25
|
+
const [activeTab, setActiveTab] = useState('overview'); // overview | sprints | files
|
|
26
|
+
const [selectedAgent, setSelectedAgent] = useState(null);
|
|
27
|
+
const [chatAgent, setChatAgent] = useState(null);
|
|
28
|
+
|
|
29
|
+
// Sprint form
|
|
30
|
+
const [showNewSprint, setShowNewSprint] = useState(false);
|
|
31
|
+
const [sprintTitle, setSprintTitle] = useState('');
|
|
32
|
+
const [sprintGoal, setSprintGoal] = useState('');
|
|
33
|
+
|
|
34
|
+
// Sprint detail
|
|
35
|
+
const [sprintDetail, setSprintDetail] = useState(null);
|
|
36
|
+
const [chatInput, setChatInput] = useState('');
|
|
37
|
+
const chatEndRef = useRef(null);
|
|
38
|
+
|
|
39
|
+
// Skills editing
|
|
40
|
+
const [editingSkills, setEditingSkills] = useState(false);
|
|
41
|
+
const [skillsInput, setSkillsInput] = useState('');
|
|
42
|
+
|
|
43
|
+
// Workspace
|
|
44
|
+
const [showWorkspaceSelector, setShowWorkspaceSelector] = useState(false);
|
|
45
|
+
const [workspaceInput, setWorkspaceInput] = useState('');
|
|
46
|
+
const [browseDirs, setBrowseDirs] = useState([]);
|
|
47
|
+
const [browseCurrentPath, setBrowseCurrentPath] = useState('');
|
|
48
|
+
const [browseParentPath, setBrowseParentPath] = useState(null);
|
|
49
|
+
const [browseLoading, setBrowseLoading] = useState(false);
|
|
50
|
+
|
|
51
|
+
// Files
|
|
52
|
+
const [previewFile, setPreviewFile] = useState(null); // { path, content, loading }
|
|
53
|
+
|
|
54
|
+
const fetchDirs = async (dirPath) => {
|
|
55
|
+
setBrowseLoading(true);
|
|
56
|
+
try {
|
|
57
|
+
const url = dirPath ? `/api/browse-dir?path=${encodeURIComponent(dirPath)}` : '/api/browse-dir';
|
|
58
|
+
const res = await fetch(url);
|
|
59
|
+
const data = await res.json();
|
|
60
|
+
if (!data.error) {
|
|
61
|
+
setBrowseDirs(data.dirs || []);
|
|
62
|
+
setBrowseCurrentPath(data.current || '');
|
|
63
|
+
setBrowseParentPath(data.parent || null);
|
|
64
|
+
}
|
|
65
|
+
} catch (e) { /* handled */ }
|
|
66
|
+
setBrowseLoading(false);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Fetch team detail
|
|
70
|
+
const loadTeam = async () => {
|
|
71
|
+
if (!activeTeamId) return;
|
|
72
|
+
const data = await fetchTeamDetail(activeTeamId);
|
|
73
|
+
if (data) setTeam(data);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
loadTeam();
|
|
78
|
+
}, [activeTeamId]);
|
|
79
|
+
|
|
80
|
+
// Auto-refresh sprint detail
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!activeSprintId || !activeTeamId) { setSprintDetail(null); return; }
|
|
83
|
+
let running = true;
|
|
84
|
+
const load = async () => {
|
|
85
|
+
const data = await fetchSprintDetail(activeTeamId, activeSprintId);
|
|
86
|
+
if (data && running) setSprintDetail(data);
|
|
87
|
+
};
|
|
88
|
+
load();
|
|
89
|
+
const interval = setInterval(load, 5000);
|
|
90
|
+
return () => { running = false; clearInterval(interval); };
|
|
91
|
+
}, [activeSprintId, activeTeamId]);
|
|
92
|
+
|
|
93
|
+
// Scroll chat to bottom
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
97
|
+
}, 80);
|
|
98
|
+
}, [sprintDetail?.groupChat?.length]);
|
|
99
|
+
|
|
100
|
+
// File preview
|
|
101
|
+
const loadFilePreview = useCallback(async (filePath) => {
|
|
102
|
+
if (!team?.departmentId) return;
|
|
103
|
+
setPreviewFile({ path: filePath, content: null, loading: true });
|
|
104
|
+
try {
|
|
105
|
+
const content = await fetchWorkspaceFile(team.departmentId, filePath);
|
|
106
|
+
setPreviewFile({ path: filePath, content: content?.content || content || '', loading: false });
|
|
107
|
+
} catch {
|
|
108
|
+
setPreviewFile({ path: filePath, content: 'Failed to read file', loading: false });
|
|
109
|
+
}
|
|
110
|
+
}, [team?.departmentId, fetchWorkspaceFile]);
|
|
111
|
+
|
|
112
|
+
if (!team) {
|
|
113
|
+
return (
|
|
114
|
+
<div className="p-6 text-center text-[var(--muted)]">
|
|
115
|
+
<div className="animate-pulse text-4xl mb-4">👥</div>
|
|
116
|
+
<p>{t('common.loading')}</p>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const handleCreateSprint = async () => {
|
|
122
|
+
if (!sprintTitle || !sprintGoal) return;
|
|
123
|
+
const result = await createSprint(activeTeamId, sprintTitle, sprintGoal);
|
|
124
|
+
if (result?.id) {
|
|
125
|
+
setShowNewSprint(false);
|
|
126
|
+
setSprintTitle('');
|
|
127
|
+
setSprintGoal('');
|
|
128
|
+
loadTeam();
|
|
129
|
+
setActiveSprintId(result.id);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const handleDiscuss = async (sprintId) => {
|
|
134
|
+
await discussSprint(activeTeamId, sprintId);
|
|
135
|
+
loadTeam();
|
|
136
|
+
const data = await fetchSprintDetail(activeTeamId, sprintId);
|
|
137
|
+
if (data) setSprintDetail(data);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const handleApprove = async (sprintId) => {
|
|
141
|
+
await approveSprint(activeTeamId, sprintId);
|
|
142
|
+
loadTeam();
|
|
143
|
+
const data = await fetchSprintDetail(activeTeamId, sprintId);
|
|
144
|
+
if (data) setSprintDetail(data);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const handleSendChat = async () => {
|
|
148
|
+
if (!chatInput.trim() || !activeSprintId) return;
|
|
149
|
+
const msg = chatInput;
|
|
150
|
+
setChatInput('');
|
|
151
|
+
await sendSprintMessage(activeTeamId, activeSprintId, msg);
|
|
152
|
+
const data = await fetchSprintDetail(activeTeamId, activeSprintId);
|
|
153
|
+
if (data) setSprintDetail(data);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const handleSaveSkills = async () => {
|
|
157
|
+
const skills = skillsInput.split(',').map(s => s.trim()).filter(Boolean);
|
|
158
|
+
await updateTeam(activeTeamId, { skills });
|
|
159
|
+
setEditingSkills(false);
|
|
160
|
+
loadTeam();
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const handleSetWorkspace = async (path) => {
|
|
164
|
+
await updateTeam(activeTeamId, { workspacePath: path });
|
|
165
|
+
setShowWorkspaceSelector(false);
|
|
166
|
+
loadTeam();
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const statusCfg = {
|
|
170
|
+
draft: { label: t('team.sprint.draft'), color: 'text-gray-400', bg: 'bg-gray-900/30', icon: '📝' },
|
|
171
|
+
discussing: { label: t('team.sprint.discussing'), color: 'text-blue-400', bg: 'bg-blue-900/30', icon: '💬' },
|
|
172
|
+
pending_approval: { label: t('team.sprint.pendingApproval'), color: 'text-yellow-400', bg: 'bg-yellow-900/30', icon: '⏳' },
|
|
173
|
+
in_progress: { label: t('team.sprint.inProgress'), color: 'text-green-400', bg: 'bg-green-900/30', icon: '⚙️' },
|
|
174
|
+
completed: { label: t('team.sprint.completed'), color: 'text-emerald-400', bg: 'bg-emerald-900/30', icon: '✅' },
|
|
175
|
+
failed: { label: t('team.sprint.failed'), color: 'text-red-400', bg: 'bg-red-900/30', icon: '❌' },
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Sprint detail view (inline) — two-column layout like RequirementDetail
|
|
179
|
+
const renderSprintDetail = () => {
|
|
180
|
+
if (!sprintDetail) return null;
|
|
181
|
+
const st = statusCfg[sprintDetail.status] || statusCfg.draft;
|
|
182
|
+
const groupChat = (sprintDetail.groupChat || []).filter(m => m.visibility !== 'flow');
|
|
183
|
+
|
|
184
|
+
// Build agentMap for GroupChatView
|
|
185
|
+
const chatAgentMap = {};
|
|
186
|
+
if (company?.departments) {
|
|
187
|
+
for (const dept of company.departments) {
|
|
188
|
+
for (const agent of (dept.members || dept.agents || [])) {
|
|
189
|
+
chatAgentMap[agent.id] = agent.name;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Adapt sendSprintMessage to GroupChatView's (id, msg) signature
|
|
195
|
+
const handleSprintChatSend = async (_id, msg) => {
|
|
196
|
+
await sendSprintMessage(activeTeamId, sprintDetail.id, msg);
|
|
197
|
+
};
|
|
198
|
+
const handleSprintChatRefresh = async () => {
|
|
199
|
+
const data = await fetchSprintDetail(activeTeamId, sprintDetail.id);
|
|
200
|
+
if (data) setSprintDetail(data);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// Find leader info for typing indicator
|
|
204
|
+
const leaderMsg = groupChat.find(m => m.from?.id !== 'boss' && m.from?.role !== 'system' && m.type !== 'system');
|
|
205
|
+
const chatLeaderInfo = leaderMsg ? { name: leaderMsg.from?.name, avatar: leaderMsg.from?.avatar } : null;
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<div className="flex-1 min-h-0 flex flex-col">
|
|
209
|
+
{/* Sprint header */}
|
|
210
|
+
<div className="shrink-0 border-b border-[var(--border)] bg-[var(--card)]/50 px-6 py-3">
|
|
211
|
+
<div className="flex items-center justify-between">
|
|
212
|
+
<div className="flex items-center gap-3">
|
|
213
|
+
<button
|
|
214
|
+
onClick={() => { setActiveSprintId(null); setSprintDetail(null); }}
|
|
215
|
+
className="text-[var(--muted)] hover:text-[var(--foreground)] text-sm"
|
|
216
|
+
>
|
|
217
|
+
← {t('team.sprint.backToList')}
|
|
218
|
+
</button>
|
|
219
|
+
<div className="w-px h-5 bg-[var(--border)]" />
|
|
220
|
+
<span className="text-lg font-bold">{sprintDetail.title}</span>
|
|
221
|
+
<span className={`text-xs px-2 py-0.5 rounded-full ${st.bg} ${st.color}`}>{st.icon} {st.label}</span>
|
|
222
|
+
</div>
|
|
223
|
+
<div className="flex items-center gap-2">
|
|
224
|
+
{sprintDetail.status === 'draft' && (
|
|
225
|
+
<button className="btn-primary text-xs" disabled={loading} onClick={() => handleDiscuss(sprintDetail.id)}>
|
|
226
|
+
{loading ? t('team.sprint.discussing') : t('team.sprint.startDiscussion')}
|
|
227
|
+
</button>
|
|
228
|
+
)}
|
|
229
|
+
{sprintDetail.status === 'pending_approval' && (
|
|
230
|
+
<button className="btn-primary text-xs" disabled={loading} onClick={() => handleApprove(sprintDetail.id)}>
|
|
231
|
+
{loading ? '...' : t('team.sprint.approve')}
|
|
232
|
+
</button>
|
|
233
|
+
)}
|
|
234
|
+
{sprintDetail.requirementId && (
|
|
235
|
+
<button
|
|
236
|
+
className="text-xs px-3 py-1 rounded bg-blue-600/20 text-blue-400 hover:bg-blue-600/30 font-medium"
|
|
237
|
+
onClick={() => navigateToRequirement(sprintDetail.requirementId)}
|
|
238
|
+
>
|
|
239
|
+
{t('team.sprint.viewRequirement')}
|
|
240
|
+
</button>
|
|
241
|
+
)}
|
|
242
|
+
<button
|
|
243
|
+
className="text-xs px-2 py-1 rounded bg-red-600/15 text-red-400 hover:bg-red-600/25"
|
|
244
|
+
onClick={async () => {
|
|
245
|
+
if (confirm(t('team.sprint.confirmDelete'))) {
|
|
246
|
+
await deleteSprint(activeTeamId, sprintDetail.id);
|
|
247
|
+
setActiveSprintId(null);
|
|
248
|
+
setSprintDetail(null);
|
|
249
|
+
loadTeam();
|
|
250
|
+
}
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
🗑️
|
|
254
|
+
</button>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
{/* Goal & Plan summary */}
|
|
258
|
+
<div className="flex items-center gap-4 mt-2 text-xs text-[var(--muted)]">
|
|
259
|
+
<span>🎯 {sprintDetail.goal}</span>
|
|
260
|
+
{sprintDetail.workflow?.nodes?.length > 0 && (
|
|
261
|
+
<span>📊 {sprintDetail.workflow.nodes.filter(n => n.status === 'completed').length}/{sprintDetail.workflow.nodes.length}</span>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
{/* Two-column body */}
|
|
267
|
+
<div className="flex-1 min-h-0 flex">
|
|
268
|
+
{/* Left: Group Chat */}
|
|
269
|
+
<div className="w-[380px] shrink-0 border-r border-white/[0.06] flex flex-col bg-[var(--background)]">
|
|
270
|
+
<div className="px-4 py-2.5 border-b border-white/[0.06] bg-[var(--card)] flex items-center justify-between">
|
|
271
|
+
<span className="text-sm font-medium">💬 {t('team.sprint.chat')}</span>
|
|
272
|
+
<span className="text-[10px] bg-white/10 px-1.5 py-0.5 rounded-full text-[var(--muted)]">{groupChat.length}</span>
|
|
273
|
+
</div>
|
|
274
|
+
<GroupChatView
|
|
275
|
+
groupChat={groupChat}
|
|
276
|
+
agentMap={chatAgentMap}
|
|
277
|
+
bossAvatar={company?.bossAvatar}
|
|
278
|
+
bossName={company?.boss || 'Boss'}
|
|
279
|
+
requirementId={sprintDetail.id}
|
|
280
|
+
onSendMessage={handleSprintChatSend}
|
|
281
|
+
fetchDetail={handleSprintChatRefresh}
|
|
282
|
+
leaderInfo={chatLeaderInfo}
|
|
283
|
+
chatEndRef={chatEndRef}
|
|
284
|
+
embedded
|
|
285
|
+
/>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
{/* Right: Sprint Plan */}
|
|
289
|
+
<div className="flex-1 min-w-0 flex flex-col">
|
|
290
|
+
<div className="flex items-center gap-2 px-6 py-2.5 border-b border-white/[0.06] bg-[var(--card)]">
|
|
291
|
+
<span className="text-sm font-medium">📋 {t('team.sprint.plan')}</span>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<div className="flex-1 min-h-0 overflow-auto p-4">
|
|
295
|
+
{sprintDetail.plan ? (
|
|
296
|
+
<div className="prose prose-invert prose-xs max-w-none font-mono text-xs leading-relaxed">
|
|
297
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]}>{sprintDetail.plan}</ReactMarkdown>
|
|
298
|
+
</div>
|
|
299
|
+
) : (
|
|
300
|
+
<div className="text-center py-8 text-[var(--muted)] text-sm">
|
|
301
|
+
<div className="text-3xl mb-2">📋</div>
|
|
302
|
+
<p>{t('team.sprint.noPlan')}</p>
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<div className="h-full flex flex-col animate-fade-in">
|
|
314
|
+
{/* Top bar */}
|
|
315
|
+
<div className="shrink-0 border-b border-[var(--border)] bg-[var(--card)]/50 backdrop-blur-sm px-6 py-4">
|
|
316
|
+
<div className="flex items-center justify-between">
|
|
317
|
+
<div className="flex items-center gap-4 min-w-0">
|
|
318
|
+
<button
|
|
319
|
+
onClick={navigateBackFromTeam}
|
|
320
|
+
className="text-[var(--muted)] hover:text-[var(--foreground)] transition-colors text-sm flex items-center gap-1 shrink-0"
|
|
321
|
+
>
|
|
322
|
+
← {t('team.back')}
|
|
323
|
+
</button>
|
|
324
|
+
<div className="w-px h-6 bg-[var(--border)]" />
|
|
325
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
326
|
+
<div className="w-10 h-10 bg-gradient-to-br from-violet-600 to-purple-700 rounded-lg flex items-center justify-center text-lg shrink-0">
|
|
327
|
+
👥
|
|
328
|
+
</div>
|
|
329
|
+
<div className="min-w-0">
|
|
330
|
+
<h1 className="text-lg font-bold flex items-center gap-2 truncate">
|
|
331
|
+
{team.name}
|
|
332
|
+
<span className="text-xs px-2 py-0.5 rounded-full bg-purple-900/30 text-purple-400">
|
|
333
|
+
{team.departmentName}
|
|
334
|
+
</span>
|
|
335
|
+
</h1>
|
|
336
|
+
<p className="text-xs text-[var(--muted)] truncate">{team.description || t('team.noDescription')}</p>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
341
|
+
<button
|
|
342
|
+
className="btn-primary flex items-center gap-1.5 text-sm"
|
|
343
|
+
onClick={() => { setShowNewSprint(true); setSprintTitle(''); setSprintGoal(''); }}
|
|
344
|
+
>
|
|
345
|
+
🚀 {t('team.newSprint')}
|
|
346
|
+
</button>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
{/* Stats */}
|
|
350
|
+
<div className="flex items-center gap-4 mt-2 text-xs text-[var(--muted)]">
|
|
351
|
+
<span>👥 {team.memberIds?.length || 0} {t('team.members')}</span>
|
|
352
|
+
<span>👔 {team.leaderName}</span>
|
|
353
|
+
<span>🔄 {team.sprints?.length || 0} {t('team.sprints')}</span>
|
|
354
|
+
{team.workspacePath && <span>📁 {team.workspacePath}</span>}
|
|
355
|
+
</div>
|
|
356
|
+
{/* Tabs */}
|
|
357
|
+
<div className="flex gap-1 mt-3">
|
|
358
|
+
{[
|
|
359
|
+
{ key: 'overview', label: t('team.tab.overview') },
|
|
360
|
+
{ key: 'sprints', label: t('team.tab.sprints') },
|
|
361
|
+
{ key: 'files', label: t('team.tab.files') },
|
|
362
|
+
].map(tab => (
|
|
363
|
+
<button
|
|
364
|
+
key={tab.key}
|
|
365
|
+
onClick={() => { setActiveTab(tab.key); setActiveSprintId(null); setSprintDetail(null); }}
|
|
366
|
+
className={`text-xs px-3 py-1.5 rounded-lg transition-colors ${
|
|
367
|
+
activeTab === tab.key
|
|
368
|
+
? 'bg-[var(--accent)] text-white'
|
|
369
|
+
: 'text-[var(--muted)] hover:bg-white/5'
|
|
370
|
+
}`}
|
|
371
|
+
>
|
|
372
|
+
{tab.label}
|
|
373
|
+
</button>
|
|
374
|
+
))}
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
{/* Main content */}
|
|
379
|
+
<div className={`flex-1 min-h-0 flex flex-col ${
|
|
380
|
+
activeSprintId
|
|
381
|
+
? 'overflow-hidden'
|
|
382
|
+
: `p-6 space-y-6 ${activeTab === 'files' ? 'overflow-hidden' : 'overflow-auto'}`
|
|
383
|
+
}`}>
|
|
384
|
+
|
|
385
|
+
{/* New Sprint Form */}
|
|
386
|
+
{showNewSprint && (
|
|
387
|
+
<div className="card border-[var(--accent)]/30 animate-fade-in space-y-4">
|
|
388
|
+
<div className="flex items-center justify-between">
|
|
389
|
+
<h3 className="text-base font-semibold">🚀 {t('team.newSprint')}</h3>
|
|
390
|
+
<button onClick={() => setShowNewSprint(false)} className="text-[var(--muted)] hover:text-white text-lg">✕</button>
|
|
391
|
+
</div>
|
|
392
|
+
<div>
|
|
393
|
+
<label className="block text-sm mb-1 text-[var(--muted)]">{t('team.sprint.titleLabel')}</label>
|
|
394
|
+
<input
|
|
395
|
+
className="input w-full"
|
|
396
|
+
placeholder={t('team.sprint.titlePlaceholder')}
|
|
397
|
+
value={sprintTitle}
|
|
398
|
+
onChange={e => setSprintTitle(e.target.value)}
|
|
399
|
+
autoFocus
|
|
400
|
+
/>
|
|
401
|
+
</div>
|
|
402
|
+
<div>
|
|
403
|
+
<label className="block text-sm mb-1 text-[var(--muted)]">{t('team.sprint.goalLabel')}</label>
|
|
404
|
+
<textarea
|
|
405
|
+
className="input w-full h-24 resize-none"
|
|
406
|
+
placeholder={t('team.sprint.goalPlaceholder')}
|
|
407
|
+
value={sprintGoal}
|
|
408
|
+
onChange={e => setSprintGoal(e.target.value)}
|
|
409
|
+
/>
|
|
410
|
+
</div>
|
|
411
|
+
<div className="flex justify-end gap-2">
|
|
412
|
+
<button className="btn-secondary" onClick={() => setShowNewSprint(false)}>{t('common.cancel')}</button>
|
|
413
|
+
<button
|
|
414
|
+
className="btn-primary"
|
|
415
|
+
disabled={!sprintTitle || !sprintGoal || loading}
|
|
416
|
+
onClick={handleCreateSprint}
|
|
417
|
+
>
|
|
418
|
+
{loading ? t('common.loading') : t('team.sprint.createBtn')}
|
|
419
|
+
</button>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
)}
|
|
423
|
+
|
|
424
|
+
{/* Overview tab */}
|
|
425
|
+
{activeTab === 'overview' && !activeSprintId && (
|
|
426
|
+
<>
|
|
427
|
+
{/* Members */}
|
|
428
|
+
<div>
|
|
429
|
+
<h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider mb-3">{t('team.membersTitle')}</h2>
|
|
430
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
431
|
+
{(team.membersDetail || []).map(member => (
|
|
432
|
+
<div
|
|
433
|
+
key={member.id}
|
|
434
|
+
className="card hover:border-[var(--accent)]/30 transition-all cursor-pointer group"
|
|
435
|
+
onClick={() => setSelectedAgent(member.id)}
|
|
436
|
+
>
|
|
437
|
+
<div className="flex items-start gap-3">
|
|
438
|
+
<CachedAvatar src={member.avatar} alt={member.name} className="w-12 h-12 rounded-full bg-[var(--border)] shrink-0" />
|
|
439
|
+
<div className="flex-1 min-w-0">
|
|
440
|
+
<div className="flex items-center gap-2">
|
|
441
|
+
<span className="font-medium text-sm truncate">{member.name}</span>
|
|
442
|
+
{team.leaderId === member.id && (
|
|
443
|
+
<span className="text-[10px] bg-yellow-900/30 text-yellow-400 px-1.5 py-0.5 rounded">{t('team.leader')}</span>
|
|
444
|
+
)}
|
|
445
|
+
<span className={`status-dot ${member.status}`} />
|
|
446
|
+
</div>
|
|
447
|
+
<div className="text-xs text-[var(--muted)]">{member.role}</div>
|
|
448
|
+
{member.signature && <div className="text-[10px] text-[var(--muted)] italic mt-1 truncate">"{member.signature}"</div>}
|
|
449
|
+
</div>
|
|
450
|
+
<button
|
|
451
|
+
className="opacity-0 group-hover:opacity-100 text-blue-400 hover:text-blue-300 text-sm transition-opacity"
|
|
452
|
+
onClick={(e) => {
|
|
453
|
+
e.stopPropagation();
|
|
454
|
+
setChatAgent({ id: member.id, name: member.name, avatar: member.avatar, role: member.role, signature: member.signature, department: team.departmentName });
|
|
455
|
+
}}
|
|
456
|
+
>
|
|
457
|
+
💬
|
|
458
|
+
</button>
|
|
459
|
+
</div>
|
|
460
|
+
<div className="flex gap-1 mt-2 flex-wrap">
|
|
461
|
+
{member.skills?.slice(0, 4).map((s, i) => (
|
|
462
|
+
<span key={i} className="text-[10px] text-[var(--muted)] bg-white/5 px-1.5 py-0.5 rounded">{s}</span>
|
|
463
|
+
))}
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
))}
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
|
|
470
|
+
{/* Skills */}
|
|
471
|
+
<div>
|
|
472
|
+
<div className="flex items-center justify-between mb-3">
|
|
473
|
+
<h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider">{t('team.skillsTitle')}</h2>
|
|
474
|
+
<button
|
|
475
|
+
className="text-xs text-[var(--accent)] hover:underline"
|
|
476
|
+
onClick={() => { setEditingSkills(true); setSkillsInput((team.skills || []).join(', ')); }}
|
|
477
|
+
>
|
|
478
|
+
✏️ {t('team.editSkills')}
|
|
479
|
+
</button>
|
|
480
|
+
</div>
|
|
481
|
+
{editingSkills ? (
|
|
482
|
+
<div className="card space-y-3">
|
|
483
|
+
<textarea
|
|
484
|
+
className="input w-full h-16 resize-none text-sm"
|
|
485
|
+
placeholder={t('team.skillsPlaceholder')}
|
|
486
|
+
value={skillsInput}
|
|
487
|
+
onChange={e => setSkillsInput(e.target.value)}
|
|
488
|
+
/>
|
|
489
|
+
<div className="flex justify-end gap-2">
|
|
490
|
+
<button className="btn-secondary text-xs" onClick={() => setEditingSkills(false)}>{t('common.cancel')}</button>
|
|
491
|
+
<button className="btn-primary text-xs" onClick={handleSaveSkills}>{t('common.save')}</button>
|
|
492
|
+
</div>
|
|
493
|
+
</div>
|
|
494
|
+
) : (
|
|
495
|
+
<div className="flex gap-2 flex-wrap">
|
|
496
|
+
{(team.skills || []).length > 0 ? (
|
|
497
|
+
team.skills.map((s, i) => (
|
|
498
|
+
<span key={i} className="text-xs bg-purple-900/30 text-purple-400 px-2 py-1 rounded">{s}</span>
|
|
499
|
+
))
|
|
500
|
+
) : (
|
|
501
|
+
<span className="text-xs text-[var(--muted)]">{t('team.noSkills')}</span>
|
|
502
|
+
)}
|
|
503
|
+
</div>
|
|
504
|
+
)}
|
|
505
|
+
</div>
|
|
506
|
+
|
|
507
|
+
{/* Workspace */}
|
|
508
|
+
<div>
|
|
509
|
+
<div className="flex items-center justify-between mb-3">
|
|
510
|
+
<h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider">{t('team.workspaceTitle')}</h2>
|
|
511
|
+
<button
|
|
512
|
+
className="text-xs text-[var(--accent)] hover:underline"
|
|
513
|
+
onClick={() => { setShowWorkspaceSelector(true); setWorkspaceInput(team.workspacePath || ''); fetchDirs(team.workspacePath || ''); }}
|
|
514
|
+
>
|
|
515
|
+
📁 {t('team.selectWorkspace')}
|
|
516
|
+
</button>
|
|
517
|
+
</div>
|
|
518
|
+
{team.workspacePath ? (
|
|
519
|
+
<div className="card text-sm font-mono">{team.workspacePath}</div>
|
|
520
|
+
) : (
|
|
521
|
+
<div className="card text-sm text-[var(--muted)]">{t('team.noWorkspace')}</div>
|
|
522
|
+
)}
|
|
523
|
+
</div>
|
|
524
|
+
|
|
525
|
+
{/* Recent sprints */}
|
|
526
|
+
<div>
|
|
527
|
+
<h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider mb-3">{t('team.recentSprints')}</h2>
|
|
528
|
+
{(team.sprints || []).length > 0 ? (
|
|
529
|
+
<div className="space-y-2">
|
|
530
|
+
{team.sprints.slice(0, 5).map(sprint => {
|
|
531
|
+
const st = statusCfg[sprint.status] || statusCfg.draft;
|
|
532
|
+
return (
|
|
533
|
+
<div
|
|
534
|
+
key={sprint.id}
|
|
535
|
+
className="card cursor-pointer hover:border-[var(--accent)]/30 transition-all"
|
|
536
|
+
onClick={() => { setActiveTab('sprints'); setActiveSprintId(sprint.id); }}
|
|
537
|
+
>
|
|
538
|
+
<div className="flex items-center justify-between">
|
|
539
|
+
<div className="flex items-center gap-2">
|
|
540
|
+
<span>{st.icon}</span>
|
|
541
|
+
<span className="text-sm font-medium">{sprint.title}</span>
|
|
542
|
+
</div>
|
|
543
|
+
<div className="flex items-center gap-2">
|
|
544
|
+
<span className={`text-[10px] px-1.5 py-0.5 rounded ${st.bg} ${st.color}`}>{st.label}</span>
|
|
545
|
+
{sprint.workflow && <span className="text-[10px] text-[var(--muted)]">📊 {sprint.workflow.completedCount || 0}/{sprint.workflow.nodeCount || 0}</span>}
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
);
|
|
550
|
+
})}
|
|
551
|
+
</div>
|
|
552
|
+
) : (
|
|
553
|
+
<div className="card text-center py-6 text-[var(--muted)]">
|
|
554
|
+
<div className="text-2xl mb-2">🚀</div>
|
|
555
|
+
<p className="text-sm">{t('team.noSprints')}</p>
|
|
556
|
+
</div>
|
|
557
|
+
)}
|
|
558
|
+
</div>
|
|
559
|
+
</>
|
|
560
|
+
)}
|
|
561
|
+
|
|
562
|
+
{/* Sprints tab / Sprint detail */}
|
|
563
|
+
{activeTab === 'sprints' && (
|
|
564
|
+
activeSprintId ? renderSprintDetail() : (
|
|
565
|
+
<div>
|
|
566
|
+
<div className="flex items-center justify-between mb-4">
|
|
567
|
+
<h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider">{t('team.tab.sprints')}</h2>
|
|
568
|
+
<button
|
|
569
|
+
className="text-xs text-[var(--accent)] hover:underline"
|
|
570
|
+
onClick={() => { setShowNewSprint(true); setSprintTitle(''); setSprintGoal(''); }}
|
|
571
|
+
>
|
|
572
|
+
+ {t('team.newSprint')}
|
|
573
|
+
</button>
|
|
574
|
+
</div>
|
|
575
|
+
{(team.sprints || []).length > 0 ? (
|
|
576
|
+
<div className="space-y-2">
|
|
577
|
+
{team.sprints.map(sprint => {
|
|
578
|
+
const st = statusCfg[sprint.status] || statusCfg.draft;
|
|
579
|
+
return (
|
|
580
|
+
<div
|
|
581
|
+
key={sprint.id}
|
|
582
|
+
className="card cursor-pointer hover:border-[var(--accent)]/30 transition-all"
|
|
583
|
+
onClick={() => setActiveSprintId(sprint.id)}
|
|
584
|
+
>
|
|
585
|
+
<div className="flex items-center justify-between">
|
|
586
|
+
<div className="flex items-center gap-2">
|
|
587
|
+
<span>{st.icon}</span>
|
|
588
|
+
<span className="text-sm font-medium">{sprint.title}</span>
|
|
589
|
+
</div>
|
|
590
|
+
<div className="flex items-center gap-2">
|
|
591
|
+
<span className={`text-[10px] px-1.5 py-0.5 rounded ${st.bg} ${st.color}`}>{st.label}</span>
|
|
592
|
+
{sprint.chatCount > 0 && <span className="text-[10px] text-[var(--muted)]">💬 {sprint.chatCount}</span>}
|
|
593
|
+
{sprint.workflow && <span className="text-[10px] text-[var(--muted)]">📊 {sprint.workflow.completedCount || 0}/{sprint.workflow.nodeCount || 0}</span>}
|
|
594
|
+
</div>
|
|
595
|
+
</div>
|
|
596
|
+
<p className="text-xs text-[var(--muted)] mt-1 truncate">🎯 {sprint.goal}</p>
|
|
597
|
+
</div>
|
|
598
|
+
);
|
|
599
|
+
})}
|
|
600
|
+
</div>
|
|
601
|
+
) : (
|
|
602
|
+
<div className="card text-center py-8 text-[var(--muted)]">
|
|
603
|
+
<div className="text-3xl mb-2">🚀</div>
|
|
604
|
+
<p className="text-sm">{t('team.noSprints')}</p>
|
|
605
|
+
</div>
|
|
606
|
+
)}
|
|
607
|
+
</div>
|
|
608
|
+
)
|
|
609
|
+
)}
|
|
610
|
+
|
|
611
|
+
{/* Overview + active sprint */}
|
|
612
|
+
{activeTab === 'overview' && activeSprintId && renderSprintDetail()}
|
|
613
|
+
|
|
614
|
+
{/* Files tab */}
|
|
615
|
+
{activeTab === 'files' && (
|
|
616
|
+
<div className="flex-1 min-h-0">
|
|
617
|
+
{team.workspacePath ? (
|
|
618
|
+
<div className="h-full">
|
|
619
|
+
<FilesView
|
|
620
|
+
departmentId={team.departmentId}
|
|
621
|
+
previewFile={previewFile}
|
|
622
|
+
onPreview={loadFilePreview}
|
|
623
|
+
onClosePreview={() => setPreviewFile(null)}
|
|
624
|
+
/>
|
|
625
|
+
</div>
|
|
626
|
+
) : (
|
|
627
|
+
<div className="card text-center py-8 text-[var(--muted)]">
|
|
628
|
+
<div className="text-3xl mb-2">📁</div>
|
|
629
|
+
<p className="text-sm">{t('team.noWorkspace')}</p>
|
|
630
|
+
<button
|
|
631
|
+
className="btn-secondary mt-3 text-sm"
|
|
632
|
+
onClick={() => { setShowWorkspaceSelector(true); fetchDirs(''); }}
|
|
633
|
+
>
|
|
634
|
+
{t('team.selectWorkspace')}
|
|
635
|
+
</button>
|
|
636
|
+
</div>
|
|
637
|
+
)}
|
|
638
|
+
</div>
|
|
639
|
+
)}
|
|
640
|
+
</div>
|
|
641
|
+
|
|
642
|
+
{/* Modals */}
|
|
643
|
+
{selectedAgent && <AgentDetailModal agentId={selectedAgent} onClose={() => setSelectedAgent(null)} />}
|
|
644
|
+
{chatAgent && (
|
|
645
|
+
<AgentChatModal
|
|
646
|
+
agentId={chatAgent.id}
|
|
647
|
+
agentName={chatAgent.name}
|
|
648
|
+
agentAvatar={chatAgent.avatar}
|
|
649
|
+
agentRole={chatAgent.role}
|
|
650
|
+
agentSignature={chatAgent.signature}
|
|
651
|
+
agentDepartment={chatAgent.department}
|
|
652
|
+
onClose={() => setChatAgent(null)}
|
|
653
|
+
/>
|
|
654
|
+
)}
|
|
655
|
+
|
|
656
|
+
{/* Workspace selector modal */}
|
|
657
|
+
{showWorkspaceSelector && (
|
|
658
|
+
<div className="fixed inset-0 z-[70] bg-black/70 flex items-center justify-center !m-0" onClick={() => setShowWorkspaceSelector(false)}>
|
|
659
|
+
<div className="card max-w-lg w-full mx-4 max-h-[70vh] flex flex-col" onClick={e => e.stopPropagation()}>
|
|
660
|
+
<div className="flex items-center justify-between pb-3 border-b border-[var(--border)]">
|
|
661
|
+
<h3 className="text-base font-semibold">📁 {t('team.selectWorkspace')}</h3>
|
|
662
|
+
<button onClick={() => setShowWorkspaceSelector(false)} className="text-[var(--muted)] hover:text-white text-lg">✕</button>
|
|
663
|
+
</div>
|
|
664
|
+
<div className="flex items-center gap-2 py-2 px-1 bg-[var(--background)] rounded-lg mt-3 mb-2">
|
|
665
|
+
<span className="text-xs text-[var(--muted)] shrink-0">📍</span>
|
|
666
|
+
<span className="text-xs font-mono text-[var(--foreground)] truncate">{browseCurrentPath}</span>
|
|
667
|
+
</div>
|
|
668
|
+
<div className="flex-1 overflow-auto space-y-0.5 min-h-[200px]">
|
|
669
|
+
{browseLoading ? (
|
|
670
|
+
<div className="text-center py-8 text-[var(--muted)] text-sm animate-pulse">{t('common.loading')}</div>
|
|
671
|
+
) : (
|
|
672
|
+
<>
|
|
673
|
+
{browseParentPath !== null && (
|
|
674
|
+
<div className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-white/5 cursor-pointer text-sm" onClick={() => fetchDirs(browseParentPath)}>
|
|
675
|
+
<span>📂</span><span className="text-[var(--muted)]">..</span>
|
|
676
|
+
</div>
|
|
677
|
+
)}
|
|
678
|
+
{browseDirs.map(dir => (
|
|
679
|
+
<div key={dir.path} className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-white/5 cursor-pointer text-sm" onClick={() => fetchDirs(dir.path)}>
|
|
680
|
+
<span>📁</span><span className="truncate">{dir.name}</span>
|
|
681
|
+
</div>
|
|
682
|
+
))}
|
|
683
|
+
</>
|
|
684
|
+
)}
|
|
685
|
+
</div>
|
|
686
|
+
<div className="flex gap-2 pt-3 border-t border-[var(--border)] mt-2">
|
|
687
|
+
<button className="btn-secondary flex-1" onClick={() => setShowWorkspaceSelector(false)}>{t('common.cancel')}</button>
|
|
688
|
+
<button className="btn-primary flex-1" onClick={() => handleSetWorkspace(browseCurrentPath)}>
|
|
689
|
+
{t('dept.newReq.selectDir')}
|
|
690
|
+
</button>
|
|
691
|
+
</div>
|
|
692
|
+
</div>
|
|
693
|
+
</div>
|
|
694
|
+
)}
|
|
695
|
+
</div>
|
|
696
|
+
);
|
|
697
|
+
}
|