idea-manager 0.2.0 → 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.
Files changed (60) hide show
  1. package/next.config.ts +0 -1
  2. package/package.json +2 -2
  3. package/{src/app/icon.svg → public/favicon.svg} +2 -2
  4. package/src/app/api/filesystem/route.ts +49 -0
  5. package/src/app/api/projects/[id]/cleanup/route.ts +32 -0
  6. package/src/app/api/projects/[id]/items/[itemId]/refine/route.ts +36 -0
  7. package/src/app/api/projects/[id]/items/[itemId]/route.ts +23 -1
  8. package/src/app/api/projects/[id]/items/route.ts +51 -1
  9. package/src/app/api/projects/[id]/scan/route.ts +73 -0
  10. package/src/app/api/projects/[id]/scan/stream/route.ts +112 -0
  11. package/src/app/api/projects/[id]/structure/route.ts +34 -3
  12. package/src/app/api/projects/[id]/structure/stream/route.ts +157 -0
  13. package/src/app/api/projects/[id]/sub-projects/[subId]/route.ts +39 -0
  14. package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.ts +60 -0
  15. package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route.ts +26 -0
  16. package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route.ts +39 -0
  17. package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/route.ts +33 -0
  18. package/src/app/api/projects/[id]/sub-projects/route.ts +31 -0
  19. package/src/app/api/projects/route.ts +1 -1
  20. package/src/app/globals.css +465 -5
  21. package/src/app/layout.tsx +3 -0
  22. package/src/app/page.tsx +260 -88
  23. package/src/app/projects/[id]/page.tsx +366 -183
  24. package/src/cli.ts +10 -10
  25. package/src/components/DirectoryPicker.tsx +137 -0
  26. package/src/components/ScanPanel.tsx +743 -0
  27. package/src/components/brainstorm/Editor.tsx +20 -4
  28. package/src/components/brainstorm/MemoPin.tsx +91 -5
  29. package/src/components/dashboard/SubProjectCard.tsx +76 -0
  30. package/src/components/dashboard/TabBar.tsx +42 -0
  31. package/src/components/task/ProjectTree.tsx +223 -0
  32. package/src/components/task/PromptEditor.tsx +107 -0
  33. package/src/components/task/StatusFlow.tsx +43 -0
  34. package/src/components/task/TaskChat.tsx +134 -0
  35. package/src/components/task/TaskDetail.tsx +205 -0
  36. package/src/components/task/TaskList.tsx +119 -0
  37. package/src/components/tree/CardView.tsx +206 -0
  38. package/src/components/tree/RefinePopover.tsx +157 -0
  39. package/src/components/tree/TreeNode.tsx +147 -38
  40. package/src/components/tree/TreeView.tsx +270 -26
  41. package/src/components/ui/ConfirmDialog.tsx +88 -0
  42. package/src/lib/ai/chat-responder.ts +4 -2
  43. package/src/lib/ai/cleanup.ts +87 -0
  44. package/src/lib/ai/client.ts +175 -58
  45. package/src/lib/ai/prompter.ts +19 -24
  46. package/src/lib/ai/refiner.ts +128 -0
  47. package/src/lib/ai/structurer.ts +340 -11
  48. package/src/lib/db/queries/context.ts +76 -0
  49. package/src/lib/db/queries/items.ts +133 -12
  50. package/src/lib/db/queries/projects.ts +12 -8
  51. package/src/lib/db/queries/sub-projects.ts +122 -0
  52. package/src/lib/db/queries/task-conversations.ts +27 -0
  53. package/src/lib/db/queries/task-prompts.ts +32 -0
  54. package/src/lib/db/queries/tasks.ts +133 -0
  55. package/src/lib/db/schema.ts +75 -0
  56. package/src/lib/mcp/server.ts +38 -39
  57. package/src/lib/mcp/tools.ts +47 -45
  58. package/src/lib/scanner.ts +573 -0
  59. package/src/lib/task-store.ts +97 -0
  60. package/src/types/index.ts +65 -0
package/src/app/page.tsx CHANGED
@@ -2,33 +2,92 @@
2
2
 
3
3
  import { useState, useEffect, useCallback } from 'react';
4
4
  import { useRouter } from 'next/navigation';
5
+ import DirectoryPicker from '@/components/DirectoryPicker';
6
+ import ConfirmDialog from '@/components/ui/ConfirmDialog';
7
+ import TabBar, { type DashboardTab } from '@/components/dashboard/TabBar';
8
+ import SubProjectCard from '@/components/dashboard/SubProjectCard';
9
+ import type { ISubProjectWithStats, ITask } from '@/types';
5
10
 
