stagent 0.1.12 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +44 -50
  2. package/package.json +1 -1
  3. package/public/readme/cost-usage-list.png +0 -0
  4. package/public/readme/dashboard-bulk-select.png +0 -0
  5. package/public/readme/dashboard-card-edit.png +0 -0
  6. package/public/readme/dashboard-create-form-ai-applied.png +0 -0
  7. package/public/readme/dashboard-create-form-ai-assist.png +0 -0
  8. package/public/readme/dashboard-create-form-empty.png +0 -0
  9. package/public/readme/dashboard-create-form-filled.png +0 -0
  10. package/public/readme/dashboard-filtered.png +0 -0
  11. package/public/readme/dashboard-list.png +0 -0
  12. package/public/readme/dashboard-workflow-confirm.png +0 -0
  13. package/public/readme/home-below-fold.png +0 -0
  14. package/public/readme/home-list.png +0 -0
  15. package/public/readme/inbox-list.png +0 -0
  16. package/public/readme/playbook-list.png +0 -0
  17. package/public/readme/profiles-list.png +0 -0
  18. package/public/readme/settings-list.png +0 -0
  19. package/public/readme/workflows-list.png +0 -0
  20. package/src/app/api/tasks/[id]/route.ts +54 -3
  21. package/src/app/api/workflows/[id]/route.ts +43 -4
  22. package/src/app/api/workflows/[id]/status/route.ts +70 -2
  23. package/src/app/api/workflows/from-assist/route.ts +6 -32
  24. package/src/app/dashboard/page.tsx +59 -21
  25. package/src/app/documents/[id]/page.tsx +10 -8
  26. package/src/app/globals.css +11 -0
  27. package/src/app/page.tsx +60 -3
  28. package/src/app/tasks/[id]/page.tsx +22 -2
  29. package/src/components/costs/cost-dashboard.tsx +1 -1
  30. package/src/components/dashboard/greeting.tsx +3 -1
  31. package/src/components/dashboard/priority-queue.tsx +58 -9
  32. package/src/components/dashboard/stats-cards.tsx +16 -2
  33. package/src/components/documents/document-chip-bar.tsx +183 -0
  34. package/src/components/documents/document-content-renderer.tsx +146 -0
  35. package/src/components/documents/document-detail-view.tsx +16 -239
  36. package/src/components/documents/image-zoom-view.tsx +60 -0
  37. package/src/components/documents/smart-extracted-text.tsx +47 -0
  38. package/src/components/documents/utils.ts +70 -0
  39. package/src/components/notifications/inbox-list.tsx +4 -5
  40. package/src/components/notifications/notification-item.tsx +72 -8
  41. package/src/components/notifications/pending-approval-host.tsx +7 -4
  42. package/src/components/playbook/playbook-detail-view.tsx +6 -4
  43. package/src/components/profiles/profile-browser.tsx +1 -0
  44. package/src/components/profiles/profile-card.tsx +16 -8
  45. package/src/components/profiles/profile-detail-view.tsx +6 -1
  46. package/src/components/shared/app-sidebar.tsx +2 -2
  47. package/src/components/tasks/__tests__/kanban-board-accessibility.test.tsx +1 -1
  48. package/src/components/tasks/ai-assist-panel.tsx +108 -78
  49. package/src/components/tasks/content-preview.tsx +2 -1
  50. package/src/components/tasks/kanban-board.tsx +57 -5
  51. package/src/components/tasks/kanban-column.tsx +34 -23
  52. package/src/components/tasks/task-bento-cell.tsx +50 -0
  53. package/src/components/tasks/task-bento-grid.tsx +155 -0
  54. package/src/components/tasks/task-card.tsx +14 -16
  55. package/src/components/tasks/task-chip-bar.tsx +207 -0
  56. package/src/components/tasks/task-detail-view.tsx +42 -190
  57. package/src/components/tasks/task-result-renderer.tsx +33 -0
  58. package/src/components/workflows/blueprint-gallery.tsx +19 -12
  59. package/src/components/workflows/blueprint-preview.tsx +8 -1
  60. package/src/components/workflows/loop-status-view.tsx +2 -8
  61. package/src/components/workflows/swarm-dashboard.tsx +2 -3
  62. package/src/components/workflows/workflow-confirmation-view.tsx +2 -7
  63. package/src/components/workflows/workflow-full-output.tsx +80 -0
  64. package/src/components/workflows/workflow-kanban-card.tsx +121 -0
  65. package/src/components/workflows/workflow-list.tsx +47 -42
  66. package/src/components/workflows/workflow-status-view.tsx +160 -20
  67. package/src/lib/agents/learning-session.ts +138 -18
  68. package/src/lib/constants/card-icons.tsx +202 -0
  69. package/src/lib/constants/prose-styles.ts +7 -0
  70. package/src/lib/constants/task-status.ts +3 -0
  71. package/src/lib/docs/reader.ts +8 -3
  72. package/src/lib/documents/context-builder.ts +41 -0
  73. package/src/lib/queries/chart-data.ts +20 -1
  74. package/src/lib/workflows/engine.ts +57 -61
  75. package/src/lib/workflows/types.ts +2 -0
  76. package/tsconfig.json +2 -1
  77. package/src/components/documents/document-preview.tsx +0 -68
