idea-manager 0.3.2 → 0.5.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 (43) hide show
  1. package/package.json +2 -1
  2. package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.ts +13 -3
  3. package/src/app/globals.css +72 -0
  4. package/src/app/projects/[id]/page.tsx +34 -6
  5. package/src/components/brainstorm/Editor.tsx +1 -84
  6. package/src/components/task/TaskChat.tsx +8 -5
  7. package/src/components/ui/AiPolicyModal.tsx +98 -0
  8. package/src/lib/ai/client.ts +45 -172
  9. package/src/lib/db/queries/projects.ts +3 -2
  10. package/src/lib/db/schema.ts +3 -70
  11. package/src/types/index.ts +1 -90
  12. package/src/app/api/projects/[id]/cleanup/route.ts +0 -32
  13. package/src/app/api/projects/[id]/conversations/route.ts +0 -50
  14. package/src/app/api/projects/[id]/items/[itemId]/prompt/route.ts +0 -51
  15. package/src/app/api/projects/[id]/items/[itemId]/refine/route.ts +0 -36
  16. package/src/app/api/projects/[id]/items/[itemId]/route.ts +0 -95
  17. package/src/app/api/projects/[id]/items/route.ts +0 -67
  18. package/src/app/api/projects/[id]/memos/route.ts +0 -18
  19. package/src/app/api/projects/[id]/scan/route.ts +0 -73
  20. package/src/app/api/projects/[id]/scan/stream/route.ts +0 -112
  21. package/src/app/api/projects/[id]/structure/route.ts +0 -59
  22. package/src/app/api/projects/[id]/structure/stream/route.ts +0 -157
  23. package/src/components/ScanPanel.tsx +0 -743
  24. package/src/components/brainstorm/MemoPin.tsx +0 -117
  25. package/src/components/tree/CardView.tsx +0 -206
  26. package/src/components/tree/ItemDetail.tsx +0 -196
  27. package/src/components/tree/LockToggle.tsx +0 -23
  28. package/src/components/tree/RefinePopover.tsx +0 -157
  29. package/src/components/tree/StatusBadge.tsx +0 -32
  30. package/src/components/tree/TreeNode.tsx +0 -227
  31. package/src/components/tree/TreeView.tsx +0 -304
  32. package/src/lib/ai/chat-responder.ts +0 -71
  33. package/src/lib/ai/cleanup.ts +0 -87
  34. package/src/lib/ai/prompter.ts +0 -78
  35. package/src/lib/ai/refiner.ts +0 -128
  36. package/src/lib/ai/structurer.ts +0 -403
  37. package/src/lib/db/queries/context.ts +0 -76
  38. package/src/lib/db/queries/conversations.ts +0 -46
  39. package/src/lib/db/queries/items.ts +0 -268
  40. package/src/lib/db/queries/memos.ts +0 -66
  41. package/src/lib/db/queries/prompts.ts +0 -68
  42. package/src/lib/scanner.ts +0 -573
  43. package/src/lib/task-store.ts +0 -97
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "idea-manager",
3
- "version": "0.3.2",
3
+ "version": "0.5.0",
4
4
  "description": "AI 기반 브레인스토밍 → 구조화 → 프롬프트 생성 도구. MCP Server 내장.",
5
5
  "keywords": [
6
6
  "brainstorm",
@@ -46,6 +46,7 @@
46
46
  "open": "^11.0.0",
47
47
  "react": "19.2.3",
48
48
  "react-dom": "19.2.3",
49
+ "react-markdown": "^10.1.0",
49
50
  "tsx": "^4.21.0"
50
51
  },
