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,465 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
4
|
+
import { useStore } from '@/lib/client-store';
|
|
5
|
+
import { useI18n } from '@/lib/i18n';
|
|
6
|
+
import dynamic from 'next/dynamic';
|
|
7
|
+
|
|
8
|
+
const MonacoEditor = dynamic(() => import('@monaco-editor/react'), { ssr: false });
|
|
9
|
+
|
|
10
|
+
const CUSTOM_THEME_NAME = 'vscode-dark-plus';
|
|
11
|
+
const defineCustomTheme = (monaco) => {
|
|
12
|
+
monaco.editor.defineTheme(CUSTOM_THEME_NAME, {
|
|
13
|
+
base: 'vs-dark',
|
|
14
|
+
inherit: true,
|
|
15
|
+
rules: [
|
|
16
|
+
{ token: '', foreground: 'd4d4d4', background: '1e1e1e' },
|
|
17
|
+
{ token: 'comment', foreground: '6a9955', fontStyle: 'italic' },
|
|
18
|
+
{ token: 'keyword', foreground: '569cd6' },
|
|
19
|
+
{ token: 'keyword.control', foreground: 'c586c0' },
|
|
20
|
+
{ token: 'string', foreground: 'ce9178' },
|
|
21
|
+
{ token: 'string.escape', foreground: 'd7ba7d' },
|
|
22
|
+
{ token: 'number', foreground: 'b5cea8' },
|
|
23
|
+
{ token: 'type', foreground: '4ec9b0' },
|
|
24
|
+
{ token: 'type.identifier', foreground: '4ec9b0' },
|
|
25
|
+
{ token: 'variable', foreground: '9cdcfe' },
|
|
26
|
+
{ token: 'variable.predefined', foreground: '4fc1ff' },
|
|
27
|
+
{ token: 'constant', foreground: '4fc1ff' },
|
|
28
|
+
{ token: 'delimiter', foreground: 'd4d4d4' },
|
|
29
|
+
{ token: 'delimiter.bracket', foreground: 'ffd700' },
|
|
30
|
+
{ token: 'tag', foreground: '569cd6' },
|
|
31
|
+
{ token: 'tag.id.pug', foreground: '4ec9b0' },
|
|
32
|
+
{ token: 'attribute.name', foreground: '9cdcfe' },
|
|
33
|
+
{ token: 'attribute.value', foreground: 'ce9178' },
|
|
34
|
+
{ token: 'string.key.json', foreground: '9cdcfe' },
|
|
35
|
+
{ token: 'string.value.json', foreground: 'ce9178' },
|
|
36
|
+
{ token: 'metatag', foreground: 'ce9178' },
|
|
37
|
+
{ token: 'metatag.content.html', foreground: 'ce9178' },
|
|
38
|
+
{ token: 'regexp', foreground: 'd16969' },
|
|
39
|
+
{ token: 'annotation', foreground: 'dcdcaa' },
|
|
40
|
+
{ token: 'function', foreground: 'dcdcaa' },
|
|
41
|
+
{ token: 'function.declaration', foreground: 'dcdcaa' },
|
|
42
|
+
{ token: 'operator', foreground: 'd4d4d4' },
|
|
43
|
+
{ token: 'namespace', foreground: '4ec9b0' },
|
|
44
|
+
],
|
|
45
|
+
colors: {
|
|
46
|
+
'editor.background': '#1e1e1e',
|
|
47
|
+
'editor.foreground': '#d4d4d4',
|
|
48
|
+
'editor.lineHighlightBackground': '#2a2d2e',
|
|
49
|
+
'editor.selectionBackground': '#264f78',
|
|
50
|
+
'editor.inactiveSelectionBackground': '#3a3d41',
|
|
51
|
+
'editorCursor.foreground': '#aeafad',
|
|
52
|
+
'editorLineNumber.foreground': '#858585',
|
|
53
|
+
'editorLineNumber.activeForeground': '#c6c6c6',
|
|
54
|
+
'editorIndentGuide.background': '#404040',
|
|
55
|
+
'editorIndentGuide.activeBackground': '#707070',
|
|
56
|
+
'editor.selectionHighlightBackground': '#add6ff26',
|
|
57
|
+
'editorBracketMatch.background': '#0064001a',
|
|
58
|
+
'editorBracketMatch.border': '#888888',
|
|
59
|
+
'editorGutter.background': '#1e1e1e',
|
|
60
|
+
'editorWidget.background': '#252526',
|
|
61
|
+
'editorWidget.border': '#454545',
|
|
62
|
+
'editorSuggestWidget.background': '#252526',
|
|
63
|
+
'editorSuggestWidget.border': '#454545',
|
|
64
|
+
'editorSuggestWidget.selectedBackground': '#04395e',
|
|
65
|
+
'input.background': '#3c3c3c',
|
|
66
|
+
'input.border': '#3c3c3c',
|
|
67
|
+
'scrollbar.shadow': '#000000',
|
|
68
|
+
'scrollbarSlider.background': '#79797966',
|
|
69
|
+
'scrollbarSlider.hoverBackground': '#646464b3',
|
|
70
|
+
'scrollbarSlider.activeBackground': '#bfbfbf66',
|
|
71
|
+
'minimap.background': '#1e1e1e',
|
|
72
|
+
'minimapSlider.background': '#79797933',
|
|
73
|
+
'minimapSlider.hoverBackground': '#64646459',
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Shared FilesView component - VSCode style left-right layout
|
|
80
|
+
* Left: file explorer (tree structure showing actual workspace files)
|
|
81
|
+
* Right: Monaco Editor code preview
|
|
82
|
+
*
|
|
83
|
+
* Props:
|
|
84
|
+
* - fileChanges: array of recent file changes from agents (used to trigger workspace reload)
|
|
85
|
+
* - departmentId: department ID for workspace API calls
|
|
86
|
+
* - previewFile: { path, content, loading } or null
|
|
87
|
+
* - onPreview: (filePath) => void
|
|
88
|
+
* - onClosePreview: () => void
|
|
89
|
+
*/
|
|
90
|
+
export default function FilesView({ fileChanges, departmentId, previewFile, onPreview, onClosePreview }) {
|
|
91
|
+
const { t } = useI18n();
|
|
92
|
+
const { fetchWorkspaceFiles } = useStore();
|
|
93
|
+
const [sidebarWidth, setSidebarWidth] = useState(260);
|
|
94
|
+
const [isResizing, setIsResizing] = useState(false);
|
|
95
|
+
const [openTabs, setOpenTabs] = useState([]);
|
|
96
|
+
const [expandedDirs, setExpandedDirs] = useState(new Set());
|
|
97
|
+
const [dirChildren, setDirChildren] = useState({});
|
|
98
|
+
const [dirLoading, setDirLoading] = useState(new Set());
|
|
99
|
+
const resizeRef = useRef(null);
|
|
100
|
+
const expandedDirsRef = useRef(expandedDirs);
|
|
101
|
+
expandedDirsRef.current = expandedDirs;
|
|
102
|
+
const [rootEntries, setRootEntries] = useState([]);
|
|
103
|
+
const [wsLoading, setWsLoading] = useState(false);
|
|
104
|
+
|
|
105
|
+
// Reload workspace file tree when fileChanges updates (e.g. after CLI agent creates files)
|
|
106
|
+
const fileChangesLen = (fileChanges || []).length;
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (!departmentId) return;
|
|
109
|
+
let cancelled = false;
|
|
110
|
+
setWsLoading(true);
|
|
111
|
+
// Clear cached sub-directory data so expanded dirs also refresh
|
|
112
|
+
setDirChildren({});
|
|
113
|
+
(async () => {
|
|
114
|
+
try {
|
|
115
|
+
const files = await fetchWorkspaceFiles(departmentId);
|
|
116
|
+
if (!cancelled && Array.isArray(files)) {
|
|
117
|
+
setRootEntries(files);
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// silently fail
|
|
121
|
+
}
|
|
122
|
+
if (!cancelled) setWsLoading(false);
|
|
123
|
+
// Re-load children for currently expanded directories
|
|
124
|
+
if (!cancelled) {
|
|
125
|
+
for (const dirPath of expandedDirsRef.current) {
|
|
126
|
+
try {
|
|
127
|
+
const children = await fetchWorkspaceFiles(departmentId, dirPath);
|
|
128
|
+
if (!cancelled && Array.isArray(children)) {
|
|
129
|
+
setDirChildren(prev => ({ ...prev, [dirPath]: children }));
|
|
130
|
+
}
|
|
131
|
+
} catch { /* ignore */ }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
})();
|
|
135
|
+
return () => { cancelled = true; };
|
|
136
|
+
}, [departmentId, fetchWorkspaceFiles, fileChangesLen]);
|
|
137
|
+
|
|
138
|
+
const loadDirChildren = useCallback(async (dirPath) => {
|
|
139
|
+
if (dirChildren[dirPath] || dirLoading.has(dirPath)) return;
|
|
140
|
+
setDirLoading(prev => new Set(prev).add(dirPath));
|
|
141
|
+
try {
|
|
142
|
+
const children = await fetchWorkspaceFiles(departmentId, dirPath);
|
|
143
|
+
if (Array.isArray(children)) {
|
|
144
|
+
setDirChildren(prev => ({ ...prev, [dirPath]: children }));
|
|
145
|
+
}
|
|
146
|
+
} catch { /* ignore */ }
|
|
147
|
+
setDirLoading(prev => {
|
|
148
|
+
const next = new Set(prev);
|
|
149
|
+
next.delete(dirPath);
|
|
150
|
+
return next;
|
|
151
|
+
});
|
|
152
|
+
}, [departmentId, dirChildren, dirLoading, fetchWorkspaceFiles]);
|
|
153
|
+
|
|
154
|
+
const toggleDir = useCallback((dirPath) => {
|
|
155
|
+
setExpandedDirs(prev => {
|
|
156
|
+
const next = new Set(prev);
|
|
157
|
+
if (next.has(dirPath)) {
|
|
158
|
+
next.delete(dirPath);
|
|
159
|
+
} else {
|
|
160
|
+
next.add(dirPath);
|
|
161
|
+
loadDirChildren(dirPath);
|
|
162
|
+
}
|
|
163
|
+
return next;
|
|
164
|
+
});
|
|
165
|
+
}, [loadDirChildren]);
|
|
166
|
+
|
|
167
|
+
const totalCount = useMemo(() => {
|
|
168
|
+
return rootEntries.length;
|
|
169
|
+
}, [rootEntries]);
|
|
170
|
+
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (!isResizing) return;
|
|
173
|
+
const handleMouseMove = (e) => {
|
|
174
|
+
const container = resizeRef.current?.parentElement;
|
|
175
|
+
if (!container) return;
|
|
176
|
+
const rect = container.getBoundingClientRect();
|
|
177
|
+
const newWidth = Math.max(180, Math.min(500, e.clientX - rect.left));
|
|
178
|
+
setSidebarWidth(newWidth);
|
|
179
|
+
};
|
|
180
|
+
const handleMouseUp = () => setIsResizing(false);
|
|
181
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
182
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
183
|
+
return () => {
|
|
184
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
185
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
186
|
+
};
|
|
187
|
+
}, [isResizing]);
|
|
188
|
+
|
|
189
|
+
const handleFileClick = (filePath) => {
|
|
190
|
+
const name = filePath?.split('/').pop() || filePath;
|
|
191
|
+
if (!openTabs.find(t => t.path === filePath)) {
|
|
192
|
+
setOpenTabs(prev => [...prev, { path: filePath, name }]);
|
|
193
|
+
}
|
|
194
|
+
onPreview(filePath);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const handleCloseTab = (e, tabPath) => {
|
|
198
|
+
e.stopPropagation();
|
|
199
|
+
setOpenTabs(prev => prev.filter(t => t.path !== tabPath));
|
|
200
|
+
if (previewFile?.path === tabPath) {
|
|
201
|
+
const remaining = openTabs.filter(t => t.path !== tabPath);
|
|
202
|
+
if (remaining.length > 0) {
|
|
203
|
+
onPreview(remaining[remaining.length - 1].path);
|
|
204
|
+
} else {
|
|
205
|
+
onClosePreview();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const getLanguage = (path) => {
|
|
211
|
+
const ext = path?.split('.').pop()?.toLowerCase();
|
|
212
|
+
const langMap = {
|
|
213
|
+
js: 'javascript', jsx: 'javascript', ts: 'typescript', tsx: 'typescript',
|
|
214
|
+
py: 'python', json: 'json', md: 'markdown', css: 'css', scss: 'scss',
|
|
215
|
+
html: 'html', xml: 'xml', yaml: 'yaml', yml: 'yaml', sh: 'shell',
|
|
216
|
+
sql: 'sql', java: 'java', go: 'go', rs: 'rust', cpp: 'cpp', c: 'c',
|
|
217
|
+
txt: 'plaintext',
|
|
218
|
+
};
|
|
219
|
+
return langMap[ext] || 'plaintext';
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const getFileIcon = (name) => {
|
|
223
|
+
const ext = name?.split('.').pop()?.toLowerCase();
|
|
224
|
+
const iconMap = {
|
|
225
|
+
js: '🟨', jsx: '⚛️', ts: '🔷', tsx: '⚛️', py: '🐍', json: '📋',
|
|
226
|
+
md: '📝', css: '🎨', html: '🌐', yaml: '⚙️', yml: '⚙️', txt: '📄',
|
|
227
|
+
sh: '🖥️', sql: '🗃️',
|
|
228
|
+
};
|
|
229
|
+
return iconMap[ext] || '📄';
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Sort helper: directories first
|
|
233
|
+
const sortEntries = (entries) =>
|
|
234
|
+
[...entries].sort((a, b) => {
|
|
235
|
+
if (a.type === 'directory' && b.type !== 'directory') return -1;
|
|
236
|
+
if (a.type !== 'directory' && b.type === 'directory') return 1;
|
|
237
|
+
return a.name.localeCompare(b.name);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const sortedRootEntries = useMemo(() => sortEntries(rootEntries), [rootEntries]);
|
|
241
|
+
|
|
242
|
+
// Empty state
|
|
243
|
+
if (rootEntries.length === 0 && !previewFile && !wsLoading) {
|
|
244
|
+
return (
|
|
245
|
+
<div className="flex items-center justify-center py-16 text-[var(--muted)]">
|
|
246
|
+
<div className="text-center">
|
|
247
|
+
<div className="text-4xl mb-2">📁</div>
|
|
248
|
+
<p>{t('reqDetail.files.noChanges')}</p>
|
|
249
|
+
<p className="text-xs mt-1">{t('reqDetail.files.noChangesHint')}</p>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
if (rootEntries.length === 0 && wsLoading) {
|
|
255
|
+
return (
|
|
256
|
+
<div className="flex items-center justify-center py-16 text-[var(--muted)]">
|
|
257
|
+
<div className="text-center">
|
|
258
|
+
<div className="text-2xl mb-2 animate-pulse">⏳</div>
|
|
259
|
+
<p>{t('reqDetail.files.loading')}</p>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const renderFileEntry = (entry, depth = 0) => {
|
|
266
|
+
if (entry.type === 'file') {
|
|
267
|
+
const filePath = entry.path || entry.name;
|
|
268
|
+
const isActive = previewFile?.path === filePath;
|
|
269
|
+
return (
|
|
270
|
+
<div
|
|
271
|
+
key={filePath}
|
|
272
|
+
className={`flex items-center gap-1.5 px-2 py-[3px] cursor-pointer text-xs transition-colors group ${
|
|
273
|
+
isActive
|
|
274
|
+
? 'bg-[var(--accent)]/15 text-[var(--accent)]'
|
|
275
|
+
: 'text-[var(--foreground)]/80 hover:bg-white/[0.06]'
|
|
276
|
+
}`}
|
|
277
|
+
style={{ paddingLeft: `${depth * 16 + 16}px` }}
|
|
278
|
+
onClick={() => handleFileClick(filePath)}
|
|
279
|
+
title={filePath}
|
|
280
|
+
>
|
|
281
|
+
<span className="text-[11px] shrink-0">{getFileIcon(entry.name)}</span>
|
|
282
|
+
<span className="truncate">{entry.name}</span>
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Directory entry
|
|
288
|
+
const dirPath = entry.path || entry.name;
|
|
289
|
+
const isExpanded = expandedDirs.has(dirPath);
|
|
290
|
+
const isLoading = dirLoading.has(dirPath);
|
|
291
|
+
const children = dirChildren[dirPath] || [];
|
|
292
|
+
const sortedChildren = sortEntries(children);
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<div key={dirPath}>
|
|
296
|
+
<div
|
|
297
|
+
className="flex items-center gap-1.5 px-2 py-[3px] cursor-pointer text-xs text-[var(--foreground)]/80 hover:bg-white/[0.06] transition-colors"
|
|
298
|
+
style={{ paddingLeft: `${depth * 16 + 16}px` }}
|
|
299
|
+
onClick={() => toggleDir(dirPath)}
|
|
300
|
+
>
|
|
301
|
+
<span className="text-[10px] text-[var(--muted)] w-3 text-center shrink-0">
|
|
302
|
+
{isLoading ? <span className="animate-pulse">⏳</span> : isExpanded ? '▼' : '▶'}
|
|
303
|
+
</span>
|
|
304
|
+
<span className="text-[11px] shrink-0">📁</span>
|
|
305
|
+
<span className="truncate font-medium">{entry.name}</span>
|
|
306
|
+
</div>
|
|
307
|
+
{isExpanded && sortedChildren.map(child =>
|
|
308
|
+
renderFileEntry(child, depth + 1)
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<div className="flex h-full min-h-[400px] overflow-hidden">
|
|
316
|
+
{/* Left: file explorer */}
|
|
317
|
+
<div
|
|
318
|
+
className="shrink-0 bg-[var(--card)] flex flex-col"
|
|
319
|
+
style={{ width: sidebarWidth }}
|
|
320
|
+
>
|
|
321
|
+
<div className="px-3 py-2 text-[10px] font-semibold tracking-wider text-[var(--muted)] uppercase flex items-center justify-between">
|
|
322
|
+
<span>{t('reqDetail.files.explorer')}</span>
|
|
323
|
+
<span className="text-[10px] normal-case font-normal text-[var(--muted)]/60">
|
|
324
|
+
{totalCount}
|
|
325
|
+
</span>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
<div className="flex-1 overflow-auto py-1 select-none">
|
|
329
|
+
{sortedRootEntries.map(entry => renderFileEntry(entry, 0))}
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
{/* Drag resize handle */}
|
|
334
|
+
<div
|
|
335
|
+
ref={resizeRef}
|
|
336
|
+
className={`w-[3px] cursor-col-resize hover:bg-[var(--accent)]/40 transition-colors shrink-0 ${
|
|
337
|
+
isResizing ? 'bg-[var(--accent)]/50' : 'bg-transparent'
|
|
338
|
+
}`}
|
|
339
|
+
onMouseDown={() => setIsResizing(true)}
|
|
340
|
+
/>
|
|
341
|
+
|
|
342
|
+
{/* Right: editor area */}
|
|
343
|
+
<div className="flex-1 flex flex-col min-w-0 min-h-0 bg-[var(--background)]">
|
|
344
|
+
{openTabs.length > 0 && (
|
|
345
|
+
<div className="flex bg-[var(--card)] overflow-x-auto shrink-0">
|
|
346
|
+
{openTabs.map(tab => {
|
|
347
|
+
const isActive = previewFile?.path === tab.path;
|
|
348
|
+
return (
|
|
349
|
+
<div
|
|
350
|
+
key={tab.path}
|
|
351
|
+
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs cursor-pointer min-w-0 group transition-colors ${
|
|
352
|
+
isActive
|
|
353
|
+
? 'bg-[var(--background)] text-[var(--foreground)]'
|
|
354
|
+
: 'text-[var(--muted)] hover:bg-[var(--card-hover)]'
|
|
355
|
+
}`}
|
|
356
|
+
onClick={() => onPreview(tab.path)}
|
|
357
|
+
>
|
|
358
|
+
<span className="text-[11px] shrink-0">{getFileIcon(tab.name)}</span>
|
|
359
|
+
<span className="truncate max-w-[120px]">{tab.name}</span>
|
|
360
|
+
{previewFile?.loading && isActive && (
|
|
361
|
+
<span className="w-2 h-2 rounded-full bg-yellow-400 animate-pulse shrink-0" />
|
|
362
|
+
)}
|
|
363
|
+
<button
|
|
364
|
+
onClick={(e) => handleCloseTab(e, tab.path)}
|
|
365
|
+
className="ml-1 text-[var(--muted)] hover:text-white opacity-0 group-hover:opacity-100 transition-opacity shrink-0 text-[10px] w-4 h-4 flex items-center justify-center rounded hover:bg-white/10"
|
|
366
|
+
>
|
|
367
|
+
✕
|
|
368
|
+
</button>
|
|
369
|
+
</div>
|
|
370
|
+
);
|
|
371
|
+
})}
|
|
372
|
+
</div>
|
|
373
|
+
)}
|
|
374
|
+
|
|
375
|
+
{previewFile ? (
|
|
376
|
+
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
|
377
|
+
<div className="flex items-center justify-between px-3 py-1 bg-[var(--card)] text-[10px] text-[var(--muted)]">
|
|
378
|
+
<div className="flex items-center gap-1 truncate">
|
|
379
|
+
{previewFile.path?.split('/').filter(Boolean).map((seg, i, arr) => (
|
|
380
|
+
<span key={i} className="flex items-center gap-1">
|
|
381
|
+
{i > 0 && <span className="text-[var(--muted)]/50">›</span>}
|
|
382
|
+
<span className={i === arr.length - 1 ? 'text-[var(--foreground)]' : ''}>{seg}</span>
|
|
383
|
+
</span>
|
|
384
|
+
))}
|
|
385
|
+
</div>
|
|
386
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
387
|
+
{previewFile.loading && (
|
|
388
|
+
<span className="text-yellow-400 animate-pulse">{t('reqDetail.files.syncingShort')}</span>
|
|
389
|
+
)}
|
|
390
|
+
<button
|
|
391
|
+
onClick={() => onPreview(previewFile.path)}
|
|
392
|
+
className="text-[var(--muted)] hover:text-white transition-colors px-1"
|
|
393
|
+
title={t('common.refresh')}
|
|
394
|
+
>
|
|
395
|
+
↻
|
|
396
|
+
</button>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
|
|
400
|
+
<div className="flex-1 min-h-0 overflow-hidden">
|
|
401
|
+
{previewFile.loading ? (
|
|
402
|
+
<div className="flex items-center justify-center h-full text-[var(--muted)] animate-pulse">
|
|
403
|
+
<div className="text-center">
|
|
404
|
+
<div className="text-2xl mb-2">⏳</div>
|
|
405
|
+
<p className="text-sm">{t('common.loading')}</p>
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
) : (
|
|
409
|
+
<MonacoEditor
|
|
410
|
+
height="100%"
|
|
411
|
+
language={getLanguage(previewFile.path)}
|
|
412
|
+
value={previewFile.content != null ? previewFile.content : t('reqDetail.files.emptyFile')}
|
|
413
|
+
theme={CUSTOM_THEME_NAME}
|
|
414
|
+
beforeMount={defineCustomTheme}
|
|
415
|
+
options={{
|
|
416
|
+
readOnly: true,
|
|
417
|
+
minimap: { enabled: true },
|
|
418
|
+
fontSize: 13,
|
|
419
|
+
lineNumbers: 'on',
|
|
420
|
+
scrollBeyondLastLine: false,
|
|
421
|
+
wordWrap: 'on',
|
|
422
|
+
automaticLayout: true,
|
|
423
|
+
renderWhitespace: 'selection',
|
|
424
|
+
smoothScrolling: true,
|
|
425
|
+
cursorBlinking: 'smooth',
|
|
426
|
+
padding: { top: 8 },
|
|
427
|
+
scrollbar: {
|
|
428
|
+
verticalScrollbarSize: 8,
|
|
429
|
+
horizontalScrollbarSize: 8,
|
|
430
|
+
},
|
|
431
|
+
}}
|
|
432
|
+
/>
|
|
433
|
+
)}
|
|
434
|
+
</div>
|
|
435
|
+
|
|
436
|
+
<div className="flex items-center justify-between px-3 py-1 bg-[var(--card)] text-[10px] text-[var(--muted)]">
|
|
437
|
+
<div className="flex items-center gap-3">
|
|
438
|
+
<span>{getLanguage(previewFile.path).toUpperCase()}</span>
|
|
439
|
+
<span>UTF-8</span>
|
|
440
|
+
{previewFile.content && (
|
|
441
|
+
<span>{previewFile.content.split('\n').length} {t('reqDetail.files.lines', { n: '' }).trim()}</span>
|
|
442
|
+
)}
|
|
443
|
+
</div>
|
|
444
|
+
<div className="flex items-center gap-3">
|
|
445
|
+
<span>{t('reqDetail.files.readOnly')}</span>
|
|
446
|
+
<span className="w-2 h-2 rounded-full bg-green-500 inline-block" />
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
) : (
|
|
451
|
+
<div className="flex-1 flex items-center justify-center text-[var(--muted)]">
|
|
452
|
+
<div className="text-center">
|
|
453
|
+
<div className="text-6xl mb-4 opacity-20">{ }</div>
|
|
454
|
+
<div className="w-16 h-16 mx-auto mb-4 rounded-xl bg-white/[0.03] flex items-center justify-center">
|
|
455
|
+
<span className="text-3xl opacity-40">📝</span>
|
|
456
|
+
</div>
|
|
457
|
+
<p className="text-sm">{t('reqDetail.files.clickToView')}</p>
|
|
458
|
+
<p className="text-[10px] mt-1 text-[var(--muted)]/60">{t('reqDetail.files.syntaxHighlight')}</p>
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
)}
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
);
|
|
465
|
+
}
|