idea-manager 1.9.0 → 2.1.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/.next/build-manifest.json +2 -2
- package/.next/routes-manifest.json +35 -0
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +2 -2
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +2 -2
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/advisor-actions/route.js +15 -0
- package/.next/server/app/api/advisor-actions/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/archive/route.js +1 -122
- package/.next/server/app/api/archive/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/filesystem/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/filesystem/tree/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/global-advisor/route.js +37 -0
- package/.next/server/app/api/global-advisor/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/global-memo/route.js +8 -0
- package/.next/server/app/api/global-memo/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/maintenance/route.js +130 -0
- package/.next/server/app/api/maintenance/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/projects/[id]/advisor/route.js +22 -11
- package/.next/server/app/api/projects/[id]/advisor/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/apply-distribute/route.js +2 -8
- package/.next/server/app/api/projects/[id]/apply-distribute/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/auto-distribute/route.js +126 -3
- package/.next/server/app/api/projects/[id]/auto-distribute/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/brainstorm/route.js +124 -1
- package/.next/server/app/api/projects/[id]/brainstorm/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/git-sync/route.js +124 -1
- package/.next/server/app/api/projects/[id]/git-sync/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/route.js +124 -1
- package/.next/server/app/api/projects/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/route.js +8 -0
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.js +1 -7
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route.js +8 -0
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route.js +2 -8
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route.js +1 -122
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/reorder/route.js +124 -0
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/reorder/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/route.js +1 -122
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/route.js +8 -0
- package/.next/server/app/api/projects/[id]/sub-projects/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/route.js +124 -1
- package/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/search/route.js +8 -0
- package/.next/server/app/api/search/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sync/route.js +8 -0
- package/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/tasks/[taskId]/move/route.js +15 -0
- package/.next/server/app/api/tasks/[taskId]/move/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/version/route_client-reference-manifest.js +1 -1
- package/.next/server/app/index.html +2 -2
- package/.next/server/app/index.rsc +3 -3
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/page.js +12 -12
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/projects/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +18 -13
- package/.next/server/chunks/{117.js → 697.js} +16 -2
- package/.next/server/pages/404.html +2 -2
- package/.next/server/pages/500.html +2 -2
- package/.next/static/KREG104cVn2mBTMPTDTvH/_buildManifest.js +1 -0
- package/.next/static/chunks/374-23189d7e246ad164.js +1 -0
- package/.next/static/chunks/app/_global-error/page-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/advisor-actions/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/archive/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/filesystem/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/filesystem/tree/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/global-advisor/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/global-memo/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/health/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/maintenance/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/advisor/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/apply-distribute/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/auto-distribute/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/brainstorm/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/git-sync/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/reorder/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/projects/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/search/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/sync/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/tasks/[taskId]/move/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/update/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/api/version/route-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/app/page-9117037f2947f4f6.js +28 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-f051f234bea7bddd.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f051f234bea7bddd.js +1 -0
- package/.next/static/css/e9071b58a99b47e4.css +3 -0
- package/package.json +1 -1
- package/src/app/api/advisor-actions/route.ts +52 -0
- package/src/app/api/global-advisor/route.ts +50 -0
- package/src/app/api/maintenance/route.ts +36 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/reorder/route.ts +24 -0
- package/src/app/api/tasks/[taskId]/move/route.ts +30 -0
- package/src/components/advisor/ActionBlock.tsx +124 -0
- package/src/components/advisor/AdvisorChat.tsx +175 -0
- package/src/components/advisor/GlobalAdvisorLayer.tsx +38 -0
- package/src/components/dashboard/DashboardPanel.tsx +2 -0
- package/src/components/memo/GlobalMemoLayer.tsx +81 -0
- package/src/components/tabs/TabBar.tsx +2 -0
- package/src/components/tabs/TabShell.tsx +6 -0
- package/src/components/task/NoteEditor.tsx +137 -0
- package/src/components/task/ProjectTree.tsx +105 -57
- package/src/components/task/TaskChat.tsx +4 -0
- package/src/components/task/TaskDetail.tsx +182 -1
- package/src/components/ui/AiActivityIndicator.tsx +66 -0
- package/src/components/ui/ShortcutOverlay.tsx +108 -0
- package/src/components/workspace/ProjectAdvisor.tsx +17 -181
- package/src/components/workspace/WorkspacePanel.tsx +75 -3
- package/src/hooks/useAiActivity.ts +6 -0
- package/src/lib/advisor-actions/parse.ts +59 -0
- package/src/lib/ai/global-context.ts +114 -0
- package/src/lib/ai/project-context.ts +22 -2
- package/src/lib/ai-activity.ts +33 -0
- package/src/lib/db/queries/global-conversations.ts +31 -0
- package/src/lib/db/queries/tasks.ts +3 -1
- package/src/lib/db/schema.ts +8 -0
- package/src/types/advisor-actions.ts +25 -0
- package/.next/static/chunks/374-769431701aab500f.js +0 -1
- package/.next/static/chunks/app/_global-error/page-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/archive/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/filesystem/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/filesystem/tree/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/global-memo/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/health/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/advisor/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/apply-distribute/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/auto-distribute/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/brainstorm/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/git-sync/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/projects/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/search/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/sync/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/update/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/api/version/route-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/app/page-e935ee928da68ca2.js +0 -28
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-3ff8f59aaa75b8f8.js +0 -1
- package/.next/static/css/e4c7cd5a570312d9.css +0 -3
- package/.next/static/pxqzEiwniZAUDOUTb5SnX/_buildManifest.js +0 -1
- /package/.next/static/{pxqzEiwniZAUDOUTb5SnX → KREG104cVn2mBTMPTDTvH}/_ssgManifest.js +0 -0
|
@@ -6,6 +6,7 @@ import StatusFlow from './StatusFlow';
|
|
|
6
6
|
import TaskChat from './TaskChat';
|
|
7
7
|
import NoteEditor, { getPromotableLine } from './NoteEditor';
|
|
8
8
|
import CommandPalette, { type RefineCommand } from './CommandPalette';
|
|
9
|
+
import { registerAiActivity, unregisterAiActivity } from '@/lib/ai-activity';
|
|
9
10
|
import type { ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
|
10
11
|
|
|
11
12
|
export default function TaskDetail({
|
|
@@ -16,6 +17,7 @@ export default function TaskDetail({
|
|
|
16
17
|
onUpdate,
|
|
17
18
|
onDelete,
|
|
18
19
|
onTaskPromoted,
|
|
20
|
+
onTaskMoved,
|
|
19
21
|
onChatStateChange,
|
|
20
22
|
}: {
|
|
21
23
|
task: ITask;
|
|
@@ -27,13 +29,18 @@ export default function TaskDetail({
|
|
|
27
29
|
onDelete: () => void;
|
|
28
30
|
/** Fired after a checkbox line is promoted to a new task. Parent should refresh its task list. */
|
|
29
31
|
onTaskPromoted?: (newTask: ITask) => void;
|
|
32
|
+
/** Fired after task is moved to another sub-project. Parent should refresh. */
|
|
33
|
+
onTaskMoved?: () => void;
|
|
30
34
|
onChatStateChange?: (taskId: string, state: 'idle' | 'loading' | 'done') => void;
|
|
31
35
|
}) {
|
|
32
36
|
const [title, setTitle] = useState(task.title);
|
|
33
37
|
const [description, setDescription] = useState(task.description);
|
|
34
38
|
const [editingTitle, setEditingTitle] = useState(false);
|
|
35
39
|
const [copied, setCopied] = useState(false);
|
|
40
|
+
const [tagInput, setTagInput] = useState('');
|
|
41
|
+
const [showTagInput, setShowTagInput] = useState(false);
|
|
36
42
|
const [chatOpen, setChatOpen] = useState(false);
|
|
43
|
+
const [focusMode, setFocusMode] = useState(false);
|
|
37
44
|
const chatWasManuallyToggled = useRef(false);
|
|
38
45
|
|
|
39
46
|
// Auto-open the chat panel while the task is being executed by the watcher —
|
|
@@ -148,6 +155,8 @@ export default function TaskDetail({
|
|
|
148
155
|
setRefineElapsed(0);
|
|
149
156
|
const abort = new AbortController();
|
|
150
157
|
refineAbortRef.current = abort;
|
|
158
|
+
const actId = `refine-${Date.now()}`;
|
|
159
|
+
registerAiActivity({ id: actId, type: 'refine', label: `Refine: ${task.title}`, startedAt: Date.now() });
|
|
151
160
|
const started = Date.now();
|
|
152
161
|
const tick = setInterval(() => setRefineElapsed(Math.floor((Date.now() - started) / 1000)), 500);
|
|
153
162
|
|
|
@@ -228,6 +237,7 @@ export default function TaskDetail({
|
|
|
228
237
|
clearInterval(tick);
|
|
229
238
|
setRefining(false);
|
|
230
239
|
refineAbortRef.current = null;
|
|
240
|
+
unregisterAiActivity(actId);
|
|
231
241
|
}
|
|
232
242
|
}, [basePath, onUpdate, task.id]);
|
|
233
243
|
|
|
@@ -244,6 +254,42 @@ export default function TaskDetail({
|
|
|
244
254
|
}, [undoSnapshot, task.id, onUpdate]);
|
|
245
255
|
|
|
246
256
|
const [promoteNotice, setPromoteNotice] = useState<string | null>(null);
|
|
257
|
+
const [showMoveModal, setShowMoveModal] = useState(false);
|
|
258
|
+
const [moveProjects, setMoveProjects] = useState<{ id: string; name: string; subs: { id: string; name: string }[] }[]>([]);
|
|
259
|
+
const [moveTargetSub, setMoveTargetSub] = useState('');
|
|
260
|
+
const [moving, setMoving] = useState(false);
|
|
261
|
+
|
|
262
|
+
const openMoveModal = useCallback(async () => {
|
|
263
|
+
try {
|
|
264
|
+
const pRes = await fetch('/api/projects');
|
|
265
|
+
const projects = await pRes.json() as { id: string; name: string }[];
|
|
266
|
+
const withSubs = await Promise.all(projects.map(async (p) => {
|
|
267
|
+
const sRes = await fetch(`/api/projects/${p.id}/sub-projects`);
|
|
268
|
+
const subs = await sRes.json() as { id: string; name: string }[];
|
|
269
|
+
return { ...p, subs };
|
|
270
|
+
}));
|
|
271
|
+
setMoveProjects(withSubs);
|
|
272
|
+
setMoveTargetSub('');
|
|
273
|
+
setShowMoveModal(true);
|
|
274
|
+
} catch { /* silent */ }
|
|
275
|
+
}, []);
|
|
276
|
+
|
|
277
|
+
const doMove = useCallback(async () => {
|
|
278
|
+
if (!moveTargetSub || moving) return;
|
|
279
|
+
setMoving(true);
|
|
280
|
+
try {
|
|
281
|
+
const res = await fetch(`/api/tasks/${task.id}/move`, {
|
|
282
|
+
method: 'POST',
|
|
283
|
+
headers: { 'Content-Type': 'application/json' },
|
|
284
|
+
body: JSON.stringify({ subProjectId: moveTargetSub }),
|
|
285
|
+
});
|
|
286
|
+
if (res.ok) {
|
|
287
|
+
setShowMoveModal(false);
|
|
288
|
+
onTaskMoved?.();
|
|
289
|
+
}
|
|
290
|
+
} catch { /* silent */ }
|
|
291
|
+
setMoving(false);
|
|
292
|
+
}, [moveTargetSub, moving, task.id, onTaskMoved]);
|
|
247
293
|
|
|
248
294
|
const promoteCheckbox = useCallback(async () => {
|
|
249
295
|
const view = editorRef.current?.view;
|
|
@@ -285,6 +331,48 @@ export default function TaskDetail({
|
|
|
285
331
|
|
|
286
332
|
const priorities: ItemPriority[] = ['high', 'medium', 'low'];
|
|
287
333
|
|
|
334
|
+
// Focus mode: Esc to exit (when not in slash autocomplete or other modal)
|
|
335
|
+
useEffect(() => {
|
|
336
|
+
if (!focusMode) return;
|
|
337
|
+
const onKey = (e: KeyboardEvent) => {
|
|
338
|
+
if (e.key === 'Escape') { setFocusMode(false); }
|
|
339
|
+
};
|
|
340
|
+
window.addEventListener('keydown', onKey);
|
|
341
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
342
|
+
}, [focusMode]);
|
|
343
|
+
|
|
344
|
+
if (focusMode) {
|
|
345
|
+
return (
|
|
346
|
+
<div className="fixed inset-0 z-40 bg-background flex flex-col">
|
|
347
|
+
<div className="flex items-center justify-between px-6 py-3 border-b border-border flex-shrink-0">
|
|
348
|
+
<div className="flex items-center gap-3">
|
|
349
|
+
<h2 className="text-lg font-semibold text-foreground">{task.title}</h2>
|
|
350
|
+
<span className="text-xs text-muted-foreground">Focus Mode</span>
|
|
351
|
+
</div>
|
|
352
|
+
<button
|
|
353
|
+
onClick={() => setFocusMode(false)}
|
|
354
|
+
className="text-xs px-3 py-1 rounded border border-border text-muted-foreground hover:text-foreground transition-colors"
|
|
355
|
+
>
|
|
356
|
+
Esc 닫기
|
|
357
|
+
</button>
|
|
358
|
+
</div>
|
|
359
|
+
<div className="flex-1 min-h-0">
|
|
360
|
+
<NoteEditor
|
|
361
|
+
ref={editorRef}
|
|
362
|
+
value={description}
|
|
363
|
+
onChange={setDescription}
|
|
364
|
+
onBlur={saveDescription}
|
|
365
|
+
onOpenCommand={openPalette}
|
|
366
|
+
onPromoteLine={promoteCheckbox}
|
|
367
|
+
extraCorpus={extraCorpus}
|
|
368
|
+
placeholder="집중 모드 — 자유롭게 작성하세요…"
|
|
369
|
+
/>
|
|
370
|
+
</div>
|
|
371
|
+
<CommandPalette open={paletteOpen} hasSelection={hasSelection} onClose={() => setPaletteOpen(false)} onRun={runRefine} />
|
|
372
|
+
</div>
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
288
376
|
return (
|
|
289
377
|
<div className="flex flex-col h-full">
|
|
290
378
|
{/* Header */}
|
|
@@ -354,6 +442,14 @@ export default function TaskDetail({
|
|
|
354
442
|
>
|
|
355
443
|
{copied ? '✓ Copied' : 'Copy as Prompt'}
|
|
356
444
|
</button>
|
|
445
|
+
<button
|
|
446
|
+
onClick={() => setFocusMode(true)}
|
|
447
|
+
title="포커스 모드 — 노트만 풀스크린"
|
|
448
|
+
className="text-xs px-2 py-0.5 rounded transition-colors border border-border
|
|
449
|
+
text-muted-foreground hover:text-foreground hover:border-muted-foreground"
|
|
450
|
+
>
|
|
451
|
+
Focus
|
|
452
|
+
</button>
|
|
357
453
|
<button
|
|
358
454
|
onClick={() => { chatWasManuallyToggled.current = true; setChatOpen(v => !v); }}
|
|
359
455
|
className={`text-xs px-2 py-0.5 rounded transition-colors border ${
|
|
@@ -365,13 +461,59 @@ export default function TaskDetail({
|
|
|
365
461
|
💬 Chat
|
|
366
462
|
</button>
|
|
367
463
|
|
|
464
|
+
<button
|
|
465
|
+
onClick={openMoveModal}
|
|
466
|
+
className="text-xs text-muted-foreground hover:text-foreground transition-colors ml-auto"
|
|
467
|
+
>
|
|
468
|
+
Move
|
|
469
|
+
</button>
|
|
368
470
|
<button
|
|
369
471
|
onClick={onDelete}
|
|
370
|
-
className="text-xs text-muted-foreground hover:text-destructive transition-colors
|
|
472
|
+
className="text-xs text-muted-foreground hover:text-destructive transition-colors"
|
|
371
473
|
>
|
|
372
474
|
Delete
|
|
373
475
|
</button>
|
|
374
476
|
</div>
|
|
477
|
+
|
|
478
|
+
{/* Tags */}
|
|
479
|
+
<div className="flex items-center gap-1.5 flex-wrap">
|
|
480
|
+
{(task.tags ?? []).map(tag => (
|
|
481
|
+
<span key={tag} className="inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground border border-border">
|
|
482
|
+
{tag}
|
|
483
|
+
<button
|
|
484
|
+
onClick={() => onUpdate({ tags: task.tags.filter(t => t !== tag) })}
|
|
485
|
+
className="text-muted-foreground/60 hover:text-destructive text-[10px] leading-none"
|
|
486
|
+
>×</button>
|
|
487
|
+
</span>
|
|
488
|
+
))}
|
|
489
|
+
{showTagInput ? (
|
|
490
|
+
<input
|
|
491
|
+
value={tagInput}
|
|
492
|
+
onChange={(e) => setTagInput(e.target.value)}
|
|
493
|
+
onKeyDown={(e) => {
|
|
494
|
+
if (e.key === 'Enter') {
|
|
495
|
+
const t = tagInput.trim();
|
|
496
|
+
if (t && !(task.tags ?? []).includes(t)) {
|
|
497
|
+
onUpdate({ tags: [...(task.tags ?? []), t] });
|
|
498
|
+
}
|
|
499
|
+
setTagInput('');
|
|
500
|
+
}
|
|
501
|
+
if (e.key === 'Escape') { setTagInput(''); setShowTagInput(false); }
|
|
502
|
+
}}
|
|
503
|
+
onBlur={() => { setTagInput(''); setShowTagInput(false); }}
|
|
504
|
+
placeholder="태그 입력…"
|
|
505
|
+
className="text-xs bg-transparent border-b border-border focus:border-primary focus:outline-none px-1 py-0.5 w-24"
|
|
506
|
+
autoFocus
|
|
507
|
+
/>
|
|
508
|
+
) : (
|
|
509
|
+
<button
|
|
510
|
+
onClick={() => setShowTagInput(true)}
|
|
511
|
+
className="text-[10px] text-muted-foreground/50 hover:text-muted-foreground transition-colors"
|
|
512
|
+
>
|
|
513
|
+
+ tag
|
|
514
|
+
</button>
|
|
515
|
+
)}
|
|
516
|
+
</div>
|
|
375
517
|
</div>
|
|
376
518
|
|
|
377
519
|
{/* Note editor */}
|
|
@@ -460,6 +602,45 @@ export default function TaskDetail({
|
|
|
460
602
|
onClose={() => setPaletteOpen(false)}
|
|
461
603
|
onRun={runRefine}
|
|
462
604
|
/>
|
|
605
|
+
|
|
606
|
+
{showMoveModal && (
|
|
607
|
+
<div
|
|
608
|
+
onClick={() => setShowMoveModal(false)}
|
|
609
|
+
className="fixed inset-0 z-50 flex items-center justify-center"
|
|
610
|
+
style={{ background: 'rgba(0,0,0,0.5)', backdropFilter: 'blur(2px)' }}
|
|
611
|
+
>
|
|
612
|
+
<div onClick={(e) => e.stopPropagation()} className="bg-card border border-border rounded-xl shadow-2xl w-full max-w-sm p-4 flex flex-col gap-3 animate-dialog-in">
|
|
613
|
+
<div className="text-sm font-semibold text-foreground">태스크 이동</div>
|
|
614
|
+
<div className="text-xs text-muted-foreground">"{task.title}" 을 다른 프로젝트로 이동합니다.</div>
|
|
615
|
+
<select
|
|
616
|
+
value={moveTargetSub}
|
|
617
|
+
onChange={(e) => setMoveTargetSub(e.target.value)}
|
|
618
|
+
className="bg-input border border-border rounded-md px-3 py-2 text-sm text-foreground focus:border-primary focus:outline-none"
|
|
619
|
+
>
|
|
620
|
+
<option value="">이동할 대상 선택…</option>
|
|
621
|
+
{moveProjects.map(p => (
|
|
622
|
+
<optgroup key={p.id} label={p.name}>
|
|
623
|
+
{p.subs.map(s => (
|
|
624
|
+
<option key={s.id} value={s.id} disabled={s.id === subProjectId}>
|
|
625
|
+
{s.name}{s.id === subProjectId ? ' (현재)' : ''}
|
|
626
|
+
</option>
|
|
627
|
+
))}
|
|
628
|
+
</optgroup>
|
|
629
|
+
))}
|
|
630
|
+
</select>
|
|
631
|
+
<div className="flex justify-end gap-2">
|
|
632
|
+
<button onClick={() => setShowMoveModal(false)} className="text-xs text-muted-foreground px-2 py-1">취소</button>
|
|
633
|
+
<button
|
|
634
|
+
onClick={doMove}
|
|
635
|
+
disabled={!moveTargetSub || moving}
|
|
636
|
+
className="text-xs px-3 py-1 bg-primary text-primary-foreground rounded disabled:opacity-40"
|
|
637
|
+
>
|
|
638
|
+
{moving ? '이동 중…' : '이동'}
|
|
639
|
+
</button>
|
|
640
|
+
</div>
|
|
641
|
+
</div>
|
|
642
|
+
</div>
|
|
643
|
+
)}
|
|
463
644
|
</div>
|
|
464
645
|
);
|
|
465
646
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { useAiActivity } from '@/hooks/useAiActivity';
|
|
5
|
+
|
|
6
|
+
function elapsed(startedAt: number): string {
|
|
7
|
+
const s = Math.floor((Date.now() - startedAt) / 1000);
|
|
8
|
+
return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m${s % 60}s`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const TYPE_LABEL: Record<string, string> = {
|
|
12
|
+
refine: '⌘K Refine',
|
|
13
|
+
'task-chat': 'Note Assistant',
|
|
14
|
+
'project-advisor': 'Project Advisor',
|
|
15
|
+
'global-advisor': 'Global Advisor',
|
|
16
|
+
watch: 'Watch',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default function AiActivityIndicator() {
|
|
20
|
+
const activities = useAiActivity();
|
|
21
|
+
const [, tick] = useState(0);
|
|
22
|
+
const [showList, setShowList] = useState(false);
|
|
23
|
+
|
|
24
|
+
// Re-render every second to update elapsed times
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (activities.length === 0) return;
|
|
27
|
+
const id = setInterval(() => tick(n => n + 1), 1000);
|
|
28
|
+
return () => clearInterval(id);
|
|
29
|
+
}, [activities.length]);
|
|
30
|
+
|
|
31
|
+
if (activities.length === 0) return null;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="relative mr-2">
|
|
35
|
+
<button
|
|
36
|
+
onClick={() => setShowList(prev => !prev)}
|
|
37
|
+
className="flex items-center gap-1.5 text-xs px-2 py-1 rounded-md border border-warning/40 bg-warning/15 text-warning hover:bg-warning/25 transition-colors"
|
|
38
|
+
title={`AI 작업 ${activities.length}개 진행 중`}
|
|
39
|
+
>
|
|
40
|
+
<span className="inline-block w-1.5 h-1.5 rounded-full bg-warning animate-pulse" />
|
|
41
|
+
<span>AI {activities.length}</span>
|
|
42
|
+
</button>
|
|
43
|
+
|
|
44
|
+
{showList && (
|
|
45
|
+
<>
|
|
46
|
+
<div className="fixed inset-0 z-[80]" onClick={() => setShowList(false)} />
|
|
47
|
+
<div className="absolute right-0 top-full mt-1 z-[81] bg-card border border-border rounded-lg shadow-xl w-72 py-1 animate-dialog-in">
|
|
48
|
+
<div className="px-3 py-1.5 text-[10px] uppercase tracking-wider text-muted-foreground/70 border-b border-border">
|
|
49
|
+
진행 중인 AI 작업
|
|
50
|
+
</div>
|
|
51
|
+
{activities.map(a => (
|
|
52
|
+
<div key={a.id} className="px-3 py-2 flex items-center gap-2 text-xs">
|
|
53
|
+
<span className="inline-block w-1.5 h-1.5 rounded-full bg-warning animate-pulse flex-shrink-0" />
|
|
54
|
+
<div className="flex-1 min-w-0">
|
|
55
|
+
<div className="text-foreground truncate">{a.label}</div>
|
|
56
|
+
<div className="text-muted-foreground/70">{TYPE_LABEL[a.type] ?? a.type}</div>
|
|
57
|
+
</div>
|
|
58
|
+
<span className="text-muted-foreground font-mono flex-shrink-0">{elapsed(a.startedAt)}</span>
|
|
59
|
+
</div>
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
</>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
const SECTIONS: { title: string; shortcuts: { keys: string; desc: string }[] }[] = [
|
|
6
|
+
{
|
|
7
|
+
title: '전역',
|
|
8
|
+
shortcuts: [
|
|
9
|
+
{ keys: '⌘P', desc: '전역 검색' },
|
|
10
|
+
{ keys: '⌘N', desc: '빠른 태스크 생성' },
|
|
11
|
+
{ keys: '⌘M', desc: '전역 메모 (Quick Memo)' },
|
|
12
|
+
{ keys: '⌘J', desc: '전역 AI 어드바이저' },
|
|
13
|
+
{ keys: '?', desc: '이 도움말' },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
title: '워크스페이스',
|
|
18
|
+
shortcuts: [
|
|
19
|
+
{ keys: '⌘L', desc: 'Project Advisor 열기/닫기' },
|
|
20
|
+
{ keys: 'B', desc: '브레인스토밍 패널 토글' },
|
|
21
|
+
{ keys: 'N', desc: '새 프로젝트 추가' },
|
|
22
|
+
{ keys: 'T', desc: '새 태스크 추가' },
|
|
23
|
+
{ keys: '⌘1', desc: '상태 → Idea' },
|
|
24
|
+
{ keys: '⌘2', desc: '상태 → Doing' },
|
|
25
|
+
{ keys: '⌘3', desc: '상태 → Done' },
|
|
26
|
+
{ keys: '⌘4', desc: '상태 → Problem' },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
title: '노트 에디터',
|
|
31
|
+
shortcuts: [
|
|
32
|
+
{ keys: '⌘K', desc: 'AI 명령 팔레트' },
|
|
33
|
+
{ keys: '⌘⇧T', desc: '체크박스/불릿 → 태스크 승격' },
|
|
34
|
+
{ keys: '/', desc: '슬래시 명령 (/todo, /table, /code…)' },
|
|
35
|
+
{ keys: '⌘↵', desc: '체크박스 토글 [ ] ↔ [x]' },
|
|
36
|
+
{ keys: '⌘⇧↵', desc: '테이블 행 추가' },
|
|
37
|
+
{ keys: '⌘⇧⌫', desc: '테이블 행 삭제' },
|
|
38
|
+
{ keys: 'Tab', desc: '고스트 자동완성 수락' },
|
|
39
|
+
{ keys: 'Esc', desc: '고스트 해제' },
|
|
40
|
+
{ keys: 'Enter', desc: '리스트 자동 이어쓰기' },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export default function ShortcutOverlay() {
|
|
46
|
+
const [open, setOpen] = useState(false);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const onKey = (e: KeyboardEvent) => {
|
|
50
|
+
if (e.key === '?' && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
|
51
|
+
const target = e.target as HTMLElement | null;
|
|
52
|
+
const isInput =
|
|
53
|
+
target instanceof HTMLInputElement ||
|
|
54
|
+
target instanceof HTMLTextAreaElement ||
|
|
55
|
+
(target?.isContentEditable ?? false) ||
|
|
56
|
+
!!target?.closest?.('.cm-editor');
|
|
57
|
+
if (isInput) return;
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
setOpen(prev => !prev);
|
|
60
|
+
}
|
|
61
|
+
if (e.key === 'Escape' && open) {
|
|
62
|
+
setOpen(false);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
window.addEventListener('keydown', onKey);
|
|
66
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
67
|
+
}, [open]);
|
|
68
|
+
|
|
69
|
+
if (!open) return null;
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div
|
|
73
|
+
onClick={() => setOpen(false)}
|
|
74
|
+
className="fixed inset-0 z-[70] flex items-center justify-center"
|
|
75
|
+
style={{ background: 'rgba(0,0,0,0.5)', backdropFilter: 'blur(3px)' }}
|
|
76
|
+
>
|
|
77
|
+
<div
|
|
78
|
+
onClick={(e) => e.stopPropagation()}
|
|
79
|
+
className="bg-card border border-border rounded-xl shadow-2xl w-full max-w-lg animate-dialog-in p-5"
|
|
80
|
+
>
|
|
81
|
+
<div className="flex items-center justify-between mb-4">
|
|
82
|
+
<h2 className="text-sm font-semibold text-foreground">Keyboard Shortcuts</h2>
|
|
83
|
+
<button onClick={() => setOpen(false)} className="text-muted-foreground hover:text-foreground text-lg leading-none">×</button>
|
|
84
|
+
</div>
|
|
85
|
+
<div className="space-y-4">
|
|
86
|
+
{SECTIONS.map((section) => (
|
|
87
|
+
<div key={section.title}>
|
|
88
|
+
<div className="text-[10px] uppercase tracking-wider text-muted-foreground/70 mb-1.5">{section.title}</div>
|
|
89
|
+
<div className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1">
|
|
90
|
+
{section.shortcuts.map((s) => (
|
|
91
|
+
<div key={s.keys} className="contents">
|
|
92
|
+
<kbd className="text-xs font-mono px-1.5 py-0.5 rounded bg-muted border border-border text-foreground text-right whitespace-nowrap">
|
|
93
|
+
{s.keys}
|
|
94
|
+
</kbd>
|
|
95
|
+
<span className="text-xs text-muted-foreground self-center">{s.desc}</span>
|
|
96
|
+
</div>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
))}
|
|
101
|
+
</div>
|
|
102
|
+
<div className="mt-4 text-[10px] text-muted-foreground/50 text-center">
|
|
103
|
+
? 를 다시 눌러 닫기
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -1,194 +1,30 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import type { IProjectConversation } from '@/types';
|
|
5
|
-
import ReactMarkdown from 'react-markdown';
|
|
6
|
-
import remarkGfm from 'remark-gfm';
|
|
3
|
+
import AdvisorChat from '@/components/advisor/AdvisorChat';
|
|
7
4
|
|
|
8
5
|
export default function ProjectAdvisor({
|
|
9
6
|
projectId,
|
|
7
|
+
projectName,
|
|
10
8
|
onClose,
|
|
11
9
|
}: {
|
|
12
10
|
projectId: string;
|
|
11
|
+
projectName?: string;
|
|
13
12
|
onClose: () => void;
|
|
14
13
|
}) {
|
|
15
|
-
const [messages, setMessages] = useState<IProjectConversation[]>([]);
|
|
16
|
-
const [input, setInput] = useState('');
|
|
17
|
-
const [loading, setLoading] = useState(false);
|
|
18
|
-
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
19
|
-
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
20
|
-
const basePath = `/api/projects/${projectId}/advisor`;
|
|
21
|
-
|
|
22
|
-
const fetchMessages = useCallback(async () => {
|
|
23
|
-
try {
|
|
24
|
-
const res = await fetch(basePath);
|
|
25
|
-
if (!res.ok) return;
|
|
26
|
-
const data = await res.json();
|
|
27
|
-
if (Array.isArray(data)) setMessages(data);
|
|
28
|
-
} catch { /* silent */ }
|
|
29
|
-
}, [basePath]);
|
|
30
|
-
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
fetchMessages();
|
|
33
|
-
}, [fetchMessages]);
|
|
34
|
-
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
37
|
-
}, [messages]);
|
|
38
|
-
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
inputRef.current?.focus();
|
|
41
|
-
}, []);
|
|
42
|
-
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
const onKey = (e: KeyboardEvent) => {
|
|
45
|
-
if (e.key === 'Escape') { e.preventDefault(); onClose(); }
|
|
46
|
-
};
|
|
47
|
-
window.addEventListener('keydown', onKey);
|
|
48
|
-
return () => window.removeEventListener('keydown', onKey);
|
|
49
|
-
}, [onClose]);
|
|
50
|
-
|
|
51
|
-
const send = useCallback(async () => {
|
|
52
|
-
const text = input.trim();
|
|
53
|
-
if (!text || loading) return;
|
|
54
|
-
setInput('');
|
|
55
|
-
setLoading(true);
|
|
56
|
-
|
|
57
|
-
const tempId = `temp-${Date.now()}`;
|
|
58
|
-
const userMsg: IProjectConversation = {
|
|
59
|
-
id: tempId, project_id: projectId, role: 'user', content: text,
|
|
60
|
-
created_at: new Date().toISOString(),
|
|
61
|
-
};
|
|
62
|
-
setMessages(prev => [...prev, userMsg]);
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
const res = await fetch(basePath, {
|
|
66
|
-
method: 'POST',
|
|
67
|
-
headers: { 'Content-Type': 'application/json' },
|
|
68
|
-
body: JSON.stringify({ message: text }),
|
|
69
|
-
});
|
|
70
|
-
if (res.ok) {
|
|
71
|
-
const data = await res.json();
|
|
72
|
-
setMessages(prev => {
|
|
73
|
-
const withoutTemp = prev.filter(m => m.id !== tempId);
|
|
74
|
-
return [...withoutTemp, data.userMessage, data.aiMessage];
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
} catch { /* silent */ }
|
|
78
|
-
setLoading(false);
|
|
79
|
-
inputRef.current?.focus();
|
|
80
|
-
}, [input, loading, basePath, projectId]);
|
|
81
|
-
|
|
82
|
-
const handleClear = useCallback(async () => {
|
|
83
|
-
await fetch(basePath, { method: 'DELETE' });
|
|
84
|
-
setMessages([]);
|
|
85
|
-
inputRef.current?.focus();
|
|
86
|
-
}, [basePath]);
|
|
87
|
-
|
|
88
|
-
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
89
|
-
if (e.key === 'Enter' && !e.shiftKey) {
|
|
90
|
-
e.preventDefault();
|
|
91
|
-
send();
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
|
|
95
14
|
return (
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
<div className="flex items-center gap-2">
|
|
111
|
-
{messages.length > 0 && (
|
|
112
|
-
<button
|
|
113
|
-
onClick={handleClear}
|
|
114
|
-
className="text-xs text-muted-foreground hover:text-foreground transition-colors px-1"
|
|
115
|
-
title="대화 초기화"
|
|
116
|
-
>
|
|
117
|
-
Clear
|
|
118
|
-
</button>
|
|
119
|
-
)}
|
|
120
|
-
<button
|
|
121
|
-
onClick={onClose}
|
|
122
|
-
className="text-muted-foreground hover:text-foreground transition-colors text-lg leading-none"
|
|
123
|
-
>
|
|
124
|
-
×
|
|
125
|
-
</button>
|
|
126
|
-
</div>
|
|
127
|
-
</div>
|
|
128
|
-
|
|
129
|
-
{/* Messages */}
|
|
130
|
-
<div className="flex-1 overflow-y-auto px-4 py-3 space-y-3 min-h-0">
|
|
131
|
-
{messages.length === 0 && !loading && (
|
|
132
|
-
<div className="flex flex-col items-center justify-center h-full text-center gap-3 text-muted-foreground">
|
|
133
|
-
<div className="text-2xl">🧭</div>
|
|
134
|
-
<div className="text-sm">프로젝트 전체 맥락을 보고 답합니다</div>
|
|
135
|
-
<div className="text-xs text-muted-foreground/70 max-w-[300px] leading-relaxed">
|
|
136
|
-
"다음 뭐부터 하면 좋겠어?"<br />
|
|
137
|
-
"빠진 작업 없나?"<br />
|
|
138
|
-
"이번 주 진행 상황 정리해줘"<br />
|
|
139
|
-
"이 방향이 맞는지 검토해줘"
|
|
140
|
-
</div>
|
|
141
|
-
</div>
|
|
142
|
-
)}
|
|
143
|
-
{messages.filter(m => m.role !== 'system').map((msg) => (
|
|
144
|
-
<div key={msg.id} className={`flex flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}>
|
|
145
|
-
<div className={`max-w-[92%] px-3 py-2 rounded-lg text-sm leading-relaxed ${
|
|
146
|
-
msg.role === 'user'
|
|
147
|
-
? 'bg-accent text-white rounded-br-sm whitespace-pre-wrap'
|
|
148
|
-
: 'bg-muted text-foreground rounded-bl-sm chat-markdown'
|
|
149
|
-
}`}>
|
|
150
|
-
{msg.role === 'assistant'
|
|
151
|
-
? <ReactMarkdown remarkPlugins={[remarkGfm]}>{msg.content}</ReactMarkdown>
|
|
152
|
-
: msg.content}
|
|
153
|
-
</div>
|
|
154
|
-
</div>
|
|
155
|
-
))}
|
|
156
|
-
{loading && (
|
|
157
|
-
<div className="flex items-start">
|
|
158
|
-
<div className="px-3 py-2 rounded-lg bg-muted text-foreground rounded-bl-sm">
|
|
159
|
-
<div className="flex gap-1">
|
|
160
|
-
<div className="w-1.5 h-1.5 rounded-full bg-muted-foreground animate-bounce" style={{ animationDelay: '0ms' }} />
|
|
161
|
-
<div className="w-1.5 h-1.5 rounded-full bg-muted-foreground animate-bounce" style={{ animationDelay: '150ms' }} />
|
|
162
|
-
<div className="w-1.5 h-1.5 rounded-full bg-muted-foreground animate-bounce" style={{ animationDelay: '300ms' }} />
|
|
163
|
-
</div>
|
|
164
|
-
</div>
|
|
165
|
-
</div>
|
|
166
|
-
)}
|
|
167
|
-
<div ref={messagesEndRef} />
|
|
168
|
-
</div>
|
|
169
|
-
|
|
170
|
-
{/* Input */}
|
|
171
|
-
<div className="flex gap-1.5 px-3 py-3 border-t border-border flex-shrink-0">
|
|
172
|
-
<textarea
|
|
173
|
-
ref={inputRef}
|
|
174
|
-
value={input}
|
|
175
|
-
onChange={(e) => setInput(e.target.value)}
|
|
176
|
-
onKeyDown={handleKeyDown}
|
|
177
|
-
placeholder="프로젝트에 대해 무엇이든 물어보세요…"
|
|
178
|
-
rows={2}
|
|
179
|
-
className="flex-1 bg-input border border-border rounded-md px-3 py-2 text-sm text-foreground resize-none focus:border-primary focus:outline-none"
|
|
180
|
-
/>
|
|
181
|
-
<button
|
|
182
|
-
onClick={send}
|
|
183
|
-
disabled={!input.trim() || loading}
|
|
184
|
-
className="px-3 py-2 bg-accent text-white text-sm rounded-md
|
|
185
|
-
disabled:opacity-40 hover:bg-accent/80 transition-colors flex-shrink-0 self-end"
|
|
186
|
-
>
|
|
187
|
-
Send
|
|
188
|
-
</button>
|
|
189
|
-
</div>
|
|
190
|
-
</div>
|
|
191
|
-
|
|
192
|
-
</div>
|
|
15
|
+
<AdvisorChat
|
|
16
|
+
basePath={`/api/projects/${projectId}/advisor`}
|
|
17
|
+
title="Project Advisor"
|
|
18
|
+
shortcutHint="⌘L"
|
|
19
|
+
placeholder="프로젝트에 대해 무엇이든 물어보세요…"
|
|
20
|
+
emptyIcon="🧭"
|
|
21
|
+
emptyHints={[
|
|
22
|
+
'프로젝트 전체 맥락을 보고 답합니다',
|
|
23
|
+
'"다음 뭐부터 하면 좋겠어?"\n"빠진 작업 없나?"\n"이번 주 진행 상황 정리해줘"',
|
|
24
|
+
]}
|
|
25
|
+
activityType="project-advisor"
|
|
26
|
+
activityLabel={projectName ? `Advisor: ${projectName}` : 'Project Advisor'}
|
|
27
|
+
onClose={onClose}
|
|
28
|
+
/>
|
|
193
29
|
);
|
|
194
30
|
}
|