6
11
  interface IProject {
7
12
  id: string;
8
13
  name: string;
9
14
  description: string;
15
+ project_path: string | null;
10
16
  created_at: string;
11
17
  updated_at: string;
12
18
  }
13
19
 
20
+ interface ProjectWithSubs extends IProject {
21
+ subProjects: ISubProjectWithStats[];
22
+ }
23
+
14
24
  export default function Dashboard() {
15
25
  const router = useRouter();
16
- const [projects, setProjects] = useState<IProject[]>([]);
26
+ const [projects, setProjects] = useState<ProjectWithSubs[]>([]);
27
+ const [todayTasks, setTodayTasks] = useState<(ITask & { projectName: string; subProjectName: string })[]>([]);
17
28
  const [showForm, setShowForm] = useState(false);
18
29
  const [name, setName] = useState('');
19
30
  const [description, setDescription] = useState('');
31
+ const [projectPath, setProjectPath] = useState('');
20
32
  const [loading, setLoading] = useState(true);
33
+ const [showDirPicker, setShowDirPicker] = useState(false);
34
+ const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
35
+ const [tab, setTab] = useState<DashboardTab>(() => {
36
+ if (typeof window !== 'undefined') {
37
+ return (localStorage.getItem('im-dashboard-tab') as DashboardTab) || 'active';
38
+ }
39
+ return 'active';
40
+ });
21
41
 
22
- const fetchProjects = useCallback(async () => {
42
+ const fetchData = useCallback(async () => {
23
43
  const res = await fetch('/api/projects');
24
- const data = await res.json();
25
- setProjects(data);
44
+ const projectList: IProject[] = await res.json();
45
+
46
+ const withSubs = await Promise.all(
47
+ projectList.map(async (p) => {
48
+ const subRes = await fetch(`/api/projects/${p.id}/sub-projects`);
49
+ const subProjects: ISubProjectWithStats[] = await subRes.json();
50
+ return { ...p, subProjects };
51
+ })
52
+ );
53
+
54
+ setProjects(withSubs);
55
+
56
+ // Gather today tasks
57
+ const allToday: (ITask & { projectName: string; subProjectName: string })[] = [];
58
+ for (const p of withSubs) {
59
+ for (const sp of p.subProjects) {
60
+ for (const pt of sp.preview_tasks) {
61
+ // preview_tasks doesn't have full task data, so we need to check is_today from API
62
+ }
63
+ }
64
+ }
65
+ // Fetch today tasks from each project's tasks
66
+ for (const p of withSubs) {
67
+ for (const sp of p.subProjects) {
68
+ if (sp.task_count > 0) {
69
+ const tasksRes = await fetch(`/api/projects/${p.id}/sub-projects/${sp.id}/tasks`);
70
+ const tasks: ITask[] = await tasksRes.json();
71
+ for (const t of tasks) {
72
+ if (t.is_today) {
73
+ allToday.push({ ...t, projectName: p.name, subProjectName: sp.name });
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
79
+ setTodayTasks(allToday);
26
80
  setLoading(false);
27
81
  }, []);
28
82
 
29
83
  useEffect(() => {
30
- fetchProjects();
31
- }, [fetchProjects]);
84
+ fetchData();
85
+ }, [fetchData]);
86
+
87
+ const handleTabChange = (newTab: DashboardTab) => {
88
+ setTab(newTab);
89
+ localStorage.setItem('im-dashboard-tab', newTab);
90
+ };
32
91
 
33
92
  const handleCreate = async (e: React.FormEvent) => {
34
93
  e.preventDefault();
@@ -37,62 +96,80 @@ export default function Dashboard() {
37
96
  const res = await fetch('/api/projects', {
38
97
  method: 'POST',
39
98
  headers: { 'Content-Type': 'application/json' },
40
- body: JSON.stringify({ name: name.trim(), description: description.trim() }),
99
+ body: JSON.stringify({ name: name.trim(), description: description.trim(), project_path: projectPath.trim() || undefined }),
41
100
  });
42
101
 
43
102
  if (res.ok) {
44
103
  const project = await res.json();
45
104
  setName('');
46
105
  setDescription('');
106
+ setProjectPath('');
47
107
  setShowForm(false);
48
108
  router.push(`/projects/${project.id}`);
49
109
  }
50
110
  };
51
111
 
52
- const handleDelete = async (id: string, e: React.MouseEvent) => {
112
+ const handleDeleteClick = (id: string, e: React.MouseEvent) => {
53
113
  e.stopPropagation();
54
- if (!confirm('이 프로젝트를 삭제하시겠습니까?')) return;
114
+ setDeleteTarget(id);
115
+ };
55
116
 
56
- await fetch(`/api/projects/${id}`, { method: 'DELETE' });
57
- fetchProjects();
117
+ const handleDeleteConfirm = async () => {
118
+ if (!deleteTarget) return;
119
+ await fetch(`/api/projects/${deleteTarget}`, { method: 'DELETE' });
120
+ setDeleteTarget(null);
121
+ fetchData();
58
122
  };
59
123
 
60
- const formatDate = (dateStr: string) => {
61
- return new Date(dateStr).toLocaleDateString('ko-KR', {
62
- year: 'numeric',
63
- month: 'short',
64
- day: 'numeric',
65
- });
124
+ // Filter sub-projects based on tab
125
+ const getVisibleCards = (): { sp: ISubProjectWithStats; projectName: string; projectId: string }[] => {
126
+ const cards: { sp: ISubProjectWithStats; projectName: string; projectId: string }[] = [];
127
+ for (const p of projects) {
128
+ for (const sp of p.subProjects) {
129
+ if (tab === 'active') {
130
+ if (sp.active_count > 0 || sp.problem_count > 0) {
131
+ cards.push({ sp, projectName: p.name, projectId: p.id });
132
+ }
133
+ } else if (tab === 'all') {
134
+ cards.push({ sp, projectName: p.name, projectId: p.id });
135
+ }
136
+ }
137
+ }
138
+ // Sort: active cards first
139
+ cards.sort((a, b) => (b.sp.active_count + b.sp.problem_count) - (a.sp.active_count + a.sp.problem_count));
140
+ return cards;
141
+ };
142
+
143
+ const STATUS_ICONS: Record<string, string> = {
144
+ idea: '\u{1F4A1}', writing: '\u{270F}\u{FE0F}', submitted: '\u{1F680}',
145
+ testing: '\u{1F9EA}', done: '\u{2705}', problem: '\u{1F534}',
66
146
  };
67
147
 
68
148
  return (
69
- <div className="min-h-screen p-8 max-w-4xl mx-auto">
70
- <header className="flex items-center justify-between mb-10">
149
+ <div className="min-h-screen p-8 max-w-5xl mx-auto">
150
+ <header className="flex items-center justify-between mb-6">
71
151
  <div>
72
- <h1 className="text-3xl font-bold tracking-tight">
73
- IM <span className="text-muted-foreground font-normal text-lg ml-2">아이디어 매니저</span>
152
+ <h1 className="text-2xl font-bold tracking-tight">
153
+ IM <span className="text-muted-foreground font-normal text-sm ml-2">Idea Manager v2</span>
74
154
  </h1>
75
- <p className="text-muted-foreground mt-1 text-sm">
76
- 자유롭게 아이디어를 쏟아내면, AI가 구조화해드립니다
77
- </p>
78
155
  </div>
79
- <button
80
- onClick={() => setShowForm(!showForm)}
81
- className="px-4 py-2 bg-primary hover:bg-primary-hover text-white rounded-lg
82
- transition-colors font-medium text-sm"
83
- >
84
- + 프로젝트
85
- </button>
156
+ <div className="flex items-center gap-3">
157
+ <TabBar value={tab} onChange={handleTabChange} />
158
+ <button
159
+ onClick={() => setShowForm(!showForm)}
160
+ className="px-4 py-2 bg-primary hover:bg-primary-hover text-white rounded-lg
161
+ transition-colors font-medium text-sm"
162
+ >
163
+ + Project
164
+ </button>
165
+ </div>
86
166
  </header>
87
167
 
88
168
  {showForm && (
89
- <form
90
- onSubmit={handleCreate}
91
- className="mb-8 p-5 bg-card rounded-lg border border-border"
92
- >
169
+ <form onSubmit={handleCreate} className="mb-6 p-5 bg-card rounded-lg border border-border">
93
170
  <input
94
171
  type="text"
95
- placeholder="프로젝트 이름"
172
+ placeholder="Project name"
96
173
  value={name}
97
174
  onChange={(e) => setName(e.target.value)}
98
175
  className="w-full bg-input border border-border rounded-lg px-4 py-2.5 mb-3
@@ -101,75 +178,170 @@ export default function Dashboard() {
101
178
  />
102
179
  <input
103
180
  type="text"
104
- placeholder="설명 (선택사항)"
181
+ placeholder="Description (optional)"
105
182
  value={description}
106
183
  onChange={(e) => setDescription(e.target.value)}
107
- className="w-full bg-input border border-border rounded-lg px-4 py-2.5 mb-4
184
+ className="w-full bg-input border border-border rounded-lg px-4 py-2.5 mb-3
108
185
  focus:border-primary focus:outline-none text-foreground"
109
186
  />
110
- <div className="flex gap-2 justify-end">
187
+ <div className="mb-4">
111
188
  <button
112
189
  type="button"
113
- onClick={() => setShowForm(false)}
114
- className="px-4 py-2 text-muted-foreground hover:text-foreground transition-colors text-sm"
190
+ onClick={() => setShowDirPicker(true)}
191
+ className="w-full bg-input border border-border rounded-lg px-4 py-2.5
192
+ text-left text-sm hover:border-primary transition-colors"
115
193
  >
116
- 취소
194
+ {projectPath ? (
195
+ <span className="font-mono text-foreground">{projectPath}</span>
196
+ ) : (
197
+ <span className="text-muted-foreground">Project folder (optional)</span>
198
+ )}
117
199
  </button>
118
- <button
119
- type="submit"
120
- className="px-4 py-2 bg-primary hover:bg-primary-hover text-white rounded-lg
121
- transition-colors text-sm"
122
- >
123
- 만들기
200
+ </div>
201
+ <div className="flex gap-2 justify-end">
202
+ <button type="button" onClick={() => setShowForm(false)}
203
+ className="px-4 py-2 text-muted-foreground hover:text-foreground transition-colors text-sm">
204
+ Cancel
205
+ </button>
206
+ <button type="submit"
207
+ className="px-4 py-2 bg-primary hover:bg-primary-hover text-white rounded-lg transition-colors text-sm">
208
+ Create
124
209
  </button>
125
210
  </div>
126
211
  </form>
127
212
  )}
128
213
 
129
214
  {loading ? (
130
- <div className="text-center text-muted-foreground py-20">로딩 중...</div>
131
- ) : projects.length === 0 ? (
132
- <div className="text-center py-20">
133
- <div className="text-5xl mb-4">💡</div>
134
- <p className="text-muted-foreground text-lg mb-2">아직 프로젝트가 없습니다</p>
135
- <p className="text-muted-foreground text-sm">
136
- &quot;새 프로젝트&quot; 버튼을 눌러 시작하세요
137
- </p>
138
- </div>
139
- ) : (
140
- <div className="space-y-3">
141
- {projects.map((project) => (
142
- <div
143
- key={project.id}
144
- onClick={() => router.push(`/projects/${project.id}`)}
145
- className="p-5 bg-card hover:bg-card-hover border border-border rounded-lg
146
- cursor-pointer transition-colors group"
147
- >
148
- <div className="flex items-start justify-between">
149
- <div className="flex-1">
150
- <h2 className="text-lg font-semibold group-hover:text-primary transition-colors">
151
- {project.name}
152
- </h2>
153
- {project.description && (
154
- <p className="text-muted-foreground text-sm mt-1">{project.description}</p>
155
- )}
156
- <p className="text-muted-foreground text-xs mt-2">
157
- 수정일 {formatDate(project.updated_at)}
158
- </p>
215
+ <div className="text-center text-muted-foreground py-20">Loading...</div>
216
+ ) : tab === 'today' ? (
217
+ /* Today tab */
218
+ todayTasks.length === 0 ? (
219
+ <div className="text-center py-20 text-muted-foreground">
220
+ <p className="text-lg mb-2">No tasks marked for today</p>
221
+ <p className="text-sm">Mark tasks with the Today button in task detail</p>
222
+ </div>
223
+ ) : (
224
+ <div className="space-y-2">
225
+ {todayTasks.map((task) => (
226
+ <div
227
+ key={task.id}
228
+ onClick={() => router.push(`/projects/${task.project_id}?sub=${task.sub_project_id}&task=${task.id}`)}
229
+ className="flex items-center gap-3 p-3 bg-card hover:bg-card-hover border border-border
230
+ rounded-lg cursor-pointer transition-colors"
231
+ >
232
+ <span className="text-sm">{STATUS_ICONS[task.status]}</span>
233
+ <div className="flex-1 min-w-0">
234
+ <span className="text-sm font-medium">{task.title}</span>
235
+ <span className="text-xs text-muted-foreground ml-2">
236
+ {task.projectName} / {task.subProjectName}
237
+ </span>
159
238
  </div>
160
- <button
161
- onClick={(e) => handleDelete(project.id, e)}
162
- className="text-muted-foreground hover:text-destructive transition-colors opacity-0
163
- group-hover:opacity-100 p-1 text-sm"
164
- title="프로젝트 삭제"
165
- >
166
- 삭제
167
- </button>
168
239
  </div>
169
- </div>
170
- ))}
171
- </div>
240
+ ))}
241
+ </div>
242
+ )
243
+ ) : (
244
+ /* Active / All tabs */
245
+ <>
246
+ {/* Project headers for All tab */}
247
+ {tab === 'all' ? (
248
+ projects.length === 0 ? (
249
+ <div className="text-center py-20">
250
+ <p className="text-muted-foreground text-lg mb-2">No projects yet</p>
251
+ <p className="text-muted-foreground text-sm">Click + Project to get started</p>
252
+ </div>
253
+ ) : (
254
+ <div className="space-y-6">
255
+ {projects.map((project) => (
256
+ <div key={project.id}>
257
+ <div className="flex items-center justify-between mb-3">
258
+ <div
259
+ className="flex items-center gap-2 cursor-pointer hover:text-primary transition-colors"
260
+ onClick={() => router.push(`/projects/${project.id}`)}
261
+ >
262
+ <h2 className="text-sm font-semibold">{project.name}</h2>
263
+ {project.project_path && (
264
+ <span className="text-xs text-muted-foreground font-mono truncate max-w-48">
265
+ {project.project_path}
266
+ </span>
267
+ )}
268
+ </div>
269
+ <button
270
+ onClick={(e) => handleDeleteClick(project.id, e)}
271
+ className="text-xs text-muted-foreground hover:text-destructive transition-colors"
272
+ >
273
+ Delete
274
+ </button>
275
+ </div>
276
+ {project.subProjects.length === 0 ? (
277
+ <div className="text-xs text-muted-foreground py-4 text-center border border-dashed border-border rounded-lg">
278
+ No sub-projects.{' '}
279
+ <span
280
+ className="text-primary cursor-pointer hover:underline"
281
+ onClick={() => router.push(`/projects/${project.id}`)}
282
+ >
283
+ Open project
284
+ </span>{' '}
285
+ to add one.
286
+ </div>
287
+ ) : (
288
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
289
+ {project.subProjects.map((sp) => (
290
+ <SubProjectCard
291
+ key={sp.id}
292
+ subProject={sp}
293
+ projectName={project.name}
294
+ onClick={() => router.push(`/projects/${project.id}?sub=${sp.id}`)}
295
+ />
296
+ ))}
297
+ </div>
298
+ )}
299
+ </div>
300
+ ))}
301
+ </div>
302
+ )
303
+ ) : (
304
+ /* Active tab */
305
+ (() => {
306
+ const cards = getVisibleCards();
307
+ return cards.length === 0 ? (
308
+ <div className="text-center py-20 text-muted-foreground">
309
+ <p className="text-lg mb-2">No active tasks</p>
310
+ <p className="text-sm">Submit tasks to see them here</p>
311
+ </div>
312
+ ) : (
313
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
314
+ {cards.map(({ sp, projectName, projectId }) => (
315
+ <SubProjectCard
316
+ key={sp.id}
317
+ subProject={sp}
318
+ projectName={projectName}
319
+ onClick={() => router.push(`/projects/${projectId}?sub=${sp.id}`)}
320
+ />
321
+ ))}
322
+ </div>
323
+ );
324
+ })()
325
+ )}
326
+ </>
327
+ )}
328
+
329
+ {showDirPicker && (
330
+ <DirectoryPicker
331
+ onSelect={(path) => { setProjectPath(path); setShowDirPicker(false); }}
332
+ onCancel={() => setShowDirPicker(false)}
333
+ />
172
334
  )}
335
+
336
+ <ConfirmDialog
337
+ open={!!deleteTarget}
338
+ title="Delete project?"
339
+ description="This will permanently delete the project and all its data."
340
+ confirmLabel="Delete"
341
+ variant="danger"
342
+ onConfirm={handleDeleteConfirm}
343
+ onCancel={() => setDeleteTarget(null)}
344
+ />
173
345
  </div>
174
346
  );
175
347
  }