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.
Files changed (63) hide show
  1. package/README.md +19 -10
  2. package/next.config.ts +0 -1
  3. package/package.json +2 -2
  4. package/public/favicon.svg +10 -0
  5. package/public/icon.svg +2 -11
  6. package/src/app/api/filesystem/route.ts +49 -0
  7. package/src/app/api/projects/[id]/cleanup/route.ts +32 -0
  8. package/src/app/api/projects/[id]/items/[itemId]/refine/route.ts +36 -0
  9. package/src/app/api/projects/[id]/items/[itemId]/route.ts +23 -1
  10. package/src/app/api/projects/[id]/items/route.ts +51 -1
  11. package/src/app/api/projects/[id]/scan/route.ts +73 -0
  12. package/src/app/api/projects/[id]/scan/stream/route.ts +112 -0
  13. package/src/app/api/projects/[id]/structure/route.ts +34 -3
  14. package/src/app/api/projects/[id]/structure/stream/route.ts +157 -0
  15. package/src/app/api/projects/[id]/sub-projects/[subId]/route.ts +39 -0
  16. package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.ts +60 -0
  17. package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route.ts +26 -0
  18. package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route.ts +39 -0
  19. package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/route.ts +33 -0
  20. package/src/app/api/projects/[id]/sub-projects/route.ts +31 -0
  21. package/src/app/api/projects/route.ts +1 -1
  22. package/src/app/globals.css +465 -5
  23. package/src/app/layout.tsx +3 -0
  24. package/src/app/page.tsx +260 -88
  25. package/src/app/projects/[id]/page.tsx +366 -183
  26. package/src/cli.ts +44 -12
  27. package/src/components/DirectoryPicker.tsx +137 -0
  28. package/src/components/ScanPanel.tsx +743 -0
  29. package/src/components/brainstorm/Editor.tsx +20 -4
  30. package/src/components/brainstorm/MemoPin.tsx +91 -5
  31. package/src/components/dashboard/SubProjectCard.tsx +76 -0
  32. package/src/components/dashboard/TabBar.tsx +42 -0
  33. package/src/components/task/ProjectTree.tsx +223 -0
  34. package/src/components/task/PromptEditor.tsx +107 -0
  35. package/src/components/task/StatusFlow.tsx +43 -0
  36. package/src/components/task/TaskChat.tsx +134 -0
  37. package/src/components/task/TaskDetail.tsx +205 -0
  38. package/src/components/task/TaskList.tsx +119 -0
  39. package/src/components/tree/CardView.tsx +206 -0
  40. package/src/components/tree/RefinePopover.tsx +157 -0
  41. package/src/components/tree/TreeNode.tsx +147 -38
  42. package/src/components/tree/TreeView.tsx +270 -26
  43. package/src/components/ui/ConfirmDialog.tsx +88 -0
  44. package/src/lib/ai/chat-responder.ts +4 -2
  45. package/src/lib/ai/cleanup.ts +87 -0
  46. package/src/lib/ai/client.ts +175 -58
  47. package/src/lib/ai/prompter.ts +19 -24
  48. package/src/lib/ai/refiner.ts +128 -0
  49. package/src/lib/ai/structurer.ts +340 -11
  50. package/src/lib/db/queries/context.ts +76 -0
  51. package/src/lib/db/queries/items.ts +133 -12
  52. package/src/lib/db/queries/projects.ts +12 -8
  53. package/src/lib/db/queries/sub-projects.ts +122 -0
  54. package/src/lib/db/queries/task-conversations.ts +27 -0
  55. package/src/lib/db/queries/task-prompts.ts +32 -0
  56. package/src/lib/db/queries/tasks.ts +133 -0
  57. package/src/lib/db/schema.ts +75 -0
  58. package/src/lib/mcp/server.ts +38 -39
  59. package/src/lib/mcp/tools.ts +47 -45
  60. package/src/lib/scanner.ts +573 -0
  61. package/src/lib/task-store.ts +97 -0
  62. package/src/types/index.ts +65 -0
  63. package/src/app/icon.svg +0 -19
