@polpo-ai/dashboard 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.
Files changed (135) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +44 -0
  3. package/app/(dashboard)/error.tsx +47 -0
  4. package/app/(dashboard)/layout.tsx +41 -0
  5. package/app/(dashboard)/loading.tsx +30 -0
  6. package/app/(dashboard)/projects/[id]/agents/[name]/memory/view.tsx +36 -0
  7. package/app/(dashboard)/projects/[id]/agents/[name]/models-view.tsx +30 -0
  8. package/app/(dashboard)/projects/[id]/agents/[name]/page.tsx +51 -0
  9. package/app/(dashboard)/projects/[id]/agents/[name]/prompt/view.tsx +211 -0
  10. package/app/(dashboard)/projects/[id]/agents/[name]/skills/view.tsx +84 -0
  11. package/app/(dashboard)/projects/[id]/agents/[name]/tools/view.tsx +525 -0
  12. package/app/(dashboard)/projects/[id]/agents/[name]/vault/view.tsx +61 -0
  13. package/app/(dashboard)/projects/[id]/agents/page.tsx +39 -0
  14. package/app/(dashboard)/projects/[id]/agents/view.tsx +283 -0
  15. package/app/(dashboard)/projects/[id]/layout.tsx +11 -0
  16. package/app/(dashboard)/projects/[id]/logs/page.tsx +27 -0
  17. package/app/(dashboard)/projects/[id]/logs/view.tsx +184 -0
  18. package/app/(dashboard)/projects/[id]/memory/page.tsx +40 -0
  19. package/app/(dashboard)/projects/[id]/memory/view.tsx +17 -0
  20. package/app/(dashboard)/projects/[id]/missions/[missionId]/page.tsx +88 -0
  21. package/app/(dashboard)/projects/[id]/missions/[missionId]/view.tsx +569 -0
  22. package/app/(dashboard)/projects/[id]/missions/page.tsx +32 -0
  23. package/app/(dashboard)/projects/[id]/missions/view.tsx +282 -0
  24. package/app/(dashboard)/projects/[id]/onboarding-checklist.tsx +359 -0
  25. package/app/(dashboard)/projects/[id]/page.tsx +68 -0
  26. package/app/(dashboard)/projects/[id]/playbooks/[name]/page.tsx +5 -0
  27. package/app/(dashboard)/projects/[id]/playbooks/[name]/view.tsx +433 -0
  28. package/app/(dashboard)/projects/[id]/playbooks/page.tsx +5 -0
  29. package/app/(dashboard)/projects/[id]/playbooks/view.tsx +222 -0
  30. package/app/(dashboard)/projects/[id]/schedules/page.tsx +44 -0
  31. package/app/(dashboard)/projects/[id]/schedules/view.tsx +385 -0
  32. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/page.tsx +55 -0
  33. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/raw-view.tsx +373 -0
  34. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/view.tsx +124 -0
  35. package/app/(dashboard)/projects/[id]/sessions/page.tsx +31 -0
  36. package/app/(dashboard)/projects/[id]/sessions/view.tsx +242 -0
  37. package/app/(dashboard)/projects/[id]/settings/page.tsx +52 -0
  38. package/app/(dashboard)/projects/[id]/skills/[name]/page.tsx +65 -0
  39. package/app/(dashboard)/projects/[id]/skills/[name]/view.tsx +227 -0
  40. package/app/(dashboard)/projects/[id]/skills/page.tsx +31 -0
  41. package/app/(dashboard)/projects/[id]/skills/view.tsx +243 -0
  42. package/app/(dashboard)/projects/[id]/storage/page.tsx +5 -0
  43. package/app/(dashboard)/projects/[id]/storage/view.tsx +385 -0
  44. package/app/(dashboard)/projects/[id]/tasks/[taskId]/activity/view.tsx +336 -0
  45. package/app/(dashboard)/projects/[id]/tasks/[taskId]/assessment/view.tsx +113 -0
  46. package/app/(dashboard)/projects/[id]/tasks/[taskId]/blueprint.ts +72 -0
  47. package/app/(dashboard)/projects/[id]/tasks/[taskId]/output/view.tsx +90 -0
  48. package/app/(dashboard)/projects/[id]/tasks/[taskId]/page.tsx +63 -0
  49. package/app/(dashboard)/projects/[id]/tasks/[taskId]/view.tsx +137 -0
  50. package/app/(dashboard)/projects/[id]/tasks/page.tsx +40 -0
  51. package/app/(dashboard)/projects/[id]/tasks/view.tsx +421 -0
  52. package/app/(dashboard)/projects/[id]/view.tsx +421 -0
  53. package/app/(dashboard)/projects/[id]/welcome-banner.tsx +156 -0
  54. package/app/(dashboard)/projects/projects-list.tsx +126 -0
  55. package/app/(playground)/error.tsx +42 -0
  56. package/app/(playground)/layout.tsx +27 -0
  57. package/app/(playground)/projects/[id]/playground/page.tsx +57 -0
  58. package/app/(playground)/projects/[id]/playground/view.tsx +167 -0
  59. package/app/(playground)/projects/[id]/playground-legacy/page.tsx +47 -0
  60. package/app/(playground)/projects/[id]/playground-legacy/skeleton.tsx +63 -0
  61. package/app/(playground)/projects/[id]/playground-legacy/view.tsx +237 -0
  62. package/app/favicon.ico +0 -0
  63. package/app/globals.css +385 -0
  64. package/app/icon.svg +6 -0
  65. package/app/layout.tsx +39 -0
  66. package/app/page.tsx +11 -0
  67. package/components/ai-elements/code-block.tsx +562 -0
  68. package/components/dashboard/agent-capabilities.tsx +326 -0
  69. package/components/dashboard/agent-identity-header.tsx +50 -0
  70. package/components/dashboard/agent-model-picker.tsx +195 -0
  71. package/components/dashboard/agent-model-selector.tsx +121 -0
  72. package/components/dashboard/agent-picker-card.tsx +46 -0
  73. package/components/dashboard/agent-studio.tsx +242 -0
  74. package/components/dashboard/agents-table.tsx +48 -0
  75. package/components/dashboard/breadcrumb.tsx +70 -0
  76. package/components/dashboard/builder-chat.tsx +418 -0
  77. package/components/dashboard/chat-shell.tsx +40 -0
  78. package/components/dashboard/client-picker.tsx +63 -0
  79. package/components/dashboard/command-snippet.tsx +77 -0
  80. package/components/dashboard/connect-dialog.tsx +680 -0
  81. package/components/dashboard/copy-button.tsx +45 -0
  82. package/components/dashboard/copy-card.tsx +48 -0
  83. package/components/dashboard/file-browser.tsx +161 -0
  84. package/components/dashboard/hint.tsx +22 -0
  85. package/components/dashboard/inference-mode.tsx +15 -0
  86. package/components/dashboard/manual-refresh-button.tsx +52 -0
  87. package/components/dashboard/markdown.tsx +31 -0
  88. package/components/dashboard/mcp-install-panel.tsx +196 -0
  89. package/components/dashboard/mission-graph.tsx +262 -0
  90. package/components/dashboard/nav-tabs.tsx +86 -0
  91. package/components/dashboard/polpo-chat.tsx +468 -0
  92. package/components/dashboard/project-copilot.tsx +253 -0
  93. package/components/dashboard/sdk-snippet-panel.tsx +194 -0
  94. package/components/dashboard/section-header.tsx +37 -0
  95. package/components/dashboard/settings-form.tsx +2477 -0
  96. package/components/dashboard/sidebar.tsx +385 -0
  97. package/components/dashboard/skeletons.tsx +1054 -0
  98. package/components/dashboard/skills-install-wizard.tsx +273 -0
  99. package/components/dashboard/swarm-runs.tsx +316 -0
  100. package/components/dashboard/tab-toggle.tsx +35 -0
  101. package/components/dashboard/task-studio.tsx +224 -0
  102. package/components/dashboard/top-header.tsx +203 -0
  103. package/components/dashboard/webhook-deliveries.tsx +234 -0
  104. package/components/icons/coding-agents.tsx +151 -0
  105. package/components/json-ld.tsx +8 -0
  106. package/components/providers.tsx +25 -0
  107. package/components/ui/badge.tsx +52 -0
  108. package/components/ui/button.tsx +60 -0
  109. package/components/ui/card.tsx +103 -0
  110. package/components/ui/chart.tsx +356 -0
  111. package/components/ui/command.tsx +196 -0
  112. package/components/ui/dialog.tsx +157 -0
  113. package/components/ui/input-group.tsx +158 -0
  114. package/components/ui/input.tsx +20 -0
  115. package/components/ui/multi-select.tsx +161 -0
  116. package/components/ui/popover.tsx +90 -0
  117. package/components/ui/select.tsx +201 -0
  118. package/components/ui/separator.tsx +25 -0
  119. package/components/ui/sheet.tsx +135 -0
  120. package/components/ui/skeleton.tsx +13 -0
  121. package/components/ui/tabs.tsx +56 -0
  122. package/components/ui/textarea.tsx +18 -0
  123. package/components/ui/tooltip.tsx +66 -0
  124. package/hooks/use-desktop-sidebar.tsx +50 -0
  125. package/hooks/use-mobile-sidebar.tsx +37 -0
  126. package/index.ts +14 -0
  127. package/lib/api.ts +194 -0
  128. package/lib/builder-context.ts +60 -0
  129. package/lib/data-client.ts +68 -0
  130. package/lib/get-query-client.ts +25 -0
  131. package/lib/polpo-client.tsx +49 -0
  132. package/lib/tool-catalog.ts +234 -0
  133. package/lib/use-event-catalog.ts +28 -0
  134. package/lib/utils.ts +6 -0
  135. package/package.json +99 -0
