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.
- package/README.md +44 -50
- package/package.json +1 -1
- package/public/readme/cost-usage-list.png +0 -0
- package/public/readme/dashboard-bulk-select.png +0 -0
- package/public/readme/dashboard-card-edit.png +0 -0
- package/public/readme/dashboard-create-form-ai-applied.png +0 -0
- package/public/readme/dashboard-create-form-ai-assist.png +0 -0
- package/public/readme/dashboard-create-form-empty.png +0 -0
- package/public/readme/dashboard-create-form-filled.png +0 -0
- package/public/readme/dashboard-filtered.png +0 -0
- package/public/readme/dashboard-list.png +0 -0
- package/public/readme/dashboard-workflow-confirm.png +0 -0
- package/public/readme/home-below-fold.png +0 -0
- package/public/readme/home-list.png +0 -0
- package/public/readme/inbox-list.png +0 -0
- package/public/readme/playbook-list.png +0 -0
- package/public/readme/profiles-list.png +0 -0
- package/public/readme/settings-list.png +0 -0
- package/public/readme/workflows-list.png +0 -0
- package/src/app/api/tasks/[id]/route.ts +54 -3
- package/src/app/api/workflows/[id]/route.ts +43 -4
- package/src/app/api/workflows/[id]/status/route.ts +70 -2
- package/src/app/api/workflows/from-assist/route.ts +6 -32
- package/src/app/dashboard/page.tsx +59 -21
- package/src/app/documents/[id]/page.tsx +10 -8
- package/src/app/globals.css +11 -0
- package/src/app/page.tsx +60 -3
- package/src/app/tasks/[id]/page.tsx +22 -2
- package/src/components/costs/cost-dashboard.tsx +1 -1
- package/src/components/dashboard/greeting.tsx +3 -1
- package/src/components/dashboard/priority-queue.tsx +58 -9
- package/src/components/dashboard/stats-cards.tsx +16 -2
- package/src/components/documents/document-chip-bar.tsx +183 -0
- package/src/components/documents/document-content-renderer.tsx +146 -0
- package/src/components/documents/document-detail-view.tsx +16 -239
- package/src/components/documents/image-zoom-view.tsx +60 -0
- package/src/components/documents/smart-extracted-text.tsx +47 -0
- package/src/components/documents/utils.ts +70 -0
- package/src/components/notifications/inbox-list.tsx +4 -5
- package/src/components/notifications/notification-item.tsx +72 -8
- package/src/components/notifications/pending-approval-host.tsx +7 -4
- package/src/components/playbook/playbook-detail-view.tsx +6 -4
- package/src/components/profiles/profile-browser.tsx +1 -0
- package/src/components/profiles/profile-card.tsx +16 -8
- package/src/components/profiles/profile-detail-view.tsx +6 -1
- package/src/components/shared/app-sidebar.tsx +2 -2
- package/src/components/tasks/__tests__/kanban-board-accessibility.test.tsx +1 -1
- package/src/components/tasks/ai-assist-panel.tsx +108 -78
- package/src/components/tasks/content-preview.tsx +2 -1
- package/src/components/tasks/kanban-board.tsx +57 -5
- package/src/components/tasks/kanban-column.tsx +34 -23
- package/src/components/tasks/task-bento-cell.tsx +50 -0
- package/src/components/tasks/task-bento-grid.tsx +155 -0
- package/src/components/tasks/task-card.tsx +14 -16
- package/src/components/tasks/task-chip-bar.tsx +207 -0
- package/src/components/tasks/task-detail-view.tsx +42 -190
- package/src/components/tasks/task-result-renderer.tsx +33 -0
- package/src/components/workflows/blueprint-gallery.tsx +19 -12
- package/src/components/workflows/blueprint-preview.tsx +8 -1
- package/src/components/workflows/loop-status-view.tsx +2 -8
- package/src/components/workflows/swarm-dashboard.tsx +2 -3
- package/src/components/workflows/workflow-confirmation-view.tsx +2 -7
- package/src/components/workflows/workflow-full-output.tsx +80 -0
- package/src/components/workflows/workflow-kanban-card.tsx +121 -0
- package/src/components/workflows/workflow-list.tsx +47 -42
- package/src/components/workflows/workflow-status-view.tsx +160 -20
- package/src/lib/agents/learning-session.ts +138 -18
- package/src/lib/constants/card-icons.tsx +202 -0
- package/src/lib/constants/prose-styles.ts +7 -0
- package/src/lib/constants/task-status.ts +3 -0
- package/src/lib/docs/reader.ts +8 -3
- package/src/lib/documents/context-builder.ts +41 -0
- package/src/lib/queries/chart-data.ts +20 -1
- package/src/lib/workflows/engine.ts +57 -61
- package/src/lib/workflows/types.ts +2 -0
- package/tsconfig.json +2 -1
- 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-
|
|
37
|
-
<
|
|
38
|
-
<
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
}
|
package/src/app/globals.css
CHANGED
|
@@ -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(
|
|
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={
|
|
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
|
|
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
|
-
|
|
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
|
-
<
|
|
73
|
+
<Link
|
|
51
74
|
key={task.id}
|
|
52
|
-
|
|
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 ${
|
|
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={
|
|
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
|
-
</
|
|
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-
|
|
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
|
+
}
|