@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,282 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { useParams } from "next/navigation";
|
|
5
|
+
import { useState, useMemo } from "react";
|
|
6
|
+
import Link from "next/link";
|
|
7
|
+
import { RefreshCw, X, ListFilter, Search } from "lucide-react";
|
|
8
|
+
// Use SDK-bundled types so we're aligned with what `polpo.getMissions()` returns.
|
|
9
|
+
// `@polpo-ai/core` and `@polpo-ai/sdk` types should match but the SDK 0.7.9
|
|
10
|
+
// build ships a degraded copy (e.g. NotificationRule.condition: unknown vs
|
|
11
|
+
// NotificationCondition). Track in OSS issue: SDK should re-export from core.
|
|
12
|
+
import type { Mission } from "@polpo-ai/sdk";
|
|
13
|
+
import { usePolpoClient } from "#/lib/polpo-client";
|
|
14
|
+
import { Button } from "#/components/ui/button";
|
|
15
|
+
import { Badge } from "#/components/ui/badge";
|
|
16
|
+
import { Popover, PopoverContent, PopoverTrigger } from "#/components/ui/popover";
|
|
17
|
+
import {
|
|
18
|
+
Command, CommandInput, CommandList, CommandEmpty,
|
|
19
|
+
CommandGroup, CommandItem,
|
|
20
|
+
} from "#/components/ui/command";
|
|
21
|
+
|
|
22
|
+
const ALL_STATUSES = [
|
|
23
|
+
"draft", "scheduled", "recurring", "active", "paused", "completed", "failed", "cancelled",
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
const statusColor: Record<string, string> = {
|
|
27
|
+
completed: "bg-brand",
|
|
28
|
+
active: "bg-foreground animate-pulse",
|
|
29
|
+
scheduled: "bg-blue-500/50",
|
|
30
|
+
recurring: "bg-blue-500",
|
|
31
|
+
failed: "bg-destructive",
|
|
32
|
+
paused: "bg-yellow-500",
|
|
33
|
+
cancelled: "bg-muted-foreground/30",
|
|
34
|
+
draft: "bg-muted-foreground/20",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const LIVE_STATUSES = new Set(["active", "paused"]);
|
|
38
|
+
|
|
39
|
+
function getMissionType(m: Mission): string {
|
|
40
|
+
if (m.status === "recurring" || (m.schedule && /^[\d*\/,-]+\s/.test(m.schedule))) return "recurring";
|
|
41
|
+
if (m.schedule) return "one-shot";
|
|
42
|
+
return "on-demand";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const typeBadge: Record<string, { label: string; className: string }> = {
|
|
46
|
+
recurring: { label: "Recurring", className: "bg-blue-500/10 text-blue-400" },
|
|
47
|
+
"one-shot": { label: "One-shot", className: "bg-amber-500/10 text-amber-400" },
|
|
48
|
+
"on-demand": { label: "On-demand", className: "bg-muted-foreground/10 text-muted-foreground" },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function timeAgo(date?: string) {
|
|
52
|
+
if (!date) return "\u2014";
|
|
53
|
+
const diff = Date.now() - new Date(date).getTime();
|
|
54
|
+
const min = Math.floor(diff / 60000);
|
|
55
|
+
if (min < 60) return `${min}m ago`;
|
|
56
|
+
const hours = Math.floor(min / 60);
|
|
57
|
+
if (hours < 24) return `${hours}h ago`;
|
|
58
|
+
return `${Math.floor(hours / 24)}d ago`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default function MissionsView({ initialMissions }: { initialMissions: Mission[] }) {
|
|
62
|
+
const { id } = useParams<{ id: string }>();
|
|
63
|
+
const [filterStatus, setFilterStatus] = useState<string | null>(null);
|
|
64
|
+
const [filterType, setFilterType] = useState<string | null>(null);
|
|
65
|
+
const [search, setSearch] = useState("");
|
|
66
|
+
const [statusOpen, setStatusOpen] = useState(false);
|
|
67
|
+
|
|
68
|
+
const polpo = usePolpoClient(id);
|
|
69
|
+
const { data: missions = [], isFetching, refetch } = useQuery({
|
|
70
|
+
queryKey: ["missions", id],
|
|
71
|
+
queryFn: () => polpo.getMissions(),
|
|
72
|
+
initialData: initialMissions,
|
|
73
|
+
refetchInterval: (query) => {
|
|
74
|
+
const rows = query.state.data ?? [];
|
|
75
|
+
return rows.some((mission) => LIVE_STATUSES.has(mission.status)) ? 5000 : false;
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const activeStatuses = useMemo(
|
|
80
|
+
() => [...new Set(missions.map(m => m.status).filter(Boolean))].sort(
|
|
81
|
+
(a, b) => ALL_STATUSES.indexOf(a as typeof ALL_STATUSES[number]) - ALL_STATUSES.indexOf(b as typeof ALL_STATUSES[number])
|
|
82
|
+
),
|
|
83
|
+
[missions]
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const filtered = useMemo(() => {
|
|
87
|
+
let result = missions;
|
|
88
|
+
if (filterStatus) result = result.filter(m => m.status === filterStatus);
|
|
89
|
+
if (filterType) result = result.filter(m => getMissionType(m) === filterType);
|
|
90
|
+
if (search) {
|
|
91
|
+
const q = search.toLowerCase();
|
|
92
|
+
result = result.filter(m =>
|
|
93
|
+
m.name?.toLowerCase().includes(q) || m.prompt?.toLowerCase().includes(q)
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}, [missions, filterStatus, filterType, search]);
|
|
98
|
+
|
|
99
|
+
const hasFilters = filterStatus !== null || filterType !== null || search !== "";
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div>
|
|
103
|
+
{/* Header */}
|
|
104
|
+
<div className="flex items-center justify-between">
|
|
105
|
+
<div>
|
|
106
|
+
<h2 className="text-lg font-semibold tracking-tight">Missions</h2>
|
|
107
|
+
<p className="mt-1 text-xs text-muted-foreground">{missions.length} total</p>
|
|
108
|
+
</div>
|
|
109
|
+
<button
|
|
110
|
+
onClick={() => refetch()}
|
|
111
|
+
disabled={isFetching}
|
|
112
|
+
className="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50"
|
|
113
|
+
>
|
|
114
|
+
<RefreshCw className={`h-3 w-3 ${isFetching ? "animate-spin" : ""}`} />
|
|
115
|
+
Refresh
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{/* Filters */}
|
|
120
|
+
{missions.length > 0 && (
|
|
121
|
+
<div className="mt-3 flex items-center gap-2 flex-wrap">
|
|
122
|
+
{/* Type tabs */}
|
|
123
|
+
<div className="flex items-center gap-1 rounded-lg bg-muted/50 p-1">
|
|
124
|
+
{(["all", "recurring", "one-shot", "on-demand"] as const).map((t) => (
|
|
125
|
+
<button
|
|
126
|
+
key={t}
|
|
127
|
+
onClick={() => setFilterType(t === "all" ? null : t)}
|
|
128
|
+
className={`rounded-md px-3 py-1 text-xs font-medium transition-colors ${
|
|
129
|
+
(t === "all" && !filterType) || filterType === t
|
|
130
|
+
? "bg-background text-foreground shadow-sm"
|
|
131
|
+
: "text-muted-foreground hover:text-foreground"
|
|
132
|
+
}`}
|
|
133
|
+
>
|
|
134
|
+
{t === "all" ? "All" : t === "one-shot" ? "One-shot" : t === "on-demand" ? "On-demand" : "Recurring"}
|
|
135
|
+
</button>
|
|
136
|
+
))}
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{/* Search */}
|
|
140
|
+
<div className="relative">
|
|
141
|
+
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground" />
|
|
142
|
+
<input
|
|
143
|
+
type="text"
|
|
144
|
+
value={search}
|
|
145
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
146
|
+
placeholder="Search missions..."
|
|
147
|
+
className="h-8 w-48 rounded-md border border-border bg-transparent pl-8 pr-3 text-xs focus:border-foreground/30 focus:outline-none transition-colors"
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{/* Status filter */}
|
|
152
|
+
<Popover open={statusOpen} onOpenChange={setStatusOpen}>
|
|
153
|
+
<PopoverTrigger
|
|
154
|
+
render={
|
|
155
|
+
<Button variant="outline" size="sm">
|
|
156
|
+
<ListFilter className="h-3 w-3" />
|
|
157
|
+
{filterStatus ? (
|
|
158
|
+
<>
|
|
159
|
+
Status: <Badge variant="secondary" className="ml-1 font-normal">{filterStatus}</Badge>
|
|
160
|
+
</>
|
|
161
|
+
) : (
|
|
162
|
+
"Status"
|
|
163
|
+
)}
|
|
164
|
+
</Button>
|
|
165
|
+
}
|
|
166
|
+
/>
|
|
167
|
+
<PopoverContent className="w-52 p-0" align="start">
|
|
168
|
+
<Command>
|
|
169
|
+
<CommandInput placeholder="Search status..." />
|
|
170
|
+
<CommandList>
|
|
171
|
+
<CommandEmpty>No status found.</CommandEmpty>
|
|
172
|
+
<CommandGroup>
|
|
173
|
+
{activeStatuses.map(s => (
|
|
174
|
+
<CommandItem
|
|
175
|
+
key={s}
|
|
176
|
+
value={s}
|
|
177
|
+
data-checked={filterStatus === s || undefined}
|
|
178
|
+
onSelect={() => {
|
|
179
|
+
setFilterStatus(filterStatus === s ? null : s);
|
|
180
|
+
setStatusOpen(false);
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
<span className={`h-2 w-2 rounded-full shrink-0 ${statusColor[s]}`} />
|
|
184
|
+
{s}
|
|
185
|
+
</CommandItem>
|
|
186
|
+
))}
|
|
187
|
+
</CommandGroup>
|
|
188
|
+
</CommandList>
|
|
189
|
+
</Command>
|
|
190
|
+
</PopoverContent>
|
|
191
|
+
</Popover>
|
|
192
|
+
|
|
193
|
+
{/* Clear */}
|
|
194
|
+
{hasFilters && (
|
|
195
|
+
<>
|
|
196
|
+
<span className="text-xs text-muted-foreground">
|
|
197
|
+
{filtered.length} of {missions.length}
|
|
198
|
+
</span>
|
|
199
|
+
<Button
|
|
200
|
+
variant="ghost"
|
|
201
|
+
size="xs"
|
|
202
|
+
onClick={() => { setFilterStatus(null); setFilterType(null); setSearch(""); }}
|
|
203
|
+
>
|
|
204
|
+
<X className="h-3 w-3" />
|
|
205
|
+
Clear
|
|
206
|
+
</Button>
|
|
207
|
+
</>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
)}
|
|
211
|
+
|
|
212
|
+
{/* Table */}
|
|
213
|
+
{filtered.length > 0 ? (
|
|
214
|
+
<div className="mt-4 border border-border overflow-hidden overflow-x-auto">
|
|
215
|
+
<table className="w-full text-sm min-w-[700px]">
|
|
216
|
+
<thead>
|
|
217
|
+
<tr className="border-b border-border bg-secondary/50">
|
|
218
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium text-muted-foreground w-24">Status</th>
|
|
219
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium text-muted-foreground">Mission</th>
|
|
220
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium text-muted-foreground w-24">Type</th>
|
|
221
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium text-muted-foreground w-24">Schedule</th>
|
|
222
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium text-muted-foreground w-16">Runs</th>
|
|
223
|
+
<th className="px-4 py-2.5 text-right text-xs font-medium text-muted-foreground w-20">Updated</th>
|
|
224
|
+
</tr>
|
|
225
|
+
</thead>
|
|
226
|
+
<tbody>
|
|
227
|
+
{filtered.map((mission) => {
|
|
228
|
+
const type = getMissionType(mission);
|
|
229
|
+
const badge = typeBadge[type];
|
|
230
|
+
return (
|
|
231
|
+
<tr key={mission.id} className="border-b border-border last:border-0">
|
|
232
|
+
<td colSpan={6} className="p-0">
|
|
233
|
+
<Link
|
|
234
|
+
href={`/projects/${id}/missions/${mission.id}`}
|
|
235
|
+
className="flex items-center hover:bg-secondary/30 transition-colors"
|
|
236
|
+
>
|
|
237
|
+
<span className="px-4 py-3 w-24">
|
|
238
|
+
<span className="inline-flex items-center gap-1.5">
|
|
239
|
+
<span className={`h-2 w-2 rounded-full ${statusColor[mission.status ?? "draft"]}`} />
|
|
240
|
+
<span className="text-xs text-muted-foreground">{mission.status}</span>
|
|
241
|
+
</span>
|
|
242
|
+
</span>
|
|
243
|
+
<span className="px-4 py-3 flex-1">
|
|
244
|
+
<span className="font-mono text-xs font-medium block">{mission.name}</span>
|
|
245
|
+
{mission.prompt && (
|
|
246
|
+
<span className="mt-0.5 text-[11px] text-muted-foreground/60 truncate max-w-xs block">{mission.prompt}</span>
|
|
247
|
+
)}
|
|
248
|
+
</span>
|
|
249
|
+
<span className="px-4 py-3 w-24">
|
|
250
|
+
<span className={`rounded px-1.5 py-0.5 text-[10px] font-medium ${badge.className}`}>
|
|
251
|
+
{badge.label}
|
|
252
|
+
</span>
|
|
253
|
+
</span>
|
|
254
|
+
<span className="px-4 py-3 w-24 font-mono text-xs text-muted-foreground">
|
|
255
|
+
{mission.schedule ?? "\u2014"}
|
|
256
|
+
</span>
|
|
257
|
+
<span className="px-4 py-3 w-16 text-xs text-muted-foreground">
|
|
258
|
+
{mission.executionCount ?? 0}
|
|
259
|
+
</span>
|
|
260
|
+
<span className="px-4 py-3 w-20 text-right text-xs text-muted-foreground">
|
|
261
|
+
{timeAgo(mission.updatedAt)}
|
|
262
|
+
</span>
|
|
263
|
+
</Link>
|
|
264
|
+
</td>
|
|
265
|
+
</tr>
|
|
266
|
+
);
|
|
267
|
+
})}
|
|
268
|
+
</tbody>
|
|
269
|
+
</table>
|
|
270
|
+
</div>
|
|
271
|
+
) : missions.length > 0 ? (
|
|
272
|
+
<div className="mt-4 border border-border p-8 text-center text-sm text-muted-foreground">
|
|
273
|
+
No missions match the current filters.
|
|
274
|
+
</div>
|
|
275
|
+
) : (
|
|
276
|
+
<div className="mt-4 border border-border p-8 text-center text-sm text-muted-foreground">
|
|
277
|
+
No missions yet.
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
280
|
+
</div>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { Check, Copy, MessageSquare, BookMarked, Brain, ListChecks, Rocket } from "lucide-react";
|
|
5
|
+
import { CopyCard } from "#/components/dashboard/copy-card";
|
|
6
|
+
import { SdkSnippetPanel } from "#/components/dashboard/sdk-snippet-panel";
|
|
7
|
+
import { TabToggle } from "#/components/dashboard/tab-toggle";
|
|
8
|
+
|
|
9
|
+
export interface ChecklistStep {
|
|
10
|
+
id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
description: string;
|
|
13
|
+
completed: boolean;
|
|
14
|
+
icon: React.ReactNode;
|
|
15
|
+
detail?: React.ReactNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
steps: ChecklistStep[];
|
|
20
|
+
projectId: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Per-project localStorage key — skip is scoped to this project so a
|
|
24
|
+
// fresh project still shows the checklist for first-time setup.
|
|
25
|
+
const skipKey = (projectId: string) => `polpo:next-steps-skipped:${projectId}`;
|
|
26
|
+
|
|
27
|
+
function PromptBlock({ value }: { value: string }) {
|
|
28
|
+
const [copied, setCopied] = useState(false);
|
|
29
|
+
return (
|
|
30
|
+
<div className="space-y-2">
|
|
31
|
+
<p className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">Ask your coding agent</p>
|
|
32
|
+
<div className="flex flex-col gap-2 border border-border bg-background p-3">
|
|
33
|
+
<span className="inline-flex h-5 w-fit items-center rounded bg-secondary px-2 text-[11px] font-medium text-muted-foreground">
|
|
34
|
+
prompt
|
|
35
|
+
</span>
|
|
36
|
+
<p className="text-sm text-foreground leading-relaxed">{value}</p>
|
|
37
|
+
</div>
|
|
38
|
+
<button
|
|
39
|
+
type="button"
|
|
40
|
+
onClick={async () => {
|
|
41
|
+
await navigator.clipboard.writeText(value);
|
|
42
|
+
setCopied(true);
|
|
43
|
+
setTimeout(() => setCopied(false), 2000);
|
|
44
|
+
}}
|
|
45
|
+
className="flex items-center gap-1.5 border border-border px-3 py-1.5 text-xs font-medium text-foreground hover:bg-secondary transition-colors"
|
|
46
|
+
>
|
|
47
|
+
{copied ? <Check className="h-3.5 w-3.5 text-brand" /> : <Copy className="h-3.5 w-3.5" />}
|
|
48
|
+
{copied ? "Copied" : "Copy prompt"}
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function SkillStepDetail({ endpoint, agent }: { endpoint: string; agent: string }) {
|
|
55
|
+
const [tab, setTab] = useState<"github" | "create">("github");
|
|
56
|
+
|
|
57
|
+
const prompts = {
|
|
58
|
+
github: `Use Polpo skills and install the "frontend-design" skill from anthropics/skills on GitHub for my agent "${agent}". Assign it in agents.json and deploy.`,
|
|
59
|
+
create: `Use Polpo skills and create a custom skill for my agent "${agent}" about REST API best practices: proper HTTP status codes, pagination patterns, error response format, and versioning conventions. Save it in .polpo/skills/, assign it, and deploy.`,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="space-y-4">
|
|
64
|
+
<TabToggle
|
|
65
|
+
value={tab}
|
|
66
|
+
onChange={setTab}
|
|
67
|
+
options={[
|
|
68
|
+
{ value: "github", label: "From GitHub" },
|
|
69
|
+
{ value: "create", label: "Custom skill" },
|
|
70
|
+
]}
|
|
71
|
+
/>
|
|
72
|
+
|
|
73
|
+
<PromptBlock value={prompts[tab]} />
|
|
74
|
+
|
|
75
|
+
<div className="space-y-2">
|
|
76
|
+
<p className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">Or via API</p>
|
|
77
|
+
|
|
78
|
+
{tab === "github" && (
|
|
79
|
+
<div className="space-y-3">
|
|
80
|
+
<p className="text-xs text-muted-foreground">
|
|
81
|
+
Install from any GitHub repository. Compatible with the{" "}
|
|
82
|
+
<a href="https://skills.sh" target="_blank" rel="noopener noreferrer" className="text-foreground hover:underline underline-offset-4">skills.sh</a>{" "}
|
|
83
|
+
format.
|
|
84
|
+
</p>
|
|
85
|
+
<CopyCard
|
|
86
|
+
label="curl"
|
|
87
|
+
lang="curl"
|
|
88
|
+
value={`curl -X POST ${endpoint}/v1/skills/add \\\n -H "Authorization: Bearer $POLPO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d '{"source": "anthropics/skills", "skillNames": ["frontend-design"]}'`}
|
|
89
|
+
/>
|
|
90
|
+
<a
|
|
91
|
+
href="https://skills.sh"
|
|
92
|
+
target="_blank"
|
|
93
|
+
rel="noopener noreferrer"
|
|
94
|
+
className="inline-flex items-center gap-1 text-xs font-medium text-foreground hover:underline underline-offset-4"
|
|
95
|
+
>
|
|
96
|
+
Browse skills on skills.sh →
|
|
97
|
+
</a>
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
{tab === "create" && (
|
|
102
|
+
<div className="space-y-3">
|
|
103
|
+
<CopyCard
|
|
104
|
+
label=".polpo/skills/my-skill/SKILL.md"
|
|
105
|
+
value={`---\nname: my-skill\ndescription: Domain knowledge for my project\n---\n\n# My Skill\n\nAdd your domain knowledge, conventions, and instructions here.`}
|
|
106
|
+
/>
|
|
107
|
+
<CopyCard label="agents.json" value={`"skills": ["my-skill"]`} />
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function deriveChecklistSteps({
|
|
116
|
+
hasSession,
|
|
117
|
+
hasSkill,
|
|
118
|
+
hasMemory,
|
|
119
|
+
hasTask,
|
|
120
|
+
hasDeployed,
|
|
121
|
+
projectSlug,
|
|
122
|
+
projectId,
|
|
123
|
+
apiUrl,
|
|
124
|
+
agentName,
|
|
125
|
+
agentNames,
|
|
126
|
+
agentConfigs,
|
|
127
|
+
}: {
|
|
128
|
+
hasSession: boolean;
|
|
129
|
+
hasSkill: boolean;
|
|
130
|
+
hasMemory: boolean;
|
|
131
|
+
hasTask: boolean;
|
|
132
|
+
hasDeployed: boolean;
|
|
133
|
+
projectSlug?: string;
|
|
134
|
+
projectId?: string;
|
|
135
|
+
apiUrl?: string;
|
|
136
|
+
agentName?: string;
|
|
137
|
+
agentNames?: string[] | null;
|
|
138
|
+
agentConfigs?: import("@polpo-ai/core").AgentConfig[];
|
|
139
|
+
}): ChecklistStep[] {
|
|
140
|
+
const endpoint = projectSlug
|
|
141
|
+
? `https://${projectSlug}.polpo.cloud`
|
|
142
|
+
: "https://{slug}.polpo.cloud";
|
|
143
|
+
const agent = agentName ?? "assistant";
|
|
144
|
+
|
|
145
|
+
return [
|
|
146
|
+
{
|
|
147
|
+
id: "chat",
|
|
148
|
+
title: "Chat with your agent",
|
|
149
|
+
description: "Send your first message",
|
|
150
|
+
completed: hasSession,
|
|
151
|
+
icon: <MessageSquare className="h-4 w-4" />,
|
|
152
|
+
detail: (
|
|
153
|
+
<SdkSnippetPanel
|
|
154
|
+
baseUrl={endpoint}
|
|
155
|
+
agents={agentNames ?? null}
|
|
156
|
+
agentConfigs={agentConfigs}
|
|
157
|
+
defaultAgent={agent}
|
|
158
|
+
projectId={projectId}
|
|
159
|
+
apiUrl={apiUrl}
|
|
160
|
+
/>
|
|
161
|
+
),
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: "skill",
|
|
165
|
+
title: "Add skill to your agent",
|
|
166
|
+
description: "Teach your agent domain knowledge",
|
|
167
|
+
completed: hasSkill,
|
|
168
|
+
icon: <BookMarked className="h-4 w-4" />,
|
|
169
|
+
detail: <SkillStepDetail endpoint={endpoint} agent={agent} />,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: "memory",
|
|
173
|
+
title: "Set agent memory",
|
|
174
|
+
description: "Persistent context across sessions",
|
|
175
|
+
completed: hasMemory,
|
|
176
|
+
icon: <Brain className="h-4 w-4" />,
|
|
177
|
+
detail: (
|
|
178
|
+
<div className="space-y-4">
|
|
179
|
+
<PromptBlock value={`Save a memory for my agent "${agent}": remember that I prefer concise code examples and that my stack is Next.js + TypeScript. Then ask the agent what it remembers about me.`} />
|
|
180
|
+
<div className="space-y-2">
|
|
181
|
+
<p className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">Or via API</p>
|
|
182
|
+
<CopyCard
|
|
183
|
+
label="curl"
|
|
184
|
+
lang="curl"
|
|
185
|
+
value={`curl -X PUT ${endpoint}/v1/memory/agent/${encodeURIComponent(agent)} \\\n -H "Authorization: Bearer $POLPO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d '{"content": "I prefer concise code examples. My stack is Next.js + TypeScript."}'`}
|
|
186
|
+
/>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
),
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
id: "task",
|
|
193
|
+
title: "Create a task",
|
|
194
|
+
description: "Assign autonomous work to an agent",
|
|
195
|
+
completed: hasTask,
|
|
196
|
+
icon: <ListChecks className="h-4 w-4" />,
|
|
197
|
+
detail: (
|
|
198
|
+
<div className="space-y-4">
|
|
199
|
+
<PromptBlock value={`Use Polpo skills and create a task for my agent "${agent}" to write a hello.txt file with a greeting message. The agent should complete this autonomously.`} />
|
|
200
|
+
<div className="space-y-2">
|
|
201
|
+
<p className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">Or via API</p>
|
|
202
|
+
<CopyCard
|
|
203
|
+
label="curl"
|
|
204
|
+
lang="curl"
|
|
205
|
+
value={`curl -X POST ${endpoint}/v1/tasks \\\n -H "Authorization: Bearer $POLPO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d '{\n "title": "Create hello.txt",\n "description": "Write a hello.txt file with a greeting message",\n "assignTo": "${agent}"\n }'`}
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
),
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: "deploy",
|
|
213
|
+
title: "Deploy from your codebase",
|
|
214
|
+
description: "Push local config to cloud",
|
|
215
|
+
completed: hasDeployed,
|
|
216
|
+
icon: <Rocket className="h-4 w-4" />,
|
|
217
|
+
detail: (
|
|
218
|
+
<div className="space-y-4">
|
|
219
|
+
<PromptBlock value={`Use Polpo skills and deploy my project to the cloud. Run polpo deploy from the terminal.`} />
|
|
220
|
+
<div className="space-y-2">
|
|
221
|
+
<p className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">Or via CLI</p>
|
|
222
|
+
<CopyCard label="terminal" value="polpo deploy" />
|
|
223
|
+
<p className="text-[11px] text-muted-foreground/60">
|
|
224
|
+
Syncs agents, teams, skills, memory, missions, and playbooks.
|
|
225
|
+
</p>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
),
|
|
229
|
+
},
|
|
230
|
+
];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Detect deploy: any agent with updatedAt different from createdAt means a PUT happened (deploy pushes updates) */
|
|
234
|
+
export function hasDeployedCheck(agents: Array<{ createdAt?: string; updatedAt?: string }>): boolean {
|
|
235
|
+
for (const agent of agents) {
|
|
236
|
+
if (agent.updatedAt && agent.createdAt && agent.updatedAt !== agent.createdAt) return true;
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
export default function OnboardingChecklist({ steps, projectId }: Props) {
|
|
243
|
+
const firstIncomplete = steps.find((s) => !s.completed);
|
|
244
|
+
const [selectedId, setSelectedId] = useState<string>(firstIncomplete?.id ?? steps[0]?.id ?? "");
|
|
245
|
+
const [dismissed, setDismissed] = useState(false);
|
|
246
|
+
|
|
247
|
+
// Honour the persistent skip on mount. Render null while the
|
|
248
|
+
// effect resolves to avoid a flash before the localStorage check.
|
|
249
|
+
const [hydrated, setHydrated] = useState(false);
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
if (typeof window !== "undefined" && localStorage.getItem(skipKey(projectId))) {
|
|
252
|
+
setDismissed(true);
|
|
253
|
+
}
|
|
254
|
+
setHydrated(true);
|
|
255
|
+
}, [projectId]);
|
|
256
|
+
|
|
257
|
+
function handleSkipForever() {
|
|
258
|
+
if (typeof window !== "undefined") {
|
|
259
|
+
localStorage.setItem(skipKey(projectId), "1");
|
|
260
|
+
}
|
|
261
|
+
setDismissed(true);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const completedCount = steps.filter((s) => s.completed).length;
|
|
265
|
+
const selected = steps.find((s) => s.id === selectedId);
|
|
266
|
+
|
|
267
|
+
if (!hydrated || dismissed) return null;
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<div className="border border-border bg-card">
|
|
271
|
+
{/* Banner header — inside the card, same feel as Get Started */}
|
|
272
|
+
<div className="flex items-center justify-between p-6 pb-4">
|
|
273
|
+
<div>
|
|
274
|
+
<h2 className="text-2xl tracking-tight text-foreground">Next Steps</h2>
|
|
275
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
276
|
+
Complete these steps to get the most out of Polpo.
|
|
277
|
+
</p>
|
|
278
|
+
</div>
|
|
279
|
+
<div className="flex items-center gap-3">
|
|
280
|
+
<span className="text-[11px] font-mono text-muted-foreground">
|
|
281
|
+
{completedCount}/{steps.length}
|
|
282
|
+
</span>
|
|
283
|
+
<div className="w-20 h-1 bg-secondary overflow-hidden">
|
|
284
|
+
<div
|
|
285
|
+
className="h-full bg-brand transition-all duration-500"
|
|
286
|
+
style={{ width: `${(completedCount / steps.length) * 100}%` }}
|
|
287
|
+
/>
|
|
288
|
+
</div>
|
|
289
|
+
<button
|
|
290
|
+
type="button"
|
|
291
|
+
onClick={handleSkipForever}
|
|
292
|
+
data-testid="next-steps-skip-forever"
|
|
293
|
+
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
294
|
+
>
|
|
295
|
+
Skip
|
|
296
|
+
</button>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
{/* Two-column: steps left, detail right */}
|
|
301
|
+
<div className="flex">
|
|
302
|
+
{/* Left: step list */}
|
|
303
|
+
<div className="w-[280px] shrink-0">
|
|
304
|
+
{steps.map((step) => {
|
|
305
|
+
const isSelected = step.id === selectedId;
|
|
306
|
+
return (
|
|
307
|
+
<button
|
|
308
|
+
key={step.id}
|
|
309
|
+
onClick={() => setSelectedId(step.id)}
|
|
310
|
+
className={`w-full flex items-center gap-3 px-6 py-4 text-left border-b border-border last:border-0 transition-colors ${
|
|
311
|
+
isSelected
|
|
312
|
+
? "bg-secondary/50"
|
|
313
|
+
: "hover:bg-secondary/20"
|
|
314
|
+
} ${step.completed ? "opacity-50" : ""}`}
|
|
315
|
+
>
|
|
316
|
+
<div className="flex-shrink-0">
|
|
317
|
+
{step.completed ? (
|
|
318
|
+
<div className="flex h-5 w-5 items-center justify-center bg-brand/15">
|
|
319
|
+
<Check className="h-3 w-3 text-brand" />
|
|
320
|
+
</div>
|
|
321
|
+
) : (
|
|
322
|
+
<div className={`h-2 w-2 rounded-full ${
|
|
323
|
+
isSelected ? "bg-foreground" : "bg-muted-foreground/30"
|
|
324
|
+
}`} />
|
|
325
|
+
)}
|
|
326
|
+
</div>
|
|
327
|
+
<div className="min-w-0">
|
|
328
|
+
<p className={`text-sm ${
|
|
329
|
+
step.completed
|
|
330
|
+
? "text-muted-foreground line-through"
|
|
331
|
+
: isSelected
|
|
332
|
+
? "text-foreground font-medium"
|
|
333
|
+
: "text-muted-foreground"
|
|
334
|
+
}`}>
|
|
335
|
+
{step.title}
|
|
336
|
+
</p>
|
|
337
|
+
<p className="text-[11px] text-muted-foreground/60 mt-0.5">
|
|
338
|
+
{step.description}
|
|
339
|
+
</p>
|
|
340
|
+
</div>
|
|
341
|
+
</button>
|
|
342
|
+
);
|
|
343
|
+
})}
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
{/* Right: detail panel — always shares bg with selected step for continuity */}
|
|
347
|
+
<div className={`flex-1 p-6 transition-colors ${
|
|
348
|
+
selected ? "bg-secondary/50" : ""
|
|
349
|
+
}`}>
|
|
350
|
+
{selected?.detail ?? (
|
|
351
|
+
<p className="text-sm text-muted-foreground">
|
|
352
|
+
Select a step to see instructions.
|
|
353
|
+
</p>
|
|
354
|
+
)}
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
);
|
|
359
|
+
}
|