@@ -0,0 +1,68 @@
1
+ import { Suspense } from "react";
2
+ import { api, dataApi } from "#/lib/api";
3
+ import type { Project } from "#/lib/api";
4
+ import type { AgentConfig, Task, Mission } from "@polpo-ai/core";
5
+ import { ProjectOverviewSkeleton } from "#/components/dashboard/skeletons";
6
+ import ProjectOverviewView from "./view";
7
+ import WelcomeBanner from "./welcome-banner";
8
+
9
+ async function OverviewData({ id }: { id: string }) {
10
+ let agents: AgentConfig[] = [];
11
+ let tasks: Task[] = [];
12
+ let missions: Mission[] = [];
13
+ let project: Project | null = null;
14
+
15
+ try {
16
+ const [agentsRes, tasksRes, missionsRes, projectRes] = await Promise.all([
17
+ dataApi<{ ok: boolean; data: AgentConfig[] }>(id, "/v1/agents").catch(
18
+ () => ({ ok: false, data: [] as AgentConfig[] }),
19
+ ),
20
+ dataApi<{ ok: boolean; data: Task[] }>(id, "/v1/tasks").catch(() => ({
21
+ ok: false,
22
+ data: [] as Task[],
23
+ })),
24
+ dataApi<{ ok: boolean; data: Mission[] }>(id, "/v1/missions").catch(
25
+ () => ({ ok: false, data: [] as Mission[] }),
26
+ ),
27
+ api<Project>(`/v1/projects/${id}`).catch(() => null),
28
+ ]);
29
+ agents = agentsRes.data ?? [];
30
+ tasks = tasksRes.data ?? [];
31
+ missions = missionsRes.data ?? [];
32
+ project = projectRes;
33
+ } catch {}
34
+
35
+ return (
36
+ <ProjectOverviewView
37
+ initialAgents={agents}
38
+ initialTasks={tasks}
39
+ initialMissions={missions}
40
+ projectName={project?.name ?? "Your project"}
41
+ projectSlug={project?.slug}
42
+ initialChecklist={project?.onboardingChecklist ?? {}}
43
+ welcomeBanner={
44
+ <WelcomeBanner
45
+ key={`welcome-${id}`}
46
+ projectId={id}
47
+ projectName={project?.name ?? "Your project"}
48
+ projectSlug={project?.slug}
49
+ agentName={agents[0]?.name ?? null}
50
+ />
51
+ }
52
+ />
53
+ );
54
+ }
55
+
56
+ export default async function ProjectOverviewPage({
57
+ params,
58
+ }: {
59
+ params: Promise<{ id: string }>;
60
+ }) {
61
+ const { id } = await params;
62
+
63
+ return (
64
+ <Suspense fallback={<ProjectOverviewSkeleton />}>
65
+ <OverviewData id={id} />
66
+ </Suspense>
67
+ );
68
+ }
@@ -0,0 +1,5 @@
1
+ import PlaybookDetailView from "./view";
2
+
3
+ export default function PlaybookDetailPage() {
4
+ return <PlaybookDetailView />;
5
+ }
@@ -0,0 +1,433 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
5
+ import { useParams, useRouter } from "next/navigation";
6
+ import Link from "next/link";
7
+ import { ArrowLeft, Play, Loader2, AlertTriangle, Workflow, Braces, Copy, Check, GitBranch, List } from "lucide-react";
8
+ import type { Task } from "@polpo-ai/core";
9
+ import { usePolpoClient } from "#/lib/polpo-client";
10
+ import { MissionGraph } from "#/components/dashboard/mission-graph";
11
+ import { ManualRefreshButton } from "#/components/dashboard/manual-refresh-button";
12
+ import {
13
+ Dialog,
14
+ DialogContent,
15
+ DialogHeader,
16
+ DialogTitle,
17
+ DialogDescription,
18
+ DialogFooter,
19
+ DialogClose,
20
+ } from "#/components/ui/dialog";
21
+
22
+ interface PlaybookParam {
23
+ name: string;
24
+ type?: string;
25
+ description?: string;
26
+ required?: boolean;
27
+ default?: unknown;
28
+ enum?: unknown[];
29
+ }
30
+
31
+ interface PlaybookTask {
32
+ title: string;
33
+ assignTo: string;
34
+ description?: string;
35
+ dependsOn?: string[];
36
+ }
37
+
38
+ interface Playbook {
39
+ name: string;
40
+ description: string;
41
+ mission: {
42
+ prompt?: string;
43
+ tasks: PlaybookTask[];
44
+ };
45
+ parameters?: PlaybookParam[];
46
+ version?: string;
47
+ author?: string;
48
+ tags?: string[];
49
+ }
50
+
51
+ function highlightJson(json: string): string {
52
+ return json.replace(
53
+ /("(?:\\.|[^"\\])*")\s*(:)?|(\b(?:true|false|null)\b)|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g,
54
+ (match, str, colon, bool, num) => {
55
+ if (str) {
56
+ if (colon) return `<span style="color:oklch(0.75 0.1 250)">${str}</span>:`;
57
+ return `<span style="color:oklch(0.75 0.15 155)">${str}</span>`;
58
+ }
59
+ if (bool) return `<span style="color:oklch(0.7 0.15 30)">${bool}</span>`;
60
+ if (num) return `<span style="color:oklch(0.8 0.12 80)">${num}</span>`;
61
+ return match;
62
+ },
63
+ );
64
+ }
65
+
66
+ export default function PlaybookDetailView() {
67
+ const { id, name } = useParams<{ id: string; name: string }>();
68
+ const decodedName = decodeURIComponent(name);
69
+ const router = useRouter();
70
+ const queryClient = useQueryClient();
71
+
72
+ const [view, setView] = useState<"graph" | "tasks" | "json">("graph");
73
+ const [showRun, setShowRun] = useState(false);
74
+ const [showDelete, setShowDelete] = useState(false);
75
+ const [paramValues, setParamValues] = useState<Record<string, string>>({});
76
+ const [copied, setCopied] = useState(false);
77
+
78
+ const polpo = usePolpoClient(id);
79
+ const { data: playbook = null, isLoading, isFetching, refetch } = useQuery({
80
+ queryKey: ["playbook", id, decodedName],
81
+ queryFn: () => polpo.getPlaybook(decodedName) as unknown as Promise<Playbook | null>,
82
+ });
83
+
84
+ const runMutation = useMutation({
85
+ mutationFn: (params: Record<string, string | number | boolean>) =>
86
+ polpo.runPlaybook(decodedName, params),
87
+ onSuccess: (data) => {
88
+ queryClient.invalidateQueries({ queryKey: ["missions", id] });
89
+ setShowRun(false);
90
+ if (data?.mission?.id) {
91
+ router.push(`/projects/${id}/missions/${data.mission.id}`);
92
+ }
93
+ },
94
+ });
95
+
96
+ const deleteMutation = useMutation({
97
+ mutationFn: () => polpo.deletePlaybook(decodedName),
98
+ onSuccess: () => {
99
+ router.push(`/projects/${id}/playbooks`);
100
+ },
101
+ });
102
+
103
+ function handleRun() {
104
+ if (!playbook) return;
105
+ const params: Record<string, string | number | boolean> = {};
106
+ for (const p of playbook.parameters ?? []) {
107
+ const raw = paramValues[p.name];
108
+ if (!raw && !p.required) continue;
109
+ if (p.type === "number") params[p.name] = Number(raw);
110
+ else if (p.type === "boolean") params[p.name] = raw === "true";
111
+ else params[p.name] = raw;
112
+ }
113
+ runMutation.mutate(params);
114
+ }
115
+
116
+ const missingRequired = playbook
117
+ ? (playbook.parameters ?? []).filter(p => p.required && !paramValues[p.name]?.trim())
118
+ : [];
119
+
120
+ if (isLoading) {
121
+ return (
122
+ <div className="space-y-3">
123
+ {[1, 2, 3].map(i => <div key={i} className="h-16 rounded-lg bg-secondary/30 animate-pulse" />)}
124
+ </div>
125
+ );
126
+ }
127
+
128
+ if (!playbook) {
129
+ return (
130
+ <div className="mt-4 border border-border p-8 text-center text-sm text-muted-foreground">
131
+ Playbook not found.
132
+ </div>
133
+ );
134
+ }
135
+
136
+ const params = playbook.parameters ?? [];
137
+ const tasks = playbook.mission?.tasks ?? [];
138
+
139
+ // Build fake Task objects for the graph
140
+ const fakeTasks = tasks.map((t, i) => ({
141
+ id: `pb-${i}`,
142
+ title: t.title,
143
+ assignTo: t.assignTo,
144
+ description: t.description ?? "",
145
+ status: "draft" as const,
146
+ dependsOn: (t.dependsOn ?? []).map(dep => {
147
+ const depIdx = tasks.findIndex(tt => tt.title === dep);
148
+ return depIdx >= 0 ? `pb-${depIdx}` : dep;
149
+ }),
150
+ retries: 0,
151
+ maxRetries: 3,
152
+ expectations: [],
153
+ metrics: [],
154
+ createdAt: "",
155
+ updatedAt: "",
156
+ }));
157
+
158
+ return (
159
+ <div>
160
+ {/* Back */}
161
+ <Link
162
+ href={`/projects/${id}/playbooks`}
163
+ className="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
164
+ >
165
+ <ArrowLeft className="h-3 w-3" />
166
+ Playbooks
167
+ </Link>
168
+
169
+ {/* Header */}
170
+ <div className="mt-4 flex items-start justify-between gap-4">
171
+ <div>
172
+ <div className="flex items-center gap-2.5">
173
+ <Workflow className="h-4 w-4 text-muted-foreground" />
174
+ <h2 className="font-mono text-lg font-extrabold tracking-tight">{playbook.name}</h2>
175
+ {playbook.version && (
176
+ <span className="rounded bg-secondary px-1.5 py-0.5 text-[10px] font-mono text-muted-foreground">
177
+ v{playbook.version}
178
+ </span>
179
+ )}
180
+ </div>
181
+ <p className="mt-1 text-sm text-muted-foreground">{playbook.description}</p>
182
+
183
+ {/* Meta */}
184
+ <div className="mt-3 flex items-center gap-3 text-xs text-muted-foreground">
185
+ <span>{tasks.length} task{tasks.length !== 1 ? "s" : ""}</span>
186
+ <span>{params.length} param{params.length !== 1 ? "s" : ""}</span>
187
+ {playbook.author && <span>by {playbook.author}</span>}
188
+ </div>
189
+
190
+ {/* Tags */}
191
+ {playbook.tags && playbook.tags.length > 0 && (
192
+ <div className="mt-2 flex gap-1">
193
+ {playbook.tags.map(t => (
194
+ <span key={t} className="rounded bg-foreground/5 px-1.5 py-0.5 text-[9px] text-muted-foreground">
195
+ {t}
196
+ </span>
197
+ ))}
198
+ </div>
199
+ )}
200
+ </div>
201
+ <ManualRefreshButton onRefresh={() => refetch()} isRefreshing={isFetching} className="mt-1 shrink-0" />
202
+ </div>
203
+
204
+ {/* Parameters */}
205
+ {params.length > 0 && (
206
+ <section className="mt-6">
207
+ <h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-3">Parameters</h3>
208
+ <div className="border border-border overflow-hidden">
209
+ <table className="w-full text-xs">
210
+ <thead>
211
+ <tr className="border-b border-border bg-muted/40 text-muted-foreground">
212
+ <th className="px-4 py-2 text-left font-medium">Name</th>
213
+ <th className="px-4 py-2 text-left font-medium w-20">Type</th>
214
+ <th className="px-4 py-2 text-left font-medium w-16">Required</th>
215
+ <th className="px-4 py-2 text-left font-medium w-24">Default</th>
216
+ <th className="px-4 py-2 text-left font-medium hidden sm:table-cell">Description</th>
217
+ </tr>
218
+ </thead>
219
+ <tbody className="font-mono">
220
+ {params.map(p => (
221
+ <tr key={p.name} className="border-b border-border last:border-0">
222
+ <td className="px-4 py-2.5 font-medium">
223
+ {p.name}
224
+ {p.enum && (
225
+ <span className="text-muted-foreground/40 font-normal ml-1">
226
+ [{p.enum.join(", ")}]
227
+ </span>
228
+ )}
229
+ </td>
230
+ <td className="px-4 py-2.5 text-muted-foreground">{p.type ?? "string"}</td>
231
+ <td className="px-4 py-2.5">
232
+ {p.required ? <span className="text-red-400">yes</span> : <span className="text-muted-foreground/30">no</span>}
233
+ </td>
234
+ <td className="px-4 py-2.5 text-muted-foreground/60">
235
+ {p.default !== undefined ? String(p.default) : "\u2014"}
236
+ </td>
237
+ <td className="px-4 py-2.5 text-muted-foreground font-sans hidden sm:table-cell">{p.description ?? "\u2014"}</td>
238
+ </tr>
239
+ ))}
240
+ </tbody>
241
+ </table>
242
+ </div>
243
+ </section>
244
+ )}
245
+
246
+ {/* Task view */}
247
+ <section className="mt-6">
248
+ <div className="flex items-center justify-between mb-3">
249
+ <h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Mission template</h3>
250
+ <div className="flex items-center gap-1 rounded-lg bg-muted/50 p-1">
251
+ {([
252
+ { key: "graph" as const, icon: GitBranch, label: "Graph" },
253
+ { key: "tasks" as const, icon: List, label: "Tasks" },
254
+ { key: "json" as const, icon: Braces, label: "JSON" },
255
+ ]).map(tab => (
256
+ <button
257
+ key={tab.key}
258
+ onClick={() => setView(tab.key)}
259
+ className={`inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors ${
260
+ view === tab.key ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
261
+ }`}
262
+ >
263
+ <tab.icon className="h-3.5 w-3.5" />
264
+ {tab.label}
265
+ </button>
266
+ ))}
267
+ </div>
268
+ </div>
269
+
270
+ {view === "graph" ? (
271
+ <MissionGraph tasks={fakeTasks as unknown as Task[]} projectId={id} readonly />
272
+ ) : view === "json" ? (
273
+ <div className="border border-border bg-card overflow-hidden">
274
+ <div className="flex items-center justify-between px-4 py-2 border-b border-border bg-muted/40">
275
+ <span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">Playbook JSON</span>
276
+ <button
277
+ onClick={() => {
278
+ navigator.clipboard.writeText(JSON.stringify(playbook, null, 2));
279
+ setCopied(true);
280
+ setTimeout(() => setCopied(false), 2000);
281
+ }}
282
+ className="inline-flex items-center gap-1.5 text-[10px] text-muted-foreground hover:text-foreground transition-colors"
283
+ >
284
+ {copied ? <Check className="h-3 w-3 text-emerald-500" /> : <Copy className="h-3 w-3" />}
285
+ {copied ? "Copied" : "Copy"}
286
+ </button>
287
+ </div>
288
+ <pre
289
+ className="p-4 text-xs font-mono overflow-y-auto max-h-[560px] whitespace-pre-wrap break-all"
290
+ dangerouslySetInnerHTML={{ __html: highlightJson(JSON.stringify(playbook, null, 2)) }}
291
+ />
292
+ </div>
293
+ ) : (
294
+ <div className="border border-border overflow-hidden">
295
+ <table className="w-full text-xs">
296
+ <thead>
297
+ <tr className="border-b border-border bg-muted/40 text-muted-foreground">
298
+ <th className="px-4 py-2.5 text-left font-medium w-8">#</th>
299
+ <th className="px-4 py-2.5 text-left font-medium">Task</th>
300
+ <th className="px-4 py-2.5 text-left font-medium w-24">Agent</th>
301
+ <th className="px-4 py-2.5 text-left font-medium hidden sm:table-cell">Dependencies</th>
302
+ </tr>
303
+ </thead>
304
+ <tbody className="font-mono">
305
+ {tasks.map((task, i) => (
306
+ <tr key={i} className="border-b border-border last:border-0">
307
+ <td className="px-4 py-3 text-muted-foreground/40">{i + 1}</td>
308
+ <td className="px-4 py-3">
309
+ <span className="font-medium">{task.title}</span>
310
+ {task.description && (
311
+ <p className="text-[10px] text-muted-foreground mt-0.5 font-sans">{task.description}</p>
312
+ )}
313
+ </td>
314
+ <td className="px-4 py-3 text-muted-foreground">{task.assignTo}</td>
315
+ <td className="px-4 py-3 text-muted-foreground/50 hidden sm:table-cell">
316
+ {task.dependsOn?.join(", ") ?? "\u2014"}
317
+ </td>
318
+ </tr>
319
+ ))}
320
+ </tbody>
321
+ </table>
322
+ </div>
323
+ )}
324
+ </section>
325
+
326
+ {/* Run dialog */}
327
+ <Dialog open={showRun} onOpenChange={setShowRun}>
328
+ <DialogContent showCloseButton={false}>
329
+ <DialogHeader>
330
+ <div className="flex items-center gap-2">
331
+ <Play className="h-4 w-4" />
332
+ <DialogTitle>Run {playbook.name}</DialogTitle>
333
+ </div>
334
+ <DialogDescription>{playbook.description}</DialogDescription>
335
+ </DialogHeader>
336
+
337
+ {params.length === 0 ? (
338
+ <p className="text-xs text-muted-foreground py-2">This playbook takes no parameters.</p>
339
+ ) : (
340
+ <div className="space-y-3 py-2">
341
+ {params.map(p => (
342
+ <div key={p.name}>
343
+ <label className="flex items-center gap-1 text-[10px] uppercase tracking-wider text-muted-foreground font-medium mb-1">
344
+ {p.name}
345
+ {p.required && <span className="text-red-400">*</span>}
346
+ <span className="text-muted-foreground/40 normal-case tracking-normal ml-1">{p.type ?? "string"}</span>
347
+ </label>
348
+ {p.type === "boolean" ? (
349
+ <select
350
+ value={paramValues[p.name] ?? ""}
351
+ onChange={(e) => setParamValues(prev => ({ ...prev, [p.name]: e.target.value }))}
352
+ className="w-full h-8 rounded-md border border-border bg-background px-3 text-xs font-mono focus:border-foreground/30 focus:outline-none"
353
+ >
354
+ <option value="">Select...</option>
355
+ <option value="true">true</option>
356
+ <option value="false">false</option>
357
+ </select>
358
+ ) : p.enum && p.enum.length > 0 ? (
359
+ <select
360
+ value={paramValues[p.name] ?? ""}
361
+ onChange={(e) => setParamValues(prev => ({ ...prev, [p.name]: e.target.value }))}
362
+ className="w-full h-8 rounded-md border border-border bg-background px-3 text-xs font-mono focus:border-foreground/30 focus:outline-none"
363
+ >
364
+ <option value="">Select...</option>
365
+ {p.enum.map(v => (
366
+ <option key={String(v)} value={String(v)}>{String(v)}</option>
367
+ ))}
368
+ </select>
369
+ ) : (
370
+ <input
371
+ type={p.type === "number" ? "number" : "text"}
372
+ value={paramValues[p.name] ?? ""}
373
+ onChange={(e) => setParamValues(prev => ({ ...prev, [p.name]: e.target.value }))}
374
+ placeholder={p.default !== undefined ? `Default: ${p.default}` : p.description ?? p.name}
375
+ className="w-full h-8 rounded-md border border-border bg-transparent px-3 text-xs font-mono placeholder:text-muted-foreground/30 focus:border-foreground/30 focus:outline-none"
376
+ />
377
+ )}
378
+ {p.description && <p className="text-[10px] text-muted-foreground mt-0.5">{p.description}</p>}
379
+ </div>
380
+ ))}
381
+ </div>
382
+ )}
383
+
384
+ <DialogFooter>
385
+ {missingRequired.length > 0 && (
386
+ <span className="text-[10px] text-red-400 mr-auto">
387
+ {missingRequired.length} required param{missingRequired.length > 1 ? "s" : ""} missing
388
+ </span>
389
+ )}
390
+ <DialogClose render={<button className="px-4 py-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors" />}>
391
+ Cancel
392
+ </DialogClose>
393
+ <button
394
+ onClick={handleRun}
395
+ disabled={missingRequired.length > 0 || runMutation.isPending}
396
+ className="inline-flex items-center gap-2 bg-foreground text-background px-4 py-1.5 text-xs font-medium transition-all hover:opacity-90 disabled:opacity-30 rounded-md"
397
+ >
398
+ {runMutation.isPending ? <Loader2 className="h-3 w-3 animate-spin" /> : <Play className="h-3 w-3" />}
399
+ Run
400
+ </button>
401
+ </DialogFooter>
402
+ </DialogContent>
403
+ </Dialog>
404
+
405
+ {/* Delete dialog */}
406
+ <Dialog open={showDelete} onOpenChange={setShowDelete}>
407
+ <DialogContent showCloseButton={false}>
408
+ <DialogHeader>
409
+ <div className="flex items-center gap-2">
410
+ <AlertTriangle className="h-4 w-4 text-destructive" />
411
+ <DialogTitle>Delete playbook</DialogTitle>
412
+ </div>
413
+ <DialogDescription>
414
+ Are you sure you want to delete <span className="font-mono font-medium text-foreground">{playbook.name}</span>? This cannot be undone.
415
+ </DialogDescription>
416
+ </DialogHeader>
417
+ <DialogFooter>
418
+ <DialogClose render={<button className="px-4 py-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors" />}>
419
+ Cancel
420
+ </DialogClose>
421
+ <button
422
+ onClick={() => deleteMutation.mutate()}
423
+ disabled={deleteMutation.isPending}
424
+ className="inline-flex items-center gap-2 bg-destructive text-destructive-foreground px-4 py-1.5 text-xs font-medium transition-all hover:opacity-90 disabled:opacity-50 rounded-md"
425
+ >
426
+ {deleteMutation.isPending ? <Loader2 className="h-3 w-3 animate-spin" /> : "Delete"}
427
+ </button>
428
+ </DialogFooter>
429
+ </DialogContent>
430
+ </Dialog>
431
+ </div>
432
+ );
433
+ }
@@ -0,0 +1,5 @@
1
+ import PlaybooksView from "./view";
2
+
3
+ export default function PlaybooksPage() {
4
+ return <PlaybooksView />;
5
+ }