idea-manager 0.4.0 → 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 (38) hide show
  1. package/package.json +1 -1
  2. package/src/app/projects/[id]/page.tsx +0 -4
  3. package/src/components/brainstorm/Editor.tsx +1 -84
  4. package/src/lib/ai/client.ts +0 -123
  5. package/src/lib/db/schema.ts +0 -70
  6. package/src/types/index.ts +0 -90
  7. package/src/app/api/projects/[id]/cleanup/route.ts +0 -32
  8. package/src/app/api/projects/[id]/conversations/route.ts +0 -50
  9. package/src/app/api/projects/[id]/items/[itemId]/prompt/route.ts +0 -51
  10. package/src/app/api/projects/[id]/items/[itemId]/refine/route.ts +0 -36
  11. package/src/app/api/projects/[id]/items/[itemId]/route.ts +0 -95
  12. package/src/app/api/projects/[id]/items/route.ts +0 -67
  13. package/src/app/api/projects/[id]/memos/route.ts +0 -18
  14. package/src/app/api/projects/[id]/scan/route.ts +0 -73
  15. package/src/app/api/projects/[id]/scan/stream/route.ts +0 -112
  16. package/src/app/api/projects/[id]/structure/route.ts +0 -59
  17. package/src/app/api/projects/[id]/structure/stream/route.ts +0 -157
  18. package/src/components/ScanPanel.tsx +0 -743
  19. package/src/components/brainstorm/MemoPin.tsx +0 -117
  20. package/src/components/tree/CardView.tsx +0 -206
  21. package/src/components/tree/ItemDetail.tsx +0 -196
  22. package/src/components/tree/LockToggle.tsx +0 -23
  23. package/src/components/tree/RefinePopover.tsx +0 -157
  24. package/src/components/tree/StatusBadge.tsx +0 -32
  25. package/src/components/tree/TreeNode.tsx +0 -227
  26. package/src/components/tree/TreeView.tsx +0 -304
  27. package/src/lib/ai/chat-responder.ts +0 -71
  28. package/src/lib/ai/cleanup.ts +0 -87
  29. package/src/lib/ai/prompter.ts +0 -78
  30. package/src/lib/ai/refiner.ts +0 -128
  31. package/src/lib/ai/structurer.ts +0 -403
  32. package/src/lib/db/queries/context.ts +0 -76
  33. package/src/lib/db/queries/conversations.ts +0 -46
  34. package/src/lib/db/queries/items.ts +0 -268
  35. package/src/lib/db/queries/memos.ts +0 -66
  36. package/src/lib/db/queries/prompts.ts +0 -68
  37. package/src/lib/scanner.ts +0 -573
  38. package/src/lib/task-store.ts +0 -97