@@ -33,14 +33,16 @@ export default async function DocumentDetailPage({
33
33
  };
34
34
 
35
35
  return (
36
- <div className="gradient-forest-dawn min-h-screen p-6">
37
- <Link href="/documents">
38
- <Button variant="ghost" size="sm" className="mb-4">
39
- <ArrowLeft className="h-4 w-4 mr-1" />
40
- Back to Documents
41
- </Button>
42
- </Link>
43
- <DocumentDetailView documentId={id} initialDocument={initialDoc} />
36
+ <div className="gradient-twilight min-h-screen p-6">
37
+ <div className="surface-page-shell rounded-xl p-6 max-w-5xl mx-auto">
38
+ <Link href="/documents">
39
+ <Button variant="ghost" size="sm" className="mb-4">
40
+ <ArrowLeft className="h-4 w-4 mr-1" />
41
+ Back to Documents
42
+ </Button>
43
+ </Link>
44
+ <DocumentDetailView documentId={id} initialDocument={initialDoc} />
45
+ </div>
44
46
  </div>
45
47
  );
46
48
  }
@@ -654,6 +654,17 @@
654
654
  border: 1px solid color-mix(in oklab, var(--border) 75%, transparent);
655
655
  }
656
656
 
657
+ /* Document reader surface — white bg for readability */
658
+ .prose-reader-surface {
659
+ background: oklch(1 0 0);
660
+ border-radius: var(--radius-lg);
661
+ padding: 1.5rem;
662
+ }
663
+
664
+ .dark .prose-reader-surface {
665
+ background: oklch(0.18 0.015 265);
666
+ }
667
+
657
668
  /* --- Progress slide animation (AI Assist) --- */
