idea-manager 2.0.0 → 2.1.1
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 +15 -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 -130
- 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 +22 -11
- package/.next/server/app/api/global-advisor/route_client-reference-manifest.js +1 -1
- 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_client-reference-manifest.js +1 -1
- 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_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_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 -130
- 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 -130
- 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_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_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/tasks/[taskId]/move/route.js +2 -131
- package/.next/server/app/api/tasks/[taskId]/move/route_client-reference-manifest.js +1 -1
- 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 +10 -10
- 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 +13 -11
- package/.next/server/chunks/{117.js → 697.js} +8 -2
- package/.next/server/pages/404.html +2 -2
- package/.next/server/pages/500.html +2 -2
- package/.next/static/DgEYXBynAPZFgESajdpuy/_buildManifest.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-517beed417b9d63a.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/31b47c8edfe1a047.css +3 -0
- package/package.json +22 -8
- package/src/app/api/advisor-actions/route.ts +52 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/reorder/route.ts +24 -0
- package/src/components/advisor/ActionBlock.tsx +124 -0
- package/src/components/advisor/AdvisorChat.tsx +19 -9
- package/src/components/brainstorm/Editor.tsx +5 -13
- package/src/components/task/NoteEditor.tsx +137 -0
- package/src/components/task/ProjectTree.tsx +105 -57
- package/src/components/task/TaskDetail.tsx +93 -0
- package/src/components/ui/ShortcutOverlay.tsx +4 -0
- package/src/components/workspace/WorkspacePanel.tsx +69 -3
- package/src/lib/advisor-actions/parse.ts +59 -0
- package/src/lib/ai/global-context.ts +24 -4
- package/src/lib/ai/project-context.ts +22 -2
- package/src/types/advisor-actions.ts +25 -0
- package/.next/static/5I00mUqwYdcGFbs7ETC9d/_buildManifest.js +0 -1
- package/.next/static/chunks/app/_global-error/page-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/archive/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/filesystem/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/filesystem/tree/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/global-advisor/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/global-memo/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/health/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/maintenance/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/advisor/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/apply-distribute/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/auto-distribute/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/brainstorm/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/git-sync/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/[subId]/tasks/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/sub-projects/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/projects/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/search/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/sync/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/tasks/[taskId]/move/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/update/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/api/version/route-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/app/page-b6580e09a4af9dff.js +0 -28
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-92496bc87f5c29a9.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-92496bc87f5c29a9.js +0 -1
- package/.next/static/css/a57b4564ec9082ab.css +0 -3
- /package/.next/static/{5I00mUqwYdcGFbs7ETC9d → DgEYXBynAPZFgESajdpuy}/_ssgManifest.js +0 -0
|
@@ -50,6 +50,7 @@ export default function ProjectTree({
|
|
|
50
50
|
onTodayToggle,
|
|
51
51
|
onDeleteTask,
|
|
52
52
|
onReorderSubs,
|
|
53
|
+
onReorderTasks,
|
|
53
54
|
onAutoDistribute,
|
|
54
55
|
chatStates,
|
|
55
56
|
}: {
|
|
@@ -67,6 +68,7 @@ export default function ProjectTree({
|
|
|
67
68
|
onTodayToggle: (taskId: string, isToday: boolean) => void;
|
|
68
69
|
onDeleteTask: (taskId: string) => void;
|
|
69
70
|
onReorderSubs?: (orderedIds: string[]) => void;
|
|
71
|
+
onReorderTasks?: (orderedIds: string[]) => void;
|
|
70
72
|
onAutoDistribute?: () => void;
|
|
71
73
|
chatStates?: Record<string, 'idle' | 'loading' | 'done'>;
|
|
72
74
|
}) {
|
|
@@ -172,6 +174,7 @@ export default function ProjectTree({
|
|
|
172
174
|
onStatusChange={onStatusChange}
|
|
173
175
|
onTodayToggle={onTodayToggle}
|
|
174
176
|
onDeleteTask={onDeleteTask}
|
|
177
|
+
onReorderTasks={onReorderTasks}
|
|
175
178
|
onAddTask={handleAddTask}
|
|
176
179
|
onSetAddingTaskFor={setAddingTaskFor}
|
|
177
180
|
onSetNewTaskTitle={setNewTaskTitle}
|
|
@@ -201,6 +204,7 @@ function SortableSubProject({
|
|
|
201
204
|
onStatusChange,
|
|
202
205
|
onTodayToggle,
|
|
203
206
|
onDeleteTask,
|
|
207
|
+
onReorderTasks,
|
|
204
208
|
onAddTask,
|
|
205
209
|
onSetAddingTaskFor,
|
|
206
210
|
onSetNewTaskTitle,
|
|
@@ -221,6 +225,7 @@ function SortableSubProject({
|
|
|
221
225
|
onStatusChange: (taskId: string, status: TaskStatus) => void;
|
|
222
226
|
onTodayToggle: (taskId: string, isToday: boolean) => void;
|
|
223
227
|
onDeleteTask: (taskId: string) => void;
|
|
228
|
+
onReorderTasks?: (orderedIds: string[]) => void;
|
|
224
229
|
onAddTask: (subId: string) => void;
|
|
225
230
|
onSetAddingTaskFor: (subId: string | null) => void;
|
|
226
231
|
onSetNewTaskTitle: (title: string) => void;
|
|
@@ -229,6 +234,23 @@ function SortableSubProject({
|
|
|
229
234
|
const [editing, setEditing] = useState(false);
|
|
230
235
|
const [editValue, setEditValue] = useState(sp.name);
|
|
231
236
|
|
|
237
|
+
const taskSensors = useSensors(
|
|
238
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
|
239
|
+
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const handleTaskDragEnd = (event: DragEndEvent) => {
|
|
243
|
+
const { active, over } = event;
|
|
244
|
+
if (!over || active.id === over.id || !onReorderTasks) return;
|
|
245
|
+
const oldIndex = subTasks.findIndex(t => t.id === active.id);
|
|
246
|
+
const newIndex = subTasks.findIndex(t => t.id === over.id);
|
|
247
|
+
if (oldIndex === -1 || newIndex === -1) return;
|
|
248
|
+
const newOrder = [...subTasks];
|
|
249
|
+
const [moved] = newOrder.splice(oldIndex, 1);
|
|
250
|
+
newOrder.splice(newIndex, 0, moved);
|
|
251
|
+
onReorderTasks(newOrder.map(t => t.id));
|
|
252
|
+
};
|
|
253
|
+
|
|
232
254
|
const {
|
|
233
255
|
attributes,
|
|
234
256
|
listeners,
|
|
@@ -348,63 +370,22 @@ function SortableSubProject({
|
|
|
348
370
|
No tasks
|
|
349
371
|
</div>
|
|
350
372
|
)}
|
|
351
|
-
{
|
|
352
|
-
<
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
className="flex-shrink-0 text-sm"
|
|
368
|
-
title={`Status: ${task.status}`}
|
|
369
|
-
>
|
|
370
|
-
{statusIcon(task.status)}
|
|
371
|
-
</button>
|
|
372
|
-
<span className={`tree-priority-dot ${PRIORITY_COLORS[task.priority]}`} />
|
|
373
|
-
<span className={`flex-1 truncate ${task.status === 'done' ? 'text-muted-foreground line-through' : ''}`}>
|
|
374
|
-
{task.title}
|
|
375
|
-
</span>
|
|
376
|
-
{chatStates?.[task.id] === 'loading' && (
|
|
377
|
-
<span className="flex-shrink-0 flex items-center gap-1 text-[10px] text-warning" title="AI 응답 대기 중">
|
|
378
|
-
<span className="inline-block w-1.5 h-1.5 rounded-full bg-warning animate-pulse" />
|
|
379
|
-
AI...
|
|
380
|
-
</span>
|
|
381
|
-
)}
|
|
382
|
-
{chatStates?.[task.id] === 'done' && (
|
|
383
|
-
<span className="flex-shrink-0 text-[10px] text-success" title="AI 응답 완료">
|
|
384
|
-
✓
|
|
385
|
-
</span>
|
|
386
|
-
)}
|
|
387
|
-
{task.is_today && (
|
|
388
|
-
<button
|
|
389
|
-
onClick={(e) => { e.stopPropagation(); onTodayToggle(task.id, false); }}
|
|
390
|
-
className="text-xs flex-shrink-0 text-primary" title="Remove from today"
|
|
391
|
-
>
|
|
392
|
-
*
|
|
393
|
-
</button>
|
|
394
|
-
)}
|
|
395
|
-
<button
|
|
396
|
-
onClick={(e) => {
|
|
397
|
-
e.stopPropagation();
|
|
398
|
-
onDeleteTask(task.id);
|
|
399
|
-
}}
|
|
400
|
-
className="flex-shrink-0 text-muted-foreground/0 group-hover/task:text-muted-foreground
|
|
401
|
-
hover:!text-destructive transition-colors text-xs px-0.5"
|
|
402
|
-
title="Delete task"
|
|
403
|
-
>
|
|
404
|
-
×
|
|
405
|
-
</button>
|
|
406
|
-
</div>
|
|
407
|
-
))}
|
|
373
|
+
<DndContext sensors={taskSensors} collisionDetection={closestCenter} onDragEnd={handleTaskDragEnd}>
|
|
374
|
+
<SortableContext items={subTasks.map(t => t.id)} strategy={verticalListSortingStrategy}>
|
|
375
|
+
{subTasks.map((task) => (
|
|
376
|
+
<SortableTask
|
|
377
|
+
key={task.id}
|
|
378
|
+
task={task}
|
|
379
|
+
isSelected={selectedTaskId === task.id}
|
|
380
|
+
chatState={chatStates?.[task.id]}
|
|
381
|
+
onSelect={() => onSelectTask(task.id)}
|
|
382
|
+
onStatusChange={onStatusChange}
|
|
383
|
+
onTodayToggle={onTodayToggle}
|
|
384
|
+
onDelete={() => onDeleteTask(task.id)}
|
|
385
|
+
/>
|
|
386
|
+
))}
|
|
387
|
+
</SortableContext>
|
|
388
|
+
</DndContext>
|
|
408
389
|
|
|
409
390
|
{/* Add task input */}
|
|
410
391
|
{addingTaskFor === sp.id ? (
|
|
@@ -455,6 +436,73 @@ function SortableSubProject({
|
|
|
455
436
|
);
|
|
456
437
|
}
|
|
457
438
|
|
|
439
|
+
function SortableTask({
|
|
440
|
+
task,
|
|
441
|
+
isSelected,
|
|
442
|
+
chatState,
|
|
443
|
+
onSelect,
|
|
444
|
+
onStatusChange,
|
|
445
|
+
onTodayToggle,
|
|
446
|
+
onDelete,
|
|
447
|
+
}: {
|
|
448
|
+
task: ITask;
|
|
449
|
+
isSelected: boolean;
|
|
450
|
+
chatState?: 'idle' | 'loading' | 'done';
|
|
451
|
+
onSelect: () => void;
|
|
452
|
+
onStatusChange: (taskId: string, status: TaskStatus) => void;
|
|
453
|
+
onTodayToggle: (taskId: string, isToday: boolean) => void;
|
|
454
|
+
onDelete: () => void;
|
|
455
|
+
}) {
|
|
456
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: task.id });
|
|
457
|
+
const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1 };
|
|
458
|
+
|
|
459
|
+
return (
|
|
460
|
+
<div
|
|
461
|
+
ref={setNodeRef}
|
|
462
|
+
style={style}
|
|
463
|
+
onClick={onSelect}
|
|
464
|
+
className={`group/task flex items-center gap-1 pl-2 pr-2 py-1.5 cursor-pointer transition-colors text-sm border-l-2 ${
|
|
465
|
+
isSelected ? 'bg-card-hover border-l-primary' : 'border-l-transparent hover:bg-card-hover/50'
|
|
466
|
+
}`}
|
|
467
|
+
>
|
|
468
|
+
<span
|
|
469
|
+
{...attributes}
|
|
470
|
+
{...listeners}
|
|
471
|
+
className="w-3 h-4 flex items-center justify-center text-[10px] text-muted-foreground/30 hover:text-muted-foreground cursor-grab active:cursor-grabbing flex-shrink-0"
|
|
472
|
+
onClick={(e) => e.stopPropagation()}
|
|
473
|
+
>
|
|
474
|
+
⠿
|
|
475
|
+
</span>
|
|
476
|
+
<button
|
|
477
|
+
onClick={(e) => { e.stopPropagation(); onStatusChange(task.id, getNextStatus(task.status)); }}
|
|
478
|
+
className="flex-shrink-0 text-sm"
|
|
479
|
+
title={`Status: ${task.status}`}
|
|
480
|
+
>
|
|
481
|
+
{statusIcon(task.status)}
|
|
482
|
+
</button>
|
|
483
|
+
<span className={`tree-priority-dot ${PRIORITY_COLORS[task.priority]}`} />
|
|
484
|
+
<span className={`flex-1 truncate ${task.status === 'done' ? 'text-muted-foreground line-through' : ''}`}>
|
|
485
|
+
{task.title}
|
|
486
|
+
</span>
|
|
487
|
+
{chatState === 'loading' && (
|
|
488
|
+
<span className="flex-shrink-0 flex items-center gap-1 text-[10px] text-warning">
|
|
489
|
+
<span className="inline-block w-1.5 h-1.5 rounded-full bg-warning animate-pulse" />
|
|
490
|
+
</span>
|
|
491
|
+
)}
|
|
492
|
+
{chatState === 'done' && <span className="flex-shrink-0 text-[10px] text-success">✓</span>}
|
|
493
|
+
{task.is_today && (
|
|
494
|
+
<button onClick={(e) => { e.stopPropagation(); onTodayToggle(task.id, false); }} className="text-xs flex-shrink-0 text-primary">*</button>
|
|
495
|
+
)}
|
|
496
|
+
<button
|
|
497
|
+
onClick={(e) => { e.stopPropagation(); onDelete(); }}
|
|
498
|
+
className="flex-shrink-0 text-muted-foreground/0 group-hover/task:text-muted-foreground hover:!text-destructive transition-colors text-xs px-0.5"
|
|
499
|
+
>
|
|
500
|
+
×
|
|
501
|
+
</button>
|
|
502
|
+
</div>
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
458
506
|
function getNextStatus(current: TaskStatus): TaskStatus {
|
|
459
507
|
const flow: TaskStatus[] = ['idea', 'doing', 'done'];
|
|
460
508
|
const idx = flow.indexOf(current);
|
|
@@ -37,7 +37,10 @@ export default function TaskDetail({
|
|
|
37
37
|
const [description, setDescription] = useState(task.description);
|
|
38
38
|
const [editingTitle, setEditingTitle] = useState(false);
|
|
39
39
|
const [copied, setCopied] = useState(false);
|
|
40
|
+
const [tagInput, setTagInput] = useState('');
|
|
41
|
+
const [showTagInput, setShowTagInput] = useState(false);
|
|
40
42
|
const [chatOpen, setChatOpen] = useState(false);
|
|
43
|
+
const [focusMode, setFocusMode] = useState(false);
|
|
41
44
|
const chatWasManuallyToggled = useRef(false);
|
|
42
45
|
|
|
43
46
|
// Auto-open the chat panel while the task is being executed by the watcher —
|
|
@@ -328,6 +331,48 @@ export default function TaskDetail({
|
|
|
328
331
|
|
|
329
332
|
const priorities: ItemPriority[] = ['high', 'medium', 'low'];
|
|
330
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
|
+
|
|
331
376
|
return (
|
|
332
377
|
<div className="flex flex-col h-full">
|
|
333
378
|
{/* Header */}
|
|
@@ -397,6 +442,14 @@ export default function TaskDetail({
|
|
|
397
442
|
>
|
|
398
443
|
{copied ? '✓ Copied' : 'Copy as Prompt'}
|
|
399
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>
|
|
400
453
|
<button
|
|
401
454
|
onClick={() => { chatWasManuallyToggled.current = true; setChatOpen(v => !v); }}
|
|
402
455
|
className={`text-xs px-2 py-0.5 rounded transition-colors border ${
|
|
@@ -421,6 +474,46 @@ export default function TaskDetail({
|
|
|
421
474
|
Delete
|
|
422
475
|
</button>
|
|
423
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>
|
|
424
517
|
</div>
|
|
425
518
|
|
|
426
519
|
{/* Note editor */}
|
|
@@ -31,6 +31,10 @@ const SECTIONS: { title: string; shortcuts: { keys: string; desc: string }[] }[]
|
|
|
31
31
|
shortcuts: [
|
|
32
32
|
{ keys: '⌘K', desc: 'AI 명령 팔레트' },
|
|
33
33
|
{ keys: '⌘⇧T', desc: '체크박스/불릿 → 태스크 승격' },
|
|
34
|
+
{ keys: '/', desc: '슬래시 명령 (/todo, /table, /code…)' },
|
|
35
|
+
{ keys: '⌘↵', desc: '체크박스 토글 [ ] ↔ [x]' },
|
|
36
|
+
{ keys: '⌘⇧↵', desc: '테이블 행 추가' },
|
|
37
|
+
{ keys: '⌘⇧⌫', desc: '테이블 행 삭제' },
|
|
34
38
|
{ keys: 'Tab', desc: '고스트 자동완성 수락' },
|
|
35
39
|
{ keys: 'Esc', desc: '고스트 해제' },
|
|
36
40
|
{ keys: 'Enter', desc: '리스트 자동 이어쓰기' },
|
|
@@ -70,6 +70,7 @@ export default function WorkspacePanel({
|
|
|
70
70
|
const [showFileTree, setShowFileTree] = useState(false);
|
|
71
71
|
const [showAutoDistribute, setShowAutoDistribute] = useState(false);
|
|
72
72
|
const [chatStates, setChatStates] = useState<Record<string, 'idle' | 'loading' | 'done'>>({});
|
|
73
|
+
const [deleteToast, setDeleteToast] = useState<{ taskId: string; title: string; timer: ReturnType<typeof setTimeout> } | null>(null);
|
|
73
74
|
const syncingRef = useRef(false);
|
|
74
75
|
|
|
75
76
|
// Resizable panel widths
|
|
@@ -156,6 +157,16 @@ export default function WorkspacePanel({
|
|
|
156
157
|
|
|
157
158
|
const selectedTask = tasks.find(t => t.id === selectedTaskId) ?? null;
|
|
158
159
|
|
|
160
|
+
// Refresh when advisor actions are applied (from any advisor — project or global)
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
const refresh = () => { loadSubProjects(); if (selectedSubId) {
|
|
163
|
+
fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks`)
|
|
164
|
+
.then(r => r.json()).then(setTasks).catch(() => {});
|
|
165
|
+
}};
|
|
166
|
+
window.addEventListener('advisor-action-applied', refresh);
|
|
167
|
+
return () => window.removeEventListener('advisor-action-applied', refresh);
|
|
168
|
+
}, [id, selectedSubId, loadSubProjects]);
|
|
169
|
+
|
|
159
170
|
const handleCreateSubProject = async () => {
|
|
160
171
|
if (!newSubName.trim()) return;
|
|
161
172
|
const res = await fetch(`/api/projects/${id}/sub-projects`, {
|
|
@@ -244,10 +255,34 @@ export default function WorkspacePanel({
|
|
|
244
255
|
});
|
|
245
256
|
};
|
|
246
257
|
|
|
247
|
-
const handleTaskDelete = (taskId?: string) => {
|
|
258
|
+
const handleTaskDelete = async (taskId?: string) => {
|
|
248
259
|
const tid = taskId || selectedTaskId;
|
|
249
|
-
if (!tid) return;
|
|
250
|
-
|
|
260
|
+
if (!tid || !selectedSubId) return;
|
|
261
|
+
const task = tasks.find(t => t.id === tid);
|
|
262
|
+
if (!task) return;
|
|
263
|
+
|
|
264
|
+
// Immediate archive (soft delete) + undo toast
|
|
265
|
+
await fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks/${tid}?mode=archive`, { method: 'DELETE' });
|
|
266
|
+
setTasks(prev => prev.filter(t => t.id !== tid));
|
|
267
|
+
if (selectedTaskId === tid) setSelectedTaskId(null);
|
|
268
|
+
loadSubProjects();
|
|
269
|
+
|
|
270
|
+
if (deleteToast?.timer) clearTimeout(deleteToast.timer);
|
|
271
|
+
const timer = setTimeout(() => setDeleteToast(null), 30000);
|
|
272
|
+
setDeleteToast({ taskId: tid, title: task.title, timer });
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const undoTaskDelete = async () => {
|
|
276
|
+
if (!deleteToast) return;
|
|
277
|
+
clearTimeout(deleteToast.timer);
|
|
278
|
+
await fetch(`/api/archive?action=restore&taskId=${deleteToast.taskId}`, { method: 'POST' });
|
|
279
|
+
// Re-fetch tasks to get the restored one
|
|
280
|
+
if (selectedSubId) {
|
|
281
|
+
const res = await fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks`);
|
|
282
|
+
if (res.ok) setTasks(await res.json());
|
|
283
|
+
}
|
|
284
|
+
loadSubProjects();
|
|
285
|
+
setDeleteToast(null);
|
|
251
286
|
};
|
|
252
287
|
|
|
253
288
|
const handleConfirmAction = async (mode?: 'archive' | 'permanent') => {
|
|
@@ -540,6 +575,18 @@ export default function WorkspacePanel({
|
|
|
540
575
|
onCreateTask={handleCreateTask} onStatusChange={handleTaskStatusChange}
|
|
541
576
|
onTodayToggle={handleTaskTodayToggle} onDeleteTask={handleTaskDelete}
|
|
542
577
|
onReorderSubs={handleReorderSubs}
|
|
578
|
+
onReorderTasks={async (orderedIds) => {
|
|
579
|
+
if (!selectedSubId) return;
|
|
580
|
+
setTasks(prev => {
|
|
581
|
+
const map = new Map(prev.map(t => [t.id, t]));
|
|
582
|
+
return orderedIds.map(id => map.get(id)!).filter(Boolean);
|
|
583
|
+
});
|
|
584
|
+
await fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks/reorder`, {
|
|
585
|
+
method: 'POST',
|
|
586
|
+
headers: { 'Content-Type': 'application/json' },
|
|
587
|
+
body: JSON.stringify({ orderedIds }),
|
|
588
|
+
});
|
|
589
|
+
}}
|
|
543
590
|
onAutoDistribute={() => setShowAutoDistribute(true)}
|
|
544
591
|
chatStates={chatStates} />
|
|
545
592
|
</div>
|
|
@@ -632,6 +679,25 @@ export default function WorkspacePanel({
|
|
|
632
679
|
onClose={() => setShowAdvisor(false)}
|
|
633
680
|
/>
|
|
634
681
|
)}
|
|
682
|
+
{deleteToast && (
|
|
683
|
+
<div className="fixed bottom-4 left-1/2 -translate-x-1/2 z-50 bg-card border border-border rounded-lg shadow-xl px-4 py-2.5 flex items-center gap-3 animate-dialog-in">
|
|
684
|
+
<span className="text-xs text-muted-foreground truncate max-w-[200px]">
|
|
685
|
+
"{deleteToast.title}" 삭제됨
|
|
686
|
+
</span>
|
|
687
|
+
<button
|
|
688
|
+
onClick={undoTaskDelete}
|
|
689
|
+
className="text-xs px-2 py-1 bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
|
|
690
|
+
>
|
|
691
|
+
되돌리기
|
|
692
|
+
</button>
|
|
693
|
+
<button
|
|
694
|
+
onClick={() => { clearTimeout(deleteToast.timer); setDeleteToast(null); }}
|
|
695
|
+
className="text-xs text-muted-foreground hover:text-foreground"
|
|
696
|
+
>
|
|
697
|
+
×
|
|
698
|
+
</button>
|
|
699
|
+
</div>
|
|
700
|
+
)}
|
|
635
701
|
</div>
|
|
636
702
|
);
|
|
637
703
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { AdvisorAction } from '@/types/advisor-actions';
|
|
2
|
+
|
|
3
|
+
const ACTION_BLOCK_RE = /```action\s*\n([\s\S]*?)```/g;
|
|
4
|
+
|
|
5
|
+
export interface ParsedContent {
|
|
6
|
+
segments: ({ type: 'markdown'; text: string } | { type: 'actions'; actions: AdvisorAction[] })[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function validateAction(obj: unknown): AdvisorAction | null {
|
|
10
|
+
if (!obj || typeof obj !== 'object') return null;
|
|
11
|
+
const a = obj as Record<string, unknown>;
|
|
12
|
+
if (a.type === 'create_task') {
|
|
13
|
+
if (typeof a.subProjectId !== 'string' || typeof a.title !== 'string') return null;
|
|
14
|
+
return {
|
|
15
|
+
type: 'create_task',
|
|
16
|
+
subProjectId: a.subProjectId,
|
|
17
|
+
projectId: typeof a.projectId === 'string' ? a.projectId : undefined,
|
|
18
|
+
title: a.title,
|
|
19
|
+
description: typeof a.description === 'string' ? a.description : undefined,
|
|
20
|
+
priority: ['high', 'medium', 'low'].includes(a.priority as string) ? a.priority as 'high' | 'medium' | 'low' : undefined,
|
|
21
|
+
status: typeof a.status === 'string' ? (a.status as string) : undefined,
|
|
22
|
+
} as AdvisorAction;
|
|
23
|
+
}
|
|
24
|
+
if (a.type === 'update_task') {
|
|
25
|
+
if (typeof a.taskId !== 'string' || !a.changes || typeof a.changes !== 'object') return null;
|
|
26
|
+
return { type: 'update_task', taskId: a.taskId, changes: a.changes as Record<string, unknown> } as AdvisorAction;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function parseAdvisorContent(content: string): ParsedContent {
|
|
32
|
+
const segments: ParsedContent['segments'] = [];
|
|
33
|
+
let lastIndex = 0;
|
|
34
|
+
|
|
35
|
+
for (const match of content.matchAll(ACTION_BLOCK_RE)) {
|
|
36
|
+
const before = content.slice(lastIndex, match.index);
|
|
37
|
+
if (before.trim()) segments.push({ type: 'markdown', text: before });
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const raw = JSON.parse(match[1]);
|
|
41
|
+
const arr = Array.isArray(raw) ? raw : [raw];
|
|
42
|
+
const valid = arr.map(validateAction).filter((a): a is AdvisorAction => a !== null);
|
|
43
|
+
if (valid.length > 0) {
|
|
44
|
+
segments.push({ type: 'actions', actions: valid });
|
|
45
|
+
} else {
|
|
46
|
+
segments.push({ type: 'markdown', text: match[0] });
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
segments.push({ type: 'markdown', text: match[0] });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
lastIndex = (match.index ?? 0) + match[0].length;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const remaining = content.slice(lastIndex);
|
|
56
|
+
if (remaining.trim()) segments.push({ type: 'markdown', text: remaining });
|
|
57
|
+
|
|
58
|
+
return { segments };
|
|
59
|
+
}
|
|
@@ -4,6 +4,25 @@ import { getTasksByProject } from '../db/queries/tasks';
|
|
|
4
4
|
import { getBrainstorm } from '../db/queries/brainstorms';
|
|
5
5
|
import type { ITask, TaskStatus } from '../../types';
|
|
6
6
|
|
|
7
|
+
const ACTION_INSTRUCTIONS = `
|
|
8
|
+
## Actions
|
|
9
|
+
사용자가 요청하면 태스크 생성/수정을 제안할 수 있습니다. \`\`\`action 블록을 사용하세요.
|
|
10
|
+
사용자가 명시적으로 요청하지 않으면 action 블록을 넣지 마세요.
|
|
11
|
+
|
|
12
|
+
형식:
|
|
13
|
+
\`\`\`action
|
|
14
|
+
[
|
|
15
|
+
{"type":"create_task","subProjectId":"<sub_id값>","projectId":"<project_id값>","title":"제목","priority":"high|medium|low"},
|
|
16
|
+
{"type":"update_task","taskId":"<task_id값>","changes":{"status":"done"}}
|
|
17
|
+
]
|
|
18
|
+
\`\`\`
|
|
19
|
+
|
|
20
|
+
규칙:
|
|
21
|
+
- subProjectId, taskId, projectId는 아래 컨텍스트의 [sub_id:...], [task_id:...], [project_id:...] 값을 정확히 사용
|
|
22
|
+
- action 블록 앞에 항상 무엇을 제안하는지 설명
|
|
23
|
+
- 사용자가 승인해야 실행됨
|
|
24
|
+
`;
|
|
25
|
+
|
|
7
26
|
const MAX_BRAINSTORM = 1500;
|
|
8
27
|
const NOTE_LIMIT = 120;
|
|
9
28
|
const MAX_HISTORY_MESSAGES = 20;
|
|
@@ -25,7 +44,8 @@ export function buildGlobalAdvisorPrompt(): string {
|
|
|
25
44
|
const parts: string[] = [];
|
|
26
45
|
parts.push('당신은 사용자의 전체 워크스페이스를 조망하는 AI 어드바이저입니다.');
|
|
27
46
|
parts.push('여러 프로젝트의 현황을 파악하고, 우선순위·방향·빠진 부분·크로스-프로젝트 이슈 등을 논의합니다.');
|
|
28
|
-
parts.push('한국어로 간결하게 답하세요. 긴 설교
|
|
47
|
+
parts.push('한국어로 간결하게 답하세요. 긴 설교 금지.');
|
|
48
|
+
parts.push(ACTION_INSTRUCTIONS);
|
|
29
49
|
|
|
30
50
|
parts.push('=== ALL WORKSPACES ===\n');
|
|
31
51
|
|
|
@@ -52,7 +72,7 @@ export function buildGlobalAdvisorPrompt(): string {
|
|
|
52
72
|
}
|
|
53
73
|
|
|
54
74
|
const lines: string[] = [];
|
|
55
|
-
lines.push(`## ${project.name}`);
|
|
75
|
+
lines.push(`## ${project.name} [project_id:${project.id}]`);
|
|
56
76
|
if (project.description) lines.push(project.description);
|
|
57
77
|
const statsStr = Object.entries(counts).map(([k, v]) => `${k}:${v}`).join(' / ');
|
|
58
78
|
lines.push(`태스크 ${allTasks.length}개 (${statsStr})`);
|
|
@@ -67,12 +87,12 @@ export function buildGlobalAdvisorPrompt(): string {
|
|
|
67
87
|
for (const sub of subs) {
|
|
68
88
|
const subTasks = allTasks.filter(t => t.sub_project_id === sub.id && t.status !== 'done');
|
|
69
89
|
if (!subTasks.length) continue;
|
|
70
|
-
lines.push(`\n### ${sub.name}`);
|
|
90
|
+
lines.push(`\n### ${sub.name} [sub_id:${sub.id}]`);
|
|
71
91
|
for (const t of subTasks) {
|
|
72
92
|
const note = truncate(t.description, NOTE_LIMIT);
|
|
73
93
|
const flags = [t.priority === 'high' ? 'HIGH' : null, t.is_today ? 'today' : null].filter(Boolean).join(', ');
|
|
74
94
|
const flagStr = flags ? ` (${flags})` : '';
|
|
75
|
-
lines.push(`- [${STATUS_ICON[t.status] ?? t.status}] **${t.title}
|
|
95
|
+
lines.push(`- [${STATUS_ICON[t.status] ?? t.status}] **${t.title}** [task_id:${t.id}]${flagStr}${note ? ' — ' + note : ''}`);
|
|
76
96
|
}
|
|
77
97
|
}
|
|
78
98
|
|
|
@@ -4,6 +4,25 @@ import { getTasksByProject } from '../db/queries/tasks';
|
|
|
4
4
|
import { getBrainstorm } from '../db/queries/brainstorms';
|
|
5
5
|
import type { ITask, TaskStatus } from '../../types';
|
|
6
6
|
|
|
7
|
+
const ACTION_INSTRUCTIONS = `
|
|
8
|
+
## Actions
|
|
9
|
+
사용자가 요청하면 태스크 생성/수정을 제안할 수 있습니다. \`\`\`action 블록을 사용하세요.
|
|
10
|
+
사용자가 명시적으로 요청하지 않으면 action 블록을 넣지 마세요.
|
|
11
|
+
|
|
12
|
+
형식:
|
|
13
|
+
\`\`\`action
|
|
14
|
+
[
|
|
15
|
+
{"type":"create_task","subProjectId":"<sub_id값>","title":"제목","priority":"high|medium|low","status":"idea"},
|
|
16
|
+
{"type":"update_task","taskId":"<task_id값>","changes":{"status":"done"}}
|
|
17
|
+
]
|
|
18
|
+
\`\`\`
|
|
19
|
+
|
|
20
|
+
규칙:
|
|
21
|
+
- subProjectId, taskId는 아래 컨텍스트의 [sub_id:...], [task_id:...] 값을 정확히 사용
|
|
22
|
+
- action 블록 앞에 항상 무엇을 제안하는지 설명
|
|
23
|
+
- 사용자가 승인해야 실행됨 (자동 실행 아님)
|
|
24
|
+
`;
|
|
25
|
+
|
|
7
26
|
const MAX_BRAINSTORM = 4000;
|
|
8
27
|
const NOTE_LIMIT_ACTIVE = 500;
|
|
9
28
|
const NOTE_LIMIT_DEFAULT = 200;
|
|
@@ -49,7 +68,7 @@ export function buildProjectAdvisorPrompt(projectId: string): string {
|
|
|
49
68
|
continue;
|
|
50
69
|
}
|
|
51
70
|
const lines: string[] = [];
|
|
52
|
-
lines.push(`### ${sub.name}`);
|
|
71
|
+
lines.push(`### ${sub.name} [sub_id:${sub.id}]`);
|
|
53
72
|
if (sub.description) lines.push(sub.description);
|
|
54
73
|
lines.push(`태스크 ${tasks.length}개:`);
|
|
55
74
|
for (const t of tasks) {
|
|
@@ -58,7 +77,7 @@ export function buildProjectAdvisorPrompt(projectId: string): string {
|
|
|
58
77
|
const flags = [t.priority === 'high' ? 'HIGH' : null, t.is_today ? 'today' : null].filter(Boolean).join(', ');
|
|
59
78
|
const flagStr = flags ? ` (${flags})` : '';
|
|
60
79
|
const noteStr = note ? ` — ${note}` : '';
|
|
61
|
-
lines.push(`- [${STATUS_ICON[t.status] ?? t.status}] **${t.title}
|
|
80
|
+
lines.push(`- [${STATUS_ICON[t.status] ?? t.status}] **${t.title}** [task_id:${t.id}]${flagStr}${noteStr}`);
|
|
62
81
|
}
|
|
63
82
|
subSections.push(lines.join('\n'));
|
|
64
83
|
}
|
|
@@ -86,6 +105,7 @@ export function buildProjectAdvisorPrompt(projectId: string): string {
|
|
|
86
105
|
parts.push(`당신은 프로젝트 "${project.name}"의 어드바이저입니다.`);
|
|
87
106
|
parts.push(`사용자가 프로젝트 방향, 우선순위, 빠진 부분, 다음 단계 등을 논의하면 프로젝트 전체 맥락을 바탕으로 간결하게 답합니다.`);
|
|
88
107
|
parts.push(`태스크를 언급할 때는 정확한 제목을 쓰세요. 한국어로 답하세요. 긴 설교는 금지.`);
|
|
108
|
+
parts.push(ACTION_INSTRUCTIONS);
|
|
89
109
|
|
|
90
110
|
if (project.ai_context) {
|
|
91
111
|
parts.push(`\nProject AI Policy:\n${project.ai_context}`);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { TaskStatus } from './index';
|
|
2
|
+
|
|
3
|
+
export interface CreateTaskAction {
|
|
4
|
+
type: 'create_task';
|
|
5
|
+
subProjectId: string;
|
|
6
|
+
projectId?: string;
|
|
7
|
+
title: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
priority?: 'high' | 'medium' | 'low';
|
|
10
|
+
status?: TaskStatus;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface UpdateTaskAction {
|
|
14
|
+
type: 'update_task';
|
|
15
|
+
taskId: string;
|
|
16
|
+
changes: {
|
|
17
|
+
status?: TaskStatus;
|
|
18
|
+
priority?: 'high' | 'medium' | 'low';
|
|
19
|
+
description?: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
is_today?: boolean;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type AdvisorAction = CreateTaskAction | UpdateTaskAction;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
var a,b,c,d;self.__BUILD_MANIFEST=(a=0,b=0,c=0,d=0,{__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},__routerFilterStatic:{numItems:15,errorRate:1e-4,numBits:288,numHashes:14,bitArray:[0,1,1,0,0,1,1,1,0,1,0,0,0,1,1,1,0,0,0,1,0,0,1,0,0,1,0,1,1,1,1,1,0,1,1,0,0,1,0,1,0,0,0,0,1,1,0,1,0,0,1,0,1,0,0,0,1,1,1,1,1,1,0,1,1,1,0,1,1,1,0,0,1,0,0,0,0,0,1,1,1,0,0,0,1,0,1,0,0,0,1,1,0,1,1,1,1,0,1,0,1,1,0,1,1,0,0,1,0,0,0,0,1,1,1,1,0,1,0,0,1,1,1,1,1,0,1,1,1,0,0,0,1,1,1,0,0,1,0,0,1,0,1,1,1,0,1,0,1,0,0,1,0,1,1,0,0,0,0,0,1,1,1,0,1,0,0,1,0,0,1,0,0,0,0,1,1,1,1,1,0,0,1,0,0,1,1,0,1,1,1,1,1,1,1,0,0,0,1,0,0,1,0,1,1,0,0,0,0,1,1,1,0,1,1,0,0,1,1,1,1,1,0,0,1,0,1,1,0,0,0,0,1,0,1,1,0,0,0,1,0,0,1,0,1,1,0,0,1,0,1,0,1,1,0,1,0,0,1,1,0,1,1,1,1,0,1,1,0,0,1,0,0,1,0,1,1,0,1,1,1,1,0,0,0,0,1,1]},__routerFilterDynamic:{numItems:3,errorRate:1e-4,numBits:58,numHashes:14,bitArray:[1,0,1,1,1,1,0,0,1,1,0,1,1,0,0,1,1,0,0,0,0,1,1,1,0,0,0,1,1,0,1,1,1,1,0,1,1,0,1,0,1,1,1,0,0,1,1,0,0,1,0,0,1,0,0,1,1,1]},sortedPages:["/_app"]}),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,181,202,212,307,363,424,437,460,514,595,711,755,770,772,819,822,851,860,896,922,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,181,202,212,307,363,424,437,460,514,595,711,755,770,772,819,822,851,860,896,922,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,181,202,212,307,363,424,437,460,514,595,711,755,770,772,819,822,851,860,896,922,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2,64,86,94,123,167,175,177,178,181,202,212,307,363,424,437,460,514,595,711,755,770,772,819,822,851,860,896,922,980,988],{4441:()=>{}},_=>{_.O(0,[441,794,358],()=>_(_.s=4441)),_N_E=_.O()}]);
|