idea-manager 0.1.2 → 0.3.0
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/README.md +19 -10
- package/next.config.ts +0 -1
- package/package.json +2 -2
- package/public/favicon.svg +10 -0
- package/public/icon.svg +2 -11
- package/src/app/api/filesystem/route.ts +49 -0
- package/src/app/api/projects/[id]/cleanup/route.ts +32 -0
- package/src/app/api/projects/[id]/items/[itemId]/refine/route.ts +36 -0
- package/src/app/api/projects/[id]/items/[itemId]/route.ts +23 -1
- package/src/app/api/projects/[id]/items/route.ts +51 -1
- package/src/app/api/projects/[id]/scan/route.ts +73 -0
- package/src/app/api/projects/[id]/scan/stream/route.ts +112 -0
- package/src/app/api/projects/[id]/structure/route.ts +34 -3
- package/src/app/api/projects/[id]/structure/stream/route.ts +157 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/route.ts +39 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.ts +60 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route.ts +26 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route.ts +39 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/route.ts +33 -0
- package/src/app/api/projects/[id]/sub-projects/route.ts +31 -0
- package/src/app/api/projects/route.ts +1 -1
- package/src/app/globals.css +465 -5
- package/src/app/layout.tsx +3 -0
- package/src/app/page.tsx +260 -88
- package/src/app/projects/[id]/page.tsx +366 -183
- package/src/cli.ts +44 -12
- package/src/components/DirectoryPicker.tsx +137 -0
- package/src/components/ScanPanel.tsx +743 -0
- package/src/components/brainstorm/Editor.tsx +20 -4
- package/src/components/brainstorm/MemoPin.tsx +91 -5
- package/src/components/dashboard/SubProjectCard.tsx +76 -0
- package/src/components/dashboard/TabBar.tsx +42 -0
- package/src/components/task/ProjectTree.tsx +223 -0
- package/src/components/task/PromptEditor.tsx +107 -0
- package/src/components/task/StatusFlow.tsx +43 -0
- package/src/components/task/TaskChat.tsx +134 -0
- package/src/components/task/TaskDetail.tsx +205 -0
- package/src/components/task/TaskList.tsx +119 -0
- package/src/components/tree/CardView.tsx +206 -0
- package/src/components/tree/RefinePopover.tsx +157 -0
- package/src/components/tree/TreeNode.tsx +147 -38
- package/src/components/tree/TreeView.tsx +270 -26
- package/src/components/ui/ConfirmDialog.tsx +88 -0
- package/src/lib/ai/chat-responder.ts +4 -2
- package/src/lib/ai/cleanup.ts +87 -0
- package/src/lib/ai/client.ts +175 -58
- package/src/lib/ai/prompter.ts +19 -24
- package/src/lib/ai/refiner.ts +128 -0
- package/src/lib/ai/structurer.ts +340 -11
- package/src/lib/db/queries/context.ts +76 -0
- package/src/lib/db/queries/items.ts +133 -12
- package/src/lib/db/queries/projects.ts +12 -8
- package/src/lib/db/queries/sub-projects.ts +122 -0
- package/src/lib/db/queries/task-conversations.ts +27 -0
- package/src/lib/db/queries/task-prompts.ts +32 -0
- package/src/lib/db/queries/tasks.ts +133 -0
- package/src/lib/db/schema.ts +75 -0
- package/src/lib/mcp/server.ts +38 -39
- package/src/lib/mcp/tools.ts +47 -45
- package/src/lib/scanner.ts +573 -0
- package/src/lib/task-store.ts +97 -0
- package/src/types/index.ts +65 -0
- package/src/app/icon.svg +0 -19
|
@@ -1,249 +1,432 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useCallback, useRef, use } from 'react';
|
|
4
|
-
import { useRouter } from 'next/navigation';
|
|
3
|
+
import { useState, useEffect, useCallback, useRef, use, Suspense } from 'react';
|
|
4
|
+
import { useRouter, useSearchParams } from 'next/navigation';
|
|
5
5
|
import Editor from '@/components/brainstorm/Editor';
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
6
|
+
import ProjectTree from '@/components/task/ProjectTree';
|
|
7
|
+
import TaskDetail from '@/components/task/TaskDetail';
|
|
8
|
+
import DirectoryPicker from '@/components/DirectoryPicker';
|
|
9
|
+
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
|
10
|
+
import type { ISubProject, ITask, TaskStatus, ISubProjectWithStats } from '@/types';
|
|
9
11
|
|
|
10
12
|
interface IProject {
|
|
11
13
|
id: string;
|
|
12
14
|
name: string;
|
|
13
15
|
description: string;
|
|
16
|
+
project_path: string | null;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
priority: string;
|
|
22
|
-
status: string;
|
|
23
|
-
is_locked: boolean;
|
|
24
|
-
children: IItemTree[];
|
|
25
|
-
}
|
|
19
|
+
function WorkspaceInner({ id }: { id: string }) {
|
|
20
|
+
const router = useRouter();
|
|
21
|
+
const searchParams = useSearchParams();
|
|
22
|
+
const initialUrlSub = useRef(searchParams.get('sub'));
|
|
23
|
+
const initialUrlTask = useRef(searchParams.get('task'));
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
const [project, setProject] = useState<IProject | null>(null);
|
|
26
|
+
const [subProjects, setSubProjects] = useState<ISubProjectWithStats[]>([]);
|
|
27
|
+
const [selectedSubId, setSelectedSubId] = useState<string | null>(null);
|
|
28
|
+
const [tasks, setTasks] = useState<ITask[]>([]);
|
|
29
|
+
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
|
|
30
|
+
const [showDirPicker, setShowDirPicker] = useState(false);
|
|
31
|
+
const [confirmAction, setConfirmAction] = useState<{ type: 'delete-sub' | 'delete-task'; id: string } | null>(null);
|
|
32
|
+
const [showAddSub, setShowAddSub] = useState(false);
|
|
33
|
+
const [showBrainstorm, setShowBrainstorm] = useState(true);
|
|
34
|
+
const [newSubName, setNewSubName] = useState('');
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
// Resizable panel widths
|
|
37
|
+
const [leftWidth, setLeftWidth] = useState(280);
|
|
38
|
+
const [centerWidth, setCenterWidth] = useState(280);
|
|
39
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
40
|
+
const draggingRef = useRef<'left' | 'center' | null>(null);
|
|
41
|
+
const startXRef = useRef(0);
|
|
42
|
+
const startWidthRef = useRef(0);
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const [memos, setMemos] = useState<IMemo[]>([]);
|
|
48
|
-
const [structuring, setStructuring] = useState(false);
|
|
49
|
-
const [chatLoading, setChatLoading] = useState(false);
|
|
50
|
-
const [error, setError] = useState<string | null>(null);
|
|
51
|
-
const [editorPercent, setEditorPercent] = useState(60);
|
|
52
|
-
const leftPanelRef = useRef<HTMLDivElement>(null);
|
|
44
|
+
const handleMouseDown = useCallback((panel: 'left' | 'center', e: React.MouseEvent) => {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
draggingRef.current = panel;
|
|
47
|
+
startXRef.current = e.clientX;
|
|
48
|
+
startWidthRef.current = panel === 'left' ? leftWidth : centerWidth;
|
|
49
|
+
}, [leftWidth, centerWidth]);
|
|
53
50
|
|
|
54
51
|
useEffect(() => {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
setProject(await res.json());
|
|
52
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
53
|
+
if (!draggingRef.current) return;
|
|
54
|
+
const delta = e.clientX - startXRef.current;
|
|
55
|
+
const newWidth = Math.max(180, Math.min(500, startWidthRef.current + delta));
|
|
56
|
+
if (draggingRef.current === 'left') setLeftWidth(newWidth);
|
|
57
|
+
else setCenterWidth(newWidth);
|
|
62
58
|
};
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
const handleMouseUp = () => { draggingRef.current = null; };
|
|
60
|
+
window.addEventListener('mousemove', handleMouseMove);
|
|
61
|
+
window.addEventListener('mouseup', handleMouseUp);
|
|
62
|
+
return () => {
|
|
63
|
+
window.removeEventListener('mousemove', handleMouseMove);
|
|
64
|
+
window.removeEventListener('mouseup', handleMouseUp);
|
|
69
65
|
};
|
|
66
|
+
}, []);
|
|
70
67
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
68
|
+
// Load project
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
fetch(`/api/projects/${id}`)
|
|
71
|
+
.then(r => { if (!r.ok) { router.push('/'); return null; } return r.json(); })
|
|
72
|
+
.then(data => data && setProject(data));
|
|
73
|
+
}, [id, router]);
|
|
77
74
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
// Load sub-projects (stable callback, no deps on selection state)
|
|
76
|
+
const loadSubProjects = useCallback(async () => {
|
|
77
|
+
const res = await fetch(`/api/projects/${id}/sub-projects`);
|
|
78
|
+
if (!res.ok) return;
|
|
79
|
+
const data: ISubProjectWithStats[] = await res.json();
|
|
80
|
+
setSubProjects(data);
|
|
81
|
+
return data;
|
|
82
|
+
}, [id]);
|
|
83
|
+
|
|
84
|
+
// Initial load: sub-projects + auto-select from URL
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
loadSubProjects().then(data => {
|
|
87
|
+
if (!data || data.length === 0) return;
|
|
88
|
+
const urlSub = initialUrlSub.current;
|
|
89
|
+
if (urlSub && data.some(s => s.id === urlSub)) {
|
|
90
|
+
setSelectedSubId(urlSub);
|
|
91
|
+
} else {
|
|
92
|
+
setSelectedSubId(data[0].id);
|
|
82
93
|
}
|
|
83
|
-
};
|
|
94
|
+
});
|
|
95
|
+
}, [loadSubProjects]);
|
|
84
96
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
97
|
+
// Load tasks when sub-project changes
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!selectedSubId) { setTasks([]); return; }
|
|
100
|
+
fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks`)
|
|
101
|
+
.then(r => r.json())
|
|
102
|
+
.then((data: ITask[]) => {
|
|
103
|
+
setTasks(data);
|
|
104
|
+
// Auto-select from URL on first load only
|
|
105
|
+
const urlTask = initialUrlTask.current;
|
|
106
|
+
if (urlTask && data.some(t => t.id === urlTask)) {
|
|
107
|
+
setSelectedTaskId(urlTask);
|
|
108
|
+
initialUrlTask.current = null; // consume once
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}, [id, selectedSubId]);
|
|
90
112
|
|
|
91
|
-
const
|
|
92
|
-
setStructuring(true);
|
|
93
|
-
setError(null);
|
|
113
|
+
const selectedTask = tasks.find(t => t.id === selectedTaskId) ?? null;
|
|
94
114
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
115
|
+
const handleCreateSubProject = async () => {
|
|
116
|
+
if (!newSubName.trim()) return;
|
|
117
|
+
const res = await fetch(`/api/projects/${id}/sub-projects`, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: { 'Content-Type': 'application/json' },
|
|
120
|
+
body: JSON.stringify({ name: newSubName.trim() }),
|
|
121
|
+
});
|
|
122
|
+
if (res.ok) {
|
|
123
|
+
const sp: ISubProject = await res.json();
|
|
124
|
+
setNewSubName('');
|
|
125
|
+
setShowAddSub(false);
|
|
126
|
+
await loadSubProjects();
|
|
127
|
+
setSelectedSubId(sp.id);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
99
130
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
setStructuring(false);
|
|
131
|
+
const handleDeleteSubProject = (subId: string) => {
|
|
132
|
+
setConfirmAction({ type: 'delete-sub', id: subId });
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const handleCreateTask = async (title: string) => {
|
|
136
|
+
if (!selectedSubId) return;
|
|
137
|
+
const res = await fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks`, {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers: { 'Content-Type': 'application/json' },
|
|
140
|
+
body: JSON.stringify({ title }),
|
|
141
|
+
});
|
|
142
|
+
if (res.ok) {
|
|
143
|
+
const task: ITask = await res.json();
|
|
144
|
+
setTasks(prev => [...prev, task]);
|
|
145
|
+
setSelectedTaskId(task.id);
|
|
146
|
+
loadSubProjects();
|
|
117
147
|
}
|
|
118
|
-
}
|
|
148
|
+
};
|
|
119
149
|
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
150
|
+
const handleTaskStatusChange = async (taskId: string, status: TaskStatus) => {
|
|
151
|
+
const res = await fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks/${taskId}`, {
|
|
152
|
+
method: 'PUT',
|
|
153
|
+
headers: { 'Content-Type': 'application/json' },
|
|
154
|
+
body: JSON.stringify({ status }),
|
|
155
|
+
});
|
|
156
|
+
if (res.ok) {
|
|
157
|
+
const updated: ITask = await res.json();
|
|
158
|
+
setTasks(prev => prev.map(t => t.id === taskId ? updated : t));
|
|
159
|
+
loadSubProjects();
|
|
160
|
+
}
|
|
161
|
+
};
|
|
127
162
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
163
|
+
const handleTaskTodayToggle = async (taskId: string, isToday: boolean) => {
|
|
164
|
+
const res = await fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks/${taskId}`, {
|
|
165
|
+
method: 'PUT',
|
|
166
|
+
headers: { 'Content-Type': 'application/json' },
|
|
167
|
+
body: JSON.stringify({ is_today: isToday }),
|
|
168
|
+
});
|
|
169
|
+
if (res.ok) {
|
|
170
|
+
const updated: ITask = await res.json();
|
|
171
|
+
setTasks(prev => prev.map(t => t.id === taskId ? updated : t));
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const handleTaskUpdate = async (data: Partial<ITask>) => {
|
|
176
|
+
if (!selectedTaskId || !selectedSubId) return;
|
|
177
|
+
const res = await fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks/${selectedTaskId}`, {
|
|
178
|
+
method: 'PUT',
|
|
179
|
+
headers: { 'Content-Type': 'application/json' },
|
|
180
|
+
body: JSON.stringify(data),
|
|
181
|
+
});
|
|
182
|
+
if (res.ok) {
|
|
183
|
+
const updated: ITask = await res.json();
|
|
184
|
+
setTasks(prev => prev.map(t => t.id === selectedTaskId ? updated : t));
|
|
185
|
+
loadSubProjects();
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const handleTaskDelete = () => {
|
|
190
|
+
if (!selectedTaskId) return;
|
|
191
|
+
setConfirmAction({ type: 'delete-task', id: selectedTaskId });
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const handleConfirmAction = async () => {
|
|
195
|
+
if (!confirmAction) return;
|
|
196
|
+
if (confirmAction.type === 'delete-sub') {
|
|
197
|
+
await fetch(`/api/projects/${id}/sub-projects/${confirmAction.id}`, { method: 'DELETE' });
|
|
198
|
+
if (selectedSubId === confirmAction.id) {
|
|
199
|
+
setSelectedSubId(null);
|
|
200
|
+
setSelectedTaskId(null);
|
|
134
201
|
}
|
|
135
|
-
|
|
136
|
-
|
|
202
|
+
loadSubProjects();
|
|
203
|
+
} else if (confirmAction.type === 'delete-task') {
|
|
204
|
+
await fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks/${confirmAction.id}`, { method: 'DELETE' });
|
|
205
|
+
setTasks(prev => prev.filter(t => t.id !== confirmAction.id));
|
|
206
|
+
if (selectedTaskId === confirmAction.id) setSelectedTaskId(null);
|
|
207
|
+
loadSubProjects();
|
|
137
208
|
}
|
|
138
|
-
|
|
209
|
+
setConfirmAction(null);
|
|
210
|
+
};
|
|
139
211
|
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
212
|
+
const handleSetPath = async (selectedPath: string) => {
|
|
213
|
+
const res = await fetch(`/api/projects/${id}`, {
|
|
214
|
+
method: 'PUT',
|
|
215
|
+
headers: { 'Content-Type': 'application/json' },
|
|
216
|
+
body: JSON.stringify({ project_path: selectedPath }),
|
|
217
|
+
});
|
|
218
|
+
if (res.ok) {
|
|
219
|
+
setProject(await res.json());
|
|
220
|
+
setShowDirPicker(false);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
143
223
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
content: message,
|
|
149
|
-
created_at: new Date().toISOString(),
|
|
150
|
-
};
|
|
151
|
-
setMessages(prev => [...prev, tempUserMsg]);
|
|
224
|
+
// Keyboard shortcuts (use e.code for Korean IME compatibility)
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
const handler = (e: KeyboardEvent) => {
|
|
227
|
+
const isInput = e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement;
|
|
152
228
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
229
|
+
// B — toggle brainstorming panel
|
|
230
|
+
if (!isInput && e.code === 'KeyB' && !e.metaKey && !e.ctrlKey) {
|
|
231
|
+
e.preventDefault();
|
|
232
|
+
setShowBrainstorm(prev => !prev);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// N — new sub-project (when not in input)
|
|
237
|
+
if (!isInput && e.code === 'KeyN' && !e.metaKey && !e.ctrlKey) {
|
|
238
|
+
e.preventDefault();
|
|
239
|
+
setShowAddSub(true);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// T — new task (when sub-project selected, not in input)
|
|
244
|
+
if (!isInput && e.code === 'KeyT' && !e.metaKey && !e.ctrlKey && selectedSubId) {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
const addBtn = document.querySelector('[data-add-task]') as HTMLButtonElement;
|
|
247
|
+
addBtn?.click();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Cmd+1~6 — status change
|
|
252
|
+
if (selectedTaskId && selectedSubId && !isInput) {
|
|
253
|
+
const statusMap: Record<string, TaskStatus> = {
|
|
254
|
+
'Digit1': 'idea', 'Digit2': 'writing', 'Digit3': 'submitted',
|
|
255
|
+
'Digit4': 'testing', 'Digit5': 'done', 'Digit6': 'problem',
|
|
256
|
+
};
|
|
159
257
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
// Replace the temp message with real messages
|
|
164
|
-
setMessages(prev => {
|
|
165
|
-
const withoutTemp = prev.filter(m => m.id !== tempUserMsg.id);
|
|
166
|
-
return [...withoutTemp, ...data.messages];
|
|
167
|
-
});
|
|
168
|
-
if (data.memos) {
|
|
169
|
-
setMemos(data.memos);
|
|
258
|
+
if ((e.metaKey || e.ctrlKey) && statusMap[e.code]) {
|
|
259
|
+
e.preventDefault();
|
|
260
|
+
handleTaskStatusChange(selectedTaskId, statusMap[e.code]);
|
|
170
261
|
}
|
|
171
|
-
} else {
|
|
172
|
-
const data = await res.json();
|
|
173
|
-
setError(data.error || '응답에 실패했습니다');
|
|
174
|
-
// Remove temp message on error
|
|
175
|
-
setMessages(prev => prev.filter(m => m.id !== tempUserMsg.id));
|
|
176
262
|
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
setChatLoading(false);
|
|
182
|
-
}
|
|
183
|
-
}, [id]);
|
|
263
|
+
};
|
|
264
|
+
window.addEventListener('keydown', handler);
|
|
265
|
+
return () => window.removeEventListener('keydown', handler);
|
|
266
|
+
});
|
|
184
267
|
|
|
185
268
|
if (!project) {
|
|
186
|
-
return
|
|
187
|
-
<div className="min-h-screen flex items-center justify-center text-muted-foreground">
|
|
188
|
-
로딩 중...
|
|
189
|
-
</div>
|
|
190
|
-
);
|
|
269
|
+
return <div className="min-h-screen flex items-center justify-center text-muted-foreground">Loading...</div>;
|
|
191
270
|
}
|
|
192
271
|
|
|
193
272
|
return (
|
|
194
273
|
<div className="h-screen flex flex-col">
|
|
195
274
|
{/* Header */}
|
|
196
|
-
<header className="flex items-center justify-between px-4 py-2 border-b border-border bg-card">
|
|
275
|
+
<header className="flex items-center justify-between px-4 py-2 border-b border-border bg-card flex-shrink-0">
|
|
197
276
|
<div className="flex items-center gap-3">
|
|
198
277
|
<button
|
|
199
278
|
onClick={() => router.push('/')}
|
|
200
279
|
className="text-muted-foreground hover:text-foreground hover:bg-muted transition-colors text-sm px-2 py-1 rounded-md"
|
|
201
280
|
>
|
|
202
|
-
←
|
|
281
|
+
← Back
|
|
203
282
|
</button>
|
|
204
283
|
<span className="text-border">|</span>
|
|
205
284
|
<h1 className="text-sm font-semibold">{project.name}</h1>
|
|
206
|
-
{project.
|
|
207
|
-
<span className="text-xs text-muted-foreground"
|
|
285
|
+
{project.project_path && (
|
|
286
|
+
<span className="text-xs text-muted-foreground font-mono truncate max-w-48" title={project.project_path}>
|
|
287
|
+
{project.project_path}
|
|
288
|
+
</span>
|
|
208
289
|
)}
|
|
209
290
|
</div>
|
|
210
291
|
<div className="flex items-center gap-2">
|
|
211
|
-
{
|
|
212
|
-
<
|
|
292
|
+
{!project.project_path && (
|
|
293
|
+
<button
|
|
294
|
+
onClick={() => setShowDirPicker(true)}
|
|
295
|
+
className="px-3 py-1.5 text-xs bg-muted hover:bg-card-hover text-foreground
|
|
296
|
+
border border-border rounded-md transition-colors"
|
|
297
|
+
>
|
|
298
|
+
Link folder
|
|
299
|
+
</button>
|
|
213
300
|
)}
|
|
301
|
+
</div>
|
|
302
|
+
</header>
|
|
303
|
+
|
|
304
|
+
{/* 3-Panel Layout with resize handles */}
|
|
305
|
+
<div ref={containerRef} className="flex-1 flex overflow-hidden">
|
|
306
|
+
{/* Left: Brainstorming (collapsible) */}
|
|
307
|
+
{showBrainstorm ? (
|
|
308
|
+
<>
|
|
309
|
+
<div style={{ width: leftWidth }} className="border-r border-border flex flex-col flex-shrink-0">
|
|
310
|
+
<Editor
|
|
311
|
+
projectId={id}
|
|
312
|
+
onContentChange={() => {}}
|
|
313
|
+
onSendMessage={() => {}}
|
|
314
|
+
memos={[]}
|
|
315
|
+
chatLoading={false}
|
|
316
|
+
onCollapse={() => setShowBrainstorm(false)}
|
|
317
|
+
/>
|
|
318
|
+
</div>
|
|
319
|
+
{/* Resize handle: left */}
|
|
320
|
+
<div
|
|
321
|
+
className="panel-resize-handle"
|
|
322
|
+
onMouseDown={(e) => handleMouseDown('left', e)}
|
|
323
|
+
>
|
|
324
|
+
<div className="panel-resize-handle-bar" />
|
|
325
|
+
</div>
|
|
326
|
+
</>
|
|
327
|
+
) : (
|
|
214
328
|
<button
|
|
215
|
-
onClick={() =>
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
329
|
+
onClick={() => setShowBrainstorm(true)}
|
|
330
|
+
className="w-8 border-r border-border flex-shrink-0 flex items-center justify-center
|
|
331
|
+
text-muted-foreground hover:text-foreground hover:bg-card-hover transition-colors
|
|
332
|
+
text-xs"
|
|
333
|
+
title="Show brainstorming (B)"
|
|
334
|
+
style={{ writingMode: 'vertical-rl' }}
|
|
219
335
|
>
|
|
220
|
-
|
|
336
|
+
Brainstorm
|
|
221
337
|
</button>
|
|
338
|
+
)}
|
|
339
|
+
|
|
340
|
+
{/* Center: Tree (Sub-projects + Tasks) */}
|
|
341
|
+
<div style={{ width: centerWidth }} className="border-r border-border flex flex-col flex-shrink-0">
|
|
342
|
+
{/* Add sub-project input */}
|
|
343
|
+
{showAddSub && (
|
|
344
|
+
<div className="px-3 py-2 border-b border-border">
|
|
345
|
+
<input
|
|
346
|
+
type="text"
|
|
347
|
+
value={newSubName}
|
|
348
|
+
onChange={(e) => setNewSubName(e.target.value)}
|
|
349
|
+
onKeyDown={(e) => {
|
|
350
|
+
if (e.key === 'Enter') handleCreateSubProject();
|
|
351
|
+
if (e.key === 'Escape') { setNewSubName(''); setShowAddSub(false); }
|
|
352
|
+
}}
|
|
353
|
+
placeholder="Sub-project name..."
|
|
354
|
+
className="w-full bg-input border border-border rounded px-2 py-1 text-xs
|
|
355
|
+
focus:border-primary focus:outline-none text-foreground"
|
|
356
|
+
autoFocus
|
|
357
|
+
/>
|
|
358
|
+
</div>
|
|
359
|
+
)}
|
|
360
|
+
|
|
361
|
+
<ProjectTree
|
|
362
|
+
subProjects={subProjects}
|
|
363
|
+
tasks={tasks}
|
|
364
|
+
selectedSubId={selectedSubId}
|
|
365
|
+
selectedTaskId={selectedTaskId}
|
|
366
|
+
onSelectSub={(subId) => { setSelectedSubId(subId); setSelectedTaskId(null); }}
|
|
367
|
+
onSelectTask={setSelectedTaskId}
|
|
368
|
+
onCreateSub={() => setShowAddSub(true)}
|
|
369
|
+
onDeleteSub={handleDeleteSubProject}
|
|
370
|
+
onCreateTask={handleCreateTask}
|
|
371
|
+
onStatusChange={handleTaskStatusChange}
|
|
372
|
+
onTodayToggle={handleTaskTodayToggle}
|
|
373
|
+
/>
|
|
222
374
|
</div>
|
|
223
|
-
</header>
|
|
224
375
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
</div>
|
|
232
|
-
<ResizeHandle onResize={setEditorPercent} containerRef={leftPanelRef} />
|
|
233
|
-
<div style={{ height: `${100 - editorPercent}%` }} className="flex flex-col min-h-0">
|
|
234
|
-
<ChatPanel
|
|
235
|
-
messages={messages}
|
|
236
|
-
loading={chatLoading || structuring}
|
|
237
|
-
onSendMessage={handleSendMessage}
|
|
238
|
-
/>
|
|
239
|
-
</div>
|
|
376
|
+
{/* Resize handle: center */}
|
|
377
|
+
<div
|
|
378
|
+
className="panel-resize-handle"
|
|
379
|
+
onMouseDown={(e) => handleMouseDown('center', e)}
|
|
380
|
+
>
|
|
381
|
+
<div className="panel-resize-handle-bar" />
|
|
240
382
|
</div>
|
|
241
383
|
|
|
242
|
-
{/* Right:
|
|
243
|
-
<div className="
|
|
244
|
-
|
|
384
|
+
{/* Right: Task Detail */}
|
|
385
|
+
<div className="flex-1 min-w-0">
|
|
386
|
+
{selectedTask ? (
|
|
387
|
+
<TaskDetail
|
|
388
|
+
task={selectedTask}
|
|
389
|
+
projectId={id}
|
|
390
|
+
subProjectId={selectedSubId!}
|
|
391
|
+
onUpdate={handleTaskUpdate}
|
|
392
|
+
onDelete={handleTaskDelete}
|
|
393
|
+
/>
|
|
394
|
+
) : (
|
|
395
|
+
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
|
|
396
|
+
{tasks.length > 0 ? 'Select a task' : selectedSubId ? 'Create a task to get started' : 'Select a sub-project'}
|
|
397
|
+
</div>
|
|
398
|
+
)}
|
|
245
399
|
</div>
|
|
246
400
|
</div>
|
|
401
|
+
|
|
402
|
+
{showDirPicker && (
|
|
403
|
+
<DirectoryPicker
|
|
404
|
+
onSelect={handleSetPath}
|
|
405
|
+
onCancel={() => setShowDirPicker(false)}
|
|
406
|
+
initialPath={project.project_path || undefined}
|
|
407
|
+
/>
|
|
408
|
+
)}
|
|
409
|
+
|
|
410
|
+
<ConfirmDialog
|
|
411
|
+
open={!!confirmAction}
|
|
412
|
+
title={confirmAction?.type === 'delete-sub' ? 'Delete sub-project?' : 'Delete task?'}
|
|
413
|
+
description={confirmAction?.type === 'delete-sub'
|
|
414
|
+
? 'This will delete the sub-project and all its tasks.'
|
|
415
|
+
: 'This task will be permanently deleted.'}
|
|
416
|
+
confirmLabel="Delete"
|
|
417
|
+
variant="danger"
|
|
418
|
+
onConfirm={handleConfirmAction}
|
|
419
|
+
onCancel={() => setConfirmAction(null)}
|
|
420
|
+
/>
|
|
247
421
|
</div>
|
|
248
422
|
);
|
|
249
423
|
}
|
|
424
|
+
|
|
425
|
+
export default function ProjectWorkspace({ params }: { params: Promise<{ id: string }> }) {
|
|
426
|
+
const { id } = use(params);
|
|
427
|
+
return (
|
|
428
|
+
<Suspense fallback={<div className="min-h-screen flex items-center justify-center text-muted-foreground">Loading...</div>}>
|
|
429
|
+
<WorkspaceInner id={id} />
|
|
430
|
+
</Suspense>
|
|
431
|
+
);
|
|
432
|
+
}
|