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,367 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useStore } from '@/lib/client-store';
|
|
5
|
+
import OrgTree from './OrgTree';
|
|
6
|
+
import { useI18n } from '@/lib/i18n';
|
|
7
|
+
|
|
8
|
+
export default function DepartmentView() {
|
|
9
|
+
const { t } = useI18n();
|
|
10
|
+
const {
|
|
11
|
+
company, planDepartment, confirmPlan, pendingPlan, setPendingPlan,
|
|
12
|
+
planAdjustment, confirmAdjustment, disbandDepartment, loading,
|
|
13
|
+
navigateToDepartment,
|
|
14
|
+
} = useStore();
|
|
15
|
+
|
|
16
|
+
// Modal states
|
|
17
|
+
const [showOrgTree, setShowOrgTree] = useState(false);
|
|
18
|
+
const [showCreate, setShowCreate] = useState(false);
|
|
19
|
+
const [showAdjust, setShowAdjust] = useState(null); // Adjust modal departmentId
|
|
20
|
+
const [showDisband, setShowDisband] = useState(null); // Disband confirm departmentId
|
|
21
|
+
const [disbandReason, setDisbandReason] = useState('');
|
|
22
|
+
const [adjustGoal, setAdjustGoal] = useState('');
|
|
23
|
+
|
|
24
|
+
// Create department
|
|
25
|
+
const [deptName, setDeptName] = useState('');
|
|
26
|
+
const [deptMission, setDeptMission] = useState('');
|
|
27
|
+
|
|
28
|
+
if (!company) return null;
|
|
29
|
+
|
|
30
|
+
const handlePlan = async () => {
|
|
31
|
+
if (!deptName || !deptMission) return;
|
|
32
|
+
try {
|
|
33
|
+
await planDepartment(deptName, deptMission);
|
|
34
|
+
} catch (e) { /* handled */ }
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleConfirm = async () => {
|
|
38
|
+
if (!pendingPlan?.planId) return;
|
|
39
|
+
try {
|
|
40
|
+
if (pendingPlan.type === 'adjustment') {
|
|
41
|
+
await confirmAdjustment(pendingPlan.planId);
|
|
42
|
+
setShowAdjust(null);
|
|
43
|
+
} else {
|
|
44
|
+
await confirmPlan(pendingPlan.planId);
|
|
45
|
+
setShowCreate(false);
|
|
46
|
+
}
|
|
47
|
+
setDeptName('');
|
|
48
|
+
setDeptMission('');
|
|
49
|
+
setAdjustGoal('');
|
|
50
|
+
} catch (e) { /* handled */ }
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleAdjustPlan = async (deptId) => {
|
|
54
|
+
if (!adjustGoal) return;
|
|
55
|
+
try {
|
|
56
|
+
await planAdjustment(deptId, adjustGoal);
|
|
57
|
+
} catch (e) { /* handled */ }
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleDisband = async () => {
|
|
61
|
+
if (!showDisband) return;
|
|
62
|
+
try {
|
|
63
|
+
await disbandDepartment(showDisband, disbandReason || 'Organization restructuring');
|
|
64
|
+
setShowDisband(null);
|
|
65
|
+
setDisbandReason('');
|
|
66
|
+
} catch (e) { /* handled */ }
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="p-6 space-y-6 animate-fade-in">
|
|
71
|
+
{/* Top title bar */}
|
|
72
|
+
<div className="flex items-center justify-between">
|
|
73
|
+
<div>
|
|
74
|
+
<h1 className="text-2xl font-bold">{t('dept.title')}</h1>
|
|
75
|
+
<p className="text-sm text-[var(--muted)] mt-1">{t('dept.subtitle')}</p>
|
|
76
|
+
</div>
|
|
77
|
+
<div className="flex gap-2">
|
|
78
|
+
<button
|
|
79
|
+
onClick={() => setShowOrgTree(true)}
|
|
80
|
+
className="btn-secondary flex items-center gap-1.5"
|
|
81
|
+
>
|
|
82
|
+
{t('dept.viewOrgTree')}
|
|
83
|
+
</button>
|
|
84
|
+
<button
|
|
85
|
+
onClick={() => { setShowCreate(true); setPendingPlan && setPendingPlan(null); }}
|
|
86
|
+
className="btn-primary"
|
|
87
|
+
>{t('dept.createDept')}</button>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Department list */}
|
|
92
|
+
{company.departments.length === 0 ? (
|
|
93
|
+
<div className="card text-center py-12 text-[var(--muted)]">
|
|
94
|
+
<div className="text-5xl mb-4">🏗️</div>
|
|
95
|
+
<p className="text-lg">{t('dept.empty')}</p>
|
|
96
|
+
<p className="text-sm mt-1">{t('dept.emptyHint')}</p>
|
|
97
|
+
</div>
|
|
98
|
+
) : (
|
|
99
|
+
<div className="space-y-4">
|
|
100
|
+
{company.departments.map((dept) => (
|
|
101
|
+
<div key={dept.id} className="card hover:border-[var(--accent)]/20 transition-all">
|
|
102
|
+
{/* Department header - click to open detail */}
|
|
103
|
+
<div
|
|
104
|
+
className="flex items-center justify-between cursor-pointer"
|
|
105
|
+
onClick={() => navigateToDepartment(dept.id)}
|
|
106
|
+
>
|
|
107
|
+
<div className="flex items-center gap-3">
|
|
108
|
+
<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">
|
|
109
|
+
🏢
|
|
110
|
+
</div>
|
|
111
|
+
<div>
|
|
112
|
+
<h3 className="font-semibold">{dept.name}</h3>
|
|
113
|
+
<p className="text-xs text-[var(--muted)] max-w-md truncate">{dept.mission}</p>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
<div className="flex items-center gap-3">
|
|
117
|
+
<span className={`text-xs px-2 py-0.5 rounded-full ${
|
|
118
|
+
dept.status === 'completed' ? 'bg-green-900/30 text-green-400' :
|
|
119
|
+
dept.status === 'active' ? 'bg-yellow-900/30 text-yellow-400' :
|
|
120
|
+
'bg-blue-900/30 text-blue-400'
|
|
121
|
+
}`}>
|
|
122
|
+
{dept.status}
|
|
123
|
+
</span>
|
|
124
|
+
<span className="text-xs text-green-400">
|
|
125
|
+
${(dept.tokenUsage?.totalCost || 0).toFixed(4)}
|
|
126
|
+
</span>
|
|
127
|
+
<span className="text-sm text-[var(--muted)]">{t('dept.members', { n: dept.members.length })}</span>
|
|
128
|
+
<span className="text-[var(--muted)] text-xs">{t('dept.viewDetail')}</span>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{/* Employee avatar preview */}
|
|
133
|
+
<div className="flex items-center gap-2 mt-3 pt-3 border-t border-[var(--border)]">
|
|
134
|
+
<div className="flex -space-x-2">
|
|
135
|
+
{dept.members.slice(0, 6).map((m) => (
|
|
136
|
+
<img
|
|
137
|
+
key={m.id}
|
|
138
|
+
src={m.avatar}
|
|
139
|
+
alt={m.name}
|
|
140
|
+
title={`${m.name} (${m.role})`}
|
|
141
|
+
className="w-7 h-7 rounded-full border-2 border-[var(--card)] bg-[var(--border)]"
|
|
142
|
+
/>
|
|
143
|
+
))}
|
|
144
|
+
{dept.members.length > 6 && (
|
|
145
|
+
<div className="w-7 h-7 rounded-full border-2 border-[var(--card)] bg-[var(--border)] flex items-center justify-center text-[10px] text-[var(--muted)]">
|
|
146
|
+
+{dept.members.length - 6}
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
<div className="flex-1" />
|
|
151
|
+
{/* Quick action buttons */}
|
|
152
|
+
<button
|
|
153
|
+
className="text-xs text-[var(--muted)] hover:text-blue-400 transition-colors px-2 py-1 rounded hover:bg-blue-900/10"
|
|
154
|
+
onClick={(e) => { e.stopPropagation(); setShowAdjust(dept.id); setAdjustGoal(''); setPendingPlan(null); }}
|
|
155
|
+
>{t('dept.detail.adjustBtn')}</button>
|
|
156
|
+
<button
|
|
157
|
+
className="text-xs text-[var(--muted)] hover:text-red-400 transition-colors px-2 py-1 rounded hover:bg-red-900/10"
|
|
158
|
+
onClick={(e) => { e.stopPropagation(); setShowDisband(dept.id); setDisbandReason(''); }}
|
|
159
|
+
>
|
|
160
|
+
{t('dept.detail.disbandBtn')}
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
))}
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
{/* ========== Org tree modal ========== */}
|
|
169
|
+
{showOrgTree && (
|
|
170
|
+
<div className="fixed inset-0 z-50 bg-black/70 flex items-center justify-center !m-0" onClick={() => setShowOrgTree(false)}>
|
|
171
|
+
<div className="bg-[var(--card)] border border-[var(--border)] rounded-2xl max-w-5xl w-full mx-4 max-h-[85vh] overflow-auto" onClick={e => e.stopPropagation()}>
|
|
172
|
+
<div className="sticky top-0 z-10 bg-[var(--card)] border-b border-[var(--border)] px-6 py-4 flex items-center justify-between">
|
|
173
|
+
<h2 className="text-lg font-semibold">{t('dept.orgTree.title')}</h2>
|
|
174
|
+
<button onClick={() => setShowOrgTree(false)} className="text-[var(--muted)] hover:text-white text-xl">✕</button>
|
|
175
|
+
</div>
|
|
176
|
+
<div className="p-2">
|
|
177
|
+
<OrgTree embedded />
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
{/* ========== Disband department confirm modal ========== */}
|
|
184
|
+
{showDisband && (
|
|
185
|
+
<div className="fixed inset-0 z-[60] bg-black/70 flex items-center justify-center !m-0" onClick={() => setShowDisband(null)}>
|
|
186
|
+
<div className="card max-w-sm w-full mx-4 space-y-4" onClick={e => e.stopPropagation()}>
|
|
187
|
+
<h3 className="text-lg font-semibold text-red-400">{t('dept.detail.disbandBtn')}</h3>
|
|
188
|
+
<p className="text-sm">
|
|
189
|
+
{t('dept.disband.desc', { name: '' })}<strong>{company.departments.find(d => d.id === showDisband)?.name}</strong>
|
|
190
|
+
<br />{t('dept.disband.descSuffix')}
|
|
191
|
+
</p>
|
|
192
|
+
<div>
|
|
193
|
+
<label className="block text-sm mb-1 text-[var(--muted)]">{t('dept.disband.reasonLabel')}</label>
|
|
194
|
+
<input className="input w-full" placeholder={t('dept.disband.reasonPlaceholder')} value={disbandReason} onChange={e => setDisbandReason(e.target.value)} />
|
|
195
|
+
</div>
|
|
196
|
+
<div className="flex gap-2">
|
|
197
|
+
<button className="btn-secondary flex-1" onClick={() => setShowDisband(null)}>{t('common.cancel')}</button>
|
|
198
|
+
<button className="btn-danger flex-1" onClick={handleDisband} disabled={loading}>
|
|
199
|
+
{loading ? t('dept.disband.disbanding') : t('dept.disband.confirmBtn')}
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
{/* ========== Adjust workforce modal (two-step flow) ========== */}
|
|
207
|
+
{showAdjust && (
|
|
208
|
+
<div className="fixed inset-0 z-[60] bg-black/70 flex items-center justify-center !m-0" onClick={() => { setShowAdjust(null); setPendingPlan(null); }}>
|
|
209
|
+
<div className="card max-w-lg w-full mx-4 space-y-4 max-h-[80vh] overflow-auto" onClick={e => e.stopPropagation()}>
|
|
210
|
+
{!pendingPlan || pendingPlan.type !== 'adjustment' ? (
|
|
211
|
+
<>
|
|
212
|
+
<h3 className="text-lg font-semibold">{t('dept.detail.adjustBtn')}</h3>
|
|
213
|
+
<p className="text-sm text-[var(--muted)]">
|
|
214
|
+
{t('dept.adjust.desc')}
|
|
215
|
+
</p>
|
|
216
|
+
<div className="bg-[var(--background)] border border-[var(--border)] rounded-lg p-3">
|
|
217
|
+
<div className="text-xs text-[var(--muted)] mb-1">{t('dept.adjust.currentDept')}</div>
|
|
218
|
+
<div className="font-medium">{company.departments.find(d => d.id === showAdjust)?.name}</div>
|
|
219
|
+
<div className="text-xs text-[var(--muted)] mt-1">
|
|
220
|
+
{t('dept.adjust.currentMembers', { n: company.departments.find(d => d.id === showAdjust)?.members.length })}
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
<div>
|
|
224
|
+
<label className="block text-sm mb-1 text-[var(--muted)]">{t('dept.adjust.goalLabel')}</label>
|
|
225
|
+
<textarea
|
|
226
|
+
className="input w-full h-20 resize-none"
|
|
227
|
+
placeholder={t('dept.adjust.goalPlaceholder')}
|
|
228
|
+
value={adjustGoal}
|
|
229
|
+
onChange={e => setAdjustGoal(e.target.value)}
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
<div className="flex gap-2">
|
|
233
|
+
<button className="btn-secondary flex-1" onClick={() => setShowAdjust(null)}>{t('common.cancel')}</button>
|
|
234
|
+
<button className="btn-primary flex-1" disabled={!adjustGoal || loading} onClick={() => handleAdjustPlan(showAdjust)}>
|
|
235
|
+
{loading ? t('dept.adjust.planning') : t('dept.adjust.planBtn')}
|
|
236
|
+
</button>
|
|
237
|
+
</div>
|
|
238
|
+
</>
|
|
239
|
+
) : (
|
|
240
|
+
<>
|
|
241
|
+
<h3 className="text-lg font-semibold">{t('dept.adjust.reviewTitle')}</h3>
|
|
242
|
+
<p className="text-sm text-[var(--muted)]">
|
|
243
|
+
{t('dept.adjust.reviewDesc', { dept: pendingPlan.departmentName })}
|
|
244
|
+
</p>
|
|
245
|
+
|
|
246
|
+
{pendingPlan.reasoning && (
|
|
247
|
+
<div className="bg-blue-900/10 border border-blue-500/20 rounded-lg p-3">
|
|
248
|
+
<div className="text-xs font-medium text-blue-400 mb-1">{t('overview.planReview.analysis')}</div>
|
|
249
|
+
<div className="text-sm text-[var(--muted)]">{pendingPlan.reasoning}</div>
|
|
250
|
+
</div>
|
|
251
|
+
)}
|
|
252
|
+
|
|
253
|
+
{/* Layoff list */}
|
|
254
|
+
{pendingPlan.fires?.length > 0 && (
|
|
255
|
+
<div className="bg-red-900/10 border border-red-500/20 rounded-lg p-3 space-y-2">
|
|
256
|
+
<div className="text-xs font-medium text-red-400 mb-1">{t('dept.adjust.firesTitle', { n: pendingPlan.fires.length })}</div>
|
|
257
|
+
{pendingPlan.fires.map((f, i) => (
|
|
258
|
+
<div key={i} className="flex items-center gap-2 text-sm">
|
|
259
|
+
<span className="text-red-400">✕</span>
|
|
260
|
+
<span className="font-medium">{f.name}</span>
|
|
261
|
+
<span className="text-xs text-[var(--muted)]">- {f.reason}</span>
|
|
262
|
+
</div>
|
|
263
|
+
))}
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
|
|
267
|
+
{/* Expansion list */}
|
|
268
|
+
{pendingPlan.hires?.length > 0 && (
|
|
269
|
+
<div className="bg-green-900/10 border border-green-500/20 rounded-lg p-3 space-y-2">
|
|
270
|
+
<div className="text-xs font-medium text-green-400 mb-1">{t('dept.adjust.hiresTitle', { n: pendingPlan.hires.length })}</div>
|
|
271
|
+
{pendingPlan.hires.map((h, i) => (
|
|
272
|
+
<div key={i} className="flex items-center gap-3 p-2 rounded-lg bg-white/5">
|
|
273
|
+
<span>🤖</span>
|
|
274
|
+
<div className="flex-1">
|
|
275
|
+
<div className="text-sm font-medium">{h.name}</div>
|
|
276
|
+
<div className="text-xs text-[var(--muted)]">{h.templateTitle || h.templateId}</div>
|
|
277
|
+
{h.providerName && <div className="text-[10px] text-purple-400/80 mt-0.5">⚡ {h.providerName}</div>}
|
|
278
|
+
{h.reason && <div className="text-[10px] text-blue-400/70 mt-0.5">💡 {h.reason}</div>}
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
))}
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
|
|
285
|
+
{pendingPlan.fires?.length === 0 && pendingPlan.hires?.length === 0 && (
|
|
286
|
+
<div className="text-center text-[var(--muted)] py-4">
|
|
287
|
+
{t('dept.adjust.noChanges')}
|
|
288
|
+
</div>
|
|
289
|
+
)}
|
|
290
|
+
|
|
291
|
+
<div className="flex gap-2">
|
|
292
|
+
<button className="btn-secondary flex-1" onClick={() => setPendingPlan(null)}>{t('overview.planReview.rejectBtn')}</button>
|
|
293
|
+
<button
|
|
294
|
+
className="btn-primary flex-1"
|
|
295
|
+
disabled={loading || (pendingPlan.fires?.length === 0 && pendingPlan.hires?.length === 0)}
|
|
296
|
+
onClick={handleConfirm}
|
|
297
|
+
>
|
|
298
|
+
{loading ? t('dept.adjust.executing') : t('dept.adjust.approveBtn')}
|
|
299
|
+
</button>
|
|
300
|
+
</div>
|
|
301
|
+
</>
|
|
302
|
+
)}
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
)}
|
|
306
|
+
|
|
307
|
+
{/* ========== Create department modal (two-step flow) ========== */}
|
|
308
|
+
{showCreate && (
|
|
309
|
+
<div className="fixed inset-0 z-50 bg-black/70 flex items-center justify-center !m-0" onClick={() => { setShowCreate(false); setPendingPlan && setPendingPlan(null); }}>
|
|
310
|
+
<div className="card max-w-lg w-full mx-4 space-y-4 max-h-[80vh] overflow-auto" onClick={e => e.stopPropagation()}>
|
|
311
|
+
{!pendingPlan || pendingPlan.type === 'adjustment' ? (
|
|
312
|
+
<>
|
|
313
|
+
<h3 className="text-lg font-semibold">{t('dept.create.title')}</h3>
|
|
314
|
+
<div>
|
|
315
|
+
<label className="block text-sm mb-1 text-[var(--muted)]">{t('overview.createDept.nameLabel')}</label>
|
|
316
|
+
<input className="input w-full" placeholder={t('dept.create.namePlaceholder')} value={deptName} onChange={e => setDeptName(e.target.value)} />
|
|
317
|
+
</div>
|
|
318
|
+
<div>
|
|
319
|
+
<label className="block text-sm mb-1 text-[var(--muted)]">{t('overview.createDept.missionLabel')}</label>
|
|
320
|
+
<textarea className="input w-full h-24 resize-none" placeholder={t('dept.create.missionPlaceholder')} value={deptMission} onChange={e => setDeptMission(e.target.value)} />
|
|
321
|
+
</div>
|
|
322
|
+
<div className="flex gap-2">
|
|
323
|
+
<button className="btn-secondary flex-1" onClick={() => setShowCreate(false)}>{t('common.cancel')}</button>
|
|
324
|
+
<button className="btn-primary flex-1" disabled={!deptName || !deptMission || loading} onClick={handlePlan}>
|
|
325
|
+
{loading ? t('dept.create.planning') : t('dept.create.planBtn')}
|
|
326
|
+
</button>
|
|
327
|
+
</div>
|
|
328
|
+
</>
|
|
329
|
+
) : (
|
|
330
|
+
<>
|
|
331
|
+
<h3 className="text-lg font-semibold">{t('overview.planReview.title')}</h3>
|
|
332
|
+
<p className="text-sm text-[var(--muted)]">{t('dept.create.reviewDesc', { dept: pendingPlan.departmentName })}</p>
|
|
333
|
+
|
|
334
|
+
{pendingPlan.reasoning && (
|
|
335
|
+
<div className="bg-blue-900/10 border border-blue-500/20 rounded-lg p-3">
|
|
336
|
+
<div className="text-xs font-medium text-blue-400 mb-1">{t('overview.planReview.analysis')}</div>
|
|
337
|
+
<div className="text-sm text-[var(--muted)]">{pendingPlan.reasoning}</div>
|
|
338
|
+
</div>
|
|
339
|
+
)}
|
|
340
|
+
|
|
341
|
+
<div className="bg-[var(--background)] border border-[var(--border)] rounded-lg p-3 space-y-2">
|
|
342
|
+
{pendingPlan.members?.map((m, i) => (
|
|
343
|
+
<div key={i} className={`flex items-center gap-3 p-2 rounded-lg ${m.isLeader ? 'bg-yellow-900/10 border border-yellow-500/20' : 'bg-white/5'}`}>
|
|
344
|
+
<span>{m.isLeader ? '👔' : '🤖'}</span>
|
|
345
|
+
<div className="flex-1">
|
|
346
|
+
<div className="text-sm font-medium">{m.name}</div>
|
|
347
|
+
<div className="text-xs text-[var(--muted)]">{m.title} {m.reportsTo ? `→ ${m.reportsTo}` : ''}</div>
|
|
348
|
+
{m.providerName && <div className="text-[10px] text-purple-400/80 mt-0.5">⚡ {m.providerName}</div>}
|
|
349
|
+
{m.reason && <div className="text-[10px] text-blue-400/70 mt-0.5">💡 {m.reason}</div>}
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
))}
|
|
353
|
+
</div>
|
|
354
|
+
<div className="flex gap-2">
|
|
355
|
+
<button className="btn-secondary flex-1" onClick={() => setPendingPlan(null)}>{t('overview.planReview.rejectBtn')}</button>
|
|
356
|
+
<button className="btn-primary flex-1" disabled={loading} onClick={handleConfirm}>
|
|
357
|
+
{loading ? t('overview.planReview.hiring') : t('dept.create.approveBtn')}
|
|
358
|
+
</button>
|
|
359
|
+
</div>
|
|
360
|
+
</>
|
|
361
|
+
)}
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
)}
|
|
365
|
+
</div>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
4
|
+
import { useStore } from '@/lib/client-store';
|
|
5
|
+
import { useI18n } from '@/lib/i18n';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 文件引用格式:
|
|
9
|
+
* 完整格式: [[file:departmentId:filePath|displayName]]
|
|
10
|
+
* 无显示名: [[file:departmentId:filePath]]
|
|
11
|
+
* 例如: [[file:dept_abc123:src/index.js|index.js]]
|
|
12
|
+
* [[file:dept_abc123:src/index.js]]
|
|
13
|
+
*/
|
|
14
|
+
const FILE_REF_FULL_REGEX = /\[\[file:([^:]+):([^|\]]+)\|([^\]]+)\]\]/g;
|
|
15
|
+
const FILE_REF_NO_NAME_REGEX = /\[\[file:([^:]+):([^\]]+)\]\]/g;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 从消息内容中解析文件引用,返回纯文本(去掉引用标记)+ 文件引用列表
|
|
19
|
+
* 同时兼容有 |displayName 和没有 |displayName 的两种格式
|
|
20
|
+
*/
|
|
21
|
+
export function parseFileReferences(content) {
|
|
22
|
+
if (!content || typeof content !== 'string') return { cleanContent: content, fileRefs: [] };
|
|
23
|
+
|
|
24
|
+
const fileRefs = [];
|
|
25
|
+
let match;
|
|
26
|
+
|
|
27
|
+
// 先匹配完整格式 [[file:deptId:path|name]]
|
|
28
|
+
const fullRegex = new RegExp(FILE_REF_FULL_REGEX.source, 'g');
|
|
29
|
+
while ((match = fullRegex.exec(content)) !== null) {
|
|
30
|
+
const filePath = match[2]?.trim();
|
|
31
|
+
const displayName = match[3]?.trim();
|
|
32
|
+
// Skip invalid refs with empty path or name
|
|
33
|
+
if (!filePath || !displayName) continue;
|
|
34
|
+
fileRefs.push({
|
|
35
|
+
fullMatch: match[0],
|
|
36
|
+
departmentId: match[1],
|
|
37
|
+
filePath,
|
|
38
|
+
displayName,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 去掉已匹配的完整格式后,再匹配无显示名格式 [[file:deptId:path]]
|
|
43
|
+
let remaining = content.replace(FILE_REF_FULL_REGEX, '\0FILE_REF_PLACEHOLDER\0');
|
|
44
|
+
const noNameRegex = new RegExp(FILE_REF_NO_NAME_REGEX.source, 'g');
|
|
45
|
+
while ((match = noNameRegex.exec(remaining)) !== null) {
|
|
46
|
+
const filePath = match[2].trim();
|
|
47
|
+
// Skip invalid refs with empty or pipe-only paths (malformed tool args)
|
|
48
|
+
if (!filePath || filePath === '|') continue;
|
|
49
|
+
const displayName = filePath.split('/').pop() || filePath;
|
|
50
|
+
fileRefs.push({
|
|
51
|
+
fullMatch: match[0],
|
|
52
|
+
departmentId: match[1],
|
|
53
|
+
filePath,
|
|
54
|
+
displayName,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 将所有文件引用标记从正文中移除,放到末尾作为附件展示
|
|
59
|
+
const cleanContent = content
|
|
60
|
+
.replace(FILE_REF_FULL_REGEX, '')
|
|
61
|
+
.replace(FILE_REF_NO_NAME_REGEX, '')
|
|
62
|
+
.trim();
|
|
63
|
+
|
|
64
|
+
return { cleanContent, fileRefs };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 文件引用标签组件 - 可点击打开文件内容弹窗
|
|
69
|
+
*/
|
|
70
|
+
export function FileRefChip({ departmentId, filePath, displayName }) {
|
|
71
|
+
const [showModal, setShowModal] = useState(false);
|
|
72
|
+
|
|
73
|
+
// 推断文件图标
|
|
74
|
+
const ext = filePath.split('.').pop()?.toLowerCase() || '';
|
|
75
|
+
const iconMap = {
|
|
76
|
+
js: '📜', jsx: '⚛️', ts: '📘', tsx: '⚛️',
|
|
77
|
+
py: '🐍', go: '🔵', rs: '🦀', java: '☕',
|
|
78
|
+
html: '🌐', css: '🎨', scss: '🎨', less: '🎨',
|
|
79
|
+
json: '📋', yaml: '📋', yml: '📋', toml: '📋',
|
|
80
|
+
md: '📝', txt: '📄', sh: '🖥️', bash: '🖥️',
|
|
81
|
+
sql: '🗃️', graphql: '🔗', proto: '📡',
|
|
82
|
+
png: '🖼️', jpg: '🖼️', svg: '🖼️', gif: '🖼️',
|
|
83
|
+
};
|
|
84
|
+
const icon = iconMap[ext] || '📄';
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<>
|
|
88
|
+
<button
|
|
89
|
+
onClick={(e) => { e.stopPropagation(); setShowModal(true); }}
|
|
90
|
+
className="inline px-2.5 py-1 my-0.5 mx-0.5 rounded-lg
|
|
91
|
+
bg-[#0f2418] border border-[#1e3a2a] hover:bg-[#152e1f] hover:border-[#2a4d38]
|
|
92
|
+
text-[#8cc9a1] text-xs font-medium transition-all cursor-pointer group text-left shadow-sm"
|
|
93
|
+
title={filePath}
|
|
94
|
+
>
|
|
95
|
+
<span className="mr-1.5">{icon}</span>
|
|
96
|
+
<span className="break-all">{displayName}</span>
|
|
97
|
+
<span className="ml-1.5 text-[#4a8a5c]/60 group-hover:text-[#6abf7e] transition-colors">↗</span>
|
|
98
|
+
</button>
|
|
99
|
+
|
|
100
|
+
{showModal && (
|
|
101
|
+
<FileViewerModal
|
|
102
|
+
departmentId={departmentId}
|
|
103
|
+
filePath={filePath}
|
|
104
|
+
displayName={displayName}
|
|
105
|
+
icon={icon}
|
|
106
|
+
onClose={() => setShowModal(false)}
|
|
107
|
+
/>
|
|
108
|
+
)}
|
|
109
|
+
</>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 文件查看弹窗
|
|
115
|
+
*/
|
|
116
|
+
function FileViewerModal({ departmentId, filePath, displayName, icon, onClose }) {
|
|
117
|
+
const { t } = useI18n();
|
|
118
|
+
const { fetchWorkspaceFile } = useStore();
|
|
119
|
+
const [content, setContent] = useState(null);
|
|
120
|
+
const [loading, setLoading] = useState(true);
|
|
121
|
+
const [error, setError] = useState(null);
|
|
122
|
+
const [copied, setCopied] = useState(false);
|
|
123
|
+
|
|
124
|
+
// 加载文件内容
|
|
125
|
+
const loadContent = useCallback(async () => {
|
|
126
|
+
setLoading(true);
|
|
127
|
+
setError(null);
|
|
128
|
+
try {
|
|
129
|
+
const data = await fetchWorkspaceFile(departmentId, filePath);
|
|
130
|
+
if (typeof data?.content === 'string') {
|
|
131
|
+
setContent(data.content);
|
|
132
|
+
} else if (typeof data === 'string') {
|
|
133
|
+
setContent(data);
|
|
134
|
+
} else if (data?.error) {
|
|
135
|
+
setError(data.error);
|
|
136
|
+
} else {
|
|
137
|
+
setError(t('fileRef.loadFailed'));
|
|
138
|
+
}
|
|
139
|
+
} catch (e) {
|
|
140
|
+
setError(e.message || t('fileRef.loadFailed'));
|
|
141
|
+
}
|
|
142
|
+
setLoading(false);
|
|
143
|
+
}, [departmentId, filePath, fetchWorkspaceFile, t]);
|
|
144
|
+
|
|
145
|
+
// 首次加载
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
loadContent();
|
|
148
|
+
}, [loadContent]);
|
|
149
|
+
|
|
150
|
+
const handleCopyPath = () => {
|
|
151
|
+
navigator.clipboard.writeText(filePath);
|
|
152
|
+
setCopied(true);
|
|
153
|
+
setTimeout(() => setCopied(false), 2000);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// 推断语言
|
|
157
|
+
const ext = filePath.split('.').pop()?.toLowerCase() || '';
|
|
158
|
+
const langMap = {
|
|
159
|
+
js: 'javascript', jsx: 'javascript', ts: 'typescript', tsx: 'typescript',
|
|
160
|
+
py: 'python', go: 'go', rs: 'rust', java: 'java',
|
|
161
|
+
html: 'html', css: 'css', scss: 'scss',
|
|
162
|
+
json: 'json', yaml: 'yaml', yml: 'yaml',
|
|
163
|
+
md: 'markdown', sh: 'bash', sql: 'sql',
|
|
164
|
+
};
|
|
165
|
+
const lang = langMap[ext] || 'text';
|
|
166
|
+
|
|
167
|
+
const lineCount = content?.split('\n').length ?? 0;
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-[100] !m-0" onClick={onClose}>
|
|
171
|
+
<div
|
|
172
|
+
className="bg-[var(--card)] border border-[var(--border)] rounded-2xl max-w-4xl w-full mx-4 max-h-[85vh] overflow-hidden flex flex-col shadow-2xl"
|
|
173
|
+
onClick={e => e.stopPropagation()}
|
|
174
|
+
>
|
|
175
|
+
{/* Header */}
|
|
176
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-[var(--border)] bg-[var(--card)]">
|
|
177
|
+
<div className="flex items-center gap-3 min-w-0 flex-1">
|
|
178
|
+
<span className="text-lg shrink-0">{icon}</span>
|
|
179
|
+
<div className="min-w-0 flex-1">
|
|
180
|
+
<div className="text-sm font-semibold truncate">{displayName}</div>
|
|
181
|
+
<div className="text-[10px] text-[var(--muted)] flex items-center gap-2 mt-0.5">
|
|
182
|
+
<span className="truncate">{filePath}</span>
|
|
183
|
+
{lineCount > 0 && (
|
|
184
|
+
<span className="shrink-0 px-1.5 py-0.5 bg-white/5 rounded">{t('fileRef.lines', { n: lineCount })}</span>
|
|
185
|
+
)}
|
|
186
|
+
<span className="shrink-0 px-1.5 py-0.5 bg-blue-900/30 text-blue-400 rounded uppercase text-[9px] font-medium">{lang}</span>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
<div className="flex items-center gap-2 shrink-0 ml-3">
|
|
191
|
+
<button
|
|
192
|
+
onClick={handleCopyPath}
|
|
193
|
+
className="text-[10px] text-[var(--muted)] hover:text-white bg-white/5 hover:bg-white/10 px-2 py-1 rounded transition-all"
|
|
194
|
+
>
|
|
195
|
+
{copied ? t('fileRef.copied') : t('fileRef.copyPath')}
|
|
196
|
+
</button>
|
|
197
|
+
<button
|
|
198
|
+
onClick={onClose}
|
|
199
|
+
className="text-[var(--muted)] hover:text-white text-lg w-7 h-7 flex items-center justify-center rounded-lg hover:bg-white/10 transition-colors"
|
|
200
|
+
>
|
|
201
|
+
✕
|
|
202
|
+
</button>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
{/* Content */}
|
|
207
|
+
<div className="flex-1 overflow-auto">
|
|
208
|
+
{loading ? (
|
|
209
|
+
<div className="flex items-center justify-center py-16">
|
|
210
|
+
<div className="text-center">
|
|
211
|
+
<div className="text-2xl animate-pulse mb-2">⏳</div>
|
|
212
|
+
<p className="text-sm text-[var(--muted)]">{t('fileRef.loading')}</p>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
) : error ? (
|
|
216
|
+
<div className="flex items-center justify-center py-16">
|
|
217
|
+
<div className="text-center">
|
|
218
|
+
<div className="text-2xl mb-2">❌</div>
|
|
219
|
+
<p className="text-sm text-red-400">{error}</p>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
) : (
|
|
223
|
+
<pre className="p-4 text-sm font-mono leading-relaxed overflow-x-auto">
|
|
224
|
+
<code>
|
|
225
|
+
{(content ?? '').split('\n').map((line, i) => (
|
|
226
|
+
<div key={i} className="flex hover:bg-white/[0.03] transition-colors">
|
|
227
|
+
<span className="inline-block w-12 text-right pr-4 text-[var(--muted)]/40 select-none text-xs leading-relaxed shrink-0">
|
|
228
|
+
{i + 1}
|
|
229
|
+
</span>
|
|
230
|
+
<span className="flex-1 whitespace-pre-wrap break-all">{line || ' '}</span>
|
|
231
|
+
</div>
|
|
232
|
+
))}
|
|
233
|
+
</code>
|
|
234
|
+
</pre>
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 文件引用列表 - 在消息气泡底部展示文件引用标签
|
|
244
|
+
*/
|
|
245
|
+
export function FileRefList({ fileRefs }) {
|
|
246
|
+
if (!fileRefs || fileRefs.length === 0) return null;
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<div className="flex flex-wrap gap-1 mt-1.5 pt-1.5 border-t border-white/[0.06]">
|
|
250
|
+
{fileRefs.map((ref, i) => (
|
|
251
|
+
<FileRefChip
|
|
252
|
+
key={`${ref.departmentId}-${ref.filePath}-${i}`}
|
|
253
|
+
departmentId={ref.departmentId}
|
|
254
|
+
filePath={ref.filePath}
|
|
255
|
+
displayName={ref.displayName}
|
|
256
|
+
/>
|
|
257
|
+
))}
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
}
|