@@ -13,7 +13,10 @@ interface Memo {
13
13
  interface EditorProps {
14
14
  projectId: string;
15
15
  onContentChange: (content: string) => void;
16
+ onSendMessage: (message: string) => void;
16
17
  memos?: Memo[];
18
+ chatLoading?: boolean;
19
+ onCollapse?: () => void;
17
20
  }
18
21
 
19
22
  interface PinPosition {
@@ -22,7 +25,7 @@ interface PinPosition {
22
25
  left: number;
23
26
  }
24
27
 
25
- export default function Editor({ projectId, onContentChange, memos = [] }: EditorProps) {
28
+ export default function Editor({ projectId, onContentChange, onSendMessage, memos = [], chatLoading, onCollapse }: EditorProps) {
26
29
  const [content, setContent] = useState('');
27
30
  const [saving, setSaving] = useState(false);
28
31
  const [loaded, setLoaded] = useState(false);
@@ -122,9 +125,20 @@ export default function Editor({ projectId, onContentChange, memos = [] }: Edito
122
125
  <div className="flex flex-col h-full">
123
126
  <div className="flex items-center justify-between px-4 py-2 border-b border-border">
124
127
  <h2 className="text-sm font-medium text-muted-foreground">브레인스토밍</h2>
125
- <span className="text-xs text-muted-foreground">
126
- {saving ? '저장 중...' : content ? '저장됨' : ''}
127
- </span>
128
+ <div className="flex items-center gap-2">
129
+ <span className="text-xs text-muted-foreground">
130
+ {saving ? '저장 중...' : content ? '저장됨' : ''}
131
+ </span>
132
+ {onCollapse && (
133
+ <button
134
+ onClick={onCollapse}
135
+ className="text-muted-foreground hover:text-foreground transition-colors text-xs px-1"
136
+ title="접기 (B)"
137
+ >
138
+ «
139
+ </button>
140
+ )}
141
+ </div>
128
142
  </div>
129
143
  <div className="editor-container">
130
144
  <textarea
@@ -153,6 +167,8 @@ export default function Editor({ projectId, onContentChange, memos = [] }: Edito
153
167
  anchorText={pin.memo.anchor_text}
154
168
  top={pin.top}
155
169
  left={pin.left}
170
+ loading={chatLoading}
171
+ onSendMessage={onSendMessage}
156
172
  />
157
173
  ))}
158
174
  </div>
@@ -1,31 +1,117 @@
1
1
  'use client';
2
2
 
3
- import { useState } from 'react';
3
+ import { useState, useRef, useEffect } from 'react';
4
4
 
5
5
  interface MemoPinProps {
6
6
  question: string;
7
7
  anchorText: string;
8
8
  top: number;
9
9
  left: number;
10
+ loading?: boolean;
11
+ onSendMessage?: (message: string) => void;
10
12
  }
11
13
 
12
- export default function MemoPin({ question, anchorText, top, left }: MemoPinProps) {
14
+ export default function MemoPin({ question, anchorText, top, left, loading, onSendMessage }: MemoPinProps) {
13
15
  const [showTooltip, setShowTooltip] = useState(false);
16
+ const [open, setOpen] = useState(false);
17
+ const [input, setInput] = useState('');
18
+ const [replies, setReplies] = useState<{ role: 'user' | 'assistant'; text: string }[]>([]);
19
+ const popoverRef = useRef<HTMLDivElement>(null);
20
+ const inputRef = useRef<HTMLTextAreaElement>(null);
21
+
22
+ // Close popover on outside click
23
+ useEffect(() => {
24
+ if (!open) return;
25
+ const handler = (e: MouseEvent) => {
26
+ if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) {
27
+ setOpen(false);
28
+ }
29
+ };
30
+ document.addEventListener('mousedown', handler);
31
+ return () => document.removeEventListener('mousedown', handler);
32
+ }, [open]);
33
+
34
+ // Focus input when popover opens
35
+ useEffect(() => {
36
+ if (open) inputRef.current?.focus();
37
+ }, [open]);
38
+
39
+ const handleClick = () => {
40
+ setOpen(!open);
41
+ setShowTooltip(false);
42
+ };
43
+
44
+ const handleSubmit = () => {
45
+ const trimmed = input.trim();
46
+ if (!trimmed || loading) return;
47
+ setReplies(prev => [...prev, { role: 'user', text: trimmed }]);
48
+ setInput('');
49
+ onSendMessage?.(trimmed);
50
+ };
51
+
52
+ const handleKeyDown = (e: React.KeyboardEvent) => {
53
+ if (e.key === 'Enter' && !e.shiftKey) {
54
+ e.preventDefault();
55
+ handleSubmit();
56
+ }
57
+ };
14
58
 
15
59
  return (
16
60
  <div
61
+ ref={popoverRef}
17
62
  className="memo-pin"
18
63
  style={{ top: `${top}px`, left: `${left}px` }}
19
- onMouseEnter={() => setShowTooltip(true)}
64
+ onMouseEnter={() => !open && setShowTooltip(true)}
20
65
  onMouseLeave={() => setShowTooltip(false)}
21
66
  >
22
- <span className="memo-pin-icon">&#x1F4CC;</span>
23
- {showTooltip && (
67
+ <span className="memo-pin-icon" onClick={handleClick}>&#x1F4CC;</span>
68
+
69
+ {/* Hover tooltip (only when popover is closed) */}
70
+ {showTooltip && !open && (
24
71
  <div className="memo-tooltip">
25
72
  <div className="memo-tooltip-anchor">&ldquo;{anchorText}&rdquo;</div>
26
73
  <div className="memo-tooltip-question">{question}</div>
27
74
  </div>
28
75
  )}
76
+
77
+ {/* Click popover with inline chat */}
78
+ {open && (
79
+ <div className="memo-popover">
80
+ <div className="memo-popover-anchor">&ldquo;{anchorText}&rdquo;</div>
81
+ <div className="memo-popover-messages">
82
+ <div className="memo-popover-bubble memo-popover-bubble-ai">{question}</div>
83
+ {replies.map((r, i) => (
84
+ <div key={i} className={`memo-popover-bubble memo-popover-bubble-${r.role === 'user' ? 'user' : 'ai'}`}>
85
+ {r.text}
86
+ </div>
87
+ ))}
88
+ {loading && (
89
+ <div className="memo-popover-bubble memo-popover-bubble-ai chat-loading">
90
+ <span className="dot" /><span className="dot" /><span className="dot" />
91
+ </div>
92
+ )}
93
+ </div>
94
+ <div className="memo-popover-input-area">
95
+ <textarea
96
+ ref={inputRef}
97
+ value={input}
98
+ onChange={(e) => setInput(e.target.value)}
99
+ onKeyDown={handleKeyDown}
100
+ placeholder="답변 입력..."
101
+ rows={1}
102
+ disabled={loading}
103
+ className="memo-popover-input"
104
+ />
105
+ <button
106
+ onClick={handleSubmit}
107
+ disabled={!input.trim() || loading}
108
+ className="memo-popover-send"
109
+ >
110
+ 전송
111
+ </button>
112
+ </div>
113
+ </div>
114
+ )}
29
115
  </div>
30
116
  );
31
117
  }
@@ -0,0 +1,76 @@
1
+ 'use client';
2
+
3
+ import type { ISubProjectWithStats, TaskStatus } from '@/types';
4
+
5
+ const STATUS_ICONS: Record<TaskStatus, string> = {
6
+ idea: '\u{1F4A1}',
7
+ writing: '\u{270F}\u{FE0F}',
8
+ submitted: '\u{1F680}',
9
+ testing: '\u{1F9EA}',
10
+ done: '\u{2705}',
11
+ problem: '\u{1F534}',
12
+ };
13
+
14
+ function timeAgo(dateStr: string | null): string {
15
+ if (!dateStr) return '';
16
+ const diff = Date.now() - new Date(dateStr).getTime();
17
+ const mins = Math.floor(diff / 60000);
18
+ if (mins < 1) return 'just now';
19
+ if (mins < 60) return `${mins}m ago`;
20
+ const hours = Math.floor(mins / 60);
21
+ if (hours < 24) return `${hours}h ago`;
22
+ const days = Math.floor(hours / 24);
23
+ return `${days}d ago`;
24
+ }
25
+
26
+ export default function SubProjectCard({
27
+ subProject,
28
+ projectName,
29
+ onClick,
30
+ }: {
31
+ subProject: ISubProjectWithStats;
32
+ projectName: string;
33
+ onClick: () => void;
34
+ }) {
35
+ const { active_count, pending_count, done_count, problem_count, task_count, preview_tasks, last_activity } = subProject;
36
+
37
+ return (
38
+ <div
39
+ onClick={onClick}
40
+ className="p-4 bg-card hover:bg-card-hover border border-border rounded-xl
41
+ cursor-pointer transition-all group hover:border-muted-foreground/30
42
+ hover:shadow-md hover:shadow-black/20"
43
+ >
44
+ <div className="flex items-start justify-between mb-2">
45
+ <h3 className="text-sm font-semibold group-hover:text-primary transition-colors truncate flex-1">
46
+ {subProject.name}
47
+ </h3>
48
+ <span className="text-xs text-muted-foreground ml-2 flex-shrink-0">{projectName}</span>
49
+ </div>
50
+
51
+ {preview_tasks.length > 0 && (
52
+ <div className="space-y-1 mb-3">
53
+ {preview_tasks.map((t, i) => (
54
+ <div key={i} className="flex items-center gap-2 text-xs">
55
+ <span className="flex-shrink-0">{STATUS_ICONS[t.status]}</span>
56
+ <span className={`truncate ${t.status === 'done' ? 'text-muted-foreground line-through' : 'text-foreground'}`}>
57
+ {t.title}
58
+ </span>
59
+ </div>
60
+ ))}
61
+ </div>
62
+ )}
63
+
64
+ <div className="flex items-center justify-between text-xs text-muted-foreground">
65
+ <div className="flex items-center gap-3">
66
+ {active_count > 0 && <span className="text-primary">active {active_count}</span>}
67
+ {pending_count > 0 && <span>pending {pending_count}</span>}
68
+ {done_count > 0 && <span className="text-success">done {done_count}</span>}
69
+ {problem_count > 0 && <span className="text-destructive">problem {problem_count}</span>}
70
+ {task_count === 0 && <span>no tasks</span>}
71
+ </div>
72
+ {last_activity && <span>{timeAgo(last_activity)}</span>}
73
+ </div>
74
+ </div>
75
+ );
76
+ }
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+
5
+ export type DashboardTab = 'active' | 'all' | 'today';
6
+
7
+ const TABS: { key: DashboardTab; label: string }[] = [
8
+ { key: 'active', label: 'Active' },
9
+ { key: 'all', label: 'All' },
10
+ { key: 'today', label: 'Today' },
11
+ ];
12
+
13
+ export default function TabBar({
14
+ value,
15
+ onChange,
16
+ }: {
17
+ value: DashboardTab;
18
+ onChange: (tab: DashboardTab) => void;
19
+ }) {
20
+ const [mounted, setMounted] = useState(false);
21
+ useEffect(() => setMounted(true), []);
22
+
23
+ if (!mounted) return null;
24
+
25
+ return (
26
+ <div className="flex gap-1 bg-muted rounded-lg p-1">
27
+ {TABS.map((tab) => (
28
+ <button
29
+ key={tab.key}
30
+ onClick={() => onChange(tab.key)}
31
+ className={`px-4 py-1.5 text-sm rounded-md transition-all ${
32
+ value === tab.key
33
+ ? 'bg-card text-foreground shadow-sm font-medium'
34
+ : 'text-muted-foreground hover:text-foreground'
35
+ }`}
36
+ >
37
+ {tab.label}
38
+ </button>
39
+ ))}
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,223 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import type { ITask, ISubProjectWithStats, TaskStatus } from '@/types';
5
+ import { statusIcon } from './StatusFlow';
6
+
7
+ const PRIORITY_COLORS: Record<string, string> = {
8
+ high: 'bg-destructive',
9
+ medium: 'bg-warning',
10
+ low: 'bg-muted-foreground',
11
+ };
12
+
13
+ export default function ProjectTree({
14
+ subProjects,
15
+ tasks,
16
+ selectedSubId,
17
+ selectedTaskId,
18
+ onSelectSub,
19
+ onSelectTask,
20
+ onCreateSub,
21
+ onDeleteSub,
22
+ onCreateTask,
23
+ onStatusChange,
24
+ onTodayToggle,
25
+ }: {
26
+ subProjects: ISubProjectWithStats[];
27
+ tasks: ITask[];
28
+ selectedSubId: string | null;
29
+ selectedTaskId: string | null;
30
+ onSelectSub: (subId: string) => void;
31
+ onSelectTask: (taskId: string) => void;
32
+ onCreateSub: () => void;
33
+ onDeleteSub: (subId: string) => void;
34
+ onCreateTask: (title: string) => void;
35
+ onStatusChange: (taskId: string, status: TaskStatus) => void;
36
+ onTodayToggle: (taskId: string, isToday: boolean) => void;
37
+ }) {
38
+ const [collapsedSubs, setCollapsedSubs] = useState<Set<string>>(new Set());
39
+ const [addingTaskFor, setAddingTaskFor] = useState<string | null>(null);
40
+ const [newTaskTitle, setNewTaskTitle] = useState('');
41
+
42
+ const toggleCollapse = (subId: string) => {
43
+ setCollapsedSubs(prev => {
44
+ const next = new Set(prev);
45
+ if (next.has(subId)) next.delete(subId);
46
+ else next.add(subId);
47
+ return next;
48
+ });
49
+ };
50
+
51
+ const handleAddTask = (subId: string) => {
52
+ const title = newTaskTitle.trim();
53
+ if (!title) return;
54
+ onSelectSub(subId);
55
+ onCreateTask(title);
56
+ setNewTaskTitle('');
57
+ setAddingTaskFor(null);
58
+ };
59
+
60
+ return (
61
+ <div className="flex flex-col h-full">
62
+ <div className="flex items-center justify-between px-3 py-2 border-b border-border flex-shrink-0">
63
+ <h2 className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Projects</h2>
64
+ <button
65
+ onClick={onCreateSub}
66
+ className="text-xs text-muted-foreground hover:text-foreground transition-colors"
67
+ title="Add sub-project (N)"
68
+ >
69
+ + <span className="text-muted-foreground/50">N</span>
70
+ </button>
71
+ </div>
72
+
73
+ <div className="flex-1 overflow-y-auto py-1">
74
+ {subProjects.length === 0 && (
75
+ <div className="text-center py-8 text-muted-foreground text-xs">
76
+ Create a sub-project to get started
77
+ </div>
78
+ )}
79
+
80
+ {subProjects.map((sp) => {
81
+ const isSelected = selectedSubId === sp.id;
82
+ const isCollapsed = collapsedSubs.has(sp.id);
83
+ const subTasks = isSelected ? tasks : [];
84
+
85
+ return (
86
+ <div key={sp.id} className="mb-0.5">
87
+ {/* Sub-project node */}
88
+ <div
89
+ onClick={() => {
90
+ onSelectSub(sp.id);
91
+ if (isCollapsed) toggleCollapse(sp.id);
92
+ }}
93
+ className={`flex items-center gap-1.5 px-2 py-1.5 cursor-pointer transition-colors group text-sm ${
94
+ isSelected
95
+ ? 'text-foreground'
96
+ : 'text-muted-foreground hover:text-foreground'
97
+ }`}
98
+ >
99
+ <button
100
+ onClick={(e) => { e.stopPropagation(); toggleCollapse(sp.id); }}
101
+ className="w-4 h-4 flex items-center justify-center text-xs text-muted-foreground flex-shrink-0"
102
+ >
103
+ {isCollapsed ? '\u25B6' : '\u25BC'}
104
+ </button>
105
+ <span className={`flex-1 truncate font-medium ${isSelected ? 'text-primary' : ''}`}>
106
+ {sp.name}
107
+ </span>
108
+ <div className="flex items-center gap-1.5">
109
+ {sp.task_count > 0 && (
110
+ <span className="text-xs text-muted-foreground tabular-nums">{sp.task_count}</span>
111
+ )}
112
+ <button
113
+ onClick={(e) => { e.stopPropagation(); onDeleteSub(sp.id); }}
114
+ className="text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity text-xs"
115
+ >
116
+ x
117
+ </button>
118
+ </div>
119
+ </div>
120
+
121
+ {/* Tasks (children) */}
122
+ {!isCollapsed && isSelected && (
123
+ <div className="ml-3 border-l border-border/50">
124
+ {subTasks.length === 0 && !addingTaskFor && (
125
+ <div className="text-xs text-muted-foreground py-2 pl-4">
126
+ No tasks
127
+ </div>
128
+ )}
129
+ {subTasks.map((task) => (
130
+ <div
131
+ key={task.id}
132
+ onClick={() => onSelectTask(task.id)}
133
+ className={`flex items-center gap-1.5 pl-4 pr-2 py-1.5 cursor-pointer transition-colors text-sm border-l-2 ${
134
+ selectedTaskId === task.id
135
+ ? 'bg-card-hover border-l-primary'
136
+ : 'border-l-transparent hover:bg-card-hover/50'
137
+ }`}
138
+ >
139
+ <button
140
+ onClick={(e) => {
141
+ e.stopPropagation();
142
+ const nextStatus = getNextStatus(task.status);
143
+ onStatusChange(task.id, nextStatus);
144
+ }}
145
+ className="flex-shrink-0 text-sm"
146
+ title={`Status: ${task.status}`}
147
+ >
148
+ {statusIcon(task.status)}
149
+ </button>
150
+ <span className={`tree-priority-dot ${PRIORITY_COLORS[task.priority]}`} />
151
+ <span className={`flex-1 truncate ${task.status === 'done' ? 'text-muted-foreground line-through' : ''}`}>
152
+ {task.title}
153
+ </span>
154
+ {task.is_today && (
155
+ <button
156
+ onClick={(e) => { e.stopPropagation(); onTodayToggle(task.id, false); }}
157
+ className="text-xs flex-shrink-0 text-primary" title="Remove from today"
158
+ >
159
+ *
160
+ </button>
161
+ )}
162
+ </div>
163
+ ))}
164
+
165
+ {/* Add task input */}
166
+ {addingTaskFor === sp.id ? (
167
+ <div className="pl-4 pr-2 py-1">
168
+ <input
169
+ type="text"
170
+ value={newTaskTitle}
171
+ onChange={(e) => setNewTaskTitle(e.target.value)}
172
+ onKeyDown={(e) => {
173
+ if (e.key === 'Enter') handleAddTask(sp.id);
174
+ if (e.key === 'Escape') { setNewTaskTitle(''); setAddingTaskFor(null); }
175
+ }}
176
+ placeholder="Task title..."
177
+ className="w-full bg-input border border-border rounded px-2 py-1 text-sm
178
+ focus:border-primary focus:outline-none text-foreground"
179
+ autoFocus
180
+ />
181
+ </div>
182
+ ) : (
183
+ <button
184
+ data-add-task
185
+ onClick={() => { onSelectSub(sp.id); setAddingTaskFor(sp.id); }}
186
+ className="pl-4 pr-2 py-1 text-xs text-muted-foreground hover:text-foreground
187
+ transition-colors text-left w-full"
188
+ >
189
+ + Add task <span className="text-muted-foreground/50 ml-1">T</span>
190
+ </button>
191
+ )}
192
+ </div>
193
+ )}
194
+
195
+ {/* Show task previews for non-selected sub-projects */}
196
+ {!isCollapsed && !isSelected && sp.preview_tasks && sp.preview_tasks.length > 0 && (
197
+ <div className="ml-3 border-l border-border/50">
198
+ {sp.preview_tasks.map((pt, i) => (
199
+ <div
200
+ key={i}
201
+ onClick={() => onSelectSub(sp.id)}
202
+ className="flex items-center gap-1.5 pl-4 pr-2 py-1 cursor-pointer text-xs text-muted-foreground hover:text-foreground transition-colors"
203
+ >
204
+ <span className="flex-shrink-0">{statusIcon(pt.status)}</span>
205
+ <span className="truncate">{pt.title}</span>
206
+ </div>
207
+ ))}
208
+ </div>
209
+ )}
210
+ </div>
211
+ );
212
+ })}
213
+ </div>
214
+ </div>
215
+ );
216
+ }
217
+
218
+ function getNextStatus(current: TaskStatus): TaskStatus {
219
+ const flow: TaskStatus[] = ['idea', 'writing', 'submitted', 'testing', 'done'];
220
+ const idx = flow.indexOf(current);
221
+ if (idx === -1) return 'idea';
222
+ return flow[(idx + 1) % flow.length];
223
+ }
@@ -0,0 +1,107 @@
1
+ 'use client';
2
+
3
+ import { useState, useRef, useEffect } from 'react';
4
+
5
+ export default function PromptEditor({
6
+ content,
7
+ onSave,
8
+ onRefine,
9
+ refining,
10
+ }: {
11
+ content: string;
12
+ onSave: (content: string) => void;
13
+ onRefine?: () => void;
14
+ refining?: boolean;
15
+ }) {
16
+ const [editing, setEditing] = useState(false);
17
+ const [draft, setDraft] = useState(content);
18
+ const [copied, setCopied] = useState(false);
19
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
20
+
21
+ useEffect(() => {
22
+ setDraft(content);
23
+ }, [content]);
24
+
25
+ useEffect(() => {
26
+ if (editing && textareaRef.current) {
27
+ textareaRef.current.focus();
28
+ textareaRef.current.style.height = 'auto';
29
+ textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px';
30
+ }
31
+ }, [editing]);
32
+
33
+ const handleSave = () => {
34
+ onSave(draft);
35
+ setEditing(false);
36
+ };
37
+
38
+ const handleCopy = async () => {
39
+ if (!content) return;
40
+ await navigator.clipboard.writeText(content);
41
+ setCopied(true);
42
+ setTimeout(() => setCopied(false), 1500);
43
+ };
44
+
45
+ return (
46
+ <div className="flex flex-col gap-2">
47
+ <div className="flex items-center justify-between">
48
+ <h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Prompt</h4>
49
+ <div className="flex items-center gap-1.5">
50
+ {onRefine && (
51
+ <button
52
+ onClick={onRefine}
53
+ disabled={refining}
54
+ className="prompt-action-btn prompt-generate-btn"
55
+ >
56
+ {refining ? 'Refining...' : 'AI Refine'}
57
+ </button>
58
+ )}
59
+ {!editing && content && (
60
+ <button onClick={handleCopy} className="prompt-action-btn">
61
+ {copied ? 'Copied!' : 'Copy'}
62
+ </button>
63
+ )}
64
+ {!editing ? (
65
+ <button onClick={() => setEditing(true)} className="prompt-action-btn">
66
+ Edit
67
+ </button>
68
+ ) : (
69
+ <>
70
+ <button onClick={() => { setDraft(content); setEditing(false); }} className="prompt-action-btn">
71
+ Cancel
72
+ </button>
73
+ <button onClick={handleSave} className="prompt-action-btn" style={{ color: 'hsl(var(--success))' }}>
74
+ Save
75
+ </button>
76
+ </>
77
+ )}
78
+ </div>
79
+ </div>
80
+
81
+ {editing ? (
82
+ <textarea
83
+ ref={textareaRef}
84
+ value={draft}
85
+ onChange={(e) => {
86
+ setDraft(e.target.value);
87
+ e.target.style.height = 'auto';
88
+ e.target.style.height = e.target.scrollHeight + 'px';
89
+ }}
90
+ onKeyDown={(e) => {
91
+ if (e.key === 'Escape') { setDraft(content); setEditing(false); }
92
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') handleSave();
93
+ }}
94
+ className="prompt-edit-textarea"
95
+ rows={4}
96
+ placeholder="Write your prompt here..."
97
+ />
98
+ ) : content ? (
99
+ <div className="prompt-content text-sm">{content}</div>
100
+ ) : (
101
+ <div className="text-sm text-muted-foreground italic py-6 text-center border border-dashed border-border rounded-lg">
102
+ No prompt yet. Click Edit to write one.
103
+ </div>
104
+ )}
105
+ </div>
106
+ );
107
+ }
@@ -0,0 +1,43 @@
1
+ 'use client';
2
+
3
+ import type { TaskStatus } from '@/types';
4
+
5
+ const STATUSES: { key: TaskStatus; label: string; icon: string; color: string }[] = [
6
+ { key: 'idea', label: 'Idea', icon: '\u{1F4A1}', color: 'text-muted-foreground' },
7
+ { key: 'writing', label: 'Writing', icon: '\u{270F}\u{FE0F}', color: 'text-warning' },
8
+ { key: 'submitted', label: 'Submitted', icon: '\u{1F680}', color: 'text-primary' },
9
+ { key: 'testing', label: 'Testing', icon: '\u{1F9EA}', color: 'text-accent' },
10
+ { key: 'done', label: 'Done', icon: '\u{2705}', color: 'text-success' },
11
+ { key: 'problem', label: 'Problem', icon: '\u{1F534}', color: 'text-destructive' },
12
+ ];
13
+
14
+ export default function StatusFlow({
15
+ status,
16
+ onChange,
17
+ }: {
18
+ status: TaskStatus;
19
+ onChange: (status: TaskStatus) => void;
20
+ }) {
21
+ return (
22
+ <div className="flex items-center gap-1">
23
+ {STATUSES.map((s) => (
24
+ <button
25
+ key={s.key}
26
+ onClick={() => onChange(s.key)}
27
+ title={s.label}
28
+ className={`px-2 py-1 rounded text-base transition-all ${
29
+ status === s.key
30
+ ? `${s.color} bg-muted scale-110`
31
+ : 'opacity-40 hover:opacity-80'
32
+ }`}
33
+ >
34
+ {s.icon}
35
+ </button>
36
+ ))}
37
+ </div>
38
+ );
39
+ }
40
+
41
+ export function statusIcon(status: TaskStatus): string {
42
+ return STATUSES.find(s => s.key === status)?.icon ?? '';
43
+ }