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.
- package/package.json +2 -1
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.ts +13 -3
- package/src/app/globals.css +72 -0
- package/src/app/projects/[id]/page.tsx +34 -6
- package/src/components/brainstorm/Editor.tsx +1 -84
- package/src/components/task/TaskChat.tsx +8 -5
- package/src/components/ui/AiPolicyModal.tsx +98 -0
- package/src/lib/ai/client.ts +45 -172
- package/src/lib/db/queries/projects.ts +3 -2
- package/src/lib/db/schema.ts +3 -70
- package/src/types/index.ts +1 -90
- package/src/app/api/projects/[id]/cleanup/route.ts +0 -32
- package/src/app/api/projects/[id]/conversations/route.ts +0 -50
- package/src/app/api/projects/[id]/items/[itemId]/prompt/route.ts +0 -51
- package/src/app/api/projects/[id]/items/[itemId]/refine/route.ts +0 -36
- package/src/app/api/projects/[id]/items/[itemId]/route.ts +0 -95
- package/src/app/api/projects/[id]/items/route.ts +0 -67
- package/src/app/api/projects/[id]/memos/route.ts +0 -18
- package/src/app/api/projects/[id]/scan/route.ts +0 -73
- package/src/app/api/projects/[id]/scan/stream/route.ts +0 -112
- package/src/app/api/projects/[id]/structure/route.ts +0 -59
- package/src/app/api/projects/[id]/structure/stream/route.ts +0 -157
- package/src/components/ScanPanel.tsx +0 -743
- package/src/components/brainstorm/MemoPin.tsx +0 -117
- package/src/components/tree/CardView.tsx +0 -206
- package/src/components/tree/ItemDetail.tsx +0 -196
- package/src/components/tree/LockToggle.tsx +0 -23
- package/src/components/tree/RefinePopover.tsx +0 -157
- package/src/components/tree/StatusBadge.tsx +0 -32
- package/src/components/tree/TreeNode.tsx +0 -227
- package/src/components/tree/TreeView.tsx +0 -304
- package/src/lib/ai/chat-responder.ts +0 -71
- package/src/lib/ai/cleanup.ts +0 -87
- package/src/lib/ai/prompter.ts +0 -78
- package/src/lib/ai/refiner.ts +0 -128
- package/src/lib/ai/structurer.ts +0 -403
- package/src/lib/db/queries/context.ts +0 -76
- package/src/lib/db/queries/conversations.ts +0 -46
- package/src/lib/db/queries/items.ts +0 -268
- package/src/lib/db/queries/memos.ts +0 -66
- package/src/lib/db/queries/prompts.ts +0 -68
- package/src/lib/scanner.ts +0 -573
- 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
|
+
"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
|
|
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
|
|
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
|
-
|
|
67
|
+
const errorMsg = addTaskConversation(taskId, 'assistant', '(AI 호출에 실패했습니다. Claude CLI가 설치되어 있는지 확인해주세요.)');
|
|
68
|
+
return NextResponse.json({ userMessage: userMsg, aiMessage: errorMsg });
|
|
59
69
|
}
|
|
60
70
|
}
|
package/src/app/globals.css
CHANGED
|
@@ -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(
|
|
38
|
-
const [centerWidth, setCenterWidth] = useState(
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
+
}
|