@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,421 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
4
|
+
import { useQuery } from "@tanstack/react-query";
|
|
5
|
+
import { useParams } from "next/navigation";
|
|
6
|
+
import Link from "next/link";
|
|
7
|
+
import type { AgentConfig, Task, Mission } from "@polpo-ai/core";
|
|
8
|
+
import { Users, ListChecks, Target, MessageSquare } from "lucide-react";
|
|
9
|
+
import { usePolpoClient } from "#/lib/polpo-client";
|
|
10
|
+
import { ManualRefreshButton } from "#/components/dashboard/manual-refresh-button";
|
|
11
|
+
import OnboardingChecklist, { deriveChecklistSteps, hasDeployedCheck } from "./onboarding-checklist";
|
|
12
|
+
import {
|
|
13
|
+
SwarmStatusBar,
|
|
14
|
+
SwarmRunList,
|
|
15
|
+
type RunRow,
|
|
16
|
+
type RunStatus,
|
|
17
|
+
type RunCounts,
|
|
18
|
+
} from "#/components/dashboard/swarm-runs";
|
|
19
|
+
|
|
20
|
+
interface SessionInfo {
|
|
21
|
+
id: string;
|
|
22
|
+
title?: string;
|
|
23
|
+
agent?: string;
|
|
24
|
+
createdAt: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Polpo Task.status → our RunStatus. The swarm taxonomy is broader
|
|
28
|
+
// (scheduled / cancelled exist as well) but Polpo doesn't surface those
|
|
29
|
+
// on a Task today — only on Missions / runs. "draft" + "assigned" both
|
|
30
|
+
// roll up to "pending" because users perceive them as "not yet started".
|
|
31
|
+
function toRunStatus(status: string): RunStatus {
|
|
32
|
+
switch (status) {
|
|
33
|
+
case "in_progress": return "running";
|
|
34
|
+
case "review": return "review";
|
|
35
|
+
case "done": return "done";
|
|
36
|
+
case "failed": return "failed";
|
|
37
|
+
case "cancelled": return "cancelled";
|
|
38
|
+
case "draft":
|
|
39
|
+
case "assigned":
|
|
40
|
+
case "pending":
|
|
41
|
+
default: return "pending";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildSwarmCounts(tasks: Array<{ status: string }>): RunCounts {
|
|
46
|
+
const c: RunCounts = {
|
|
47
|
+
scheduled: 0, pending: 0, running: 0, review: 0,
|
|
48
|
+
done: 0, failed: 0, cancelled: 0, total: tasks.length,
|
|
49
|
+
};
|
|
50
|
+
for (const t of tasks) c[toRunStatus(t.status)]++;
|
|
51
|
+
return c;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
export default function ProjectOverviewView({
|
|
56
|
+
initialAgents,
|
|
57
|
+
initialTasks,
|
|
58
|
+
initialMissions,
|
|
59
|
+
projectName,
|
|
60
|
+
projectSlug,
|
|
61
|
+
initialChecklist,
|
|
62
|
+
welcomeBanner,
|
|
63
|
+
}: {
|
|
64
|
+
initialAgents: AgentConfig[];
|
|
65
|
+
initialTasks: Task[];
|
|
66
|
+
initialMissions: Mission[];
|
|
67
|
+
projectName: string;
|
|
68
|
+
projectSlug?: string;
|
|
69
|
+
initialChecklist?: import("#/lib/api").OnboardingChecklist;
|
|
70
|
+
welcomeBanner?: React.ReactNode;
|
|
71
|
+
}) {
|
|
72
|
+
const { id } = useParams<{ id: string }>();
|
|
73
|
+
|
|
74
|
+
// Polling interval for checklist auto-completion (15s)
|
|
75
|
+
const CHECKLIST_POLL = 15_000;
|
|
76
|
+
|
|
77
|
+
const polpo = usePolpoClient(id);
|
|
78
|
+
const { data: agents = [], isFetching: agentsFetching, refetch: refetchAgents } = useQuery({
|
|
79
|
+
queryKey: ["agents", id],
|
|
80
|
+
queryFn: () => polpo.getAgents() as unknown as Promise<AgentConfig[]>,
|
|
81
|
+
initialData: initialAgents,
|
|
82
|
+
refetchInterval: CHECKLIST_POLL,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const { data: tasks = [], isFetching: tasksFetching, refetch: refetchTasks } = useQuery({
|
|
86
|
+
queryKey: ["tasks", id],
|
|
87
|
+
queryFn: () => polpo.getTasks() as unknown as Promise<Task[]>,
|
|
88
|
+
initialData: initialTasks,
|
|
89
|
+
refetchInterval: CHECKLIST_POLL,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const { data: missions = [], isFetching: missionsFetching, refetch: refetchMissions } = useQuery({
|
|
93
|
+
queryKey: ["missions", id],
|
|
94
|
+
queryFn: () => polpo.getMissions() as unknown as Promise<Mission[]>,
|
|
95
|
+
initialData: initialMissions,
|
|
96
|
+
refetchInterval: CHECKLIST_POLL,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const { data: sessions = [], isFetching: sessionsFetching, refetch: refetchSessions } = useQuery({
|
|
100
|
+
queryKey: ["sessions", id],
|
|
101
|
+
queryFn: async () => {
|
|
102
|
+
const r = await polpo.getSessions();
|
|
103
|
+
return (r.sessions ?? []) as unknown as SessionInfo[];
|
|
104
|
+
},
|
|
105
|
+
refetchInterval: CHECKLIST_POLL,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const { data: skillsList = [], isFetching: skillsFetching, refetch: refetchSkills } = useQuery({
|
|
109
|
+
queryKey: ["skills", id],
|
|
110
|
+
queryFn: () => polpo.getSkills() as unknown as Promise<Array<{ name: string }>>,
|
|
111
|
+
refetchInterval: CHECKLIST_POLL,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Has any memory? Either shared or on any agent. A single truthy
|
|
115
|
+
// signals the step done.
|
|
116
|
+
const { data: hasMemory = false, isFetching: memoryFetching, refetch: refetchMemory } = useQuery({
|
|
117
|
+
queryKey: ["memory-any", id, agents.map((a) => a.name).join(",")],
|
|
118
|
+
queryFn: async () => {
|
|
119
|
+
const shared = await polpo.getMemory().then((r) => r.exists === true).catch(() => false);
|
|
120
|
+
if (shared) return true;
|
|
121
|
+
for (const a of agents) {
|
|
122
|
+
const agentMem = await polpo
|
|
123
|
+
.getAgentMemory(a.name)
|
|
124
|
+
.then((r) => r.exists === true)
|
|
125
|
+
.catch(() => false);
|
|
126
|
+
if (agentMem) return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
},
|
|
130
|
+
enabled: agents.length > 0,
|
|
131
|
+
refetchInterval: CHECKLIST_POLL,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Persist onboarding checklist flags to control plane (idempotent).
|
|
135
|
+
// initialChecklist comes from the SSR-fetched project row. We track
|
|
136
|
+
// which flags we've already PATCHed in this session to avoid repeat
|
|
137
|
+
// calls (the PATCH is idempotent anyway, but there's no point).
|
|
138
|
+
const persistedFlags = useRef<Set<string>>(
|
|
139
|
+
new Set(
|
|
140
|
+
Object.entries(initialChecklist ?? {})
|
|
141
|
+
.filter(([, v]) => typeof v === "string")
|
|
142
|
+
.map(([k]) => k),
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const derived = useMemo(() => ({
|
|
147
|
+
firstCompletion: sessions.length > 0,
|
|
148
|
+
firstSkill: skillsList.length > 0,
|
|
149
|
+
firstMemory: hasMemory,
|
|
150
|
+
firstTask: tasks.length > 0,
|
|
151
|
+
firstDeploy: hasDeployedCheck(agents),
|
|
152
|
+
}), [sessions, skillsList, hasMemory, agents, tasks]);
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
const toSend: Record<string, boolean> = {};
|
|
156
|
+
for (const [key, value] of Object.entries(derived)) {
|
|
157
|
+
if (value && !persistedFlags.current.has(key)) {
|
|
158
|
+
toSend[key] = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (Object.keys(toSend).length === 0) return;
|
|
162
|
+
|
|
163
|
+
const ctrl = new AbortController();
|
|
164
|
+
fetch(`${process.env.NEXT_PUBLIC_API_URL}/v1/projects/${id}/onboarding-checklist`, {
|
|
165
|
+
method: "PATCH",
|
|
166
|
+
credentials: "include",
|
|
167
|
+
headers: { "Content-Type": "application/json" },
|
|
168
|
+
body: JSON.stringify(toSend),
|
|
169
|
+
signal: ctrl.signal,
|
|
170
|
+
})
|
|
171
|
+
.then(() => {
|
|
172
|
+
for (const k of Object.keys(toSend)) persistedFlags.current.add(k);
|
|
173
|
+
})
|
|
174
|
+
.catch(() => { /* best-effort */ });
|
|
175
|
+
|
|
176
|
+
return () => ctrl.abort();
|
|
177
|
+
}, [derived, id]);
|
|
178
|
+
|
|
179
|
+
// Single-pass derivation of all stats — avoids 13+ filter passes on every render
|
|
180
|
+
const stats = useMemo(() => {
|
|
181
|
+
let taskDone = 0, taskInProgress = 0, taskPending = 0, taskFailed = 0, taskReview = 0;
|
|
182
|
+
const busyAgentNames = new Set<string>();
|
|
183
|
+
|
|
184
|
+
for (const t of tasks) {
|
|
185
|
+
const agent = t.assignTo ?? "";
|
|
186
|
+
|
|
187
|
+
switch (t.status) {
|
|
188
|
+
case "done": taskDone++; break;
|
|
189
|
+
case "in_progress": taskInProgress++; busyAgentNames.add(agent); break;
|
|
190
|
+
case "failed": taskFailed++; break;
|
|
191
|
+
case "review": taskReview++; break;
|
|
192
|
+
case "pending":
|
|
193
|
+
case "assigned":
|
|
194
|
+
taskPending++; break;
|
|
195
|
+
case "draft": taskPending++; break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let missionActive = 0, missionCompleted = 0;
|
|
200
|
+
for (const m of missions) {
|
|
201
|
+
if (m.status === "active") missionActive++;
|
|
202
|
+
else if (m.status === "completed") missionCompleted++;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const recentActivity = tasks
|
|
206
|
+
.filter(t => t.updatedAt)
|
|
207
|
+
.sort((a, b) => new Date(b.updatedAt!).getTime() - new Date(a.updatedAt!).getTime())
|
|
208
|
+
.slice(0, 8)
|
|
209
|
+
.map(t => ({
|
|
210
|
+
time: new Date(t.updatedAt!).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false }),
|
|
211
|
+
date: new Date(t.updatedAt!).toLocaleDateString("en-US", { month: "short", day: "numeric" }),
|
|
212
|
+
event: `${t.assignTo ?? "agent"} ${t.status === "done" ? "completed" : t.status === "in_progress" ? "started" : t.status === "failed" ? "failed" : "updated"} "${t.title}"`,
|
|
213
|
+
type: t.status === "done" ? "success" : t.status === "failed" ? "error" : t.status === "in_progress" ? "active" : "info",
|
|
214
|
+
}));
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
taskDone, taskInProgress, taskPending, taskFailed, taskReview,
|
|
218
|
+
missionActive, missionCompleted,
|
|
219
|
+
isActive: taskInProgress > 0 || missionActive > 0,
|
|
220
|
+
busyAgentNames,
|
|
221
|
+
recentActivity,
|
|
222
|
+
};
|
|
223
|
+
}, [tasks, missions, agents]);
|
|
224
|
+
|
|
225
|
+
const {
|
|
226
|
+
taskDone, taskInProgress, taskFailed,
|
|
227
|
+
missionActive, missionCompleted,
|
|
228
|
+
isActive, busyAgentNames,
|
|
229
|
+
recentActivity,
|
|
230
|
+
} = stats;
|
|
231
|
+
|
|
232
|
+
const isRefreshing = agentsFetching || tasksFetching || missionsFetching || sessionsFetching || skillsFetching || memoryFetching;
|
|
233
|
+
|
|
234
|
+
async function refreshOverview() {
|
|
235
|
+
await Promise.all([
|
|
236
|
+
refetchAgents(),
|
|
237
|
+
refetchTasks(),
|
|
238
|
+
refetchMissions(),
|
|
239
|
+
refetchSessions(),
|
|
240
|
+
refetchSkills(),
|
|
241
|
+
refetchMemory(),
|
|
242
|
+
]);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<div>
|
|
247
|
+
{/* Project title + live state badge — always visible at page top */}
|
|
248
|
+
<div className="mb-6 flex items-center justify-between gap-4">
|
|
249
|
+
<div className="flex items-center gap-3">
|
|
250
|
+
<h1 className="text-xl tracking-tight text-foreground">
|
|
251
|
+
{projectName}
|
|
252
|
+
</h1>
|
|
253
|
+
<span className="inline-flex items-center gap-1.5 rounded-full border border-border bg-secondary/40 px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
|
|
254
|
+
<span className={`h-1.5 w-1.5 rounded-full bg-brand ${isActive ? "animate-pulse" : ""}`} />
|
|
255
|
+
{isActive ? "Running" : "Ready"}
|
|
256
|
+
</span>
|
|
257
|
+
</div>
|
|
258
|
+
<ManualRefreshButton onRefresh={refreshOverview} isRefreshing={isRefreshing} className="shrink-0" />
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
{/* First-visit welcome banner — rendered by server parent when the
|
|
262
|
+
?welcome=1 flag is set. Sits between the title and the stats. */}
|
|
263
|
+
{welcomeBanner}
|
|
264
|
+
|
|
265
|
+
{/* Stats grid */}
|
|
266
|
+
<div className="grid gap-3 grid-cols-2 sm:grid-cols-4">
|
|
267
|
+
<Link href={`/projects/${id}/agents`} className="border border-border bg-card p-4 hover:border-foreground/10 transition-colors">
|
|
268
|
+
<div className="flex items-center gap-2 mb-2">
|
|
269
|
+
<Users className="h-3.5 w-3.5 text-muted-foreground" />
|
|
270
|
+
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">Agents</span>
|
|
271
|
+
</div>
|
|
272
|
+
<span className="text-2xl font-extrabold">{agents.length}</span>
|
|
273
|
+
{busyAgentNames.size > 0 && (
|
|
274
|
+
<p className="text-[10px] text-brand mt-1">{busyAgentNames.size} busy</p>
|
|
275
|
+
)}
|
|
276
|
+
</Link>
|
|
277
|
+
|
|
278
|
+
<Link href={`/projects/${id}/sessions`} className="border border-border bg-card p-4 hover:border-foreground/10 transition-colors">
|
|
279
|
+
<div className="flex items-center gap-2 mb-2">
|
|
280
|
+
<MessageSquare className="h-3.5 w-3.5 text-muted-foreground" />
|
|
281
|
+
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">Chat Sessions</span>
|
|
282
|
+
</div>
|
|
283
|
+
<span className="text-2xl font-extrabold">{sessions.length}</span>
|
|
284
|
+
</Link>
|
|
285
|
+
|
|
286
|
+
<Link href={`/projects/${id}/tasks`} className="border border-border bg-card p-4 hover:border-foreground/10 transition-colors">
|
|
287
|
+
<div className="flex items-center gap-2 mb-2">
|
|
288
|
+
<ListChecks className="h-3.5 w-3.5 text-muted-foreground" />
|
|
289
|
+
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">Tasks</span>
|
|
290
|
+
</div>
|
|
291
|
+
<span className="text-2xl font-extrabold">{tasks.length}</span>
|
|
292
|
+
<div className="flex items-center gap-2 mt-1 text-[10px]">
|
|
293
|
+
{taskDone > 0 && <span className="text-brand">{taskDone} done</span>}
|
|
294
|
+
{taskInProgress > 0 && <span className="text-foreground">{taskInProgress} run</span>}
|
|
295
|
+
{taskFailed > 0 && <span className="text-destructive">{taskFailed} fail</span>}
|
|
296
|
+
</div>
|
|
297
|
+
</Link>
|
|
298
|
+
|
|
299
|
+
<Link href={`/projects/${id}/missions`} className="border border-border bg-card p-4 hover:border-foreground/10 transition-colors">
|
|
300
|
+
<div className="flex items-center gap-2 mb-2">
|
|
301
|
+
<Target className="h-3.5 w-3.5 text-muted-foreground" />
|
|
302
|
+
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">Missions</span>
|
|
303
|
+
</div>
|
|
304
|
+
<span className="text-2xl font-extrabold">{missions.length}</span>
|
|
305
|
+
<div className="flex items-center gap-2 mt-1 text-[10px]">
|
|
306
|
+
{missionActive > 0 && <span className="text-foreground">{missionActive} active</span>}
|
|
307
|
+
{missionCompleted > 0 && <span className="text-brand">{missionCompleted} done</span>}
|
|
308
|
+
</div>
|
|
309
|
+
</Link>
|
|
310
|
+
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
{/* Onboarding checklist — below stats, auto-derived from project data.
|
|
314
|
+
Replaces recent activity while visible. */}
|
|
315
|
+
{(() => {
|
|
316
|
+
const checklistSteps = deriveChecklistSteps({
|
|
317
|
+
hasSession: sessions.length > 0,
|
|
318
|
+
hasSkill: skillsList.length > 0,
|
|
319
|
+
hasMemory,
|
|
320
|
+
hasTask: tasks.length > 0,
|
|
321
|
+
hasDeployed: hasDeployedCheck(agents),
|
|
322
|
+
projectSlug,
|
|
323
|
+
projectId: id,
|
|
324
|
+
apiUrl: process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000",
|
|
325
|
+
agentName: agents[0]?.name,
|
|
326
|
+
agentNames: agents.map((a) => a.name),
|
|
327
|
+
agentConfigs: agents,
|
|
328
|
+
});
|
|
329
|
+
const allChecklistDone = checklistSteps.every((s) => s.completed);
|
|
330
|
+
if (allChecklistDone) return null;
|
|
331
|
+
return (
|
|
332
|
+
<div className="mt-6">
|
|
333
|
+
<OnboardingChecklist steps={checklistSteps} projectId={id} />
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
})()}
|
|
337
|
+
|
|
338
|
+
{/* Swarm status — custom status bar (all 7 run states, always
|
|
339
|
+
shown — 0 across the board for a fresh project) + a scannable
|
|
340
|
+
recent-runs list (only when there are tasks). The status bar is
|
|
341
|
+
our own component, not @lumea-labs/orchestrator's, because that
|
|
342
|
+
one is hardcoded to 3 buckets (running/done/failed). */}
|
|
343
|
+
<div className="mt-8 flex flex-col gap-4">
|
|
344
|
+
<SwarmStatusBar counts={buildSwarmCounts(tasks)} />
|
|
345
|
+
|
|
346
|
+
{tasks.length > 0 && (
|
|
347
|
+
<div className="border border-border bg-card">
|
|
348
|
+
<div className="flex items-center justify-between border-b border-border px-4 py-2.5">
|
|
349
|
+
<h3 className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
350
|
+
Recent runs
|
|
351
|
+
</h3>
|
|
352
|
+
<Link
|
|
353
|
+
href={`/projects/${id}/tasks`}
|
|
354
|
+
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
355
|
+
>
|
|
356
|
+
View all →
|
|
357
|
+
</Link>
|
|
358
|
+
</div>
|
|
359
|
+
<SwarmRunList
|
|
360
|
+
runs={tasks
|
|
361
|
+
.slice()
|
|
362
|
+
.sort((a, b) =>
|
|
363
|
+
new Date(b.updatedAt ?? b.createdAt ?? 0).getTime()
|
|
364
|
+
- new Date(a.updatedAt ?? a.createdAt ?? 0).getTime(),
|
|
365
|
+
)
|
|
366
|
+
.slice(0, 8)
|
|
367
|
+
.map<RunRow>((t) => ({
|
|
368
|
+
id: t.id,
|
|
369
|
+
status: toRunStatus(t.status),
|
|
370
|
+
agentName: t.assignTo ?? "—",
|
|
371
|
+
title: t.title ?? "Untitled task",
|
|
372
|
+
startedAt: t.updatedAt,
|
|
373
|
+
finishedAt: t.status === "done" || t.status === "failed"
|
|
374
|
+
? t.updatedAt
|
|
375
|
+
: undefined,
|
|
376
|
+
href: `/projects/${id}/tasks/${t.id}`,
|
|
377
|
+
}))}
|
|
378
|
+
/>
|
|
379
|
+
</div>
|
|
380
|
+
)}
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
{/* Recent activity — only shown once onboarding checklist is fully completed */}
|
|
384
|
+
{(() => {
|
|
385
|
+
const checkDone = sessions.length > 0
|
|
386
|
+
&& skillsList.length > 0
|
|
387
|
+
&& hasMemory
|
|
388
|
+
&& tasks.length > 0
|
|
389
|
+
&& hasDeployedCheck(agents);
|
|
390
|
+
if (!checkDone) return null;
|
|
391
|
+
return (
|
|
392
|
+
<section className="mt-8">
|
|
393
|
+
<h3 className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground mb-3">Recent activity</h3>
|
|
394
|
+
{recentActivity.length > 0 ? (
|
|
395
|
+
<div className="border border-border overflow-hidden">
|
|
396
|
+
{recentActivity.map((a, i) => (
|
|
397
|
+
<div key={`${a.date}-${a.time}-${a.event}-${i}`} className="flex items-center gap-3 border-b border-border last:border-0 px-4 py-2.5">
|
|
398
|
+
<span className="font-mono text-[10px] text-muted-foreground/40 w-20 shrink-0">
|
|
399
|
+
{a.date} {a.time}
|
|
400
|
+
</span>
|
|
401
|
+
<span className={`h-1.5 w-1.5 rounded-full shrink-0 ${
|
|
402
|
+
a.type === "success" ? "bg-brand"
|
|
403
|
+
: a.type === "error" ? "bg-destructive"
|
|
404
|
+
: a.type === "active" ? "bg-foreground animate-pulse"
|
|
405
|
+
: "bg-muted-foreground/30"
|
|
406
|
+
}`} />
|
|
407
|
+
<span className="text-xs text-muted-foreground truncate">{a.event}</span>
|
|
408
|
+
</div>
|
|
409
|
+
))}
|
|
410
|
+
</div>
|
|
411
|
+
) : (
|
|
412
|
+
<div className="border border-border p-8 text-center text-sm text-muted-foreground">
|
|
413
|
+
No recent activity.
|
|
414
|
+
</div>
|
|
415
|
+
)}
|
|
416
|
+
</section>
|
|
417
|
+
);
|
|
418
|
+
})()}
|
|
419
|
+
</div>
|
|
420
|
+
);
|
|
421
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* First-visit Get-started banner — rendered at the top of the project
|
|
5
|
+
* dashboard when the URL carries `?welcome=1`.
|
|
6
|
+
*
|
|
7
|
+
* Two steps matching the first two tabs of the ConnectDialog:
|
|
8
|
+
* 1. CLI — `polpo link --project-id <uuid>`
|
|
9
|
+
* 2. Coding Agent — `polpo install --client <selected>`
|
|
10
|
+
*
|
|
11
|
+
* The ClientPicker and CopyCard are shared with ConnectDialog (DRY).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useEffect, useState } from "react";
|
|
15
|
+
import { useRouter } from "next/navigation";
|
|
16
|
+
import { X } from "lucide-react";
|
|
17
|
+
import { CopyCard } from "#/components/dashboard/copy-card";
|
|
18
|
+
import { ClientPicker, buildInstallCommand } from "#/components/dashboard/client-picker";
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
projectId: string;
|
|
22
|
+
projectName: string;
|
|
23
|
+
projectSlug: string | undefined;
|
|
24
|
+
agentName: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Per-project localStorage key — skip is scoped to this project so a user
|
|
28
|
+
// can still see the banner on a freshly created project even after
|
|
29
|
+
// dismissing it elsewhere.
|
|
30
|
+
const skipKey = (projectId: string) => `polpo:welcome-skipped:${projectId}`;
|
|
31
|
+
|
|
32
|
+
export default function WelcomeBanner({ projectId }: Props) {
|
|
33
|
+
const router = useRouter();
|
|
34
|
+
const [show, setShow] = useState(false);
|
|
35
|
+
const [dismissed, setDismissed] = useState(false);
|
|
36
|
+
const [clients, setClients] = useState<string[]>(["claude-code"]);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (typeof window === "undefined") return;
|
|
40
|
+
// Permanent skip wins over ?welcome=1 — if the user previously chose
|
|
41
|
+
// "Don't show again", we never re-render the banner for this project
|
|
42
|
+
// even when they re-land here via a welcome=1 link.
|
|
43
|
+
if (localStorage.getItem(skipKey(projectId))) {
|
|
44
|
+
setShow(false);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const hasFlag = new URLSearchParams(window.location.search).get("welcome") === "1";
|
|
48
|
+
setShow(hasFlag);
|
|
49
|
+
}, [projectId]);
|
|
50
|
+
|
|
51
|
+
function clearWelcomeParam() {
|
|
52
|
+
if (typeof window === "undefined") return;
|
|
53
|
+
const url = new URL(window.location.href);
|
|
54
|
+
url.searchParams.delete("welcome");
|
|
55
|
+
window.history.replaceState({}, "", url.toString());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function handleDismiss() {
|
|
59
|
+
setDismissed(true);
|
|
60
|
+
clearWelcomeParam();
|
|
61
|
+
router.refresh();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Permanent skip — sets the localStorage flag so the banner never shows
|
|
65
|
+
// again on this project, even if the user re-visits with ?welcome=1.
|
|
66
|
+
function handleSkipForever() {
|
|
67
|
+
if (typeof window !== "undefined") {
|
|
68
|
+
localStorage.setItem(skipKey(projectId), "1");
|
|
69
|
+
}
|
|
70
|
+
setDismissed(true);
|
|
71
|
+
clearWelcomeParam();
|
|
72
|
+
router.refresh();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!show || dismissed) return null;
|
|
76
|
+
|
|
77
|
+
const linkCmd = `npx @polpo-ai/cli link --project-id ${projectId}`;
|
|
78
|
+
const installCmd = buildInstallCommand(clients);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className="mb-8 w-full max-w-2xl border border-border bg-card p-6">
|
|
82
|
+
<div className="flex items-start justify-between gap-3">
|
|
83
|
+
<div>
|
|
84
|
+
<h2 className="text-2xl tracking-tight text-foreground">
|
|
85
|
+
Get started
|
|
86
|
+
</h2>
|
|
87
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
88
|
+
Connect your codebase and set up your coding agent.
|
|
89
|
+
</p>
|
|
90
|
+
</div>
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
onClick={handleDismiss}
|
|
94
|
+
aria-label="Dismiss"
|
|
95
|
+
className="p-1 text-muted-foreground hover:bg-secondary hover:text-foreground transition-colors"
|
|
96
|
+
>
|
|
97
|
+
<X className="h-4 w-4" />
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div className="mt-5 flex flex-col gap-5">
|
|
102
|
+
{/* Step 1: Link */}
|
|
103
|
+
<div className="flex gap-4">
|
|
104
|
+
<div className="flex flex-col items-center gap-2 pt-0.5 shrink-0">
|
|
105
|
+
<div className="flex size-5 items-center justify-center rounded-full bg-secondary">
|
|
106
|
+
<span className="text-xs font-medium leading-4 text-foreground">1</span>
|
|
107
|
+
</div>
|
|
108
|
+
<div className="w-px flex-1 bg-border" />
|
|
109
|
+
</div>
|
|
110
|
+
<div className="flex-1 min-w-0 flex flex-col gap-2 pb-2">
|
|
111
|
+
<div>
|
|
112
|
+
<p className="text-sm font-medium leading-6 text-foreground">Link project</p>
|
|
113
|
+
<p className="text-xs leading-5 text-muted-foreground">
|
|
114
|
+
Connect this cloud project to a local folder.
|
|
115
|
+
</p>
|
|
116
|
+
</div>
|
|
117
|
+
<CopyCard label="terminal" value={linkCmd} />
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* Step 2: Install coding agent skills */}
|
|
122
|
+
<div className="flex gap-4">
|
|
123
|
+
<div className="flex flex-col items-center gap-2 pt-0.5 shrink-0">
|
|
124
|
+
<div className="flex size-5 items-center justify-center rounded-full bg-secondary">
|
|
125
|
+
<span className="text-xs font-medium leading-4 text-foreground">2</span>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
<div className="flex-1 min-w-0 flex flex-col gap-3">
|
|
129
|
+
<div>
|
|
130
|
+
<p className="text-sm font-medium leading-6 text-foreground">Set up your coding agent</p>
|
|
131
|
+
<p className="text-xs leading-5 text-muted-foreground">
|
|
132
|
+
Install Polpo skills so your coding agent knows how to work with the project.
|
|
133
|
+
</p>
|
|
134
|
+
</div>
|
|
135
|
+
<ClientPicker value={clients} onChange={setClients} />
|
|
136
|
+
<CopyCard label="terminal" value={installCmd} />
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{/* Footer — explicit "don't show again". The X above is a one-shot
|
|
142
|
+
dismiss (banner returns if the user revisits with ?welcome=1);
|
|
143
|
+
this is the permanent opt-out. */}
|
|
144
|
+
<div className="mt-5 flex justify-end border-t border-border pt-3">
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
onClick={handleSkipForever}
|
|
148
|
+
data-testid="welcome-skip-forever"
|
|
149
|
+
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
150
|
+
>
|
|
151
|
+
Skip — don't show again
|
|
152
|
+
</button>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|