idea-manager 0.7.10 → 0.8.1
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "idea-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "AI 기반 브레인스토밍 → 구조화 → 프롬프트 생성 도구. MCP Server 내장.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"brainstorm",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"react": "19.2.3",
|
|
52
52
|
"react-dom": "19.2.3",
|
|
53
53
|
"react-markdown": "^10.1.0",
|
|
54
|
+
"remark-gfm": "^4.0.1",
|
|
54
55
|
"tsx": "^4.21.0"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
package/public/sw.js
CHANGED
package/src/app/globals.css
CHANGED
|
@@ -1016,6 +1016,29 @@ textarea:focus {
|
|
|
1016
1016
|
margin: 0.5em 0;
|
|
1017
1017
|
}
|
|
1018
1018
|
|
|
1019
|
+
.chat-markdown table {
|
|
1020
|
+
border-collapse: collapse;
|
|
1021
|
+
width: 100%;
|
|
1022
|
+
margin: 0.5em 0;
|
|
1023
|
+
font-size: 0.85em;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
.chat-markdown th,
|
|
1027
|
+
.chat-markdown td {
|
|
1028
|
+
border: 1px solid hsl(var(--border));
|
|
1029
|
+
padding: 4px 8px;
|
|
1030
|
+
text-align: left;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
.chat-markdown th {
|
|
1034
|
+
background: hsl(var(--muted));
|
|
1035
|
+
font-weight: 600;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
.chat-markdown tr:nth-child(even) {
|
|
1039
|
+
background: hsl(var(--muted) / 0.3);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1019
1042
|
/* Dialog animation */
|
|
1020
1043
|
@keyframes dialogIn {
|
|
1021
1044
|
from { opacity: 0; transform: scale(0.95) translateY(4px); }
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import ReactMarkdown from 'react-markdown';
|
|
4
|
+
import remarkGfm from 'remark-gfm';
|
|
5
|
+
|
|
3
6
|
interface ChatMessageProps {
|
|
4
7
|
role: 'assistant' | 'user';
|
|
5
8
|
content: string;
|
|
@@ -16,11 +19,17 @@ export default function ChatMessage({ role, content, createdAt }: ChatMessagePro
|
|
|
16
19
|
return (
|
|
17
20
|
<div className={`chat-message ${isAi ? 'chat-message-ai' : 'chat-message-user'}`}>
|
|
18
21
|
<div className={`chat-bubble ${isAi ? 'chat-bubble-ai' : 'chat-bubble-user'}`}>
|
|
19
|
-
{
|
|
20
|
-
<
|
|
21
|
-
{
|
|
22
|
-
</
|
|
23
|
-
)
|
|
22
|
+
{isAi ? (
|
|
23
|
+
<div className="chat-markdown">
|
|
24
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
|
|
25
|
+
</div>
|
|
26
|
+
) : (
|
|
27
|
+
content.split('\n').map((line, i) => (
|
|
28
|
+
<p key={i} className={i > 0 ? 'mt-1' : ''}>
|
|
29
|
+
{line}
|
|
30
|
+
</p>
|
|
31
|
+
))
|
|
32
|
+
)}
|
|
24
33
|
</div>
|
|
25
34
|
<span className="chat-time">{time}</span>
|
|
26
35
|
</div>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
4
4
|
import type { ITaskConversation, TaskStatus } from '@/types';
|
|
5
5
|
import ReactMarkdown from 'react-markdown';
|
|
6
|
+
import remarkGfm from 'remark-gfm';
|
|
6
7
|
|
|
7
8
|
const POLL_INTERVAL = 3000; // Poll every 3s when task is testing
|
|
8
9
|
|
|
@@ -112,7 +113,7 @@ export default function TaskChat({
|
|
|
112
113
|
: 'bg-muted text-foreground rounded-bl-sm chat-markdown'
|
|
113
114
|
}`}>
|
|
114
115
|
{msg.role === 'assistant'
|
|
115
|
-
? <ReactMarkdown>{msg.content}</ReactMarkdown>
|
|
116
|
+
? <ReactMarkdown remarkPlugins={[remarkGfm]}>{msg.content}</ReactMarkdown>
|
|
116
117
|
: msg.content}
|
|
117
118
|
</div>
|
|
118
119
|
{msg.role === 'assistant' && (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
4
|
import type { ITask, TaskStatus, ItemPriority } from '@/types';
|
|
5
5
|
import StatusFlow from './StatusFlow';
|
|
6
6
|
import PromptEditor from './PromptEditor';
|
|
@@ -24,20 +24,15 @@ export default function TaskDetail({
|
|
|
24
24
|
const [promptContent, setPromptContent] = useState('');
|
|
25
25
|
const [refining, setRefining] = useState(false);
|
|
26
26
|
const [editingTitle, setEditingTitle] = useState(false);
|
|
27
|
-
const [
|
|
27
|
+
const [showPromptModal, setShowPromptModal] = useState(false);
|
|
28
28
|
|
|
29
29
|
const basePath = `/api/projects/${projectId}/sub-projects/${subProjectId}/tasks/${task.id}`;
|
|
30
|
-
|
|
31
|
-
// Auto-show chat when task is being executed by watcher
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
if (task.status === 'testing') setShowChat(true);
|
|
34
|
-
}, [task.status]);
|
|
30
|
+
const overlayRef = useRef<HTMLDivElement>(null);
|
|
35
31
|
|
|
36
32
|
// Load prompt
|
|
37
33
|
useEffect(() => {
|
|
38
34
|
setTitle(task.title);
|
|
39
35
|
setDescription(task.description);
|
|
40
|
-
setShowChat(task.status === 'testing');
|
|
41
36
|
fetch(`${basePath}/prompt`)
|
|
42
37
|
.then(r => r.json())
|
|
43
38
|
.then(data => setPromptContent(data.content || ''));
|
|
@@ -97,113 +92,128 @@ export default function TaskDetail({
|
|
|
97
92
|
|
|
98
93
|
return (
|
|
99
94
|
<div className="flex flex-col h-full">
|
|
100
|
-
{/*
|
|
101
|
-
<div className=
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
<
|
|
105
|
-
{
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
95
|
+
{/* Compact header: Title + Status + Actions */}
|
|
96
|
+
<div className="px-4 py-3 border-b border-border flex-shrink-0 space-y-2">
|
|
97
|
+
{/* Title */}
|
|
98
|
+
{editingTitle ? (
|
|
99
|
+
<input
|
|
100
|
+
value={title}
|
|
101
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
102
|
+
onBlur={saveTitle}
|
|
103
|
+
onKeyDown={(e) => { if (e.key === 'Enter') saveTitle(); if (e.key === 'Escape') { setTitle(task.title); setEditingTitle(false); } }}
|
|
104
|
+
className="w-full bg-transparent text-lg font-semibold border-b border-primary
|
|
105
|
+
focus:outline-none pb-1 text-foreground"
|
|
106
|
+
autoFocus
|
|
107
|
+
/>
|
|
108
|
+
) : (
|
|
109
|
+
<h2
|
|
110
|
+
onClick={() => setEditingTitle(true)}
|
|
111
|
+
className="text-lg font-semibold cursor-text hover:text-primary transition-colors"
|
|
112
|
+
>
|
|
113
|
+
{task.title}
|
|
114
|
+
</h2>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
{/* Status + Priority + Today + Prompt + Delete */}
|
|
118
|
+
<div className="flex items-center gap-3 flex-wrap">
|
|
119
|
+
<StatusFlow status={task.status} onChange={(status: TaskStatus) => onUpdate({ status })} />
|
|
120
|
+
<div className="flex items-center gap-1">
|
|
121
|
+
{priorities.map(p => (
|
|
122
|
+
<button
|
|
123
|
+
key={p}
|
|
124
|
+
onClick={() => onUpdate({ priority: p })}
|
|
125
|
+
className={`px-2 py-0.5 text-xs rounded transition-colors ${
|
|
126
|
+
task.priority === p
|
|
127
|
+
? p === 'high' ? 'bg-destructive/20 text-destructive' : p === 'medium' ? 'bg-warning/20 text-warning' : 'bg-muted text-muted-foreground'
|
|
128
|
+
: 'text-muted-foreground/40 hover:text-muted-foreground'
|
|
129
|
+
}`}
|
|
119
130
|
>
|
|
120
|
-
{
|
|
121
|
-
</
|
|
122
|
-
)}
|
|
123
|
-
</div>
|
|
124
|
-
|
|
125
|
-
{/* Status + Priority + Today */}
|
|
126
|
-
<div className="flex items-center gap-4 flex-wrap">
|
|
127
|
-
<StatusFlow status={task.status} onChange={(status: TaskStatus) => onUpdate({ status })} />
|
|
128
|
-
<div className="flex items-center gap-1">
|
|
129
|
-
{priorities.map(p => (
|
|
130
|
-
<button
|
|
131
|
-
key={p}
|
|
132
|
-
onClick={() => onUpdate({ priority: p })}
|
|
133
|
-
className={`px-2.5 py-1 text-sm rounded transition-colors ${
|
|
134
|
-
task.priority === p
|
|
135
|
-
? p === 'high' ? 'bg-destructive/20 text-destructive' : p === 'medium' ? 'bg-warning/20 text-warning' : 'bg-muted text-muted-foreground'
|
|
136
|
-
: 'text-muted-foreground/40 hover:text-muted-foreground'
|
|
137
|
-
}`}
|
|
138
|
-
>
|
|
139
|
-
{p}
|
|
140
|
-
</button>
|
|
141
|
-
))}
|
|
142
|
-
</div>
|
|
143
|
-
<button
|
|
144
|
-
onClick={() => onUpdate({ is_today: !task.is_today })}
|
|
145
|
-
className={`text-sm px-2.5 py-1 rounded transition-colors ${
|
|
146
|
-
task.is_today
|
|
147
|
-
? 'bg-primary/20 text-primary'
|
|
148
|
-
: 'text-muted-foreground hover:text-foreground'
|
|
149
|
-
}`}
|
|
150
|
-
>
|
|
151
|
-
{task.is_today ? 'Today *' : 'Mark today'}
|
|
152
|
-
</button>
|
|
153
|
-
</div>
|
|
154
|
-
|
|
155
|
-
{/* Description */}
|
|
156
|
-
<div>
|
|
157
|
-
<textarea
|
|
158
|
-
value={description}
|
|
159
|
-
onChange={(e) => setDescription(e.target.value)}
|
|
160
|
-
onBlur={saveDescription}
|
|
161
|
-
placeholder="Background, conditions, notes..."
|
|
162
|
-
className="w-full bg-input border border-border rounded-lg px-3 py-2.5 text-sm
|
|
163
|
-
focus:border-primary focus:outline-none text-foreground resize-y min-h-[60px]
|
|
164
|
-
leading-relaxed"
|
|
165
|
-
rows={3}
|
|
166
|
-
/>
|
|
131
|
+
{p}
|
|
132
|
+
</button>
|
|
133
|
+
))}
|
|
167
134
|
</div>
|
|
135
|
+
<button
|
|
136
|
+
onClick={() => onUpdate({ is_today: !task.is_today })}
|
|
137
|
+
className={`text-xs px-2 py-0.5 rounded transition-colors ${
|
|
138
|
+
task.is_today
|
|
139
|
+
? 'bg-primary/20 text-primary'
|
|
140
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
141
|
+
}`}
|
|
142
|
+
>
|
|
143
|
+
{task.is_today ? 'Today *' : 'Mark today'}
|
|
144
|
+
</button>
|
|
145
|
+
|
|
146
|
+
<span className="text-border">|</span>
|
|
147
|
+
|
|
148
|
+
<button
|
|
149
|
+
onClick={() => setShowPromptModal(true)}
|
|
150
|
+
className={`text-xs px-2 py-0.5 rounded transition-colors border ${
|
|
151
|
+
promptContent
|
|
152
|
+
? 'bg-accent/15 text-accent border-accent/30 hover:bg-accent/25'
|
|
153
|
+
: 'text-muted-foreground border-border hover:text-foreground hover:border-muted-foreground'
|
|
154
|
+
}`}
|
|
155
|
+
>
|
|
156
|
+
Prompt{promptContent ? ' *' : ''}
|
|
157
|
+
</button>
|
|
158
|
+
|
|
159
|
+
<button
|
|
160
|
+
onClick={onDelete}
|
|
161
|
+
className="text-xs text-muted-foreground hover:text-destructive transition-colors ml-auto"
|
|
162
|
+
>
|
|
163
|
+
Delete
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
168
166
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
167
|
+
{/* Description - compact */}
|
|
168
|
+
<textarea
|
|
169
|
+
value={description}
|
|
170
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
171
|
+
onBlur={saveDescription}
|
|
172
|
+
placeholder="Background, conditions, notes..."
|
|
173
|
+
className="w-full bg-input border border-border rounded-lg px-3 py-2 text-sm
|
|
174
|
+
focus:border-primary focus:outline-none text-foreground resize-none
|
|
175
|
+
leading-relaxed"
|
|
176
|
+
rows={2}
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
176
179
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
: 'text-muted-foreground hover:text-foreground border-border hover:border-muted-foreground'
|
|
185
|
-
}`}
|
|
186
|
-
>
|
|
187
|
-
{showChat ? 'Hide AI Chat' : 'AI Chat'}
|
|
188
|
-
</button>
|
|
189
|
-
<button
|
|
190
|
-
onClick={onDelete}
|
|
191
|
-
className="text-xs text-muted-foreground hover:text-destructive transition-colors"
|
|
192
|
-
>
|
|
193
|
-
Delete task
|
|
194
|
-
</button>
|
|
195
|
-
</div>
|
|
196
|
-
</div>
|
|
180
|
+
{/* AI Chat - takes remaining space */}
|
|
181
|
+
<div className="flex-1 min-h-0">
|
|
182
|
+
<TaskChat
|
|
183
|
+
basePath={basePath}
|
|
184
|
+
taskStatus={task.status}
|
|
185
|
+
onApplyToPrompt={handleApplyToPrompt}
|
|
186
|
+
/>
|
|
197
187
|
</div>
|
|
198
188
|
|
|
199
|
-
{/*
|
|
200
|
-
{
|
|
201
|
-
<div
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
189
|
+
{/* Prompt Modal */}
|
|
190
|
+
{showPromptModal && (
|
|
191
|
+
<div
|
|
192
|
+
ref={overlayRef}
|
|
193
|
+
onClick={(e) => { if (e.target === overlayRef.current) setShowPromptModal(false); }}
|
|
194
|
+
className="fixed inset-0 z-50 flex items-center justify-center"
|
|
195
|
+
style={{ background: 'rgba(0,0,0,0.5)', backdropFilter: 'blur(2px)' }}
|
|
196
|
+
>
|
|
197
|
+
<div className="bg-card border border-border rounded-xl shadow-2xl shadow-black/40
|
|
198
|
+
w-full max-w-2xl mx-4 max-h-[80vh] flex flex-col animate-dialog-in">
|
|
199
|
+
<div className="flex items-center justify-between px-5 py-3 border-b border-border">
|
|
200
|
+
<h3 className="text-sm font-semibold text-foreground">Prompt</h3>
|
|
201
|
+
<button
|
|
202
|
+
onClick={() => setShowPromptModal(false)}
|
|
203
|
+
className="text-muted-foreground hover:text-foreground transition-colors text-sm"
|
|
204
|
+
>
|
|
205
|
+
Close
|
|
206
|
+
</button>
|
|
207
|
+
</div>
|
|
208
|
+
<div className="flex-1 overflow-y-auto p-5">
|
|
209
|
+
<PromptEditor
|
|
210
|
+
content={promptContent}
|
|
211
|
+
onSave={savePrompt}
|
|
212
|
+
onRefine={handleRefine}
|
|
213
|
+
refining={refining}
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
207
217
|
</div>
|
|
208
218
|
)}
|
|
209
219
|
</div>
|