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.7.10",
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
@@ -1,4 +1,4 @@
1
- const CACHE_NAME = 'im-v3';
1
+ const CACHE_NAME = 'im-v4';
2
2
 
3
3
  self.addEventListener('install', (event) => {
4
4
  self.skipWaiting();
@@ -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
- {content.split('\n').map((line, i) => (
20
- <p key={i} className={i > 0 ? 'mt-1' : ''}>
21
- {line}
22
- </p>
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 [showChat, setShowChat] = useState(false);
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
- {/* Upper: Task info + Prompt */}
101
- <div className={`overflow-y-auto ${showChat ? 'flex-1 min-h-0' : 'flex-1'}`}>
102
- <div className="p-4 space-y-4">
103
- {/* Title */}
104
- <div>
105
- {editingTitle ? (
106
- <input
107
- value={title}
108
- onChange={(e) => setTitle(e.target.value)}
109
- onBlur={saveTitle}
110
- onKeyDown={(e) => { if (e.key === 'Enter') saveTitle(); if (e.key === 'Escape') { setTitle(task.title); setEditingTitle(false); } }}
111
- className="w-full bg-transparent text-xl font-semibold border-b border-primary
112
- focus:outline-none pb-1 text-foreground"
113
- autoFocus
114
- />
115
- ) : (
116
- <h2
117
- onClick={() => setEditingTitle(true)}
118
- className="text-xl font-semibold cursor-text hover:text-primary transition-colors"
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
- {task.title}
121
- </h2>
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
- {/* Prompt */}
170
- <PromptEditor
171
- content={promptContent}
172
- onSave={savePrompt}
173
- onRefine={handleRefine}
174
- refining={refining}
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
- {/* Actions */}
178
- <div className="pt-4 border-t border-border flex items-center justify-between">
179
- <button
180
- onClick={() => setShowChat(!showChat)}
181
- className={`text-xs px-2.5 py-1 rounded-md transition-colors border ${
182
- showChat
183
- ? 'bg-accent/20 text-accent border-accent/30'
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
- {/* Lower: AI Chat */}
200
- {showChat && (
201
- <div className="h-[45%] flex-shrink-0">
202
- <TaskChat
203
- basePath={basePath}
204
- taskStatus={task.status}
205
- onApplyToPrompt={handleApplyToPrompt}
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>