idea-manager 1.8.0 → 2.0.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 +28 -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/archive/route.js +21 -4
- 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 +26 -0
- package/.next/server/app/api/global-advisor/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/global-memo/route.js +17 -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 +34 -0
- package/.next/server/app/api/projects/[id]/advisor/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/projects/[id]/apply-distribute/route.js +6 -6
- 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 +3 -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_client-reference-manifest.js +1 -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_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/route.js +20 -3
- 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 +4 -4
- 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 +17 -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 +4 -4
- 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 +21 -4
- 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/route.js +21 -4
- 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 +20 -3
- package/.next/server/app/api/projects/[id]/sub-projects/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/search/route.js +17 -0
- package/.next/server/app/api/search/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sync/route.js +17 -0
- package/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/tasks/[taskId]/move/route.js +144 -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 +9 -9
- 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 +15 -11
- package/.next/server/chunks/117.js +17 -0
- package/.next/server/pages/404.html +2 -2
- package/.next/server/pages/500.html +2 -2
- package/.next/static/5I00mUqwYdcGFbs7ETC9d/_buildManifest.js +1 -0
- package/.next/static/chunks/app/_global-error/page-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/archive/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/filesystem/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/filesystem/tree/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/global-advisor/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/global-memo/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/health/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/maintenance/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/advisor/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/apply-distribute/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/auto-distribute/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/brainstorm/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/git-sync/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/projects/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/search/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/sync/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/tasks/[taskId]/move/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/update/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/api/version/route-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/app/page-b6580e09a4af9dff.js +28 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-92496bc87f5c29a9.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-92496bc87f5c29a9.js +1 -0
- package/.next/static/css/a57b4564ec9082ab.css +3 -0
- package/README.ja.md +2 -0
- package/README.ko.md +3 -1
- package/README.md +3 -1
- package/README.zh.md +2 -0
- package/package.json +1 -1
- 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]/advisor/route.ts +71 -0
- package/src/app/api/tasks/[taskId]/move/route.ts +30 -0
- package/src/components/advisor/AdvisorChat.tsx +165 -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/TaskChat.tsx +4 -0
- package/src/components/task/TaskDetail.tsx +89 -1
- package/src/components/ui/AiActivityIndicator.tsx +66 -0
- package/src/components/ui/ShortcutOverlay.tsx +104 -0
- package/src/components/workspace/ProjectAdvisor.tsx +30 -0
- package/src/components/workspace/WorkspacePanel.tsx +29 -0
- package/src/hooks/useAiActivity.ts +6 -0
- package/src/lib/ai/global-context.ts +94 -0
- package/src/lib/ai/project-context.ts +111 -0
- package/src/lib/ai-activity.ts +33 -0
- package/src/lib/db/queries/global-conversations.ts +31 -0
- package/src/lib/db/queries/project-conversations.ts +29 -0
- package/src/lib/db/queries/tasks.ts +3 -1
- package/src/lib/db/schema.ts +19 -0
- package/src/types/index.ts +8 -0
- package/.next/static/Fy-Z5gkec2a0fese1C5rW/_buildManifest.js +0 -1
- package/.next/static/chunks/app/_global-error/page-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/app/api/archive/route-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/app/api/filesystem/route-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/app/api/filesystem/tree/route-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/app/api/global-memo/route-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/app/api/health/route-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/apply-distribute/route-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/auto-distribute/route-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/brainstorm/route-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/git-sync/route-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/route-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/route-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/route-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/route-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/app/api/projects/route-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/app/api/search/route-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/app/api/sync/route-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/app/api/update/route-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/app/api/version/route-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/app/page-9a1dc101e82c397c.js +0 -28
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-e6a77f238d2cdbb9.js +0 -1
- package/.next/static/css/eab748b03f49c43a.css +0 -3
- /package/.next/static/{Fy-Z5gkec2a0fese1C5rW → 5I00mUqwYdcGFbs7ETC9d}/_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,6 +29,8 @@ 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);
|
|
@@ -148,6 +152,8 @@ export default function TaskDetail({
|
|
|
148
152
|
setRefineElapsed(0);
|
|
149
153
|
const abort = new AbortController();
|
|
150
154
|
refineAbortRef.current = abort;
|
|
155
|
+
const actId = `refine-${Date.now()}`;
|
|
156
|
+
registerAiActivity({ id: actId, type: 'refine', label: `Refine: ${task.title}`, startedAt: Date.now() });
|
|
151
157
|
const started = Date.now();
|
|
152
158
|
const tick = setInterval(() => setRefineElapsed(Math.floor((Date.now() - started) / 1000)), 500);
|
|
153
159
|
|
|
@@ -228,6 +234,7 @@ export default function TaskDetail({
|
|
|
228
234
|
clearInterval(tick);
|
|
229
235
|
setRefining(false);
|
|
230
236
|
refineAbortRef.current = null;
|
|
237
|
+
unregisterAiActivity(actId);
|
|
231
238
|
}
|
|
232
239
|
}, [basePath, onUpdate, task.id]);
|
|
233
240
|
|
|
@@ -244,6 +251,42 @@ export default function TaskDetail({
|
|
|
244
251
|
}, [undoSnapshot, task.id, onUpdate]);
|
|
245
252
|
|
|
246
253
|
const [promoteNotice, setPromoteNotice] = useState<string | null>(null);
|
|
254
|
+
const [showMoveModal, setShowMoveModal] = useState(false);
|
|
255
|
+
const [moveProjects, setMoveProjects] = useState<{ id: string; name: string; subs: { id: string; name: string }[] }[]>([]);
|
|
256
|
+
const [moveTargetSub, setMoveTargetSub] = useState('');
|
|
257
|
+
const [moving, setMoving] = useState(false);
|
|
258
|
+
|
|
259
|
+
const openMoveModal = useCallback(async () => {
|
|
260
|
+
try {
|
|
261
|
+
const pRes = await fetch('/api/projects');
|
|
262
|
+
const projects = await pRes.json() as { id: string; name: string }[];
|
|
263
|
+
const withSubs = await Promise.all(projects.map(async (p) => {
|
|
264
|
+
const sRes = await fetch(`/api/projects/${p.id}/sub-projects`);
|
|
265
|
+
const subs = await sRes.json() as { id: string; name: string }[];
|
|
266
|
+
return { ...p, subs };
|
|
267
|
+
}));
|
|
268
|
+
setMoveProjects(withSubs);
|
|
269
|
+
setMoveTargetSub('');
|
|
270
|
+
setShowMoveModal(true);
|
|
271
|
+
} catch { /* silent */ }
|
|
272
|
+
}, []);
|
|
273
|
+
|
|
274
|
+
const doMove = useCallback(async () => {
|
|
275
|
+
if (!moveTargetSub || moving) return;
|
|
276
|
+
setMoving(true);
|
|
277
|
+
try {
|
|
278
|
+
const res = await fetch(`/api/tasks/${task.id}/move`, {
|
|
279
|
+
method: 'POST',
|
|
280
|
+
headers: { 'Content-Type': 'application/json' },
|
|
281
|
+
body: JSON.stringify({ subProjectId: moveTargetSub }),
|
|
282
|
+
});
|
|
283
|
+
if (res.ok) {
|
|
284
|
+
setShowMoveModal(false);
|
|
285
|
+
onTaskMoved?.();
|
|
286
|
+
}
|
|
287
|
+
} catch { /* silent */ }
|
|
288
|
+
setMoving(false);
|
|
289
|
+
}, [moveTargetSub, moving, task.id, onTaskMoved]);
|
|
247
290
|
|
|
248
291
|
const promoteCheckbox = useCallback(async () => {
|
|
249
292
|
const view = editorRef.current?.view;
|
|
@@ -365,9 +408,15 @@ export default function TaskDetail({
|
|
|
365
408
|
💬 Chat
|
|
366
409
|
</button>
|
|
367
410
|
|
|
411
|
+
<button
|
|
412
|
+
onClick={openMoveModal}
|
|
413
|
+
className="text-xs text-muted-foreground hover:text-foreground transition-colors ml-auto"
|
|
414
|
+
>
|
|
415
|
+
Move
|
|
416
|
+
</button>
|
|
368
417
|
<button
|
|
369
418
|
onClick={onDelete}
|
|
370
|
-
className="text-xs text-muted-foreground hover:text-destructive transition-colors
|
|
419
|
+
className="text-xs text-muted-foreground hover:text-destructive transition-colors"
|
|
371
420
|
>
|
|
372
421
|
Delete
|
|
373
422
|
</button>
|
|
@@ -460,6 +509,45 @@ export default function TaskDetail({
|
|
|
460
509
|
onClose={() => setPaletteOpen(false)}
|
|
461
510
|
onRun={runRefine}
|
|
462
511
|
/>
|
|
512
|
+
|
|
513
|
+
{showMoveModal && (
|
|
514
|
+
<div
|
|
515
|
+
onClick={() => setShowMoveModal(false)}
|
|
516
|
+
className="fixed inset-0 z-50 flex items-center justify-center"
|
|
517
|
+
style={{ background: 'rgba(0,0,0,0.5)', backdropFilter: 'blur(2px)' }}
|
|
518
|
+
>
|
|
519
|
+
<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">
|
|
520
|
+
<div className="text-sm font-semibold text-foreground">태스크 이동</div>
|
|
521
|
+
<div className="text-xs text-muted-foreground">"{task.title}" 을 다른 프로젝트로 이동합니다.</div>
|
|
522
|
+
<select
|
|
523
|
+
value={moveTargetSub}
|
|
524
|
+
onChange={(e) => setMoveTargetSub(e.target.value)}
|
|
525
|
+
className="bg-input border border-border rounded-md px-3 py-2 text-sm text-foreground focus:border-primary focus:outline-none"
|
|
526
|
+
>
|
|
527
|
+
<option value="">이동할 대상 선택…</option>
|
|
528
|
+
{moveProjects.map(p => (
|
|
529
|
+
<optgroup key={p.id} label={p.name}>
|
|
530
|
+
{p.subs.map(s => (
|
|
531
|
+
<option key={s.id} value={s.id} disabled={s.id === subProjectId}>
|
|
532
|
+
{s.name}{s.id === subProjectId ? ' (현재)' : ''}
|
|
533
|
+
</option>
|
|
534
|
+
))}
|
|
535
|
+
</optgroup>
|
|
536
|
+
))}
|
|
537
|
+
</select>
|
|
538
|
+
<div className="flex justify-end gap-2">
|
|
539
|
+
<button onClick={() => setShowMoveModal(false)} className="text-xs text-muted-foreground px-2 py-1">취소</button>
|
|
540
|
+
<button
|
|
541
|
+
onClick={doMove}
|
|
542
|
+
disabled={!moveTargetSub || moving}
|
|
543
|
+
className="text-xs px-3 py-1 bg-primary text-primary-foreground rounded disabled:opacity-40"
|
|
544
|
+
>
|
|
545
|
+
{moving ? '이동 중…' : '이동'}
|
|
546
|
+
</button>
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
</div>
|
|
550
|
+
)}
|
|
463
551
|
</div>
|
|
464
552
|
);
|
|
465
553
|
}
|
|
@@ -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,104 @@
|
|
|
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: 'Tab', desc: '고스트 자동완성 수락' },
|
|
35
|
+
{ keys: 'Esc', desc: '고스트 해제' },
|
|
36
|
+
{ keys: 'Enter', desc: '리스트 자동 이어쓰기' },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export default function ShortcutOverlay() {
|
|
42
|
+
const [open, setOpen] = useState(false);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const onKey = (e: KeyboardEvent) => {
|
|
46
|
+
if (e.key === '?' && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
|
47
|
+
const target = e.target as HTMLElement | null;
|
|
48
|
+
const isInput =
|
|
49
|
+
target instanceof HTMLInputElement ||
|
|
50
|
+
target instanceof HTMLTextAreaElement ||
|
|
51
|
+
(target?.isContentEditable ?? false) ||
|
|
52
|
+
!!target?.closest?.('.cm-editor');
|
|
53
|
+
if (isInput) return;
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
setOpen(prev => !prev);
|
|
56
|
+
}
|
|
57
|
+
if (e.key === 'Escape' && open) {
|
|
58
|
+
setOpen(false);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
window.addEventListener('keydown', onKey);
|
|
62
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
63
|
+
}, [open]);
|
|
64
|
+
|
|
65
|
+
if (!open) return null;
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div
|
|
69
|
+
onClick={() => setOpen(false)}
|
|
70
|
+
className="fixed inset-0 z-[70] flex items-center justify-center"
|
|
71
|
+
style={{ background: 'rgba(0,0,0,0.5)', backdropFilter: 'blur(3px)' }}
|
|
72
|
+
>
|
|
73
|
+
<div
|
|
74
|
+
onClick={(e) => e.stopPropagation()}
|
|
75
|
+
className="bg-card border border-border rounded-xl shadow-2xl w-full max-w-lg animate-dialog-in p-5"
|
|
76
|
+
>
|
|
77
|
+
<div className="flex items-center justify-between mb-4">
|
|
78
|
+
<h2 className="text-sm font-semibold text-foreground">Keyboard Shortcuts</h2>
|
|
79
|
+
<button onClick={() => setOpen(false)} className="text-muted-foreground hover:text-foreground text-lg leading-none">×</button>
|
|
80
|
+
</div>
|
|
81
|
+
<div className="space-y-4">
|
|
82
|
+
{SECTIONS.map((section) => (
|
|
83
|
+
<div key={section.title}>
|
|
84
|
+
<div className="text-[10px] uppercase tracking-wider text-muted-foreground/70 mb-1.5">{section.title}</div>
|
|
85
|
+
<div className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1">
|
|
86
|
+
{section.shortcuts.map((s) => (
|
|
87
|
+
<div key={s.keys} className="contents">
|
|
88
|
+
<kbd className="text-xs font-mono px-1.5 py-0.5 rounded bg-muted border border-border text-foreground text-right whitespace-nowrap">
|
|
89
|
+
{s.keys}
|
|
90
|
+
</kbd>
|
|
91
|
+
<span className="text-xs text-muted-foreground self-center">{s.desc}</span>
|
|
92
|
+
</div>
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
<div className="mt-4 text-[10px] text-muted-foreground/50 text-center">
|
|
99
|
+
? 를 다시 눌러 닫기
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import AdvisorChat from '@/components/advisor/AdvisorChat';
|
|
4
|
+
|
|
5
|
+
export default function ProjectAdvisor({
|
|
6
|
+
projectId,
|
|
7
|
+
projectName,
|
|
8
|
+
onClose,
|
|
9
|
+
}: {
|
|
10
|
+
projectId: string;
|
|
11
|
+
projectName?: string;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
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
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -11,6 +11,7 @@ import AiPolicyModal from '@/components/ui/AiPolicyModal';
|
|
|
11
11
|
import GitSyncResultsModal from '@/components/dashboard/GitSyncResultsModal';
|
|
12
12
|
import FileTreeDrawer from '@/components/ui/FileTreeDrawer';
|
|
13
13
|
import AutoDistributeModal from '@/components/ui/AutoDistributeModal';
|
|
14
|
+
import ProjectAdvisor from '@/components/workspace/ProjectAdvisor';
|
|
14
15
|
import type { ISubProject, ITask, TaskStatus, ISubProjectWithStats, IGitSyncResult } from '@/types';
|
|
15
16
|
|
|
16
17
|
interface IProject {
|
|
@@ -62,6 +63,7 @@ export default function WorkspacePanel({
|
|
|
62
63
|
const [showBrainstorm, setShowBrainstorm] = useState(true);
|
|
63
64
|
const [newSubName, setNewSubName] = useState('');
|
|
64
65
|
const [showAiPolicy, setShowAiPolicy] = useState(false);
|
|
66
|
+
const [showAdvisor, setShowAdvisor] = useState(false);
|
|
65
67
|
const [syncing, setSyncing] = useState(false);
|
|
66
68
|
const [syncResults, setSyncResults] = useState<IGitSyncResult[] | null>(null);
|
|
67
69
|
const [lastSyncTime, setLastSyncTime] = useState<Date | null>(null);
|
|
@@ -359,6 +361,12 @@ export default function WorkspacePanel({
|
|
|
359
361
|
addBtn?.click();
|
|
360
362
|
return;
|
|
361
363
|
}
|
|
364
|
+
// ⌘L — toggle project advisor (works even from input/editor)
|
|
365
|
+
if ((e.metaKey || e.ctrlKey) && e.code === 'KeyL') {
|
|
366
|
+
e.preventDefault();
|
|
367
|
+
setShowAdvisor(prev => !prev);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
362
370
|
if (selectedTaskId && selectedSubId && !isInput) {
|
|
363
371
|
const statusMap: Record<string, TaskStatus> = {
|
|
364
372
|
'Digit1': 'idea', 'Digit2': 'doing', 'Digit3': 'done', 'Digit4': 'problem',
|
|
@@ -441,6 +449,15 @@ export default function WorkspacePanel({
|
|
|
441
449
|
}`}>
|
|
442
450
|
AI Policy{project.ai_context ? ' *' : ''}
|
|
443
451
|
</button>
|
|
452
|
+
<button onClick={() => setShowAdvisor(true)}
|
|
453
|
+
className={`px-3 py-1.5 text-xs border rounded-md transition-colors ${
|
|
454
|
+
showAdvisor
|
|
455
|
+
? 'bg-primary/15 text-primary border-primary/30'
|
|
456
|
+
: 'bg-muted hover:bg-card-hover text-muted-foreground border-border'
|
|
457
|
+
}`}
|
|
458
|
+
title="프로젝트 어드바이저 (⌘L)">
|
|
459
|
+
Advisor
|
|
460
|
+
</button>
|
|
444
461
|
{project.project_path ? (
|
|
445
462
|
<div className="flex items-center gap-1.5">
|
|
446
463
|
<button
|
|
@@ -537,6 +554,11 @@ export default function WorkspacePanel({
|
|
|
537
554
|
siblingTasks={tasks}
|
|
538
555
|
onUpdate={handleTaskUpdate} onDelete={handleTaskDelete}
|
|
539
556
|
onTaskPromoted={(newTask) => setTasks(prev => [...prev, newTask])}
|
|
557
|
+
onTaskMoved={() => {
|
|
558
|
+
// Task moved away — remove from current list and deselect
|
|
559
|
+
setTasks(prev => prev.filter(t => t.id !== selectedTaskId));
|
|
560
|
+
setSelectedTaskId(null);
|
|
561
|
+
}}
|
|
540
562
|
onChatStateChange={(taskId, state) => {
|
|
541
563
|
setChatStates(prev => ({ ...prev, [taskId]: state }));
|
|
542
564
|
}} />
|
|
@@ -603,6 +625,13 @@ export default function WorkspacePanel({
|
|
|
603
625
|
onClose={() => setShowAutoDistribute(false)}
|
|
604
626
|
onApplied={() => { loadSubProjects(); }}
|
|
605
627
|
/>
|
|
628
|
+
{showAdvisor && (
|
|
629
|
+
<ProjectAdvisor
|
|
630
|
+
projectId={id}
|
|
631
|
+
projectName={project.name}
|
|
632
|
+
onClose={() => setShowAdvisor(false)}
|
|
633
|
+
/>
|
|
634
|
+
)}
|
|
606
635
|
</div>
|
|
607
636
|
);
|
|
608
637
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react';
|
|
2
|
+
import { getAiActivities, subscribeAiActivity, type AiActivity } from '@/lib/ai-activity';
|
|
3
|
+
|
|
4
|
+
export function useAiActivity(): AiActivity[] {
|
|
5
|
+
return useSyncExternalStore(subscribeAiActivity, getAiActivities, getAiActivities);
|
|
6
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { listProjects } from '../db/queries/projects';
|
|
2
|
+
import { getSubProjects } from '../db/queries/sub-projects';
|
|
3
|
+
import { getTasksByProject } from '../db/queries/tasks';
|
|
4
|
+
import { getBrainstorm } from '../db/queries/brainstorms';
|
|
5
|
+
import type { ITask, TaskStatus } from '../../types';
|
|
6
|
+
|
|
7
|
+
const MAX_BRAINSTORM = 1500;
|
|
8
|
+
const NOTE_LIMIT = 120;
|
|
9
|
+
const MAX_HISTORY_MESSAGES = 20;
|
|
10
|
+
|
|
11
|
+
function truncate(s: string | null | undefined, max: number): string {
|
|
12
|
+
if (!s) return '';
|
|
13
|
+
if (s.length <= max) return s;
|
|
14
|
+
return s.slice(0, max) + '…';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const STATUS_ICON: Record<string, string> = {
|
|
18
|
+
idea: 'idea', doing: 'DOING', writing: 'writing', submitted: 'submitted',
|
|
19
|
+
testing: 'testing', done: 'done', problem: 'PROBLEM',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function buildGlobalAdvisorPrompt(): string {
|
|
23
|
+
const projects = listProjects();
|
|
24
|
+
|
|
25
|
+
const parts: string[] = [];
|
|
26
|
+
parts.push('당신은 사용자의 전체 워크스페이스를 조망하는 AI 어드바이저입니다.');
|
|
27
|
+
parts.push('여러 프로젝트의 현황을 파악하고, 우선순위·방향·빠진 부분·크로스-프로젝트 이슈 등을 논의합니다.');
|
|
28
|
+
parts.push('한국어로 간결하게 답하세요. 긴 설교 금지.\n');
|
|
29
|
+
|
|
30
|
+
parts.push('=== ALL WORKSPACES ===\n');
|
|
31
|
+
|
|
32
|
+
let totalTasks = 0;
|
|
33
|
+
let totalDone = 0;
|
|
34
|
+
let totalProblem = 0;
|
|
35
|
+
|
|
36
|
+
for (const project of projects) {
|
|
37
|
+
const subs = getSubProjects(project.id);
|
|
38
|
+
const allTasks = getTasksByProject(project.id);
|
|
39
|
+
const brainstorm = getBrainstorm(project.id);
|
|
40
|
+
|
|
41
|
+
totalTasks += allTasks.length;
|
|
42
|
+
totalDone += allTasks.filter(t => t.status === 'done').length;
|
|
43
|
+
totalProblem += allTasks.filter(t => t.status === 'problem').length;
|
|
44
|
+
|
|
45
|
+
const counts: Record<string, number> = {};
|
|
46
|
+
const todayTasks: string[] = [];
|
|
47
|
+
const problemTasks: string[] = [];
|
|
48
|
+
for (const t of allTasks) {
|
|
49
|
+
counts[t.status] = (counts[t.status] ?? 0) + 1;
|
|
50
|
+
if (t.is_today) todayTasks.push(t.title);
|
|
51
|
+
if (t.status === 'problem') problemTasks.push(t.title);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const lines: string[] = [];
|
|
55
|
+
lines.push(`## ${project.name}`);
|
|
56
|
+
if (project.description) lines.push(project.description);
|
|
57
|
+
const statsStr = Object.entries(counts).map(([k, v]) => `${k}:${v}`).join(' / ');
|
|
58
|
+
lines.push(`태스크 ${allTasks.length}개 (${statsStr})`);
|
|
59
|
+
if (todayTasks.length) lines.push(`Today: ${todayTasks.join(', ')}`);
|
|
60
|
+
if (problemTasks.length) lines.push(`문제: ${problemTasks.join(', ')}`);
|
|
61
|
+
|
|
62
|
+
if (brainstorm?.content) {
|
|
63
|
+
lines.push(`\n브레인스토밍 요약: ${truncate(brainstorm.content, MAX_BRAINSTORM)}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Show active (non-done, non-archived) tasks by sub-project
|
|
67
|
+
for (const sub of subs) {
|
|
68
|
+
const subTasks = allTasks.filter(t => t.sub_project_id === sub.id && t.status !== 'done');
|
|
69
|
+
if (!subTasks.length) continue;
|
|
70
|
+
lines.push(`\n### ${sub.name}`);
|
|
71
|
+
for (const t of subTasks) {
|
|
72
|
+
const note = truncate(t.description, NOTE_LIMIT);
|
|
73
|
+
const flags = [t.priority === 'high' ? 'HIGH' : null, t.is_today ? 'today' : null].filter(Boolean).join(', ');
|
|
74
|
+
const flagStr = flags ? ` (${flags})` : '';
|
|
75
|
+
lines.push(`- [${STATUS_ICON[t.status] ?? t.status}] **${t.title}**${flagStr}${note ? ' — ' + note : ''}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
parts.push(lines.join('\n'));
|
|
80
|
+
parts.push('');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
parts.push('---');
|
|
84
|
+
parts.push(`전체: ${projects.length}개 워크스페이스, ${totalTasks}개 태스크 (완료 ${totalDone}, 문제 ${totalProblem})`);
|
|
85
|
+
|
|
86
|
+
return parts.join('\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function trimHistory(
|
|
90
|
+
messages: { role: string; content: string }[],
|
|
91
|
+
): { role: string; content: string }[] {
|
|
92
|
+
if (messages.length <= MAX_HISTORY_MESSAGES) return messages;
|
|
93
|
+
return messages.slice(-MAX_HISTORY_MESSAGES);
|
|
94
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { getProject } from '../db/queries/projects';
|
|
2
|
+
import { getSubProjects } from '../db/queries/sub-projects';
|
|
3
|
+
import { getTasksByProject } from '../db/queries/tasks';
|
|
4
|
+
import { getBrainstorm } from '../db/queries/brainstorms';
|
|
5
|
+
import type { ITask, TaskStatus } from '../../types';
|
|
6
|
+
|
|
7
|
+
const MAX_BRAINSTORM = 4000;
|
|
8
|
+
const NOTE_LIMIT_ACTIVE = 500;
|
|
9
|
+
const NOTE_LIMIT_DEFAULT = 200;
|
|
10
|
+
const MAX_HISTORY_MESSAGES = 20;
|
|
11
|
+
|
|
12
|
+
function truncate(s: string | null | undefined, max: number): string {
|
|
13
|
+
if (!s) return '';
|
|
14
|
+
if (s.length <= max) return s;
|
|
15
|
+
return s.slice(0, max) + '…';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isActive(task: ITask): boolean {
|
|
19
|
+
return (['doing', 'problem', 'testing'] as TaskStatus[]).includes(task.status) || task.is_today;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const STATUS_ICON: Record<string, string> = {
|
|
23
|
+
idea: 'idea', doing: 'DOING', writing: 'writing', submitted: 'submitted',
|
|
24
|
+
testing: 'testing', done: 'done', problem: 'PROBLEM',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function buildProjectAdvisorPrompt(projectId: string): string {
|
|
28
|
+
const project = getProject(projectId);
|
|
29
|
+
if (!project) return '';
|
|
30
|
+
|
|
31
|
+
const brainstorm = getBrainstorm(projectId);
|
|
32
|
+
const subs = getSubProjects(projectId);
|
|
33
|
+
const allTasks = getTasksByProject(projectId);
|
|
34
|
+
|
|
35
|
+
// Group tasks by sub-project
|
|
36
|
+
const tasksBySub = new Map<string, ITask[]>();
|
|
37
|
+
for (const t of allTasks) {
|
|
38
|
+
const list = tasksBySub.get(t.sub_project_id) ?? [];
|
|
39
|
+
list.push(t);
|
|
40
|
+
tasksBySub.set(t.sub_project_id, list);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Build task summary per sub-project
|
|
44
|
+
const subSections: string[] = [];
|
|
45
|
+
for (const sub of subs) {
|
|
46
|
+
const tasks = tasksBySub.get(sub.id) ?? [];
|
|
47
|
+
if (tasks.length === 0) {
|
|
48
|
+
subSections.push(`### ${sub.name}\n${sub.description || '(설명 없음)'}\n태스크 없음.`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const lines: string[] = [];
|
|
52
|
+
lines.push(`### ${sub.name}`);
|
|
53
|
+
if (sub.description) lines.push(sub.description);
|
|
54
|
+
lines.push(`태스크 ${tasks.length}개:`);
|
|
55
|
+
for (const t of tasks) {
|
|
56
|
+
const noteLimit = isActive(t) ? NOTE_LIMIT_ACTIVE : NOTE_LIMIT_DEFAULT;
|
|
57
|
+
const note = truncate(t.description, noteLimit);
|
|
58
|
+
const flags = [t.priority === 'high' ? 'HIGH' : null, t.is_today ? 'today' : null].filter(Boolean).join(', ');
|
|
59
|
+
const flagStr = flags ? ` (${flags})` : '';
|
|
60
|
+
const noteStr = note ? ` — ${note}` : '';
|
|
61
|
+
lines.push(`- [${STATUS_ICON[t.status] ?? t.status}] **${t.title}**${flagStr}${noteStr}`);
|
|
62
|
+
}
|
|
63
|
+
subSections.push(lines.join('\n'));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Stats
|
|
67
|
+
const counts: Record<string, number> = {};
|
|
68
|
+
let todayCount = 0;
|
|
69
|
+
const problemTasks: string[] = [];
|
|
70
|
+
for (const t of allTasks) {
|
|
71
|
+
counts[t.status] = (counts[t.status] ?? 0) + 1;
|
|
72
|
+
if (t.is_today) todayCount++;
|
|
73
|
+
if (t.status === 'problem') problemTasks.push(t.title);
|
|
74
|
+
}
|
|
75
|
+
const statsLines = [
|
|
76
|
+
`- 전체: ${allTasks.length}개`,
|
|
77
|
+
...Object.entries(counts).map(([k, v]) => ` - ${k}: ${v}`),
|
|
78
|
+
`- Today 표시: ${todayCount}개`,
|
|
79
|
+
];
|
|
80
|
+
if (problemTasks.length > 0) {
|
|
81
|
+
statsLines.push(`- 문제 태스크: ${problemTasks.join(', ')}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Assemble
|
|
85
|
+
const parts: string[] = [];
|
|
86
|
+
parts.push(`당신은 프로젝트 "${project.name}"의 어드바이저입니다.`);
|
|
87
|
+
parts.push(`사용자가 프로젝트 방향, 우선순위, 빠진 부분, 다음 단계 등을 논의하면 프로젝트 전체 맥락을 바탕으로 간결하게 답합니다.`);
|
|
88
|
+
parts.push(`태스크를 언급할 때는 정확한 제목을 쓰세요. 한국어로 답하세요. 긴 설교는 금지.`);
|
|
89
|
+
|
|
90
|
+
if (project.ai_context) {
|
|
91
|
+
parts.push(`\nProject AI Policy:\n${project.ai_context}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
parts.push('\n=== PROJECT CONTEXT ===');
|
|
95
|
+
|
|
96
|
+
if (brainstorm?.content) {
|
|
97
|
+
parts.push(`\n## 브레인스토밍\n${truncate(brainstorm.content, MAX_BRAINSTORM)}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
parts.push(`\n## 프로젝트 & 태스크\n${subSections.join('\n\n')}`);
|
|
101
|
+
parts.push(`\n## 통계\n${statsLines.join('\n')}`);
|
|
102
|
+
|
|
103
|
+
return parts.join('\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function trimConversationHistory(
|
|
107
|
+
messages: { role: string; content: string }[],
|
|
108
|
+
): { role: string; content: string }[] {
|
|
109
|
+
if (messages.length <= MAX_HISTORY_MESSAGES) return messages;
|
|
110
|
+
return messages.slice(-MAX_HISTORY_MESSAGES);
|
|
111
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface AiActivity {
|
|
2
|
+
id: string;
|
|
3
|
+
type: 'refine' | 'task-chat' | 'project-advisor' | 'global-advisor' | 'watch';
|
|
4
|
+
label: string;
|
|
5
|
+
startedAt: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Module-level store — survives React component unmounts.
|
|
9
|
+
let activities: AiActivity[] = [];
|
|
10
|
+
let listeners: Set<() => void> = new Set();
|
|
11
|
+
|
|
12
|
+
function notify() {
|
|
13
|
+
for (const fn of listeners) fn();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function registerAiActivity(activity: AiActivity) {
|
|
17
|
+
activities = [...activities, activity];
|
|
18
|
+
notify();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function unregisterAiActivity(id: string) {
|
|
22
|
+
activities = activities.filter(a => a.id !== id);
|
|
23
|
+
notify();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getAiActivities(): AiActivity[] {
|
|
27
|
+
return activities;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function subscribeAiActivity(listener: () => void): () => void {
|
|
31
|
+
listeners.add(listener);
|
|
32
|
+
return () => { listeners.delete(listener); };
|
|
33
|
+
}
|