@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.
- package/LICENSE +13 -0
- package/README.md +44 -0
- package/app/(dashboard)/error.tsx +47 -0
- package/app/(dashboard)/layout.tsx +41 -0
- package/app/(dashboard)/loading.tsx +30 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/memory/view.tsx +36 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/models-view.tsx +30 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/page.tsx +51 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/prompt/view.tsx +211 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/skills/view.tsx +84 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/tools/view.tsx +525 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/vault/view.tsx +61 -0
- package/app/(dashboard)/projects/[id]/agents/page.tsx +39 -0
- package/app/(dashboard)/projects/[id]/agents/view.tsx +283 -0
- package/app/(dashboard)/projects/[id]/layout.tsx +11 -0
- package/app/(dashboard)/projects/[id]/logs/page.tsx +27 -0
- package/app/(dashboard)/projects/[id]/logs/view.tsx +184 -0
- package/app/(dashboard)/projects/[id]/memory/page.tsx +40 -0
- package/app/(dashboard)/projects/[id]/memory/view.tsx +17 -0
- package/app/(dashboard)/projects/[id]/missions/[missionId]/page.tsx +88 -0
- package/app/(dashboard)/projects/[id]/missions/[missionId]/view.tsx +569 -0
- package/app/(dashboard)/projects/[id]/missions/page.tsx +32 -0
- package/app/(dashboard)/projects/[id]/missions/view.tsx +282 -0
- package/app/(dashboard)/projects/[id]/onboarding-checklist.tsx +359 -0
- package/app/(dashboard)/projects/[id]/page.tsx +68 -0
- package/app/(dashboard)/projects/[id]/playbooks/[name]/page.tsx +5 -0
- package/app/(dashboard)/projects/[id]/playbooks/[name]/view.tsx +433 -0
- package/app/(dashboard)/projects/[id]/playbooks/page.tsx +5 -0
- package/app/(dashboard)/projects/[id]/playbooks/view.tsx +222 -0
- package/app/(dashboard)/projects/[id]/schedules/page.tsx +44 -0
- package/app/(dashboard)/projects/[id]/schedules/view.tsx +385 -0
- package/app/(dashboard)/projects/[id]/sessions/[sessionId]/page.tsx +55 -0
- package/app/(dashboard)/projects/[id]/sessions/[sessionId]/raw-view.tsx +373 -0
- package/app/(dashboard)/projects/[id]/sessions/[sessionId]/view.tsx +124 -0
- package/app/(dashboard)/projects/[id]/sessions/page.tsx +31 -0
- package/app/(dashboard)/projects/[id]/sessions/view.tsx +242 -0
- package/app/(dashboard)/projects/[id]/settings/page.tsx +52 -0
- package/app/(dashboard)/projects/[id]/skills/[name]/page.tsx +65 -0
- package/app/(dashboard)/projects/[id]/skills/[name]/view.tsx +227 -0
- package/app/(dashboard)/projects/[id]/skills/page.tsx +31 -0
- package/app/(dashboard)/projects/[id]/skills/view.tsx +243 -0
- package/app/(dashboard)/projects/[id]/storage/page.tsx +5 -0
- package/app/(dashboard)/projects/[id]/storage/view.tsx +385 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/activity/view.tsx +336 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/assessment/view.tsx +113 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/blueprint.ts +72 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/output/view.tsx +90 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/page.tsx +63 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/view.tsx +137 -0
- package/app/(dashboard)/projects/[id]/tasks/page.tsx +40 -0
- package/app/(dashboard)/projects/[id]/tasks/view.tsx +421 -0
- package/app/(dashboard)/projects/[id]/view.tsx +421 -0
- package/app/(dashboard)/projects/[id]/welcome-banner.tsx +156 -0
- package/app/(dashboard)/projects/projects-list.tsx +126 -0
- package/app/(playground)/error.tsx +42 -0
- package/app/(playground)/layout.tsx +27 -0
- package/app/(playground)/projects/[id]/playground/page.tsx +57 -0
- package/app/(playground)/projects/[id]/playground/view.tsx +167 -0
- package/app/(playground)/projects/[id]/playground-legacy/page.tsx +47 -0
- package/app/(playground)/projects/[id]/playground-legacy/skeleton.tsx +63 -0
- package/app/(playground)/projects/[id]/playground-legacy/view.tsx +237 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +385 -0
- package/app/icon.svg +6 -0
- package/app/layout.tsx +39 -0
- package/app/page.tsx +11 -0
- package/components/ai-elements/code-block.tsx +562 -0
- package/components/dashboard/agent-capabilities.tsx +326 -0
- package/components/dashboard/agent-identity-header.tsx +50 -0
- package/components/dashboard/agent-model-picker.tsx +195 -0
- package/components/dashboard/agent-model-selector.tsx +121 -0
- package/components/dashboard/agent-picker-card.tsx +46 -0
- package/components/dashboard/agent-studio.tsx +242 -0
- package/components/dashboard/agents-table.tsx +48 -0
- package/components/dashboard/breadcrumb.tsx +70 -0
- package/components/dashboard/builder-chat.tsx +418 -0
- package/components/dashboard/chat-shell.tsx +40 -0
- package/components/dashboard/client-picker.tsx +63 -0
- package/components/dashboard/command-snippet.tsx +77 -0
- package/components/dashboard/connect-dialog.tsx +680 -0
- package/components/dashboard/copy-button.tsx +45 -0
- package/components/dashboard/copy-card.tsx +48 -0
- package/components/dashboard/file-browser.tsx +161 -0
- package/components/dashboard/hint.tsx +22 -0
- package/components/dashboard/inference-mode.tsx +15 -0
- package/components/dashboard/manual-refresh-button.tsx +52 -0
- package/components/dashboard/markdown.tsx +31 -0
- package/components/dashboard/mcp-install-panel.tsx +196 -0
- package/components/dashboard/mission-graph.tsx +262 -0
- package/components/dashboard/nav-tabs.tsx +86 -0
- package/components/dashboard/polpo-chat.tsx +468 -0
- package/components/dashboard/project-copilot.tsx +253 -0
- package/components/dashboard/sdk-snippet-panel.tsx +194 -0
- package/components/dashboard/section-header.tsx +37 -0
- package/components/dashboard/settings-form.tsx +2477 -0
- package/components/dashboard/sidebar.tsx +385 -0
- package/components/dashboard/skeletons.tsx +1054 -0
- package/components/dashboard/skills-install-wizard.tsx +273 -0
- package/components/dashboard/swarm-runs.tsx +316 -0
- package/components/dashboard/tab-toggle.tsx +35 -0
- package/components/dashboard/task-studio.tsx +224 -0
- package/components/dashboard/top-header.tsx +203 -0
- package/components/dashboard/webhook-deliveries.tsx +234 -0
- package/components/icons/coding-agents.tsx +151 -0
- package/components/json-ld.tsx +8 -0
- package/components/providers.tsx +25 -0
- package/components/ui/badge.tsx +52 -0
- package/components/ui/button.tsx +60 -0
- package/components/ui/card.tsx +103 -0
- package/components/ui/chart.tsx +356 -0
- package/components/ui/command.tsx +196 -0
- package/components/ui/dialog.tsx +157 -0
- package/components/ui/input-group.tsx +158 -0
- package/components/ui/input.tsx +20 -0
- package/components/ui/multi-select.tsx +161 -0
- package/components/ui/popover.tsx +90 -0
- package/components/ui/select.tsx +201 -0
- package/components/ui/separator.tsx +25 -0
- package/components/ui/sheet.tsx +135 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/tabs.tsx +56 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/hooks/use-desktop-sidebar.tsx +50 -0
- package/hooks/use-mobile-sidebar.tsx +37 -0
- package/index.ts +14 -0
- package/lib/api.ts +194 -0
- package/lib/builder-context.ts +60 -0
- package/lib/data-client.ts +68 -0
- package/lib/get-query-client.ts +25 -0
- package/lib/polpo-client.tsx +49 -0
- package/lib/tool-catalog.ts +234 -0
- package/lib/use-event-catalog.ts +28 -0
- package/lib/utils.ts +6 -0
- 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,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
|
+
}
|