stagent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +191 -0
- package/README.md +399 -0
- package/components.json +21 -0
- package/dist/cli.js +171 -0
- package/drizzle.config.ts +12 -0
- package/next.config.mjs +15 -0
- package/package.json +114 -0
- package/postcss.config.mjs +8 -0
- package/public/icon-512.png +0 -0
- package/public/icon.svg +13 -0
- package/public/readme/home-workspace.png +0 -0
- package/public/readme/inbox-approvals.png +0 -0
- package/public/readme/workflow-blueprints.png +0 -0
- package/public/stagent-s-128.png +0 -0
- package/public/stagent-s-64.png +0 -0
- package/src/app/api/blueprints/[id]/instantiate/route.ts +27 -0
- package/src/app/api/blueprints/[id]/route.ts +39 -0
- package/src/app/api/blueprints/import/route.ts +68 -0
- package/src/app/api/blueprints/route.ts +29 -0
- package/src/app/api/command-palette/recent/route.ts +31 -0
- package/src/app/api/data/clear/route.ts +22 -0
- package/src/app/api/data/seed/route.ts +22 -0
- package/src/app/api/documents/[id]/file/route.ts +44 -0
- package/src/app/api/documents/[id]/route.ts +123 -0
- package/src/app/api/documents/route.ts +59 -0
- package/src/app/api/logs/stream/route.ts +101 -0
- package/src/app/api/notifications/[id]/route.ts +36 -0
- package/src/app/api/notifications/mark-all-read/route.ts +13 -0
- package/src/app/api/notifications/pending-approvals/route.ts +10 -0
- package/src/app/api/notifications/pending-approvals/stream/route.ts +101 -0
- package/src/app/api/notifications/route.ts +34 -0
- package/src/app/api/permissions/route.ts +46 -0
- package/src/app/api/profiles/[id]/route.ts +79 -0
- package/src/app/api/profiles/[id]/test/route.ts +42 -0
- package/src/app/api/profiles/import/route.ts +108 -0
- package/src/app/api/profiles/route.ts +50 -0
- package/src/app/api/projects/[id]/route.ts +72 -0
- package/src/app/api/projects/route.ts +53 -0
- package/src/app/api/schedules/[id]/route.ts +185 -0
- package/src/app/api/schedules/route.ts +117 -0
- package/src/app/api/settings/budgets/route.ts +24 -0
- package/src/app/api/settings/openai/route.ts +24 -0
- package/src/app/api/settings/route.ts +21 -0
- package/src/app/api/settings/test/route.ts +26 -0
- package/src/app/api/tasks/[id]/cancel/route.ts +21 -0
- package/src/app/api/tasks/[id]/execute/route.ts +90 -0
- package/src/app/api/tasks/[id]/logs/route.ts +95 -0
- package/src/app/api/tasks/[id]/output/route.ts +47 -0
- package/src/app/api/tasks/[id]/respond/route.ts +64 -0
- package/src/app/api/tasks/[id]/resume/route.ts +76 -0
- package/src/app/api/tasks/[id]/route.ts +77 -0
- package/src/app/api/tasks/assist/route.ts +35 -0
- package/src/app/api/tasks/route.ts +82 -0
- package/src/app/api/uploads/[id]/route.ts +81 -0
- package/src/app/api/uploads/cleanup/route.ts +7 -0
- package/src/app/api/uploads/route.ts +66 -0
- package/src/app/api/workflows/[id]/execute/route.ts +82 -0
- package/src/app/api/workflows/[id]/route.ts +133 -0
- package/src/app/api/workflows/[id]/status/route.ts +54 -0
- package/src/app/api/workflows/[id]/steps/[stepId]/retry/route.ts +22 -0
- package/src/app/api/workflows/route.ts +61 -0
- package/src/app/apple-icon.tsx +31 -0
- package/src/app/costs/page.tsx +256 -0
- package/src/app/dashboard/page.tsx +44 -0
- package/src/app/documents/[id]/page.tsx +46 -0
- package/src/app/documents/page.tsx +45 -0
- package/src/app/error.tsx +26 -0
- package/src/app/global-error.tsx +23 -0
- package/src/app/globals.css +733 -0
- package/src/app/icon.tsx +30 -0
- package/src/app/inbox/loading.tsx +15 -0
- package/src/app/inbox/page.tsx +35 -0
- package/src/app/layout.tsx +78 -0
- package/src/app/manifest.ts +32 -0
- package/src/app/monitor/page.tsx +37 -0
- package/src/app/page.tsx +162 -0
- package/src/app/profiles/[id]/edit/page.tsx +39 -0
- package/src/app/profiles/[id]/page.tsx +33 -0
- package/src/app/profiles/new/page.tsx +22 -0
- package/src/app/profiles/page.tsx +19 -0
- package/src/app/projects/[id]/page.tsx +134 -0
- package/src/app/projects/loading.tsx +17 -0
- package/src/app/projects/page.tsx +32 -0
- package/src/app/schedules/[id]/page.tsx +47 -0
- package/src/app/schedules/page.tsx +18 -0
- package/src/app/settings/loading.tsx +24 -0
- package/src/app/settings/page.tsx +27 -0
- package/src/app/tasks/[id]/page.tsx +45 -0
- package/src/app/tasks/new/page.tsx +27 -0
- package/src/app/workflows/[id]/edit/page.tsx +66 -0
- package/src/app/workflows/[id]/page.tsx +37 -0
- package/src/app/workflows/blueprints/[id]/page.tsx +40 -0
- package/src/app/workflows/blueprints/new/page.tsx +20 -0
- package/src/app/workflows/blueprints/page.tsx +11 -0
- package/src/app/workflows/new/page.tsx +36 -0
- package/src/app/workflows/page.tsx +18 -0
- package/src/components/charts/donut-ring.tsx +64 -0
- package/src/components/charts/mini-bar.tsx +75 -0
- package/src/components/charts/sparkline.tsx +107 -0
- package/src/components/costs/cost-dashboard.tsx +877 -0
- package/src/components/costs/cost-filters.tsx +179 -0
- package/src/components/dashboard/activity-feed.tsx +95 -0
- package/src/components/dashboard/greeting.tsx +30 -0
- package/src/components/dashboard/priority-queue.tsx +79 -0
- package/src/components/dashboard/quick-actions.tsx +62 -0
- package/src/components/dashboard/recent-projects.tsx +79 -0
- package/src/components/dashboard/stats-cards.tsx +114 -0
- package/src/components/documents/document-browser.tsx +235 -0
- package/src/components/documents/document-detail-view.tsx +367 -0
- package/src/components/documents/document-grid.tsx +78 -0
- package/src/components/documents/document-preview.tsx +68 -0
- package/src/components/documents/document-table.tsx +119 -0
- package/src/components/documents/document-upload-dialog.tsx +153 -0
- package/src/components/documents/types.ts +6 -0
- package/src/components/documents/utils.ts +57 -0
- package/src/components/monitoring/connection-indicator.tsx +14 -0
- package/src/components/monitoring/log-entry.tsx +79 -0
- package/src/components/monitoring/log-filters.tsx +57 -0
- package/src/components/monitoring/log-stream.tsx +144 -0
- package/src/components/monitoring/monitor-overview-wrapper.tsx +64 -0
- package/src/components/monitoring/monitor-overview.tsx +119 -0
- package/src/components/notifications/failure-action.tsx +38 -0
- package/src/components/notifications/inbox-list.tsx +165 -0
- package/src/components/notifications/message-response.tsx +196 -0
- package/src/components/notifications/notification-item.tsx +250 -0
- package/src/components/notifications/pending-approval-host.tsx +478 -0
- package/src/components/notifications/permission-action.tsx +37 -0
- package/src/components/notifications/permission-response-actions.tsx +126 -0
- package/src/components/notifications/unread-badge.tsx +35 -0
- package/src/components/profiles/profile-browser.tsx +117 -0
- package/src/components/profiles/profile-card.tsx +78 -0
- package/src/components/profiles/profile-detail-view.tsx +564 -0
- package/src/components/profiles/profile-form-view.tsx +480 -0
- package/src/components/profiles/profile-import-dialog.tsx +113 -0
- package/src/components/projects/project-card.tsx +58 -0
- package/src/components/projects/project-create-dialog.tsx +140 -0
- package/src/components/projects/project-detail.tsx +68 -0
- package/src/components/projects/project-edit-dialog.tsx +219 -0
- package/src/components/projects/project-list.tsx +108 -0
- package/src/components/schedules/schedule-create-dialog.tsx +403 -0
- package/src/components/schedules/schedule-detail-view.tsx +274 -0
- package/src/components/schedules/schedule-list.tsx +242 -0
- package/src/components/schedules/schedule-status-badge.tsx +16 -0
- package/src/components/settings/api-key-form.tsx +141 -0
- package/src/components/settings/auth-config-section.tsx +141 -0
- package/src/components/settings/auth-method-selector.tsx +67 -0
- package/src/components/settings/auth-status-badge.tsx +40 -0
- package/src/components/settings/auth-status-dot.tsx +59 -0
- package/src/components/settings/budget-guardrails-section.tsx +842 -0
- package/src/components/settings/data-management-section.tsx +141 -0
- package/src/components/settings/openai-runtime-section.tsx +104 -0
- package/src/components/settings/permissions-section.tsx +91 -0
- package/src/components/shared/app-sidebar.tsx +123 -0
- package/src/components/shared/card-skeleton.tsx +42 -0
- package/src/components/shared/command-palette.tsx +250 -0
- package/src/components/shared/confirm-dialog.tsx +52 -0
- package/src/components/shared/empty-state.tsx +24 -0
- package/src/components/shared/error-state.tsx +32 -0
- package/src/components/shared/form-section-card.tsx +33 -0
- package/src/components/shared/section-heading.tsx +14 -0
- package/src/components/shared/stagent-logo.tsx +21 -0
- package/src/components/shared/theme-toggle.tsx +46 -0
- package/src/components/tasks/ai-assist-panel.tsx +210 -0
- package/src/components/tasks/content-preview.tsx +89 -0
- package/src/components/tasks/empty-board.tsx +12 -0
- package/src/components/tasks/file-upload.tsx +120 -0
- package/src/components/tasks/kanban-board.tsx +275 -0
- package/src/components/tasks/kanban-column.tsx +75 -0
- package/src/components/tasks/skeleton-board.tsx +21 -0
- package/src/components/tasks/task-attachments.tsx +114 -0
- package/src/components/tasks/task-card.tsx +101 -0
- package/src/components/tasks/task-create-panel.tsx +360 -0
- package/src/components/tasks/task-detail-view.tsx +356 -0
- package/src/components/ui/alert-dialog.tsx +196 -0
- package/src/components/ui/badge.tsx +50 -0
- package/src/components/ui/button.tsx +71 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/checkbox.tsx +32 -0
- package/src/components/ui/command.tsx +184 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/form.tsx +167 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/popover.tsx +89 -0
- package/src/components/ui/progress.tsx +31 -0
- package/src/components/ui/radio-group.tsx +45 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/sidebar.tsx +726 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +63 -0
- package/src/components/ui/sonner.tsx +36 -0
- package/src/components/ui/switch.tsx +35 -0
- package/src/components/ui/table.tsx +116 -0
- package/src/components/ui/tabs.tsx +91 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +57 -0
- package/src/components/workflows/blueprint-editor.tsx +109 -0
- package/src/components/workflows/blueprint-gallery.tsx +155 -0
- package/src/components/workflows/blueprint-preview.tsx +240 -0
- package/src/components/workflows/loop-status-view.tsx +272 -0
- package/src/components/workflows/swarm-dashboard.tsx +185 -0
- package/src/components/workflows/workflow-form-view.tsx +1376 -0
- package/src/components/workflows/workflow-list.tsx +230 -0
- package/src/components/workflows/workflow-status-view.tsx +477 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/instrumentation.ts +7 -0
- package/src/lib/agents/claude-agent.ts +737 -0
- package/src/lib/agents/execution-manager.ts +27 -0
- package/src/lib/agents/profiles/assignment-validation.ts +75 -0
- package/src/lib/agents/profiles/builtins/code-reviewer/SKILL.md +21 -0
- package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +28 -0
- package/src/lib/agents/profiles/builtins/data-analyst/SKILL.md +25 -0
- package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/devops-engineer/SKILL.md +34 -0
- package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/document-writer/SKILL.md +16 -0
- package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/general/SKILL.md +13 -0
- package/src/lib/agents/profiles/builtins/general/profile.yaml +18 -0
- package/src/lib/agents/profiles/builtins/health-fitness-coach/SKILL.md +34 -0
- package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/learning-coach/SKILL.md +35 -0
- package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/project-manager/SKILL.md +26 -0
- package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/researcher/SKILL.md +15 -0
- package/src/lib/agents/profiles/builtins/researcher/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +34 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/technical-writer/SKILL.md +31 -0
- package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +29 -0
- package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +23 -0
- package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +24 -0
- package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +26 -0
- package/src/lib/agents/profiles/compatibility.ts +109 -0
- package/src/lib/agents/profiles/registry.ts +293 -0
- package/src/lib/agents/profiles/test-runner.ts +18 -0
- package/src/lib/agents/profiles/test-types.ts +20 -0
- package/src/lib/agents/profiles/types.ts +43 -0
- package/src/lib/agents/router.ts +56 -0
- package/src/lib/agents/runtime/catalog.ts +85 -0
- package/src/lib/agents/runtime/claude-sdk.ts +12 -0
- package/src/lib/agents/runtime/claude.ts +370 -0
- package/src/lib/agents/runtime/codex-app-server-client.ts +289 -0
- package/src/lib/agents/runtime/index.ts +167 -0
- package/src/lib/agents/runtime/openai-codex.ts +1089 -0
- package/src/lib/agents/runtime/task-assist-types.ts +8 -0
- package/src/lib/agents/runtime/types.ts +30 -0
- package/src/lib/constants/settings.ts +13 -0
- package/src/lib/constants/status-colors.ts +44 -0
- package/src/lib/constants/task-status.ts +49 -0
- package/src/lib/data/clear.ts +63 -0
- package/src/lib/data/seed-data/documents.ts +715 -0
- package/src/lib/data/seed-data/logs.ts +195 -0
- package/src/lib/data/seed-data/notifications.ts +141 -0
- package/src/lib/data/seed-data/profiles.ts +175 -0
- package/src/lib/data/seed-data/projects.ts +61 -0
- package/src/lib/data/seed-data/schedules.ts +108 -0
- package/src/lib/data/seed-data/tasks.ts +341 -0
- package/src/lib/data/seed-data/usage-ledger.ts +130 -0
- package/src/lib/data/seed-data/workflows.ts +213 -0
- package/src/lib/data/seed.ts +129 -0
- package/src/lib/db/index.ts +221 -0
- package/src/lib/db/migrations/0000_aromatic_gargoyle.sql +59 -0
- package/src/lib/db/migrations/0001_first_iron_patriot.sql +6 -0
- package/src/lib/db/migrations/0002_add_resume_count.sql +1 -0
- package/src/lib/db/migrations/0003_add_settings.sql +5 -0
- package/src/lib/db/migrations/0004_add_documents.sql +20 -0
- package/src/lib/db/migrations/0005_add_document_preprocessing.sql +4 -0
- package/src/lib/db/migrations/0006_add_agent_profile.sql +2 -0
- package/src/lib/db/migrations/0007_add_usage_metering_ledger.sql +30 -0
- package/src/lib/db/migrations/0008_add_document_version.sql +1 -0
- package/src/lib/db/migrations/meta/0000_snapshot.json +416 -0
- package/src/lib/db/migrations/meta/0001_snapshot.json +461 -0
- package/src/lib/db/migrations/meta/0002_snapshot.json +469 -0
- package/src/lib/db/migrations/meta/_journal.json +27 -0
- package/src/lib/db/schema.ts +227 -0
- package/src/lib/documents/cleanup.ts +50 -0
- package/src/lib/documents/context-builder.ts +75 -0
- package/src/lib/documents/output-scanner.ts +166 -0
- package/src/lib/documents/processor.ts +120 -0
- package/src/lib/documents/processors/image.ts +21 -0
- package/src/lib/documents/processors/office.ts +36 -0
- package/src/lib/documents/processors/pdf.ts +12 -0
- package/src/lib/documents/processors/spreadsheet.ts +18 -0
- package/src/lib/documents/processors/text.ts +8 -0
- package/src/lib/documents/registry.ts +25 -0
- package/src/lib/notifications/actionable.ts +108 -0
- package/src/lib/notifications/permissions.ts +169 -0
- package/src/lib/queries/chart-data.ts +184 -0
- package/src/lib/schedules/interval-parser.ts +110 -0
- package/src/lib/schedules/scheduler.ts +220 -0
- package/src/lib/settings/auth.ts +98 -0
- package/src/lib/settings/budget-guardrails.ts +590 -0
- package/src/lib/settings/helpers.ts +23 -0
- package/src/lib/settings/openai-auth.ts +80 -0
- package/src/lib/settings/permissions.ts +102 -0
- package/src/lib/usage/ledger.ts +489 -0
- package/src/lib/usage/pricing.ts +68 -0
- package/src/lib/utils/crypto.ts +90 -0
- package/src/lib/utils/format-timestamp.ts +46 -0
- package/src/lib/utils/session-cleanup.ts +26 -0
- package/src/lib/utils/stagent-paths.ts +18 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/validators/blueprint.ts +43 -0
- package/src/lib/validators/profile.ts +64 -0
- package/src/lib/validators/project.ts +17 -0
- package/src/lib/validators/settings.ts +57 -0
- package/src/lib/validators/task.ts +30 -0
- package/src/lib/workflows/blueprints/builtins/code-review-pipeline.yaml +72 -0
- package/src/lib/workflows/blueprints/builtins/documentation-generation.yaml +62 -0
- package/src/lib/workflows/blueprints/builtins/investment-research.yaml +81 -0
- package/src/lib/workflows/blueprints/builtins/meal-planning.yaml +73 -0
- package/src/lib/workflows/blueprints/builtins/product-research.yaml +72 -0
- package/src/lib/workflows/blueprints/builtins/research-report.yaml +77 -0
- package/src/lib/workflows/blueprints/builtins/sprint-planning.yaml +77 -0
- package/src/lib/workflows/blueprints/builtins/travel-planning.yaml +80 -0
- package/src/lib/workflows/blueprints/instantiator.ts +131 -0
- package/src/lib/workflows/blueprints/registry.ts +128 -0
- package/src/lib/workflows/blueprints/template.ts +58 -0
- package/src/lib/workflows/blueprints/types.ts +38 -0
- package/src/lib/workflows/definition-validation.ts +121 -0
- package/src/lib/workflows/engine.ts +1113 -0
- package/src/lib/workflows/loop-executor.ts +270 -0
- package/src/lib/workflows/parallel.ts +55 -0
- package/src/lib/workflows/swarm.ts +97 -0
- package/src/lib/workflows/types.ts +112 -0
- package/tsconfig.json +41 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useRef } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Input } from "@/components/ui/input";
|
|
7
|
+
import {
|
|
8
|
+
Select,
|
|
9
|
+
SelectContent,
|
|
10
|
+
SelectItem,
|
|
11
|
+
SelectTrigger,
|
|
12
|
+
SelectValue,
|
|
13
|
+
} from "@/components/ui/select";
|
|
14
|
+
import { LayoutGrid, LayoutList, Upload, Trash2, Search } from "lucide-react";
|
|
15
|
+
import { toast } from "sonner";
|
|
16
|
+
import { DocumentTable } from "./document-table";
|
|
17
|
+
import { DocumentGrid } from "./document-grid";
|
|
18
|
+
import { DocumentUploadDialog } from "./document-upload-dialog";
|
|
19
|
+
import type { DocumentWithRelations } from "./types";
|
|
20
|
+
|
|
21
|
+
interface DocumentBrowserProps {
|
|
22
|
+
initialDocuments: DocumentWithRelations[];
|
|
23
|
+
projects: { id: string; name: string }[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function DocumentBrowser({
|
|
27
|
+
initialDocuments,
|
|
28
|
+
projects,
|
|
29
|
+
}: DocumentBrowserProps) {
|
|
30
|
+
const [docs, setDocs] = useState(initialDocuments);
|
|
31
|
+
const [view, setView] = useState<"table" | "grid">("table");
|
|
32
|
+
const [search, setSearch] = useState("");
|
|
33
|
+
const [statusFilter, setStatusFilter] = useState<string>("all");
|
|
34
|
+
const [directionFilter, setDirectionFilter] = useState<string>("all");
|
|
35
|
+
const [projectFilter, setProjectFilter] = useState<string>("all");
|
|
36
|
+
const router = useRouter();
|
|
37
|
+
const [selected, setSelected] = useState<Set<string>>(new Set());
|
|
38
|
+
const [uploadOpen, setUploadOpen] = useState(false);
|
|
39
|
+
const [deleting, setDeleting] = useState(false);
|
|
40
|
+
const uploadButtonRef = useRef<HTMLButtonElement>(null);
|
|
41
|
+
|
|
42
|
+
const refresh = useCallback(async () => {
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch("/api/documents");
|
|
45
|
+
if (res.ok) {
|
|
46
|
+
const data = await res.json();
|
|
47
|
+
setDocs(data);
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// Silent refresh failure
|
|
51
|
+
}
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
const filtered = docs.filter((doc) => {
|
|
55
|
+
if (
|
|
56
|
+
search &&
|
|
57
|
+
!doc.originalName.toLowerCase().includes(search.toLowerCase()) &&
|
|
58
|
+
!(doc.extractedText ?? "").toLowerCase().includes(search.toLowerCase())
|
|
59
|
+
) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (statusFilter !== "all" && doc.status !== statusFilter) return false;
|
|
63
|
+
if (directionFilter !== "all" && doc.direction !== directionFilter) return false;
|
|
64
|
+
if (projectFilter !== "all" && doc.projectId !== projectFilter) return false;
|
|
65
|
+
return true;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function toggleSelect(id: string) {
|
|
69
|
+
setSelected((prev) => {
|
|
70
|
+
const next = new Set(prev);
|
|
71
|
+
if (next.has(id)) next.delete(id);
|
|
72
|
+
else next.add(id);
|
|
73
|
+
return next;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function toggleSelectAll() {
|
|
78
|
+
if (selected.size === filtered.length) {
|
|
79
|
+
setSelected(new Set());
|
|
80
|
+
} else {
|
|
81
|
+
setSelected(new Set(filtered.map((d) => d.id)));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function handleBulkDelete() {
|
|
86
|
+
if (selected.size === 0) return;
|
|
87
|
+
setDeleting(true);
|
|
88
|
+
let deleted = 0;
|
|
89
|
+
for (const id of selected) {
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch(`/api/documents/${id}`, { method: "DELETE" });
|
|
92
|
+
if (res.ok) deleted++;
|
|
93
|
+
} catch {
|
|
94
|
+
// Continue with remaining
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
toast.success(`Deleted ${deleted} document${deleted !== 1 ? "s" : ""}`);
|
|
98
|
+
setSelected(new Set());
|
|
99
|
+
setDeleting(false);
|
|
100
|
+
await refresh();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<>
|
|
105
|
+
<div className="flex items-center justify-between">
|
|
106
|
+
<h1 className="text-2xl font-bold tracking-tight">Documents</h1>
|
|
107
|
+
<Button ref={uploadButtonRef} onClick={() => setUploadOpen(true)} size="sm">
|
|
108
|
+
<Upload className="h-4 w-4 mr-1.5" />
|
|
109
|
+
Upload
|
|
110
|
+
</Button>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
114
|
+
<div className="relative flex-1 min-w-[200px]">
|
|
115
|
+
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
116
|
+
<Input
|
|
117
|
+
placeholder="Search by name or content..."
|
|
118
|
+
value={search}
|
|
119
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
120
|
+
className="pl-9"
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
125
|
+
<SelectTrigger className="w-[140px]">
|
|
126
|
+
<SelectValue placeholder="Status" />
|
|
127
|
+
</SelectTrigger>
|
|
128
|
+
<SelectContent>
|
|
129
|
+
<SelectItem value="all">All statuses</SelectItem>
|
|
130
|
+
<SelectItem value="uploaded">Uploaded</SelectItem>
|
|
131
|
+
<SelectItem value="processing">Processing</SelectItem>
|
|
132
|
+
<SelectItem value="ready">Ready</SelectItem>
|
|
133
|
+
<SelectItem value="error">Error</SelectItem>
|
|
134
|
+
</SelectContent>
|
|
135
|
+
</Select>
|
|
136
|
+
|
|
137
|
+
<Select value={directionFilter} onValueChange={setDirectionFilter}>
|
|
138
|
+
<SelectTrigger className="w-[150px]">
|
|
139
|
+
<SelectValue placeholder="Direction" />
|
|
140
|
+
</SelectTrigger>
|
|
141
|
+
<SelectContent>
|
|
142
|
+
<SelectItem value="all">All directions</SelectItem>
|
|
143
|
+
<SelectItem value="input">Inputs</SelectItem>
|
|
144
|
+
<SelectItem value="output">Outputs</SelectItem>
|
|
145
|
+
</SelectContent>
|
|
146
|
+
</Select>
|
|
147
|
+
|
|
148
|
+
<Select value={projectFilter} onValueChange={setProjectFilter}>
|
|
149
|
+
<SelectTrigger className="w-[160px]">
|
|
150
|
+
<SelectValue placeholder="Project" />
|
|
151
|
+
</SelectTrigger>
|
|
152
|
+
<SelectContent>
|
|
153
|
+
<SelectItem value="all">All projects</SelectItem>
|
|
154
|
+
{projects.map((p) => (
|
|
155
|
+
<SelectItem key={p.id} value={p.id}>
|
|
156
|
+
{p.name}
|
|
157
|
+
</SelectItem>
|
|
158
|
+
))}
|
|
159
|
+
</SelectContent>
|
|
160
|
+
</Select>
|
|
161
|
+
|
|
162
|
+
<div className="flex items-center gap-1 border border-border rounded-md p-0.5">
|
|
163
|
+
<Button
|
|
164
|
+
variant={view === "table" ? "secondary" : "ghost"}
|
|
165
|
+
size="icon"
|
|
166
|
+
className="h-7 w-7"
|
|
167
|
+
onClick={() => setView("table")}
|
|
168
|
+
aria-label="Table view"
|
|
169
|
+
>
|
|
170
|
+
<LayoutList className="h-3.5 w-3.5" />
|
|
171
|
+
</Button>
|
|
172
|
+
<Button
|
|
173
|
+
variant={view === "grid" ? "secondary" : "ghost"}
|
|
174
|
+
size="icon"
|
|
175
|
+
className="h-7 w-7"
|
|
176
|
+
onClick={() => setView("grid")}
|
|
177
|
+
aria-label="Grid view"
|
|
178
|
+
>
|
|
179
|
+
<LayoutGrid className="h-3.5 w-3.5" />
|
|
180
|
+
</Button>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
{selected.size > 0 && (
|
|
184
|
+
<Button
|
|
185
|
+
variant="destructive"
|
|
186
|
+
size="sm"
|
|
187
|
+
onClick={handleBulkDelete}
|
|
188
|
+
disabled={deleting}
|
|
189
|
+
>
|
|
190
|
+
<Trash2 className="h-3.5 w-3.5 mr-1.5" />
|
|
191
|
+
Delete {selected.size}
|
|
192
|
+
</Button>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{filtered.length === 0 ? (
|
|
197
|
+
<div className="text-center py-16 text-muted-foreground">
|
|
198
|
+
{docs.length === 0 ? (
|
|
199
|
+
<>
|
|
200
|
+
<p className="text-lg font-medium mb-1">No documents yet</p>
|
|
201
|
+
<p className="text-sm">Upload files to get started.</p>
|
|
202
|
+
</>
|
|
203
|
+
) : (
|
|
204
|
+
<>
|
|
205
|
+
<p className="text-lg font-medium mb-1">No matching documents</p>
|
|
206
|
+
<p className="text-sm">Try adjusting your filters or search.</p>
|
|
207
|
+
</>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
) : view === "table" ? (
|
|
211
|
+
<DocumentTable
|
|
212
|
+
documents={filtered}
|
|
213
|
+
selected={selected}
|
|
214
|
+
onToggleSelect={toggleSelect}
|
|
215
|
+
onToggleSelectAll={toggleSelectAll}
|
|
216
|
+
onOpen={(doc) => router.push(`/documents/${doc.id}`)}
|
|
217
|
+
/>
|
|
218
|
+
) : (
|
|
219
|
+
<DocumentGrid
|
|
220
|
+
documents={filtered}
|
|
221
|
+
selected={selected}
|
|
222
|
+
onToggleSelect={toggleSelect}
|
|
223
|
+
onOpen={(doc) => router.push(`/documents/${doc.id}`)}
|
|
224
|
+
/>
|
|
225
|
+
)}
|
|
226
|
+
|
|
227
|
+
<DocumentUploadDialog
|
|
228
|
+
open={uploadOpen}
|
|
229
|
+
onClose={() => setUploadOpen(false)}
|
|
230
|
+
onUploaded={refresh}
|
|
231
|
+
restoreFocusElement={uploadButtonRef.current}
|
|
232
|
+
/>
|
|
233
|
+
</>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
6
|
+
import { Badge } from "@/components/ui/badge";
|
|
7
|
+
import { Button } from "@/components/ui/button";
|
|
8
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
9
|
+
import {
|
|
10
|
+
Select,
|
|
11
|
+
SelectContent,
|
|
12
|
+
SelectItem,
|
|
13
|
+
SelectTrigger,
|
|
14
|
+
SelectValue,
|
|
15
|
+
} from "@/components/ui/select";
|
|
16
|
+
import {
|
|
17
|
+
Download,
|
|
18
|
+
Trash2,
|
|
19
|
+
Unlink,
|
|
20
|
+
HardDrive,
|
|
21
|
+
Clock,
|
|
22
|
+
ArrowUpRight,
|
|
23
|
+
ArrowDownLeft,
|
|
24
|
+
Link2,
|
|
25
|
+
FolderKanban,
|
|
26
|
+
FileText,
|
|
27
|
+
AlertTriangle,
|
|
28
|
+
} from "lucide-react";
|
|
29
|
+
import { toast } from "sonner";
|
|
30
|
+
import { DocumentPreview } from "./document-preview";
|
|
31
|
+
import { getFileIcon, formatSize, getStatusColor, formatRelativeTime } from "./utils";
|
|
32
|
+
import type { DocumentWithRelations } from "./types";
|
|
33
|
+
|
|
34
|
+
/** Serialized version of DocumentWithRelations (Date fields become strings from server) */
|
|
35
|
+
type SerializedDocument = Omit<DocumentWithRelations, "createdAt" | "updatedAt"> & {
|
|
36
|
+
createdAt: string | Date;
|
|
37
|
+
updatedAt: string | Date;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
interface DocumentDetailViewProps {
|
|
41
|
+
documentId: string;
|
|
42
|
+
initialDocument?: SerializedDocument;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getStatusDotColor(status: string): string {
|
|
46
|
+
switch (status) {
|
|
47
|
+
case "ready":
|
|
48
|
+
return "bg-status-completed";
|
|
49
|
+
case "processing":
|
|
50
|
+
return "bg-status-running";
|
|
51
|
+
case "error":
|
|
52
|
+
return "bg-status-failed";
|
|
53
|
+
case "uploaded":
|
|
54
|
+
return "bg-status-warning";
|
|
55
|
+
default:
|
|
56
|
+
return "bg-muted-foreground";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function DocumentDetailView({ documentId, initialDocument }: DocumentDetailViewProps) {
|
|
61
|
+
const router = useRouter();
|
|
62
|
+
const [doc, setDoc] = useState<DocumentWithRelations | null>(
|
|
63
|
+
initialDocument ? (initialDocument as unknown as DocumentWithRelations) : null
|
|
64
|
+
);
|
|
65
|
+
const [loaded, setLoaded] = useState(!!initialDocument);
|
|
66
|
+
const [projects, setProjects] = useState<{ id: string; name: string }[]>([]);
|
|
67
|
+
const [deleting, setDeleting] = useState(false);
|
|
68
|
+
const [linking, setLinking] = useState(false);
|
|
69
|
+
|
|
70
|
+
const refresh = useCallback(async () => {
|
|
71
|
+
try {
|
|
72
|
+
const res = await fetch(`/api/documents/${documentId}`);
|
|
73
|
+
if (res.ok) {
|
|
74
|
+
setDoc(await res.json());
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// silent
|
|
78
|
+
}
|
|
79
|
+
setLoaded(true);
|
|
80
|
+
}, [documentId]);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
// If server provided initial data, only fetch supplementary data (projects list)
|
|
84
|
+
// and enrich with relation names in the background
|
|
85
|
+
if (!initialDocument) {
|
|
86
|
+
refresh();
|
|
87
|
+
} else {
|
|
88
|
+
// Background refresh to fill in taskTitle/projectName relation fields
|
|
89
|
+
refresh();
|
|
90
|
+
}
|
|
91
|
+
fetch("/api/projects")
|
|
92
|
+
.then((r) => r.ok ? r.json() : [])
|
|
93
|
+
.then(setProjects)
|
|
94
|
+
.catch(() => {});
|
|
95
|
+
}, [refresh, initialDocument]);
|
|
96
|
+
|
|
97
|
+
async function handleDelete() {
|
|
98
|
+
if (!doc) return;
|
|
99
|
+
setDeleting(true);
|
|
100
|
+
try {
|
|
101
|
+
const res = await fetch(`/api/documents/${doc.id}`, { method: "DELETE" });
|
|
102
|
+
if (res.ok) {
|
|
103
|
+
toast.success("Document deleted");
|
|
104
|
+
router.push("/documents");
|
|
105
|
+
} else {
|
|
106
|
+
toast.error("Failed to delete document");
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
toast.error("Network error");
|
|
110
|
+
} finally {
|
|
111
|
+
setDeleting(false);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function handleLinkProject(projectId: string) {
|
|
116
|
+
if (!doc) return;
|
|
117
|
+
setLinking(true);
|
|
118
|
+
try {
|
|
119
|
+
const value = projectId === "none" ? null : projectId;
|
|
120
|
+
const res = await fetch(`/api/documents/${doc.id}`, {
|
|
121
|
+
method: "PATCH",
|
|
122
|
+
headers: { "Content-Type": "application/json" },
|
|
123
|
+
body: JSON.stringify({ projectId: value }),
|
|
124
|
+
});
|
|
125
|
+
if (res.ok) {
|
|
126
|
+
toast.success(value ? "Linked to project" : "Unlinked from project");
|
|
127
|
+
refresh();
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
toast.error("Failed to update link");
|
|
131
|
+
} finally {
|
|
132
|
+
setLinking(false);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function handleUnlinkTask() {
|
|
137
|
+
if (!doc) return;
|
|
138
|
+
try {
|
|
139
|
+
const res = await fetch(`/api/documents/${doc.id}`, {
|
|
140
|
+
method: "PATCH",
|
|
141
|
+
headers: { "Content-Type": "application/json" },
|
|
142
|
+
body: JSON.stringify({ taskId: null }),
|
|
143
|
+
});
|
|
144
|
+
if (res.ok) {
|
|
145
|
+
toast.success("Unlinked from task");
|
|
146
|
+
refresh();
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
toast.error("Failed to unlink");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!loaded) {
|
|
154
|
+
return (
|
|
155
|
+
<div className="space-y-4">
|
|
156
|
+
<Skeleton className="h-8 w-64" />
|
|
157
|
+
<Skeleton className="h-32 w-full" />
|
|
158
|
+
<Skeleton className="h-48 w-full" />
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!doc) {
|
|
164
|
+
return <p className="text-muted-foreground">Document not found.</p>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const Icon = getFileIcon(doc.mimeType);
|
|
168
|
+
const DirectionIcon = doc.direction === "output" ? ArrowUpRight : ArrowDownLeft;
|
|
169
|
+
const wordCount = doc.extractedText
|
|
170
|
+
? doc.extractedText.split(/\s+/).filter(Boolean).length
|
|
171
|
+
: 0;
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<div className="space-y-6" aria-live="polite">
|
|
175
|
+
{/* Header */}
|
|
176
|
+
<div className="flex items-center justify-between">
|
|
177
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
178
|
+
<Icon className="h-6 w-6 text-muted-foreground shrink-0" />
|
|
179
|
+
<h1 className="text-2xl font-bold truncate">{doc.originalName}</h1>
|
|
180
|
+
</div>
|
|
181
|
+
<div className="flex items-center gap-2">
|
|
182
|
+
<Button variant="outline" size="sm" asChild>
|
|
183
|
+
<a
|
|
184
|
+
href={`/api/documents/${doc.id}/file`}
|
|
185
|
+
target="_blank"
|
|
186
|
+
rel="noopener noreferrer"
|
|
187
|
+
>
|
|
188
|
+
<Download className="h-3.5 w-3.5 mr-1" />
|
|
189
|
+
Download
|
|
190
|
+
</a>
|
|
191
|
+
</Button>
|
|
192
|
+
<Button
|
|
193
|
+
variant="destructive"
|
|
194
|
+
size="sm"
|
|
195
|
+
onClick={handleDelete}
|
|
196
|
+
disabled={deleting}
|
|
197
|
+
>
|
|
198
|
+
<Trash2 className="h-3.5 w-3.5 mr-1" />
|
|
199
|
+
Delete
|
|
200
|
+
</Button>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
{/* Bento Grid: Extracted Text + Metadata + Links */}
|
|
205
|
+
<div className="grid grid-cols-1 md:grid-cols-[2fr_1fr] gap-4">
|
|
206
|
+
{/* Extracted Text — spans 2 rows on desktop */}
|
|
207
|
+
{doc.extractedText ? (
|
|
208
|
+
<Card className="md:row-span-2">
|
|
209
|
+
<CardHeader className="pb-2">
|
|
210
|
+
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
|
211
|
+
<FileText className="h-4 w-4 text-muted-foreground" />
|
|
212
|
+
Extracted Text
|
|
213
|
+
<Badge variant="secondary" className="text-xs ml-auto">
|
|
214
|
+
{wordCount.toLocaleString()} words
|
|
215
|
+
</Badge>
|
|
216
|
+
</CardTitle>
|
|
217
|
+
</CardHeader>
|
|
218
|
+
<CardContent>
|
|
219
|
+
<pre className="text-xs bg-muted p-3 rounded-md max-h-80 overflow-y-auto whitespace-pre-wrap break-words">
|
|
220
|
+
{doc.extractedText.slice(0, 2000)}
|
|
221
|
+
{doc.extractedText.length > 2000 && "\n\n... (truncated)"}
|
|
222
|
+
</pre>
|
|
223
|
+
</CardContent>
|
|
224
|
+
</Card>
|
|
225
|
+
) : (
|
|
226
|
+
<Card className="md:row-span-2 flex items-center justify-center">
|
|
227
|
+
<CardContent className="pt-4 text-center">
|
|
228
|
+
<FileText className="h-8 w-8 text-muted-foreground/40 mx-auto mb-2" />
|
|
229
|
+
<p className="text-sm text-muted-foreground">No extracted text</p>
|
|
230
|
+
</CardContent>
|
|
231
|
+
</Card>
|
|
232
|
+
)}
|
|
233
|
+
|
|
234
|
+
{/* Metadata Strip */}
|
|
235
|
+
<Card>
|
|
236
|
+
<CardHeader className="pb-2">
|
|
237
|
+
<CardTitle className="text-sm font-medium">Details</CardTitle>
|
|
238
|
+
</CardHeader>
|
|
239
|
+
<CardContent className="space-y-3">
|
|
240
|
+
{/* Type */}
|
|
241
|
+
<div className="flex items-center gap-2 text-sm">
|
|
242
|
+
<Icon className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
243
|
+
<Badge variant="outline" className="text-xs font-normal">
|
|
244
|
+
{doc.mimeType}
|
|
245
|
+
</Badge>
|
|
246
|
+
</div>
|
|
247
|
+
{/* Size */}
|
|
248
|
+
<div className="flex items-center gap-2 text-sm">
|
|
249
|
+
<HardDrive className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
250
|
+
<span>{formatSize(doc.size)}</span>
|
|
251
|
+
</div>
|
|
252
|
+
{/* Status */}
|
|
253
|
+
<div className="flex items-center gap-2 text-sm">
|
|
254
|
+
<span className={`w-2 h-2 rounded-full shrink-0 ${getStatusDotColor(doc.status)}`} />
|
|
255
|
+
<Badge variant="outline" className={`text-xs ${getStatusColor(doc.status)}`}>
|
|
256
|
+
{doc.status}
|
|
257
|
+
</Badge>
|
|
258
|
+
</div>
|
|
259
|
+
{/* Direction */}
|
|
260
|
+
<div className="flex items-center gap-2 text-sm">
|
|
261
|
+
<DirectionIcon className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
262
|
+
<span className="capitalize">{doc.direction}</span>
|
|
263
|
+
</div>
|
|
264
|
+
{doc.direction === "output" && (
|
|
265
|
+
<div className="flex items-center gap-2 text-sm">
|
|
266
|
+
<Clock className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
267
|
+
<span>Version {doc.version}</span>
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
{/* Date */}
|
|
271
|
+
<div className="flex items-center gap-2 text-sm">
|
|
272
|
+
<Clock className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
273
|
+
<span title={new Date(doc.createdAt).toLocaleString()}>
|
|
274
|
+
{formatRelativeTime(typeof doc.createdAt === "number" ? doc.createdAt : new Date(doc.createdAt).getTime())}
|
|
275
|
+
</span>
|
|
276
|
+
</div>
|
|
277
|
+
</CardContent>
|
|
278
|
+
</Card>
|
|
279
|
+
|
|
280
|
+
{/* Links */}
|
|
281
|
+
<Card>
|
|
282
|
+
<CardHeader className="pb-2">
|
|
283
|
+
<CardTitle className="text-sm font-medium">Links</CardTitle>
|
|
284
|
+
</CardHeader>
|
|
285
|
+
<CardContent className="space-y-3">
|
|
286
|
+
{/* Task link */}
|
|
287
|
+
<div>
|
|
288
|
+
<div className="flex items-center gap-1.5 text-xs text-muted-foreground mb-1">
|
|
289
|
+
<Link2 className="h-3 w-3" />
|
|
290
|
+
<span>Task</span>
|
|
291
|
+
</div>
|
|
292
|
+
{doc.taskTitle ? (
|
|
293
|
+
<div className="flex items-center justify-between text-sm">
|
|
294
|
+
<span className="truncate">{doc.taskTitle}</span>
|
|
295
|
+
<Button
|
|
296
|
+
variant="ghost"
|
|
297
|
+
size="sm"
|
|
298
|
+
onClick={handleUnlinkTask}
|
|
299
|
+
aria-label="Unlink from task"
|
|
300
|
+
className="h-7 px-2"
|
|
301
|
+
>
|
|
302
|
+
<Unlink className="h-3 w-3" />
|
|
303
|
+
</Button>
|
|
304
|
+
</div>
|
|
305
|
+
) : (
|
|
306
|
+
<p className="text-sm text-muted-foreground">Not linked</p>
|
|
307
|
+
)}
|
|
308
|
+
</div>
|
|
309
|
+
{/* Project selector */}
|
|
310
|
+
<div>
|
|
311
|
+
<div className="flex items-center gap-1.5 text-xs text-muted-foreground mb-1">
|
|
312
|
+
<FolderKanban className="h-3 w-3" />
|
|
313
|
+
<span>Project</span>
|
|
314
|
+
</div>
|
|
315
|
+
<Select
|
|
316
|
+
value={doc.projectId ?? "none"}
|
|
317
|
+
onValueChange={handleLinkProject}
|
|
318
|
+
disabled={linking}
|
|
319
|
+
>
|
|
320
|
+
<SelectTrigger className="w-full h-8 text-sm">
|
|
321
|
+
<SelectValue placeholder="Select project" />
|
|
322
|
+
</SelectTrigger>
|
|
323
|
+
<SelectContent>
|
|
324
|
+
<SelectItem value="none">No project</SelectItem>
|
|
325
|
+
{projects.map((p) => (
|
|
326
|
+
<SelectItem key={p.id} value={p.id}>
|
|
327
|
+
{p.name}
|
|
328
|
+
</SelectItem>
|
|
329
|
+
))}
|
|
330
|
+
</SelectContent>
|
|
331
|
+
</Select>
|
|
332
|
+
</div>
|
|
333
|
+
</CardContent>
|
|
334
|
+
</Card>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
{/* Preview — collapsible, full width */}
|
|
338
|
+
<details className="group">
|
|
339
|
+
<summary className="flex items-center gap-2 cursor-pointer list-none text-sm font-medium p-3 rounded-lg border bg-card hover:bg-accent/50 transition-colors">
|
|
340
|
+
<Icon className="h-4 w-4 text-muted-foreground" />
|
|
341
|
+
<span>Preview</span>
|
|
342
|
+
<span className="text-muted-foreground text-xs group-open:rotate-90 transition-transform">▶</span>
|
|
343
|
+
</summary>
|
|
344
|
+
<div className="mt-2">
|
|
345
|
+
<Card>
|
|
346
|
+
<CardContent className="pt-4">
|
|
347
|
+
<DocumentPreview document={doc} />
|
|
348
|
+
</CardContent>
|
|
349
|
+
</Card>
|
|
350
|
+
</div>
|
|
351
|
+
</details>
|
|
352
|
+
|
|
353
|
+
{/* Processing Error — conditional, red accent */}
|
|
354
|
+
{doc.processingError && (
|
|
355
|
+
<div className="rounded-lg border border-destructive/30 border-l-2 border-l-destructive bg-card p-4">
|
|
356
|
+
<div className="flex items-start gap-2">
|
|
357
|
+
<AlertTriangle className="h-4 w-4 text-destructive shrink-0 mt-0.5" />
|
|
358
|
+
<div>
|
|
359
|
+
<p className="text-sm font-medium text-destructive">Processing Error</p>
|
|
360
|
+
<p className="text-xs text-muted-foreground mt-1">{doc.processingError}</p>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
)}
|
|
365
|
+
</div>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Badge } from "@/components/ui/badge";
|
|
4
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
5
|
+
import { Card } from "@/components/ui/card";
|
|
6
|
+
import { getFileIcon, formatSize, getStatusColor } from "./utils";
|
|
7
|
+
import type { DocumentWithRelations } from "./types";
|
|
8
|
+
|
|
9
|
+
interface DocumentGridProps {
|
|
10
|
+
documents: DocumentWithRelations[];
|
|
11
|
+
selected: Set<string>;
|
|
12
|
+
onToggleSelect: (id: string) => void;
|
|
13
|
+
onOpen: (doc: DocumentWithRelations) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function DocumentGrid({
|
|
17
|
+
documents,
|
|
18
|
+
selected,
|
|
19
|
+
onToggleSelect,
|
|
20
|
+
onOpen,
|
|
21
|
+
}: DocumentGridProps) {
|
|
22
|
+
return (
|
|
23
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
|
|
24
|
+
{documents.map((doc) => {
|
|
25
|
+
const Icon = getFileIcon(doc.mimeType);
|
|
26
|
+
const isImage = doc.mimeType.startsWith("image/");
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Card
|
|
30
|
+
key={doc.id}
|
|
31
|
+
className="group relative p-3 gap-0 cursor-pointer transition-colors"
|
|
32
|
+
onClick={() => onOpen(doc)}
|
|
33
|
+
>
|
|
34
|
+
<div
|
|
35
|
+
className="absolute top-2 left-2 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
36
|
+
onClick={(e) => e.stopPropagation()}
|
|
37
|
+
>
|
|
38
|
+
<Checkbox
|
|
39
|
+
checked={selected.has(doc.id)}
|
|
40
|
+
onCheckedChange={() => onToggleSelect(doc.id)}
|
|
41
|
+
aria-label={`Select ${doc.originalName}`}
|
|
42
|
+
/>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div className="flex flex-col items-center gap-2 py-3">
|
|
46
|
+
{isImage ? (
|
|
47
|
+
<img
|
|
48
|
+
src={`/api/documents/${doc.id}/file?inline=1`}
|
|
49
|
+
alt={doc.originalName}
|
|
50
|
+
className="h-16 w-16 object-cover rounded"
|
|
51
|
+
/>
|
|
52
|
+
) : (
|
|
53
|
+
<Icon className="h-10 w-10 text-muted-foreground" />
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<p className="text-sm font-medium truncate">{doc.originalName}</p>
|
|
58
|
+
<div className="flex items-center justify-between mt-1">
|
|
59
|
+
<span className="text-xs text-muted-foreground">
|
|
60
|
+
{formatSize(doc.size)}
|
|
61
|
+
</span>
|
|
62
|
+
<Badge
|
|
63
|
+
variant="outline"
|
|
64
|
+
className={`text-[10px] px-1 py-0 ${getStatusColor(doc.status)}`}
|
|
65
|
+
>
|
|
66
|
+
{doc.status}
|
|
67
|
+
</Badge>
|
|
68
|
+
</div>
|
|
69
|
+
<div className="mt-1 flex items-center justify-between text-[10px] text-muted-foreground">
|
|
70
|
+
<span className="capitalize">{doc.direction}</span>
|
|
71
|
+
{doc.direction === "output" && <span>v{doc.version}</span>}
|
|
72
|
+
</div>
|
|
73
|
+
</Card>
|
|
74
|
+
);
|
|
75
|
+
})}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|