@@ -1,117 +0,0 @@
1
- 'use client';
2
-
3
- import { useState, useRef, useEffect } from 'react';
4
-
5
- interface MemoPinProps {
6
- question: string;
7
- anchorText: string;
8
- top: number;
9
- left: number;
10
- loading?: boolean;
11
- onSendMessage?: (message: string) => void;
12
- }
13
-
14
- export default function MemoPin({ question, anchorText, top, left, loading, onSendMessage }: MemoPinProps) {
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
- };
58
-
59
- return (
60
- <div
61
- ref={popoverRef}
62
- className="memo-pin"
63
- style={{ top: `${top}px`, left: `${left}px` }}
64
- onMouseEnter={() => !open && setShowTooltip(true)}
65
- onMouseLeave={() => setShowTooltip(false)}
66
- >
67
- <span className="memo-pin-icon" onClick={handleClick}>&#x1F4CC;</span>
68
-
69
- {/* Hover tooltip (only when popover is closed) */}
70
- {showTooltip && !open && (
71
- <div className="memo-tooltip">
72
- <div className="memo-tooltip-anchor">&ldquo;{anchorText}&rdquo;</div>
73
- <div className="memo-tooltip-question">{question}</div>
74
- </div>
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
- )}
115
- </div>
116
- );
117
- }
@@ -1,206 +0,0 @@
1
- 'use client';
2
-
3
- import { useState } from 'react';
4
- import StatusBadge from './StatusBadge';
5
-
6
- interface IItemTree {
7
- id: string;
8
- title: string;
9
- description: string;
10
- item_type: string;
11
- priority: string;
12
- status: string;
13
- is_locked: boolean;
14
- is_pinned: boolean;
15
- children: IItemTree[];
16
- }
17
-
18
- interface CardViewProps {
19
- items: IItemTree[];
20
- onItemUpdate: (itemId: string, data: Record<string, unknown>) => void;
21
- onItemDelete: (itemId: string) => void;
22
- }
23
-
24
- const typeConfig: Record<string, { icon: string; color: string }> = {
25
- feature: { icon: '\u{1F4E6}', color: 'var(--primary)' },
26
- task: { icon: '\u{2705}', color: 'var(--success)' },
27
- bug: { icon: '\u{1F41B}', color: 'var(--destructive)' },
28
- idea: { icon: '\u{1F4A1}', color: 'var(--warning)' },
29
- note: { icon: '\u{1F4DD}', color: 'var(--muted-foreground)' },
30
- };
31
-
32
- function countAll(items: IItemTree[]): { total: number; done: number; inProgress: number; pending: number } {
33
- let total = 0, done = 0, inProgress = 0, pending = 0;
34
- for (const item of items) {
35
- total++;
36
- if (item.status === 'done') done++;
37
- else if (item.status === 'in_progress') inProgress++;
38
- else pending++;
39
- const sub = countAll(item.children);
40
- total += sub.total;
41
- done += sub.done;
42
- inProgress += sub.inProgress;
43
- pending += sub.pending;
44
- }
45
- return { total, done, inProgress, pending };
46
- }
47
-
48
- function flattenChildren(item: IItemTree, maxDepth = 2, depth = 0): { item: IItemTree; depth: number }[] {
49
- const result: { item: IItemTree; depth: number }[] = [];
50
- for (const child of item.children) {
51
- result.push({ item: child, depth });
52
- if (child.children.length > 0 && depth < maxDepth - 1) {
53
- result.push(...flattenChildren(child, maxDepth, depth + 1));
54
- }
55
- }
56
- return result;
57
- }
58
-
59
- function ProjectCard({ item, onItemUpdate, onItemDelete }: {
60
- item: IItemTree;
61
- onItemUpdate: CardViewProps['onItemUpdate'];
62
- onItemDelete: CardViewProps['onItemDelete'];
63
- }) {
64
- const [expanded, setExpanded] = useState(false);
65
- const baseCfg = typeConfig[item.item_type] || typeConfig.note;
66
- const isDone = item.status === 'done';
67
- const cfg = isDone
68
- ? { icon: '\u{2705}', color: 'var(--success)' }
69
- : item.status === 'in_progress'
70
- ? { icon: baseCfg.icon, color: 'var(--primary)' }
71
- : baseCfg;
72
- const stats = countAll(item.children);
73
- const totalWithSelf = stats.total + 1;
74
- const doneWithSelf = stats.done + (isDone ? 1 : 0);
75
- const progressPct = totalWithSelf > 0 ? (doneWithSelf / totalWithSelf) * 100 : 0;
76
- const flatChildren = flattenChildren(item);
77
- const hasMore = flatChildren.length > 5;
78
- const displayChildren = expanded ? flatChildren : flatChildren.slice(0, 5);
79
-
80
- const progressColor = progressPct === 100 ? 'hsl(var(--success))'
81
- : progressPct > 50 ? 'hsl(var(--primary))'
82
- : 'hsl(var(--accent))';
83
-
84
- return (
85
- <div className="project-card" style={{ borderTopColor: `hsl(${cfg.color})`, borderTopWidth: '3px' }}>
86
- <div className="project-card-header group">
87
- <span className="project-card-icon">{cfg.icon}</span>
88
- <div className="flex-1 min-w-0">
89
- <div className="project-card-title">{item.title}</div>
90
- </div>
91
- <button
92
- onClick={() => onItemDelete(item.id)}
93
- className="text-[10px] text-muted-foreground/40 hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity px-1"
94
- title="삭제"
95
- >
96
-
97
- </button>
98
- <StatusBadge
99
- status={item.status}
100
- onStatusChange={(status) => onItemUpdate(item.id, { status })}
101
- />
102
- </div>
103
-
104
- {item.description && (
105
- <p className="project-card-desc">{item.description}</p>
106
- )}
107
-
108
- {/* Progress bar */}
109
- {stats.total > 0 && (
110
- <div className="project-card-progress">
111
- <div
112
- className="project-card-progress-fill"
113
- style={{ width: `${progressPct}%`, background: progressColor }}
114
- />
115
- </div>
116
- )}
117
-
118
- {/* Stats */}
119
- <div className="project-card-stats">
120
- {stats.done > 0 && (
121
- <span className="project-card-stat">
122
- <span style={{ color: 'hsl(var(--success))' }}>●</span> {stats.done} 완료
123
- </span>
124
- )}
125
- {stats.inProgress > 0 && (
126
- <span className="project-card-stat">
127
- <span style={{ color: 'hsl(var(--primary))' }}>●</span> {stats.inProgress} 진행
128
- </span>
129
- )}
130
- {stats.pending > 0 && (
131
- <span className="project-card-stat">
132
- <span style={{ color: 'hsl(var(--muted-foreground))' }}>●</span> {stats.pending} 대기
133
- </span>
134
- )}
135
- <span className="ml-auto">{stats.total}개 항목</span>
136
- </div>
137
-
138
- {/* Children list */}
139
- {displayChildren.length > 0 && (
140
- <div className="project-card-children">
141
- {displayChildren.map(({ item: child, depth }) => {
142
- const childBaseCfg = typeConfig[child.item_type] || typeConfig.note;
143
- const childIcon = child.status === 'done' ? '\u{2705}' : childBaseCfg.icon;
144
- const isDone = child.status === 'done';
145
- return (
146
- <div
147
- key={child.id}
148
- className={`project-card-child group/child ${isDone ? 'project-card-child-done' : ''}`}
149
- style={{ paddingLeft: `${14 + depth * 16}px` }}
150
- >
151
- <span className="text-[11px]">{childIcon}</span>
152
- <span className="flex-1 truncate">{child.title}</span>
153
- <button
154
- onClick={() => onItemDelete(child.id)}
155
- className="text-[10px] text-muted-foreground/30 hover:text-destructive opacity-0 group-hover/child:opacity-100 transition-opacity px-0.5"
156
- >
157
-
158
- </button>
159
- <span className="tree-priority-dot flex-shrink-0" style={{
160
- background: child.priority === 'high' ? 'hsl(var(--destructive))'
161
- : child.priority === 'medium' ? 'hsl(var(--warning))'
162
- : 'hsl(var(--success))'
163
- }} />
164
- <StatusBadge
165
- status={child.status}
166
- onStatusChange={(status) => onItemUpdate(child.id, { status })}
167
- />
168
- </div>
169
- );
170
- })}
171
- </div>
172
- )}
173
-
174
- {/* Expand toggle */}
175
- {hasMore && (
176
- <div className="project-card-expand" onClick={() => setExpanded(!expanded)}>
177
- {expanded ? '접기' : `+${flatChildren.length - 5}개 더 보기`}
178
- </div>
179
- )}
180
- </div>
181
- );
182
- }
183
-
184
- export default function CardView({ items, onItemUpdate, onItemDelete }: CardViewProps) {
185
- if (items.length === 0) {
186
- return (
187
- <div className="flex flex-col items-center justify-center h-full text-muted-foreground text-sm">
188
- <div className="text-4xl mb-3">&#x1F5C2;</div>
189
- <p>아직 구조화된 항목이 없습니다</p>
190
- </div>
191
- );
192
- }
193
-
194
- return (
195
- <div className="card-grid">
196
- {items.map((item) => (
197
- <ProjectCard
198
- key={item.id}
199
- item={item}
200
- onItemUpdate={onItemUpdate}
201
- onItemDelete={onItemDelete}
202
- />
203
- ))}
204
- </div>
205
- );
206
- }
@@ -1,196 +0,0 @@
1
- 'use client';
2
-
3
- import { useState, useEffect } from 'react';
4
-
5
- interface ItemDetailProps {
6
- itemId: string;
7
- projectId: string;
8
- title: string;
9
- description: string;
10
- itemType: string;
11
- priority: string;
12
- status: string;
13
- isLocked: boolean;
14
- depth: number;
15
- }
16
-
17
- interface IPrompt {
18
- id: string;
19
- content: string;
20
- prompt_type: string;
21
- version: number;
22
- }
23
-
24
- const typeLabels: Record<string, string> = {
25
- feature: '기능',
26
- task: '작업',
27
- bug: '버그',
28
- idea: '아이디어',
29
- note: '메모',
30
- };
31
-
32
- const priorityLabels: Record<string, string> = {
33
- high: '높음',
34
- medium: '보통',
35
- low: '낮음',
36
- };
37
-
38
- const statusLabels: Record<string, string> = {
39
- pending: '대기',
40
- in_progress: '진행 중',
41
- done: '완료',
42
- };
43
-
44
- export default function ItemDetail({
45
- itemId,
46
- projectId,
47
- title,
48
- description,
49
- itemType,
50
- priority,
51
- status,
52
- isLocked,
53
- depth,
54
- }: ItemDetailProps) {
55
- const [prompt, setPrompt] = useState<IPrompt | null>(null);
56
- const [loadingPrompt, setLoadingPrompt] = useState(false);
57
- const [generating, setGenerating] = useState(false);
58
- const [copied, setCopied] = useState(false);
59
- const [editing, setEditing] = useState(false);
60
- const [editContent, setEditContent] = useState('');
61
-
62
- // Load existing prompt
63
- useEffect(() => {
64
- const loadPrompt = async () => {
65
- setLoadingPrompt(true);
66
- const res = await fetch(`/api/projects/${projectId}/items/${itemId}/prompt`);
67
- if (res.ok) {
68
- setPrompt(await res.json());
69
- }
70
- setLoadingPrompt(false);
71
- };
72
- loadPrompt();
73
- }, [itemId, projectId]);
74
-
75
- const handleGenerate = async () => {
76
- setGenerating(true);
77
- try {
78
- const res = await fetch(`/api/projects/${projectId}/items/${itemId}/prompt`, {
79
- method: 'POST',
80
- headers: { 'Content-Type': 'application/json' },
81
- body: JSON.stringify({}),
82
- });
83
- if (res.ok) {
84
- setPrompt(await res.json());
85
- }
86
- } finally {
87
- setGenerating(false);
88
- }
89
- };
90
-
91
- const handleCopy = async () => {
92
- if (!prompt) return;
93
- await navigator.clipboard.writeText(prompt.content);
94
- setCopied(true);
95
- setTimeout(() => setCopied(false), 2000);
96
- };
97
-
98
- const handleSaveEdit = async () => {
99
- if (!editContent.trim()) return;
100
- setGenerating(true);
101
- try {
102
- const res = await fetch(`/api/projects/${projectId}/items/${itemId}/prompt`, {
103
- method: 'POST',
104
- headers: { 'Content-Type': 'application/json' },
105
- body: JSON.stringify({ content: editContent }),
106
- });
107
- if (res.ok) {
108
- setPrompt(await res.json());
109
- setEditing(false);
110
- }
111
- } finally {
112
- setGenerating(false);
113
- }
114
- };
115
-
116
- return (
117
- <div
118
- className="item-detail"
119
- style={{ marginLeft: `${depth * 20 + 8}px` }}
120
- >
121
- <p className="text-muted-foreground mb-2 text-xs">{description || '설명 없음'}</p>
122
-
123
- <div className="flex gap-3 text-xs text-muted-foreground mb-3">
124
- <span>유형: {typeLabels[itemType] || itemType}</span>
125
- <span>우선순위: {priorityLabels[priority] || priority}</span>
126
- <span>상태: {statusLabels[status] || status}</span>
127
- <span>잠금: {isLocked ? '잠금' : '해제'}</span>
128
- </div>
129
-
130
- {/* Prompt section */}
131
- <div className="prompt-section">
132
- <div className="prompt-header">
133
- <span className="text-xs font-medium text-muted-foreground">프롬프트</span>
134
- <div className="flex gap-1">
135
- {prompt && (
136
- <>
137
- <button onClick={handleCopy} className="prompt-action-btn" title="복사">
138
- {copied ? '복사됨' : '복사'}
139
- </button>
140
- <button
141
- onClick={() => { setEditing(!editing); setEditContent(prompt.content); }}
142
- className="prompt-action-btn"
143
- title="수정"
144
- >
145
- {editing ? '취소' : '수정'}
146
- </button>
147
- </>
148
- )}
149
- <button
150
- onClick={handleGenerate}
151
- disabled={generating}
152
- className="prompt-action-btn prompt-generate-btn"
153
- title={prompt ? '재생성' : '생성'}
154
- >
155
- {generating ? '생성 중...' : prompt ? '재생성' : '생성'}
156
- </button>
157
- </div>
158
- </div>
159
-
160
- {loadingPrompt ? (
161
- <p className="text-xs text-muted-foreground">로딩 중...</p>
162
- ) : editing ? (
163
- <div className="prompt-edit">
164
- <textarea
165
- value={editContent}
166
- onChange={(e) => setEditContent(e.target.value)}
167
- className="prompt-edit-textarea"
168
- rows={4}
169
- />
170
- <button
171
- onClick={handleSaveEdit}
172
- disabled={generating}
173
- className="prompt-action-btn prompt-generate-btn mt-1"
174
- >
175
- 저장
176
- </button>
177
- </div>
178
- ) : prompt ? (
179
- <div className="prompt-content">
180
- {prompt.content}
181
- </div>
182
- ) : (
183
- <p className="text-xs text-muted-foreground/60">
184
- 아직 프롬프트가 없습니다. &quot;생성&quot; 버튼을 클릭하세요.
185
- </p>
186
- )}
187
-
188
- {prompt && (
189
- <div className="text-[10px] text-muted-foreground/40 mt-1">
190
- v{prompt.version} · {prompt.prompt_type === 'manual' ? '수동' : '자동'}
191
- </div>
192
- )}
193
- </div>
194
- </div>
195
- );
196
- }
@@ -1,23 +0,0 @@
1
- 'use client';
2
-
3
- interface LockToggleProps {
4
- isLocked: boolean;
5
- onToggle: (locked: boolean) => void;
6
- disabled?: boolean;
7
- }
8
-
9
- export default function LockToggle({ isLocked, onToggle, disabled }: LockToggleProps) {
10
- return (
11
- <button
12
- onClick={(e) => {
13
- e.stopPropagation();
14
- onToggle(!isLocked);
15
- }}
16
- disabled={disabled}
17
- className="lock-toggle"
18
- title={isLocked ? '잠금 해제' : '잠금'}
19
- >
20
- {isLocked ? '\u{1F510}' : '\u{1F513}'}
21
- </button>
22
- );
23
- }
@@ -1,157 +0,0 @@
1
- 'use client';
2
-
3
- import { useState, useRef, useEffect } from 'react';
4
-
5
- interface IItemTree {
6
- id: string;
7
- title: string;
8
- description: string;
9
- item_type: string;
10
- priority: string;
11
- status: string;
12
- is_locked: boolean;
13
- is_pinned: boolean;
14
- children: IItemTree[];
15
- }
16
-
17
- interface RefinePopoverProps {
18
- itemId: string;
19
- projectId: string;
20
- title: string;
21
- description: string;
22
- onClose: () => void;
23
- onItemUpdate: (itemId: string, data: Record<string, unknown>) => void;
24
- onTreeRefresh: (tree: IItemTree[]) => void;
25
- }
26
-
27
- export default function RefinePopover({
28
- itemId,
29
- projectId,
30
- title,
31
- description,
32
- onClose,
33
- onItemUpdate,
34
- onTreeRefresh,
35
- }: RefinePopoverProps) {
36
- const [input, setInput] = useState('');
37
- const [loading, setLoading] = useState(false);
38
- const [messages, setMessages] = useState<{ role: 'user' | 'assistant'; text: string }[]>([]);
39
- const inputRef = useRef<HTMLTextAreaElement>(null);
40
- const messagesEndRef = useRef<HTMLDivElement>(null);
41
-
42
- useEffect(() => {
43
- inputRef.current?.focus();
44
- }, []);
45
-
46
- useEffect(() => {
47
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
48
- }, [messages]);
49
-
50
- const handleSubmit = async () => {
51
- const trimmed = input.trim();
52
- if (!trimmed || loading) return;
53
-
54
- setMessages(prev => [...prev, { role: 'user', text: trimmed }]);
55
- setInput('');
56
- setLoading(true);
57
-
58
- try {
59
- const res = await fetch(`/api/projects/${projectId}/items/${itemId}/refine`, {
60
- method: 'POST',
61
- headers: { 'Content-Type': 'application/json' },
62
- body: JSON.stringify({ message: trimmed }),
63
- });
64
-
65
- if (res.ok) {
66
- const data = await res.json();
67
- setMessages(prev => [
68
- ...prev,
69
- { role: 'assistant', text: `"${data.title}"\n${data.description}` },
70
- ]);
71
- // Refresh the entire tree if structural changes were made
72
- if (data.tree) {
73
- onTreeRefresh(data.tree);
74
- } else {
75
- onItemUpdate(itemId, { title: data.title, description: data.description });
76
- }
77
- } else {
78
- const data = await res.json();
79
- setMessages(prev => [
80
- ...prev,
81
- { role: 'assistant', text: `오류: ${data.error || '다듬기에 실패했습니다'}` },
82
- ]);
83
- }
84
- } catch {
85
- setMessages(prev => [
86
- ...prev,
87
- { role: 'assistant', text: '오류: AI 연결에 실패했습니다' },
88
- ]);
89
- } finally {
90
- setLoading(false);
91
- }
92
- };
93
-
94
- const handleKeyDown = (e: React.KeyboardEvent) => {
95
- if (e.key === 'Enter' && !e.shiftKey) {
96
- e.preventDefault();
97
- handleSubmit();
98
- }
99
- if (e.key === 'Escape') {
100
- onClose();
101
- }
102
- };
103
-
104
- return (
105
- <div className="refine-popover">
106
- <div className="refine-header">
107
- <span className="text-xs font-medium">다듬기: {title}</span>
108
- <button onClick={onClose} className="refine-close">&times;</button>
109
- </div>
110
-
111
- {description && (
112
- <div className="refine-context">{description}</div>
113
- )}
114
-
115
- <div className="refine-messages">
116
- {messages.length === 0 && (
117
- <div className="text-xs text-muted-foreground text-center py-2">
118
- 이 항목을 어떻게 다듬을지 알려주세요
119
- </div>
120
- )}
121
- {messages.map((m, i) => (
122
- <div key={i} className={`memo-popover-bubble memo-popover-bubble-${m.role === 'user' ? 'user' : 'ai'}`}>
123
- {m.text.split('\n').map((line, j) => (
124
- <p key={j} className={j > 0 ? 'mt-1' : ''}>{line}</p>
125
- ))}
126
- </div>
127
- ))}
128
- {loading && (
129
- <div className="memo-popover-bubble memo-popover-bubble-ai chat-loading">
130
- <span className="dot" /><span className="dot" /><span className="dot" />
131
- </div>
132
- )}
133
- <div ref={messagesEndRef} />
134
- </div>
135
-
136
- <div className="memo-popover-input-area">
137
- <textarea
138
- ref={inputRef}
139
- value={input}
140
- onChange={(e) => setInput(e.target.value)}
141
- onKeyDown={handleKeyDown}
142
- placeholder="예: 하위 항목을 상세하게 추가해줘, 범위를 줄여줘..."
143
- rows={1}
144
- disabled={loading}
145
- className="memo-popover-input"
146
- />
147
- <button
148
- onClick={handleSubmit}
149
- disabled={!input.trim() || loading}
150
- className="memo-popover-send"
151
- >
152
- 전송
153
- </button>
154
- </div>
155
- </div>
156
- );
157
- }