51
52
  "devDependencies": {
@@ -3,6 +3,7 @@ import { getTaskConversations, addTaskConversation } from '@/lib/db/queries/task
3
3
  import { getTask } from '@/lib/db/queries/tasks';
4
4
  import { getTaskPrompt } from '@/lib/db/queries/task-prompts';
5
5
  import { getBrainstorm } from '@/lib/db/queries/brainstorms';
6
+ import { getProject } from '@/lib/db/queries/projects';
6
7
  import { runClaude } from '@/lib/ai/client';
7
8
 
8
9
  export async function GET(
@@ -37,9 +38,12 @@ export async function POST(
37
38
  const history = getTaskConversations(taskId);
38
39
  const prompt = getTaskPrompt(taskId);
39
40
  const brainstorm = getBrainstorm(projectId);
41
+ const project = getProject(projectId);
40
42
 
41
- const systemPrompt = `You are a helpful assistant helping refine a development task. Respond in Korean. Be concise.
43
+ const aiPolicy = project?.ai_context ? `\n\nProject AI Policy:\n${project.ai_context}` : '';
42
44
 
45
+ const systemPrompt = `You are a helpful assistant helping refine a development task. Respond in Korean. Be concise.
46
+ ${aiPolicy}
43
47
  Task: ${task.title}
44
48
  Description: ${task.description}
45
49
  Status: ${task.status}
@@ -52,9 +56,15 @@ ${brainstorm?.content ? `\nBrainstorming context:\n${brainstorm.content.slice(0,
52
56
 
53
57
  try {
54
58
  const aiResponse = await runClaude(`${systemPrompt}\n\nConversation:\n${conversationText}`);
55
- const aiMsg = addTaskConversation(taskId, 'assistant', aiResponse.trim());
59
+ const trimmed = aiResponse.trim();
60
+ if (!trimmed) {
61
+ const fallbackMsg = addTaskConversation(taskId, 'assistant', '(AI 응답을 생성하지 못했습니다. 다시 시도해주세요.)');
62
+ return NextResponse.json({ userMessage: userMsg, aiMessage: fallbackMsg });
63
+ }
64
+ const aiMsg = addTaskConversation(taskId, 'assistant', trimmed);
56
65
  return NextResponse.json({ userMessage: userMsg, aiMessage: aiMsg });
57
66
  } catch {
58
- return NextResponse.json({ error: 'AI response failed' }, { status: 500 });
67
+ const errorMsg = addTaskConversation(taskId, 'assistant', '(AI 호출에 실패했습니다. Claude CLI가 설치되어 있는지 확인해주세요.)');
68
+ return NextResponse.json({ userMessage: userMsg, aiMessage: errorMsg });
59
69
  }
60
70
  }
@@ -886,6 +886,78 @@ textarea:focus {
886
886
  background: hsl(var(--primary) / 0.6);
887
887
  }
888
888
 
889
+ /* Chat markdown styling */
890
+ .chat-markdown p {
891
+ margin: 0.25em 0;
892
+ }
893
+
894
+ .chat-markdown p:first-child {
895
+ margin-top: 0;
896
+ }
897
+
898
+ .chat-markdown p:last-child {
899
+ margin-bottom: 0;
900
+ }
901
+
902
+ .chat-markdown strong {
903
+ font-weight: 700;
904
+ color: hsl(var(--foreground));
905
+ }
906
+
907
+ .chat-markdown code {
908
+ background: hsl(var(--background));
909
+ padding: 1px 5px;
910
+ border-radius: 4px;
911
+ font-size: 0.9em;
912
+ font-family: var(--font-mono);
913
+ }
914
+
915
+ .chat-markdown pre {
916
+ background: hsl(var(--background));
917
+ padding: 8px 12px;
918
+ border-radius: 6px;
919
+ overflow-x: auto;
920
+ margin: 0.5em 0;
921
+ }
922
+
923
+ .chat-markdown pre code {
924
+ background: none;
925
+ padding: 0;
926
+ }
927
+
928
+ .chat-markdown ul, .chat-markdown ol {
929
+ padding-left: 1.4em;
930
+ margin: 0.3em 0;
931
+ }
932
+
933
+ .chat-markdown li {
934
+ margin: 0.15em 0;
935
+ }
936
+
937
+ .chat-markdown h1, .chat-markdown h2, .chat-markdown h3,
938
+ .chat-markdown h4, .chat-markdown h5, .chat-markdown h6 {
939
+ font-weight: 700;
940
+ margin: 0.5em 0 0.25em;
941
+ line-height: 1.3;
942
+ }
943
+
944
+ .chat-markdown h1 { font-size: 1.2em; }
945
+ .chat-markdown h2 { font-size: 1.1em; }
946
+ .chat-markdown h3 { font-size: 1.05em; }
947
+
948
+ .chat-markdown blockquote {
949
+ border-left: 3px solid hsl(var(--border));
950
+ padding-left: 10px;
951
+ margin: 0.4em 0;
952
+ color: hsl(var(--muted-foreground));
953
+ }
954
+
955
+ .chat-markdown hr {
956
+ border: none;
957
+ border-top: 1px solid hsl(var(--border));
958
+ margin: 0.5em 0;
959
+ }
960
+
889
961
  /* Dialog animation */
890
962
  @keyframes dialogIn {
891
963
  from { opacity: 0; transform: scale(0.95) translateY(4px); }
@@ -7,6 +7,7 @@ import ProjectTree from '@/components/task/ProjectTree';
7
7
  import TaskDetail from '@/components/task/TaskDetail';
8
8
  import DirectoryPicker from '@/components/DirectoryPicker';
9
9
  import ConfirmDialog from '@/components/ui/ConfirmDialog';
10
+ import AiPolicyModal from '@/components/ui/AiPolicyModal';
10
11
  import type { ISubProject, ITask, TaskStatus, ISubProjectWithStats } from '@/types';
11
12
 
12
13
  interface IProject {
@@ -14,6 +15,7 @@ interface IProject {
14
15
  name: string;
15
16
  description: string;
16
17
  project_path: string | null;
18
+ ai_context: string;
17
19
  }
18
20
 
19
21
  function WorkspaceInner({ id }: { id: string }) {
@@ -32,10 +34,11 @@ function WorkspaceInner({ id }: { id: string }) {
32
34
  const [showAddSub, setShowAddSub] = useState(false);
33
35
  const [showBrainstorm, setShowBrainstorm] = useState(true);
34
36
  const [newSubName, setNewSubName] = useState('');
37
+ const [showAiPolicy, setShowAiPolicy] = useState(false);
35
38
 
36
39
  // Resizable panel widths
37
- const [leftWidth, setLeftWidth] = useState(280);
38
- const [centerWidth, setCenterWidth] = useState(280);
40
+ const [leftWidth, setLeftWidth] = useState(500);
41
+ const [centerWidth, setCenterWidth] = useState(500);
39
42
  const containerRef = useRef<HTMLDivElement>(null);
40
43
  const draggingRef = useRef<'left' | 'center' | null>(null);
41
44
  const startXRef = useRef(0);
@@ -221,6 +224,18 @@ function WorkspaceInner({ id }: { id: string }) {
221
224
  }
222
225
  };
223
226
 
227
+ const handleSaveAiPolicy = async (aiContext: string) => {
228
+ const res = await fetch(`/api/projects/${id}`, {
229
+ method: 'PUT',
230
+ headers: { 'Content-Type': 'application/json' },
231
+ body: JSON.stringify({ ai_context: aiContext }),
232
+ });
233
+ if (res.ok) {
234
+ setProject(await res.json());
235
+ setShowAiPolicy(false);
236
+ }
237
+ };
238
+
224
239
  // Keyboard shortcuts (use e.code for Korean IME compatibility)
225
240
  useEffect(() => {
226
241
  const handler = (e: KeyboardEvent) => {
@@ -289,6 +304,16 @@ function WorkspaceInner({ id }: { id: string }) {
289
304
  )}
290
305
  </div>
291
306
  <div className="flex items-center gap-2">
307
+ <button
308
+ onClick={() => setShowAiPolicy(true)}
309
+ className={`px-3 py-1.5 text-xs border rounded-md transition-colors ${
310
+ project.ai_context
311
+ ? 'bg-accent/15 text-accent border-accent/30 hover:bg-accent/25'
312
+ : 'bg-muted hover:bg-card-hover text-muted-foreground border-border'
313
+ }`}
314
+ >
315
+ AI Policy{project.ai_context ? ' *' : ''}
316
+ </button>
292
317
  {!project.project_path && (
293
318
  <button
294
319
  onClick={() => setShowDirPicker(true)}
@@ -309,10 +334,6 @@ function WorkspaceInner({ id }: { id: string }) {
309
334
  <div style={{ width: leftWidth }} className="border-r border-border flex flex-col flex-shrink-0">
310
335
  <Editor
311
336
  projectId={id}
312
- onContentChange={() => {}}
313
- onSendMessage={() => {}}
314
- memos={[]}
315
- chatLoading={false}
316
337
  onCollapse={() => setShowBrainstorm(false)}
317
338
  />
318
339
  </div>
@@ -418,6 +439,13 @@ function WorkspaceInner({ id }: { id: string }) {
418
439
  onConfirm={handleConfirmAction}
419
440
  onCancel={() => setConfirmAction(null)}
420
441
  />
442
+
443
+ <AiPolicyModal
444
+ open={showAiPolicy}
445
+ content={project.ai_context || ''}
446
+ onSave={handleSaveAiPolicy}
447
+ onClose={() => setShowAiPolicy(false)}
448
+ />
421
449
  </div>
422
450
  );
423
451
  }
@@ -1,39 +1,18 @@
1
1
  'use client';
2
2
 
3
3
  import { useState, useEffect, useRef, useCallback } from 'react';
4
- import MemoPin from './MemoPin';
5
-
6
- interface Memo {
7
- id: string;
8
- anchor_text: string;
9
- question: string;
10
- is_resolved: boolean;
11
- }
12
4
 
13
5
  interface EditorProps {
14
6
  projectId: string;
15
- onContentChange: (content: string) => void;
16
- onSendMessage: (message: string) => void;
17
- memos?: Memo[];
18
- chatLoading?: boolean;
19
7
  onCollapse?: () => void;
20
8
  }
21
9
 
22
- interface PinPosition {
23
- memo: Memo;
24
- top: number;
25
- left: number;
26
- }
27
-
28
- export default function Editor({ projectId, onContentChange, onSendMessage, memos = [], chatLoading, onCollapse }: EditorProps) {
10
+ export default function Editor({ projectId, onCollapse }: EditorProps) {
29
11
  const [content, setContent] = useState('');
30
12
  const [saving, setSaving] = useState(false);
31
13
  const [loaded, setLoaded] = useState(false);
32
- const [pinPositions, setPinPositions] = useState<PinPosition[]>([]);
33
14
  const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
34
- const structureTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
35
15
  const textareaRef = useRef<HTMLTextAreaElement>(null);
36
- const overlayRef = useRef<HTMLDivElement>(null);
37
16
 
38
17
  // Load brainstorm content
39
18
  useEffect(() => {
@@ -46,37 +25,6 @@ export default function Editor({ projectId, onContentChange, onSendMessage, memo
46
25
  load();
47
26
  }, [projectId]);
48
27
 
49
- // Calculate pin positions when memos or content change
50
- useEffect(() => {
51
- if (!textareaRef.current || !content) {
52
- setPinPositions([]);
53
- return;
54
- }
55
-
56
- const textarea = textareaRef.current;
57
- const unresolvedMemos = memos.filter(m => !m.is_resolved);
58
- const positions: PinPosition[] = [];
59
-
60
- for (const memo of unresolvedMemos) {
61
- const idx = content.indexOf(memo.anchor_text);
62
- if (idx === -1) continue;
63
-
64
- // Calculate approximate position based on character index
65
- const textBefore = content.substring(0, idx);
66
- const lines = textBefore.split('\n');
67
- const lineNumber = lines.length - 1;
68
- const lineHeight = parseFloat(getComputedStyle(textarea).lineHeight) || 22;
69
- const paddingTop = parseFloat(getComputedStyle(textarea).paddingTop) || 16;
70
-
71
- const top = paddingTop + lineNumber * lineHeight;
72
- const left = textarea.clientWidth - 28;
73
-
74
- positions.push({ memo, top, left });
75
- }
76
-
77
- setPinPositions(positions);
78
- }, [memos, content]);
79
-
80
28
  const saveContent = useCallback(async (text: string) => {
81
29
  setSaving(true);
82
30
  await fetch(`/api/projects/${projectId}/brainstorm`, {
@@ -96,21 +44,6 @@ export default function Editor({ projectId, onContentChange, onSendMessage, memo
96
44
  saveTimerRef.current = setTimeout(() => {
97
45
  saveContent(newContent);
98
46
  }, 1000);
99
-
100
- // Trigger AI structuring with 3s debounce
101
- if (structureTimerRef.current) clearTimeout(structureTimerRef.current);
102
- if (newContent.trim()) {
103
- structureTimerRef.current = setTimeout(() => {
104
- onContentChange(newContent);
105
- }, 3000);
106
- }
107
- };
108
-
109
- // Sync scroll between textarea and overlay
110
- const handleScroll = () => {
111
- if (textareaRef.current && overlayRef.current) {
112
- overlayRef.current.scrollTop = textareaRef.current.scrollTop;
113
- }
114
47
  };
115
48
 
116
49
  if (!loaded) {
@@ -145,7 +78,6 @@ export default function Editor({ projectId, onContentChange, onSendMessage, memo
145
78
  ref={textareaRef}
146
79
  value={content}
147
80
  onChange={handleChange}
148
- onScroll={handleScroll}
149
81
  placeholder={`자유롭게 아이디어를 적어보세요...
150
82
 
151
83
  예시:
@@ -158,21 +90,6 @@ export default function Editor({ projectId, onContentChange, onSendMessage, memo
158
90
  placeholder:text-muted-foreground/40 font-mono text-sm leading-relaxed"
159
91
  spellCheck={false}
160
92
  />
161
- {pinPositions.length > 0 && (
162
- <div ref={overlayRef} className="memo-overlay">
163
- {pinPositions.map((pin) => (
164
- <MemoPin
165
- key={pin.memo.id}
166
- question={pin.memo.question}
167
- anchorText={pin.memo.anchor_text}
168
- top={pin.top}
169
- left={pin.left}
170
- loading={chatLoading}
171
- onSendMessage={onSendMessage}
172
- />
173
- ))}
174
- </div>
175
- )}
176
93
  </div>
177
94
  </div>
178
95
  );
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useState, useEffect, useRef, useCallback } from 'react';
4
4
  import type { ITaskConversation } from '@/types';
5
+ import ReactMarkdown from 'react-markdown';
5
6
 
6
7
  export default function TaskChat({
7
8
  basePath,
@@ -79,14 +80,16 @@ export default function TaskChat({
79
80
  Ask AI to help refine your task or prompt
80
81
  </div>
81
82
  )}
82
- {messages.map((msg) => (
83
+ {messages.filter(msg => msg.content).map((msg) => (
83
84
  <div key={msg.id} className={`flex flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}>
84
- <div className={`max-w-[90%] px-3 py-2 rounded-lg text-sm leading-relaxed whitespace-pre-wrap ${
85
+ <div className={`max-w-[90%] px-3 py-2 rounded-lg text-sm leading-relaxed ${
85
86
  msg.role === 'user'
86
- ? 'bg-accent text-white rounded-br-sm'
87
- : 'bg-muted text-foreground rounded-bl-sm'
87
+ ? 'bg-accent text-white rounded-br-sm whitespace-pre-wrap'
88
+ : 'bg-muted text-foreground rounded-bl-sm chat-markdown'
88
89
  }`}>
89
- {msg.content}
90
+ {msg.role === 'assistant'
91
+ ? <ReactMarkdown>{msg.content}</ReactMarkdown>
92
+ : msg.content}
90
93
  </div>
91
94
  {msg.role === 'assistant' && (
92
95
  <button
@@ -0,0 +1,98 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef } from 'react';
4
+
5
+ export default function AiPolicyModal({
6
+ open,
7
+ content,
8
+ onSave,
9
+ onClose,
10
+ }: {
11
+ open: boolean;
12
+ content: string;
13
+ onSave: (content: string) => void;
14
+ onClose: () => void;
15
+ }) {
16
+ const [draft, setDraft] = useState(content);
17
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
18
+
19
+ useEffect(() => {
20
+ if (open) {
21
+ setDraft(content);
22
+ setTimeout(() => textareaRef.current?.focus(), 50);
23
+ }
24
+ }, [open, content]);
25
+
26
+ useEffect(() => {
27
+ if (!open) return;
28
+ const handler = (e: KeyboardEvent) => {
29
+ if (e.key === 'Escape') onClose();
30
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
31
+ e.preventDefault();
32
+ onSave(draft);
33
+ }
34
+ };
35
+ window.addEventListener('keydown', handler);
36
+ return () => window.removeEventListener('keydown', handler);
37
+ }, [open, draft, onSave, onClose]);
38
+
39
+ if (!open) return null;
40
+
41
+ return (
42
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
43
+ <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
44
+ <div className="relative bg-card border border-border rounded-xl shadow-2xl w-[640px] max-h-[80vh] flex flex-col animate-dialog-in">
45
+ <div className="flex items-center justify-between px-5 py-3 border-b border-border">
46
+ <div>
47
+ <h3 className="text-sm font-semibold">AI Policy</h3>
48
+ <p className="text-xs text-muted-foreground mt-0.5">
49
+ AI 채팅과 프롬프트 다듬기에 항상 포함되는 프로젝트 컨텍스트
50
+ </p>
51
+ </div>
52
+ <button onClick={onClose} className="text-muted-foreground hover:text-foreground text-lg px-1">
53
+ x
54
+ </button>
55
+ </div>
56
+
57
+ <div className="flex-1 p-4 overflow-y-auto">
58
+ <textarea
59
+ ref={textareaRef}
60
+ value={draft}
61
+ onChange={(e) => setDraft(e.target.value)}
62
+ placeholder={`프로젝트 컨텍스트와 AI 지침을 작성하세요.
63
+
64
+ 예시:
65
+ - 이 프로젝트는 JABIS 스마트워크 시스템입니다
66
+ - 기술 스택: React + TypeScript + Vite (monorepo)
67
+ - DB: PostgreSQL (jabis 스키마)
68
+ - 한국어로 응답할 것
69
+ - 코드 제안 시 기존 컨벤션을 따를 것`}
70
+ className="w-full bg-input border border-border rounded-lg px-4 py-3 text-sm
71
+ text-foreground resize-none focus:border-primary focus:outline-none
72
+ leading-relaxed font-mono min-h-[300px]"
73
+ />
74
+ </div>
75
+
76
+ <div className="flex items-center justify-between px-5 py-3 border-t border-border">
77
+ <span className="text-xs text-muted-foreground">Cmd+Enter to save</span>
78
+ <div className="flex items-center gap-2">
79
+ <button
80
+ onClick={onClose}
81
+ className="px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground
82
+ border border-border rounded-md transition-colors"
83
+ >
84
+ Cancel
85
+ </button>
86
+ <button
87
+ onClick={() => onSave(draft)}
88
+ className="px-3 py-1.5 text-xs bg-primary text-white rounded-md
89
+ hover:bg-primary-hover transition-colors"
90
+ >
91
+ Save
92
+ </button>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ );
98
+ }