stagent 0.1.11 → 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 +74 -49
- package/package.json +3 -2
- 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/__tests__/e2e/blueprint.test.ts +63 -0
- package/src/__tests__/e2e/cross-runtime.test.ts +77 -0
- package/src/__tests__/e2e/helpers.ts +286 -0
- package/src/__tests__/e2e/parallel-workflow.test.ts +120 -0
- package/src/__tests__/e2e/sequence-workflow.test.ts +109 -0
- package/src/__tests__/e2e/setup.ts +156 -0
- package/src/__tests__/e2e/single-task.test.ts +170 -0
- package/src/app/api/command-palette/recent/route.ts +41 -18
- package/src/app/api/context/batch/route.ts +44 -0
- package/src/app/api/permissions/presets/route.ts +80 -0
- package/src/app/api/playbook/status/route.ts +15 -0
- package/src/app/api/profiles/route.ts +23 -20
- package/src/app/api/settings/pricing/route.ts +15 -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/costs/page.tsx +53 -43
- 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/playbook/[slug]/page.tsx +76 -0
- package/src/app/playbook/page.tsx +54 -0
- package/src/app/profiles/page.tsx +7 -4
- package/src/app/settings/page.tsx +2 -2
- package/src/app/tasks/[id]/page.tsx +22 -2
- package/src/components/costs/cost-dashboard.tsx +226 -320
- package/src/components/dashboard/activity-feed.tsx +6 -2
- 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/batch-proposal-review.tsx +150 -0
- package/src/components/notifications/inbox-list.tsx +4 -5
- package/src/components/notifications/notification-item.tsx +73 -6
- package/src/components/notifications/pending-approval-host.tsx +63 -14
- package/src/components/playbook/adoption-heatmap.tsx +69 -0
- package/src/components/playbook/journey-card.tsx +110 -0
- package/src/components/playbook/playbook-action-button.tsx +22 -0
- package/src/components/playbook/playbook-browser.tsx +143 -0
- package/src/components/playbook/playbook-card.tsx +102 -0
- package/src/components/playbook/playbook-detail-view.tsx +225 -0
- package/src/components/playbook/playbook-homepage.tsx +142 -0
- package/src/components/playbook/playbook-toc.tsx +90 -0
- package/src/components/playbook/playbook-updated-badge.tsx +23 -0
- package/src/components/playbook/related-docs.tsx +30 -0
- package/src/components/profiles/__tests__/learned-context-panel.test.tsx +175 -0
- package/src/components/profiles/context-proposal-review.tsx +7 -3
- package/src/components/profiles/learned-context-panel.tsx +116 -8
- 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 +12 -4
- package/src/components/settings/__tests__/auth-config-section.test.tsx +147 -0
- package/src/components/settings/api-key-form.tsx +5 -43
- package/src/components/settings/auth-config-section.tsx +10 -6
- package/src/components/settings/auth-status-badge.tsx +8 -0
- package/src/components/settings/budget-guardrails-section.tsx +403 -620
- package/src/components/settings/connection-test-control.tsx +63 -0
- package/src/components/settings/permissions-section.tsx +85 -75
- package/src/components/settings/permissions-sections.tsx +24 -0
- package/src/components/settings/presets-section.tsx +159 -0
- package/src/components/settings/pricing-registry-panel.tsx +164 -0
- package/src/components/shared/app-sidebar.tsx +4 -2
- package/src/components/shared/command-palette.tsx +30 -0
- package/src/components/shared/light-markdown.tsx +134 -0
- 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 -4
- 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 +163 -16
- package/src/lib/agents/learned-context.ts +27 -15
- package/src/lib/agents/learning-session.ts +354 -0
- package/src/lib/agents/pattern-extractor.ts +19 -0
- package/src/lib/agents/profiles/__tests__/sort.test.ts +42 -0
- package/src/lib/agents/profiles/sort.ts +7 -0
- package/src/lib/constants/card-icons.tsx +202 -0
- package/src/lib/constants/prose-styles.ts +7 -0
- package/src/lib/constants/settings.ts +1 -0
- package/src/lib/constants/task-status.ts +3 -0
- package/src/lib/db/schema.ts +3 -0
- package/src/lib/docs/adoption.ts +105 -0
- package/src/lib/docs/journey-tracker.ts +21 -0
- package/src/lib/docs/reader.ts +107 -0
- package/src/lib/docs/types.ts +54 -0
- package/src/lib/docs/usage-stage.ts +60 -0
- package/src/lib/documents/context-builder.ts +41 -0
- package/src/lib/notifications/actionable.ts +18 -10
- package/src/lib/queries/chart-data.ts +20 -1
- package/src/lib/settings/__tests__/budget-guardrails.test.ts +86 -24
- package/src/lib/settings/budget-guardrails.ts +213 -85
- package/src/lib/settings/permission-presets.ts +150 -0
- package/src/lib/settings/runtime-setup.ts +71 -0
- package/src/lib/usage/__tests__/ledger.test.ts +2 -2
- package/src/lib/usage/__tests__/pricing-registry.test.ts +78 -0
- package/src/lib/usage/ledger.ts +1 -1
- package/src/lib/usage/pricing-registry.ts +570 -0
- package/src/lib/usage/pricing.ts +15 -95
- package/src/lib/utils/__tests__/learned-context-history.test.ts +171 -0
- package/src/lib/utils/learned-context-history.ts +150 -0
- package/src/lib/validators/__tests__/settings.test.ts +23 -16
- package/src/lib/validators/settings.ts +3 -9
- package/src/lib/workflows/engine.ts +75 -61
- package/src/lib/workflows/types.ts +2 -0
- package/tsconfig.json +2 -1
- package/src/components/documents/document-preview.tsx +0 -68
|
@@ -73,9 +73,13 @@ export function ActivityFeed({ entries, hourlyActivity }: ActivityFeedProps) {
|
|
|
73
73
|
<span className="text-muted-foreground"> — {entry.taskTitle}</span>
|
|
74
74
|
)}
|
|
75
75
|
</p>
|
|
76
|
-
<p className="text-xs text-muted-foreground">
|
|
76
|
+
<p className="text-xs text-muted-foreground" suppressHydrationWarning>
|
|
77
77
|
{new Date(entry.timestamp).toLocaleTimeString()}
|
|
78
|
-
{entry.payload &&
|
|
78
|
+
{entry.payload && (() => {
|
|
79
|
+
const chars = Array.from(entry.payload);
|
|
80
|
+
const truncated = chars.length > 60 ? chars.slice(0, 60).join("") + "..." : entry.payload;
|
|
81
|
+
return ` · ${truncated}`;
|
|
82
|
+
})()}
|
|
79
83
|
</p>
|
|
80
84
|
</div>
|
|
81
85
|
</div>
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { memo } from "react";
|
|
4
|
+
import ReactMarkdown from "react-markdown";
|
|
5
|
+
import remarkGfm from "remark-gfm";
|
|
6
|
+
import { Loader2, Download, FileQuestion } from "lucide-react";
|
|
7
|
+
import { Badge } from "@/components/ui/badge";
|
|
8
|
+
import { Button } from "@/components/ui/button";
|
|
9
|
+
import { ErrorState } from "@/components/shared/error-state";
|
|
10
|
+
import { EmptyState } from "@/components/shared/empty-state";
|
|
11
|
+
import { ImageZoomView } from "./image-zoom-view";
|
|
12
|
+
import { SmartExtractedText } from "./smart-extracted-text";
|
|
13
|
+
import {
|
|
14
|
+
isMarkdown,
|
|
15
|
+
isCode,
|
|
16
|
+
isPlainText,
|
|
17
|
+
isPdf,
|
|
18
|
+
isImage,
|
|
19
|
+
getLanguageLabel,
|
|
20
|
+
} from "./utils";
|
|
21
|
+
import { PROSE_READER } from "@/lib/constants/prose-styles";
|
|
22
|
+
import type { DocumentWithRelations } from "./types";
|
|
23
|
+
|
|
24
|
+
interface DocumentContentRendererProps {
|
|
25
|
+
doc: DocumentWithRelations;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function DocumentContentRendererInner({ doc }: DocumentContentRendererProps) {
|
|
29
|
+
const fileUrl = `/api/documents/${doc.id}/file?inline=1`;
|
|
30
|
+
|
|
31
|
+
// Tier 0: Processing status
|
|
32
|
+
if (doc.status === "processing") {
|
|
33
|
+
return (
|
|
34
|
+
<div className="flex flex-col items-center justify-center gap-3 py-20">
|
|
35
|
+
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
36
|
+
<p className="text-sm text-muted-foreground">Processing document...</p>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Tier 0: Error status
|
|
42
|
+
if (doc.status === "error") {
|
|
43
|
+
return (
|
|
44
|
+
<ErrorState
|
|
45
|
+
heading="Processing failed"
|
|
46
|
+
description={doc.processingError ?? "An unknown error occurred while processing this document."}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Tier 1: Markdown — full render, no truncation
|
|
52
|
+
if (isMarkdown(doc.mimeType) && doc.extractedText) {
|
|
53
|
+
return (
|
|
54
|
+
<div className={PROSE_READER}>
|
|
55
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
|
56
|
+
{doc.extractedText}
|
|
57
|
+
</ReactMarkdown>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Tier 1: Code — monospace with language label
|
|
63
|
+
if (isCode(doc.mimeType) && doc.extractedText) {
|
|
64
|
+
return (
|
|
65
|
+
<div className="relative">
|
|
66
|
+
<Badge
|
|
67
|
+
variant="secondary"
|
|
68
|
+
className="absolute top-3 right-3 text-xs"
|
|
69
|
+
>
|
|
70
|
+
{getLanguageLabel(doc.mimeType)}
|
|
71
|
+
</Badge>
|
|
72
|
+
<pre className="font-mono text-sm bg-muted p-4 rounded-lg overflow-x-auto whitespace-pre-wrap break-words">
|
|
73
|
+
{doc.extractedText}
|
|
74
|
+
</pre>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Tier 1: Plain text — flowing prose paragraphs
|
|
80
|
+
if (isPlainText(doc.mimeType) && doc.extractedText) {
|
|
81
|
+
const paragraphs = doc.extractedText.split(/\n{2,}/).filter(Boolean);
|
|
82
|
+
return (
|
|
83
|
+
<div className={`${PROSE_READER} space-y-4`}>
|
|
84
|
+
{paragraphs.map((p, i) => (
|
|
85
|
+
<p key={i} className="whitespace-pre-wrap">
|
|
86
|
+
{p}
|
|
87
|
+
</p>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Tier 2: PDF — full-height iframe
|
|
94
|
+
if (isPdf(doc.mimeType)) {
|
|
95
|
+
return (
|
|
96
|
+
<div className="rounded-lg overflow-hidden border border-border">
|
|
97
|
+
<iframe
|
|
98
|
+
src={fileUrl}
|
|
99
|
+
className="w-full min-h-[60vh] md:min-h-[80vh]"
|
|
100
|
+
title={doc.originalName}
|
|
101
|
+
onError={() => {
|
|
102
|
+
// iframe onerror doesn't fire reliably — the fallback link below covers this
|
|
103
|
+
}}
|
|
104
|
+
/>
|
|
105
|
+
<div className="p-3 bg-muted/30 text-center">
|
|
106
|
+
<a
|
|
107
|
+
href={`/api/documents/${doc.id}/file`}
|
|
108
|
+
download
|
|
109
|
+
className="text-xs text-muted-foreground underline underline-offset-2 hover:text-foreground"
|
|
110
|
+
>
|
|
111
|
+
PDF not loading? Download instead
|
|
112
|
+
</a>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Tier 2: Image — full size with zoom
|
|
119
|
+
if (isImage(doc.mimeType)) {
|
|
120
|
+
return <ImageZoomView src={fileUrl} alt={doc.originalName} />;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Tier 3: Other with extracted text — smart formatting
|
|
124
|
+
if (doc.extractedText) {
|
|
125
|
+
return <SmartExtractedText text={doc.extractedText} />;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Tier 4: Fallback — no content available
|
|
129
|
+
return (
|
|
130
|
+
<EmptyState
|
|
131
|
+
icon={FileQuestion}
|
|
132
|
+
heading="No preview available"
|
|
133
|
+
description="This file type can't be previewed. Download it to view the contents."
|
|
134
|
+
action={
|
|
135
|
+
<Button variant="outline" size="sm" asChild>
|
|
136
|
+
<a href={`/api/documents/${doc.id}/file`} download>
|
|
137
|
+
<Download className="h-3.5 w-3.5 mr-1" />
|
|
138
|
+
Download File
|
|
139
|
+
</a>
|
|
140
|
+
</Button>
|
|
141
|
+
}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const DocumentContentRenderer = memo(DocumentContentRendererInner);
|