658
669
  @keyframes progress-slide {
659
670
  0% { transform: translateX(-100%); }
package/src/app/page.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import { db } from "@/lib/db";
2
- import { tasks, projects, agentLogs, notifications } from "@/lib/db/schema";
2
+ import { tasks, projects, agentLogs, notifications, workflows } from "@/lib/db/schema";
3
3
  import { eq, count, gte, and, desc, sql, inArray } from "drizzle-orm";
4
+ import { parseWorkflowState } from "@/lib/workflows/engine";
4
5
  import { Greeting } from "@/components/dashboard/greeting";
5
6
  import { StatsCards } from "@/components/dashboard/stats-cards";
6
7
  import { PriorityQueue } from "@/components/dashboard/priority-queue";
@@ -16,6 +17,7 @@ import {
16
17
  getActiveProjectActivityByDay,
17
18
  getAgentActivityByHour,
18
19
  getNotificationsByDay,
20
+ getWorkflowActivityByDay,
19
21
  } from "@/lib/queries/chart-data";
20
22
 
21
23
  export const dynamic = "force-dynamic";
@@ -33,6 +35,8 @@ export default async function HomePage() {
33
35
  [awaitingResult],
34
36
  [activeProjectsResult],
35
37
  priorityTasks,
38
+ activeWorkflows,
39
+ [activeWorkflowCountResult],
36
40
  recentLogs,
37
41
  allProjects,
38
42
  recentActiveProjects,
@@ -41,6 +45,7 @@ export default async function HomePage() {
41
45
  projectCreationsByDay,
42
46
  agentActivityByHour,
43
47
  notificationsByDay,
48
+ workflowsByDay,
44
49
  ] = await Promise.all([
45
50
  db.select({ count: count() }).from(tasks).where(eq(tasks.status, "running")),
46
51
  db.select({ count: count() }).from(tasks).where(eq(tasks.status, "failed")),
@@ -62,7 +67,11 @@ export default async function HomePage() {
62
67
  // Priority queue: failed + running tasks, sorted by priority
63
68
  db.select().from(tasks).where(
64
69
  inArray(tasks.status, ["failed", "running", "queued"])
65
- ).orderBy(tasks.priority, desc(tasks.updatedAt)).limit(5),
70
+ ).orderBy(tasks.priority, desc(tasks.updatedAt)).limit(8),
71
+ // All workflows for priority queue (match kanban board behavior)
72
+ db.select().from(workflows).orderBy(desc(workflows.updatedAt)).limit(8),
73
+ // Count active workflows for stats
74
+ db.select({ count: count() }).from(workflows).where(eq(workflows.status, "active")),
66
75
  // Recent agent logs
67
76
  db.select().from(agentLogs).orderBy(desc(agentLogs.timestamp)).limit(6),
68
77
  // All projects for quick actions
@@ -81,11 +90,13 @@ export default async function HomePage() {
81
90
  getActiveProjectActivityByDay(7),
82
91
  getAgentActivityByHour(),
83
92
  getNotificationsByDay(7),
93
+ getWorkflowActivityByDay(7),
84
94
  ]);
85
95
 
86
96
  // Build project name lookup for priority tasks
87
97
  const projectMap = new Map(allProjects.map((p) => [p.id, p.name]));
88
98
 
99
+ // Serialize priority tasks (no more workflow linkage via parent task)
89
100
  const serializedPriorityTasks: PriorityTask[] = priorityTasks.map((t) => ({
90
101
  id: t.id,
91
102
  title: t.title,
@@ -94,6 +105,49 @@ export default async function HomePage() {
94
105
  projectName: t.projectId ? projectMap.get(t.projectId) ?? undefined : undefined,
95
106
  }));
96
107
 
108
+ // Build workflow priority items directly
109
+ const workflowPriorityItems: PriorityTask[] = activeWorkflows.map((w) => {
110
+ let workflowProgress: PriorityTask["workflowProgress"];
111
+
112
+ try {
113
+ const { definition: def, state } = parseWorkflowState(w.definition);
114
+ if (state && def.steps) {
115
+ const completed = state.stepStates.filter((s) => s.status === "completed").length;
116
+ const running = state.stepStates.find((s) => s.status === "running");
117
+ const runningStep = running
118
+ ? def.steps.find((step) => step.id === running.stepId)
119
+ : undefined;
120
+ workflowProgress = {
121
+ current: completed,
122
+ total: def.steps.length,
123
+ currentStepName: runningStep?.name,
124
+ workflowId: w.id,
125
+ workflowStatus: w.status,
126
+ };
127
+ }
128
+ } catch { /* skip parse errors */ }
129
+
130
+ return {
131
+ id: w.id,
132
+ title: w.name,
133
+ status: w.status,
134
+ priority: 1, // Workflows always high priority in the attention queue
135
+ projectName: w.projectId ? projectMap.get(w.projectId) ?? undefined : undefined,
136
+ workflowProgress,
137
+ isWorkflow: true,
138
+ };
139
+ });
140
+
141
+ // Urgency ranking: actionable items surface first
142
+ const urgencyRank: Record<string, number> = {
143
+ failed: 0, running: 1, active: 1, queued: 2, paused: 3, draft: 4, completed: 5,
144
+ };
145
+
146
+ // Merge, sort by urgency, and limit to 8 items
147
+ const allPriorityItems = [...workflowPriorityItems, ...serializedPriorityTasks]
148
+ .sort((a, b) => (urgencyRank[a.status] ?? 6) - (urgencyRank[b.status] ?? 6))
149
+ .slice(0, 8);
150
+
97
151
  // Get task titles for log entries
98
152
  const logTaskIds = [...new Set(recentLogs.filter((l) => l.taskId).map((l) => l.taskId!))];
99
153
  const logTasks = logTaskIds.length > 0
@@ -132,6 +186,7 @@ export default async function HomePage() {
132
186
  runningCount={runningResult.count}
133
187
  awaitingCount={awaitingResult.count}
134
188
  failedCount={failedResult.count}
189
+ activeWorkflows={activeWorkflowCountResult.count}
135
190
  />
136
191
  <StatsCards
137
192
  runningCount={runningResult.count}
@@ -139,16 +194,18 @@ export default async function HomePage() {
139
194
  completedAllTime={completedAllTimeResult.count}
140
195
  awaitingReview={awaitingResult.count}
141
196
  activeProjects={activeProjectsResult.count}
197
+ activeWorkflows={activeWorkflowCountResult.count}
142
198
  sparklines={{
143
199
  completions: completionsByDay,
144
200
  creations: taskCreationsByDay,
145
201
  projects: projectCreationsByDay,
146
202
  notifications: notificationsByDay,
203
+ workflows: workflowsByDay,
147
204
  }}
148
205
  />
149
206
  <div className="grid grid-cols-1 gap-6 lg:grid-cols-5 mb-6">
150
207
  <div className="lg:col-span-3">
151
- <PriorityQueue tasks={serializedPriorityTasks} />
208
+ <PriorityQueue tasks={allPriorityItems} />
152
209
  </div>
153
210
  <div className="lg:col-span-2">
154
211
  <ActivityFeed entries={serializedLogs} hourlyActivity={agentActivityByHour} />
@@ -1,7 +1,7 @@
1
1
  import { notFound } from "next/navigation";
2
2
  import Link from "next/link";
3
3
  import { db } from "@/lib/db";
4
- import { tasks } from "@/lib/db/schema";
4
+ import { tasks, projects, workflows, schedules } from "@/lib/db/schema";
5
5
  import { eq } from "drizzle-orm";
6
6
  import { Button } from "@/components/ui/button";
7
7
  import { ArrowLeft } from "lucide-react";
@@ -23,12 +23,32 @@ export default async function TaskDetailPage({
23
23
 
24
24
  if (!task) notFound();
25
25
 
26
+ // Join relationship names server-side for initial render
27
+ let projectName: string | undefined;
28
+ let workflowName: string | undefined;
29
+ let scheduleName: string | undefined;
30
+
31
+ if (task.projectId) {
32
+ const [p] = await db.select({ name: projects.name }).from(projects).where(eq(projects.id, task.projectId));
33
+ projectName = p?.name;
34
+ }
35
+ if (task.workflowId) {
36
+ const [w] = await db.select({ name: workflows.name }).from(workflows).where(eq(workflows.id, task.workflowId));
37
+ workflowName = w?.name;
38
+ }
39
+ if (task.scheduleId) {
40
+ const [s] = await db.select({ name: schedules.name }).from(schedules).where(eq(schedules.id, task.scheduleId));
41
+ scheduleName = s?.name;
42
+ }
43
+
26
44
  // Serialize Date timestamps to ISO strings for client component
27
45
  const initialTask = {
28
46
  ...task,
29
47
  createdAt: task.createdAt instanceof Date ? task.createdAt.toISOString() : String(task.createdAt),
30
48
  updatedAt: task.updatedAt instanceof Date ? task.updatedAt.toISOString() : String(task.updatedAt),
31
- projectName: undefined,
49
+ projectName,
50
+ workflowName,
51
+ scheduleName,
32
52
  };
33
53
 
34
54
  return (
@@ -139,7 +139,7 @@ function formatActivityLabel(value: UsageAuditEntry["activityType"]) {
139
139
  case "context_summarization":
140
140
  return "Context summarization";
141
141
  default:
142
- return value
142
+ return (value as string)
143
143
  .split("_")
144
144
  .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
145
145
  .join(" ");
@@ -2,6 +2,7 @@ interface GreetingProps {
2
2
  runningCount: number;
3
3
  awaitingCount: number;
4
4
  failedCount: number;
5
+ activeWorkflows: number;
5
6
  }
6
7
 
7
8
  function getGreeting(): string {
@@ -11,9 +12,10 @@ function getGreeting(): string {
11
12
  return "Good evening";
12
13
  }
13
14
 
14
- export function Greeting({ runningCount, awaitingCount, failedCount }: GreetingProps) {
15
+ export function Greeting({ runningCount, awaitingCount, failedCount, activeWorkflows }: GreetingProps) {
15
16
  const parts: string[] = [];
16
17
  if (runningCount > 0) parts.push(`${runningCount} task${runningCount !== 1 ? "s" : ""} running`);
18
+ if (activeWorkflows > 0) parts.push(`${activeWorkflows} workflow${activeWorkflows !== 1 ? "s" : ""} active`);
17
19
  if (awaitingCount > 0) parts.push(`${awaitingCount} awaiting your review`);
18
20
  if (failedCount > 0) parts.push(`${failedCount} failed task${failedCount !== 1 ? "s" : ""} to address`);
19
21
 
@@ -4,8 +4,8 @@ import Link from "next/link";
4
4
  import { Badge } from "@/components/ui/badge";
5
5
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
6
6
  import { Button } from "@/components/ui/button";
7
- import { AlertTriangle, Clock, Shield, ArrowRight } from "lucide-react";
8
- import { taskStatusVariant } from "@/lib/constants/status-colors";
7
+ import { AlertTriangle, Clock, Shield, ArrowRight, Workflow as WorkflowIcon, FilePen, Pause, CheckCircle2 } from "lucide-react";
8
+ import { taskStatusVariant, workflowStatusVariant } from "@/lib/constants/status-colors";
9
9
 
10
10
  export interface PriorityTask {
11
11
  id: string;
@@ -13,6 +13,14 @@ export interface PriorityTask {
13
13
  status: string;
14
14
  priority: number;
15
15
  projectName?: string;
16
+ isWorkflow?: boolean;
17
+ workflowProgress?: {
18
+ current: number;
19
+ total: number;
20
+ currentStepName?: string;
21
+ workflowId: string;
22
+ workflowStatus: string;
23
+ };
16
24
  }
17
25
 
18
26
  interface PriorityQueueProps {
@@ -24,6 +32,14 @@ const statusIcon: Record<string, typeof AlertTriangle> = {
24
32
  running: Clock,
25
33
  };
26
34
 
35
+ const workflowStatusIcon: Record<string, typeof AlertTriangle> = {
36
+ draft: FilePen,
37
+ active: WorkflowIcon,
38
+ paused: Pause,
39
+ completed: CheckCircle2,
40
+ failed: AlertTriangle,
41
+ };
42
+
27
43
  const priorityColors = ["text-priority-critical", "text-priority-high", "text-status-warning", "text-muted-foreground"];
28
44
 
29
45
  export function PriorityQueue({ tasks }: PriorityQueueProps) {
@@ -40,28 +56,61 @@ export function PriorityQueue({ tasks }: PriorityQueueProps) {
40
56
  className="text-sm text-muted-foreground py-4 text-center"
41
57
  aria-live="polite"
42
58
  >
43
- No tasks need attention right now.
59
+ No tasks or workflows need attention right now.
44
60
  </p>
45
61
  ) : (
46
62
  <div className="space-y-1" aria-live="polite">
47
63
  {tasks.map((task) => {
48
- const Icon = statusIcon[task.status] ?? Shield;
64
+ // Workflow items use workflow-specific status icons; tasks use task status icons
65
+ const Icon = task.isWorkflow
66
+ ? workflowStatusIcon[task.status] ?? WorkflowIcon
67
+ : statusIcon[task.status] ?? Shield;
68
+ const linkHref = task.isWorkflow
69
+ ? `/workflows/${task.workflowProgress?.workflowId ?? task.id}`
70
+ : `/tasks/${task.id}`;
71
+
49
72
  return (
50
- <div
73
+ <Link
51
74
  key={task.id}
52
- className="flex items-center gap-3 py-2.5 border-b border-border/50 last:border-b-0"
75
+ href={linkHref}
76
+ className="flex items-center gap-3 py-2.5 border-b border-border/50 last:border-b-0 hover:bg-accent/30 rounded-md px-1 -mx-1 transition-colors"
53
77
  >
54
- <Icon className={`h-4 w-4 flex-shrink-0 ${priorityColors[task.priority] ?? priorityColors[3]}`} />
78
+ <Icon className={`h-4 w-4 flex-shrink-0 ${
79
+ task.isWorkflow ? "text-primary" : priorityColors[task.priority] ?? priorityColors[3]
80
+ }`} />
55
81
  <div className="flex-1 min-w-0">
56
82
  <p className="text-sm font-medium truncate">{task.title}</p>
57
83
  {task.projectName && (
58
84
  <p className="text-xs text-muted-foreground">{task.projectName}</p>
59
85
  )}
86
+ {task.workflowProgress && (
87
+ <div className="flex items-center gap-1.5 mt-1">
88
+ <span className="text-xs text-muted-foreground">
89
+ {task.workflowProgress.current}/{task.workflowProgress.total}
90
+ </span>
91
+ {task.workflowProgress.currentStepName && (
92
+ <span className="text-[11px] text-muted-foreground truncate">
93
+ {task.workflowProgress.currentStepName}
94
+ </span>
95
+ )}
96
+ {/* Mini progress bar */}
97
+ <div className="h-1 flex-1 max-w-16 rounded-full bg-muted overflow-hidden">
98
+ <div
99
+ className="h-full rounded-full bg-primary transition-all"
100
+ style={{ width: `${(task.workflowProgress.current / task.workflowProgress.total) * 100}%` }}
101
+ />
102
+ </div>
103
+ </div>
104
+ )}
60
105
  </div>
61
- <Badge variant={taskStatusVariant[task.status] ?? "secondary"} className="text-xs">
106
+ <Badge variant={
107
+ task.isWorkflow
108
+ ? workflowStatusVariant[task.status] ?? "secondary"
109
+ : taskStatusVariant[task.status] ?? "secondary"
110
+ } className="text-xs">
62
111
  {task.status}
63
112
  </Badge>
64
- </div>
113
+ </Link>
65
114
  );
66
115
  })}
67
116
  </div>
@@ -2,7 +2,7 @@
2
2
 
3
3
  import Link from "next/link";
4
4
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5
- import { Activity, CheckCircle, MessageSquare, FolderKanban } from "lucide-react";
5
+ import { Activity, CheckCircle, MessageSquare, FolderKanban, Workflow } from "lucide-react";
6
6
  import { Sparkline } from "@/components/charts/sparkline";
7
7
 
8
8
  interface StatsCardsProps {
@@ -11,11 +11,13 @@ interface StatsCardsProps {
11
11
  completedAllTime: number;
12
12
  awaitingReview: number;
13
13
  activeProjects: number;
14
+ activeWorkflows: number;
14
15
  sparklines?: {
15
16
  completions: number[];
16
17
  creations: number[];
17
18
  projects: number[];
18
19
  notifications: number[];
20
+ workflows: number[];
19
21
  };
20
22
  }
21
23
 
@@ -25,6 +27,7 @@ export function StatsCards({
25
27
  completedAllTime,
26
28
  awaitingReview,
27
29
  activeProjects,
30
+ activeWorkflows,
28
31
  sparklines,
29
32
  }: StatsCardsProps) {
30
33
  const stats = [
@@ -72,10 +75,21 @@ export function StatsCards({
72
75
  destination: "Projects",
73
76
  sparklineData: sparklines?.projects,
74
77
  },
78
+ {
79
+ title: "Workflows Active",
80
+ value: activeWorkflows,
81
+ subtitle: "In progress",
82
+ icon: Workflow,
83
+ color: "text-primary",
84
+ chartColor: "var(--chart-5)",
85
+ href: "/workflows",
86
+ destination: "Workflows",
87
+ sparklineData: sparklines?.workflows,
88
+ },
75
89
  ];
76
90
 
77
91
  return (
78
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
92
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 mb-6">
79
93
  {stats.map((s) => (
80
94
  <Link key={s.title} href={s.href}>
81
95
  <Card className="surface-card glass-shimmer cursor-pointer transition-colors hover:bg-accent/50 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-xl">
@@ -0,0 +1,183 @@
1
+ "use client";
2
+
3
+ import { useRouter } from "next/navigation";
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { Button } from "@/components/ui/button";
6
+ import {
7
+ Select,
8
+ SelectContent,
9
+ SelectItem,
10
+ SelectTrigger,
11
+ SelectValue,
12
+ } from "@/components/ui/select";
13
+ import {
14
+ Download,
15
+ Trash2,
16
+ Unlink,
17
+ ArrowUpRight,
18
+ ArrowDownLeft,
19
+ Link2,
20
+ FolderKanban,
21
+ } from "lucide-react";
22
+ import {
23
+ getFileIcon,
24
+ formatSize,
25
+ getStatusColor,
26
+ getStatusDotColor,
27
+ formatRelativeTime,
28
+ } from "./utils";
29
+ import type { DocumentWithRelations } from "./types";
30
+
31
+ interface DocumentChipBarProps {
32
+ doc: DocumentWithRelations;
33
+ onDelete: () => void;
34
+ onUnlinkTask: () => void;
35
+ onLinkProject: (projectId: string) => void;
36
+ projects: { id: string; name: string }[];
37
+ deleting: boolean;
38
+ linking: boolean;
39
+ }
40
+
41
+ export function DocumentChipBar({
42
+ doc,
43
+ onDelete,
44
+ onUnlinkTask,
45
+ onLinkProject,
46
+ projects,
47
+ deleting,
48
+ linking,
49
+ }: DocumentChipBarProps) {
50
+ const router = useRouter();
51
+ const Icon = getFileIcon(doc.mimeType);
52
+ const DirectionIcon = doc.direction === "output" ? ArrowUpRight : ArrowDownLeft;
53
+
54
+ return (
55
+ <div className="surface-control rounded-lg p-4 space-y-3">
56
+ {/* Row 1: Name + Actions */}
57
+ <div className="flex items-center gap-3 min-w-0">
58
+ <Icon className="h-5 w-5 text-muted-foreground shrink-0" />
59
+ <h1 className="text-lg font-semibold truncate flex-1 min-w-0">
60
+ {doc.originalName}
61
+ </h1>
62
+ <div className="flex items-center gap-2 shrink-0">
63
+ <Button variant="outline" size="sm" asChild>
64
+ <a
65
+ href={`/api/documents/${doc.id}/file`}
66
+ target="_blank"
67
+ rel="noopener noreferrer"
68
+ >
69
+ <Download className="h-3.5 w-3.5 mr-1" />
70
+ Download
71
+ </a>
72
+ </Button>
73
+ <Button
74
+ variant="destructive"
75
+ size="sm"
76
+ onClick={onDelete}
77
+ disabled={deleting}
78
+ >
79
+ <Trash2 className="h-3.5 w-3.5 mr-1" />
80
+ Delete
81
+ </Button>
82
+ </div>
83
+ </div>
84
+
85
+ {/* Row 2: Metadata chips */}
86
+ <div className="flex flex-wrap items-center gap-2">
87
+ {/* MIME type */}
88
+ <Badge variant="outline" className="text-xs font-normal">
89
+ {doc.mimeType}
90
+ </Badge>
91
+
92
+ {/* File size */}
93
+ <Badge variant="outline" className="text-xs font-normal">
94
+ {formatSize(doc.size)}
95
+ </Badge>
96
+
97
+ {/* Status */}
98
+ <Badge variant="outline" className={`text-xs ${getStatusColor(doc.status)}`}>
99
+ <span className={`w-1.5 h-1.5 rounded-full mr-1.5 ${getStatusDotColor(doc.status)}`} />
100
+ {doc.status}
101
+ </Badge>
102
+
103
+ {/* Direction */}
104
+ <Badge variant="outline" className="text-xs font-normal">
105
+ <DirectionIcon className="h-3 w-3 mr-1" />
106
+ {doc.direction}
107
+ </Badge>
108
+
109
+ {/* Version (output only) */}
110
+ {doc.direction === "output" && (
111
+ <Badge variant="outline" className="text-xs font-normal">
112
+ v{doc.version}
113
+ </Badge>
114
+ )}
115
+
116
+ {/* Created date */}
117
+ <Badge
118
+ variant="outline"
119
+ className="text-xs font-normal"
120
+ title={new Date(doc.createdAt).toLocaleString()}
121
+ >
122
+ {formatRelativeTime(
123
+ typeof doc.createdAt === "number"
124
+ ? doc.createdAt
125
+ : new Date(doc.createdAt).getTime()
126
+ )}
127
+ </Badge>
128
+ </div>
129
+
130
+ {/* Row 3: Links — task, workflow, project */}
131
+ <div className="flex flex-wrap items-center gap-2">
132
+ {/* Task link */}
133
+ {doc.taskTitle ? (
134
+ <Badge
135
+ variant="secondary"
136
+ className="text-xs cursor-pointer hover:bg-accent gap-1"
137
+ onClick={() => doc.taskId && router.push(`/tasks/${doc.taskId}`)}
138
+ >
139
+ <Link2 className="h-3 w-3" />
140
+ {doc.taskTitle}
141
+ <button
142
+ onClick={(e) => {
143
+ e.stopPropagation();
144
+ onUnlinkTask();
145
+ }}
146
+ className="ml-1 hover:text-destructive"
147
+ aria-label="Unlink from task"
148
+ >
149
+ <Unlink className="h-3 w-3" />
150
+ </button>
151
+ </Badge>
152
+ ) : (
153
+ <Badge variant="outline" className="text-xs font-normal text-muted-foreground">
154
+ <Link2 className="h-3 w-3 mr-1" />
155
+ No task
156
+ </Badge>
157
+ )}
158
+
159
+ {/* Project selector */}
160
+ <div className="flex items-center gap-1.5">
161
+ <FolderKanban className="h-3.5 w-3.5 text-muted-foreground" />
162
+ <Select
163
+ value={doc.projectId ?? "none"}
164
+ onValueChange={onLinkProject}
165
+ disabled={linking}
166
+ >
167
+ <SelectTrigger className="h-7 w-[220px] text-xs border-dashed">
168
+ <SelectValue placeholder="Project" />
169
+ </SelectTrigger>
170
+ <SelectContent>
171
+ <SelectItem value="none">No project</SelectItem>
172
+ {projects.map((p) => (
173
+ <SelectItem key={p.id} value={p.id}>
174
+ {p.name}
175
+ </SelectItem>
176
+ ))}
177
+ </SelectContent>
178
+ </Select>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ );
183
+ }