@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,680 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import { useQuery } from "@tanstack/react-query";
|
|
6
|
+
import { fetchControlPlane } from "#/lib/data-client";
|
|
7
|
+
import { Plug, KeyRound, Terminal, CodeXml, SquareTerminal } from "lucide-react";
|
|
8
|
+
import { CopyCard } from "#/components/dashboard/copy-card";
|
|
9
|
+
import { ClientPicker } from "#/components/dashboard/client-picker";
|
|
10
|
+
import { SdkSnippetPanel } from "#/components/dashboard/sdk-snippet-panel";
|
|
11
|
+
import { McpInstallPanel } from "#/components/dashboard/mcp-install-panel";
|
|
12
|
+
import {
|
|
13
|
+
Dialog,
|
|
14
|
+
DialogContent,
|
|
15
|
+
DialogHeader,
|
|
16
|
+
DialogTitle,
|
|
17
|
+
DialogDescription,
|
|
18
|
+
DialogFooter,
|
|
19
|
+
DialogClose,
|
|
20
|
+
} from "#/components/ui/dialog";
|
|
21
|
+
import type { Project } from "#/lib/api";
|
|
22
|
+
import { createPolpoClient } from "#/lib/polpo-client";
|
|
23
|
+
|
|
24
|
+
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "https://api.polpo.sh";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Per-project "Connect" dialog. Shows the URL + install snippets the user
|
|
28
|
+
* needs to hit their project from code, curl, or CLI.
|
|
29
|
+
*
|
|
30
|
+
* When a project is in scope (the user is inside /projects/[id]/...) the
|
|
31
|
+
* snippets use the project's slug-based subdomain — the canonical
|
|
32
|
+
* data-plane URL post-F2 (`{slug}.polpo.cloud`). Otherwise the dialog
|
|
33
|
+
* still renders, but with a short "open a project first" hint instead
|
|
34
|
+
* of placeholder slugs that don't resolve.
|
|
35
|
+
*
|
|
36
|
+
* The ConnectButton wrapper is responsible for resolving the current
|
|
37
|
+
* project: it reads `[id]` from the URL params and lazy-fetches the
|
|
38
|
+
* project row only when the dialog is opened (cheap, avoids an extra
|
|
39
|
+
* request on every dashboard page).
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
const APEX_FALLBACK = "https://api.polpo.sh";
|
|
43
|
+
|
|
44
|
+
/** Discord brand glyph — lucide-react doesn't ship one, so we inline it. */
|
|
45
|
+
function DiscordIcon({ className }: { className?: string }) {
|
|
46
|
+
return (
|
|
47
|
+
<svg
|
|
48
|
+
viewBox="0 -28.5 256 256"
|
|
49
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
50
|
+
preserveAspectRatio="xMidYMid"
|
|
51
|
+
className={className}
|
|
52
|
+
aria-hidden="true"
|
|
53
|
+
>
|
|
54
|
+
<path
|
|
55
|
+
d="M216.856 16.597C200.285 8.843 182.566 3.208 164.042 0c-2.275 4.113-4.933 9.646-6.766 14.046-19.692-2.961-39.203-2.961-58.533 0C96.911 9.645 94.193 4.113 91.897 0 73.353 3.208 55.613 8.864 39.042 16.638 5.618 67.147-3.443 116.4 1.087 164.956c22.169 16.555 43.654 26.612 64.775 33.192 5.215-7.177 9.866-14.807 13.873-22.848-7.631-2.9-14.94-6.478-21.847-10.632a106.78 106.78 0 0 0 5.358-4.237c42.123 19.702 87.89 19.702 129.51 0 1.752 1.46 3.544 2.879 5.356 4.237-6.927 4.175-14.256 7.753-21.887 10.653 4.007 8.02 8.638 15.67 13.873 22.847 21.142-6.58 42.647-16.637 64.815-33.192 5.316-56.287-9.081-105.09-38.056-148.36zM85.474 135.095c-12.645 0-23.015-11.805-23.015-26.18 0-14.375 10.148-26.2 23.015-26.2 12.867 0 23.236 11.804 23.015 26.2.02 14.375-10.148 26.18-23.015 26.18zm85.051 0c-12.645 0-23.015-11.805-23.015-26.18 0-14.375 10.148-26.2 23.015-26.2 12.867 0 23.237 11.804 23.015 26.2 0 14.375-10.148 26.18-23.015 26.18z"
|
|
56
|
+
fill="currentColor"
|
|
57
|
+
/>
|
|
58
|
+
</svg>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Short relative-time formatter for the key list ("used 3h ago").
|
|
64
|
+
* Intentionally minimal: no locale plumbing, coarse buckets.
|
|
65
|
+
*/
|
|
66
|
+
function formatRelative(iso: string): string {
|
|
67
|
+
const then = new Date(iso).getTime();
|
|
68
|
+
if (Number.isNaN(then)) return "recently";
|
|
69
|
+
const diffMs = Date.now() - then;
|
|
70
|
+
const m = Math.floor(diffMs / 60_000);
|
|
71
|
+
if (m < 1) return "just now";
|
|
72
|
+
if (m < 60) return `${m}m ago`;
|
|
73
|
+
const h = Math.floor(m / 60);
|
|
74
|
+
if (h < 24) return `${h}h ago`;
|
|
75
|
+
const d = Math.floor(h / 24);
|
|
76
|
+
if (d < 30) return `${d}d ago`;
|
|
77
|
+
const mo = Math.floor(d / 30);
|
|
78
|
+
if (mo < 12) return `${mo}mo ago`;
|
|
79
|
+
return `${Math.floor(mo / 12)}y ago`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function tenantUrl(slug: string | undefined): string {
|
|
83
|
+
// `{slug}.polpo.cloud` is the dedicated tenant zone. Worker injects
|
|
84
|
+
// `x-polpo-slug` into the header and forwards to the backend.
|
|
85
|
+
return slug ? `https://${slug}.polpo.cloud` : APEX_FALLBACK;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Step header — circle with step number + optional vertical connector +
|
|
90
|
+
* title + description. Used in the Coding Agent tab's 2-step flow.
|
|
91
|
+
*/
|
|
92
|
+
function StepHeader({
|
|
93
|
+
n,
|
|
94
|
+
last,
|
|
95
|
+
title,
|
|
96
|
+
description,
|
|
97
|
+
}: {
|
|
98
|
+
n: number;
|
|
99
|
+
last: boolean;
|
|
100
|
+
title: string;
|
|
101
|
+
description: string;
|
|
102
|
+
}) {
|
|
103
|
+
return (
|
|
104
|
+
<div className="flex w-[240px] shrink-0 gap-2">
|
|
105
|
+
<div className="flex flex-col items-center gap-2 pt-0.5">
|
|
106
|
+
<div className="flex size-5 shrink-0 items-center justify-center rounded-full bg-secondary">
|
|
107
|
+
<span className="text-xs font-medium leading-4 text-foreground">{n}</span>
|
|
108
|
+
</div>
|
|
109
|
+
{!last && <div className="w-px flex-1 bg-border" />}
|
|
110
|
+
</div>
|
|
111
|
+
<div className="flex flex-1 flex-col gap-2">
|
|
112
|
+
<p className="text-sm font-medium leading-6 text-foreground">{title}</p>
|
|
113
|
+
<p className="text-sm leading-6 text-muted-foreground">{description}</p>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
type BadgeTone = "brand" | "muted";
|
|
120
|
+
|
|
121
|
+
interface OptionCardProps {
|
|
122
|
+
icon: React.ComponentType<{ className?: string; strokeWidth?: number }>;
|
|
123
|
+
title: string;
|
|
124
|
+
badge: string;
|
|
125
|
+
badgeTone?: BadgeTone;
|
|
126
|
+
active: boolean;
|
|
127
|
+
onClick: () => void;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Connect tab: center-aligned icon + label + pill badge, stacked
|
|
132
|
+
* vertically. Sits inside a segmented strip (shared outer border,
|
|
133
|
+
* vertical divider between segments).
|
|
134
|
+
*/
|
|
135
|
+
function OptionCard({ icon: Icon, title, badge, badgeTone = "brand", active, onClick }: OptionCardProps) {
|
|
136
|
+
const badgeClasses =
|
|
137
|
+
badgeTone === "brand"
|
|
138
|
+
? "bg-brand/10 text-brand"
|
|
139
|
+
: "bg-secondary text-muted-foreground";
|
|
140
|
+
return (
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
onClick={onClick}
|
|
144
|
+
className={`group relative flex flex-1 flex-col items-center justify-center gap-3 py-4 transition-colors ${
|
|
145
|
+
active ? "bg-secondary" : "hover:bg-secondary/60"
|
|
146
|
+
}`}
|
|
147
|
+
>
|
|
148
|
+
<Icon
|
|
149
|
+
className={`h-6 w-6 ${active ? "text-muted-foreground" : "text-muted-foreground/60 group-hover:text-muted-foreground"}`}
|
|
150
|
+
strokeWidth={1.5}
|
|
151
|
+
/>
|
|
152
|
+
<div className="flex flex-col items-center gap-1.5">
|
|
153
|
+
<span className={`text-[13px] leading-[18px] ${active ? "text-foreground" : "text-foreground/90"}`}>{title}</span>
|
|
154
|
+
<span className={`inline-flex items-center rounded px-2 py-0.5 text-[11px] font-medium ${badgeClasses}`}>
|
|
155
|
+
{badge}
|
|
156
|
+
</span>
|
|
157
|
+
</div>
|
|
158
|
+
</button>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
type Method = "cli" | "mcp" | "api" | "curl";
|
|
163
|
+
|
|
164
|
+
type KeyScope = { type: "platform" } | { type: "project"; projectId: string };
|
|
165
|
+
interface ExistingKey {
|
|
166
|
+
id: string;
|
|
167
|
+
name: string;
|
|
168
|
+
keyPrefix: string;
|
|
169
|
+
scopes: KeyScope[];
|
|
170
|
+
createdAt: string;
|
|
171
|
+
lastUsedAt: string | null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const AGENT_PLACEHOLDER = "my-agent";
|
|
175
|
+
|
|
176
|
+
export function ConnectDialog({
|
|
177
|
+
open,
|
|
178
|
+
onOpenChange,
|
|
179
|
+
project,
|
|
180
|
+
projectId,
|
|
181
|
+
}: {
|
|
182
|
+
open: boolean;
|
|
183
|
+
onOpenChange: (open: boolean) => void;
|
|
184
|
+
project?: Pick<Project, "id" | "slug" | "name" | "orgId">;
|
|
185
|
+
/** Raw project id from the URL — available BEFORE the project row has been
|
|
186
|
+
* fetched. Lets the CLI tab render the real link command immediately. */
|
|
187
|
+
projectId?: string;
|
|
188
|
+
}) {
|
|
189
|
+
const [method, setMethod] = useState<Method>("cli");
|
|
190
|
+
// Coding Agent tab has two distinct install paths: local skill pack
|
|
191
|
+
// (scaffolds .polpo/ in the repo) vs remote MCP server (zero files,
|
|
192
|
+
// OAuth-authenticated live tools). Kept as a sub-mode instead of a
|
|
193
|
+
// fifth top-level tab to avoid tab-bar clutter.
|
|
194
|
+
const [codingMode, setCodingMode] = useState<"skills" | "mcp">("skills");
|
|
195
|
+
const [agents, setAgents] = useState<string[] | null>(null);
|
|
196
|
+
const [agent, setAgent] = useState<string>(AGENT_PLACEHOLDER);
|
|
197
|
+
const [clients, setClients] = useState<string[]>(["claude-code"]);
|
|
198
|
+
const [generatedKey, setGeneratedKey] = useState<string | null>(null);
|
|
199
|
+
const [generating, setGenerating] = useState(false);
|
|
200
|
+
const [keyError, setKeyError] = useState<string | null>(null);
|
|
201
|
+
const [existingKeys, setExistingKeys] = useState<ExistingKey[] | null>(null);
|
|
202
|
+
|
|
203
|
+
// Eagerly fetch the org's existing keys and filter to those usable on
|
|
204
|
+
// this project (platform-scoped OR project-scoped matching `project.id`).
|
|
205
|
+
// Shown inline so the user can recognise a key they already saved
|
|
206
|
+
// elsewhere instead of being funneled into generating a new one.
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (!open || !project?.orgId) return;
|
|
209
|
+
let cancelled = false;
|
|
210
|
+
(async () => {
|
|
211
|
+
try {
|
|
212
|
+
const res = await fetch(
|
|
213
|
+
`${API_URL}/v1/api-keys?orgId=${encodeURIComponent(project.orgId)}`,
|
|
214
|
+
{ credentials: "include" },
|
|
215
|
+
);
|
|
216
|
+
if (!res.ok) return;
|
|
217
|
+
const all = (await res.json()) as ExistingKey[];
|
|
218
|
+
if (cancelled) return;
|
|
219
|
+
const relevant = all.filter((k) =>
|
|
220
|
+
k.scopes.some(
|
|
221
|
+
(s) => s.type === "project" && s.projectId === project.id,
|
|
222
|
+
),
|
|
223
|
+
);
|
|
224
|
+
setExistingKeys(relevant);
|
|
225
|
+
} catch {
|
|
226
|
+
if (!cancelled) setExistingKeys([]);
|
|
227
|
+
}
|
|
228
|
+
})();
|
|
229
|
+
return () => {
|
|
230
|
+
cancelled = true;
|
|
231
|
+
};
|
|
232
|
+
}, [open, project?.orgId, project?.id]);
|
|
233
|
+
|
|
234
|
+
// Drop the plaintext key when the dialog closes. Keeping it alive across
|
|
235
|
+
// open/close would leak it via sessionStorage-equivalent React state and
|
|
236
|
+
// undermine the "shown once" security contract. Tab switches within a
|
|
237
|
+
// single open session keep the key (state is retained), which is what
|
|
238
|
+
// the user asked for.
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
if (!open) {
|
|
241
|
+
setGeneratedKey(null);
|
|
242
|
+
setKeyError(null);
|
|
243
|
+
}
|
|
244
|
+
}, [open]);
|
|
245
|
+
|
|
246
|
+
async function handleGenerateKey() {
|
|
247
|
+
if (!project) return;
|
|
248
|
+
setGenerating(true);
|
|
249
|
+
setKeyError(null);
|
|
250
|
+
try {
|
|
251
|
+
const res = await fetch(`${API_URL}/v1/api-keys`, {
|
|
252
|
+
method: "POST",
|
|
253
|
+
credentials: "include",
|
|
254
|
+
headers: { "Content-Type": "application/json" },
|
|
255
|
+
body: JSON.stringify({
|
|
256
|
+
orgId: project.orgId,
|
|
257
|
+
name: "Connect dialog",
|
|
258
|
+
scopes: [{ type: "project", projectId: project.id }],
|
|
259
|
+
}),
|
|
260
|
+
});
|
|
261
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
262
|
+
const data = (await res.json()) as { rawKey: string };
|
|
263
|
+
setGeneratedKey(data.rawKey);
|
|
264
|
+
} catch (e) {
|
|
265
|
+
setKeyError((e as Error).message || "Failed to generate key");
|
|
266
|
+
} finally {
|
|
267
|
+
setGenerating(false);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Lazy-fetch the project's agent list when the dialog opens. Purely
|
|
272
|
+
// cosmetic — if the fetch fails we fall back to `my-agent` placeholder.
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
if (!open || !project?.id) return;
|
|
275
|
+
let cancelled = false;
|
|
276
|
+
(async () => {
|
|
277
|
+
try {
|
|
278
|
+
const polpo = createPolpoClient(project.id);
|
|
279
|
+
const list = await polpo.getAgents();
|
|
280
|
+
if (cancelled) return;
|
|
281
|
+
const names = list.map(a => a.name).filter(Boolean);
|
|
282
|
+
setAgents(names);
|
|
283
|
+
if (names.length > 0) setAgent(names[0]);
|
|
284
|
+
} catch {
|
|
285
|
+
if (!cancelled) setAgents([]);
|
|
286
|
+
}
|
|
287
|
+
})();
|
|
288
|
+
return () => {
|
|
289
|
+
cancelled = true;
|
|
290
|
+
};
|
|
291
|
+
}, [open, project?.id]);
|
|
292
|
+
|
|
293
|
+
const baseUrl = tenantUrl(project?.slug);
|
|
294
|
+
|
|
295
|
+
// CLI tab — agentic connect: `link` kicks off the device-code browser
|
|
296
|
+
// flow lazily and scaffolds the project. One command, real project id
|
|
297
|
+
// from the URL (available instantly, no fetch needed).
|
|
298
|
+
const effectiveProjectId = projectId ?? project?.id;
|
|
299
|
+
const cliSnippet = effectiveProjectId
|
|
300
|
+
? `npx @polpo-ai/cli link --project-id ${effectiveProjectId}`
|
|
301
|
+
: null;
|
|
302
|
+
|
|
303
|
+
// Coding Agent tab — single deterministic command: `polpo install`
|
|
304
|
+
// wraps device-code auth (if needed) + skills install for the picked
|
|
305
|
+
// clients. Keeps `link` separate on purpose (install is per-machine,
|
|
306
|
+
// link is per-project).
|
|
307
|
+
const clientFlag = clients.length > 0 ? clients.join(",") : "claude-code";
|
|
308
|
+
const codingInstallSnippet = `npx @polpo-ai/cli install --client ${clientFlag}`;
|
|
309
|
+
const codingVerifyPrompt = `I'm using Polpo as my backend for AI agents. List the Polpo agents configured in this project.`;
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
313
|
+
<DialogContent className="sm:max-w-4xl bg-card ring-foreground/15 shadow-2xl">
|
|
314
|
+
<DialogHeader className="pb-1">
|
|
315
|
+
<DialogTitle className="text-base font-normal">
|
|
316
|
+
{project ? (
|
|
317
|
+
<>
|
|
318
|
+
Connect to <span className="font-semibold">{project.name}</span>
|
|
319
|
+
</>
|
|
320
|
+
) : (
|
|
321
|
+
"Connect your agents"
|
|
322
|
+
)}
|
|
323
|
+
</DialogTitle>
|
|
324
|
+
<DialogDescription className="text-sm">
|
|
325
|
+
<>
|
|
326
|
+
Reach your agents from your app, your terminal, or a coding assistant.{" "}
|
|
327
|
+
<a
|
|
328
|
+
href="https://docs.polpo.sh"
|
|
329
|
+
target="_blank"
|
|
330
|
+
rel="noopener noreferrer"
|
|
331
|
+
className="font-medium text-foreground hover:underline underline-offset-4"
|
|
332
|
+
>
|
|
333
|
+
Read the docs
|
|
334
|
+
</a>
|
|
335
|
+
</>
|
|
336
|
+
</DialogDescription>
|
|
337
|
+
</DialogHeader>
|
|
338
|
+
|
|
339
|
+
{/* Segmented strip — three squared tabs glued together with a single
|
|
340
|
+
outer border + vertical dividers. Centered cards. */}
|
|
341
|
+
<div className="mt-2 flex items-stretch overflow-hidden border border-border divide-x divide-border">
|
|
342
|
+
<OptionCard
|
|
343
|
+
icon={SquareTerminal}
|
|
344
|
+
title="CLI"
|
|
345
|
+
badge="Agentic Connect"
|
|
346
|
+
active={method === "cli"}
|
|
347
|
+
onClick={() => setMethod("cli")}
|
|
348
|
+
/>
|
|
349
|
+
<OptionCard
|
|
350
|
+
icon={Terminal}
|
|
351
|
+
title="Coding Agent"
|
|
352
|
+
badge="Agentic Connect"
|
|
353
|
+
active={method === "mcp"}
|
|
354
|
+
onClick={() => setMethod("mcp")}
|
|
355
|
+
/>
|
|
356
|
+
<OptionCard
|
|
357
|
+
icon={KeyRound}
|
|
358
|
+
title="API Key"
|
|
359
|
+
badge="Manage Project Keys"
|
|
360
|
+
badgeTone="muted"
|
|
361
|
+
active={method === "api"}
|
|
362
|
+
onClick={() => setMethod("api")}
|
|
363
|
+
/>
|
|
364
|
+
<OptionCard
|
|
365
|
+
icon={CodeXml}
|
|
366
|
+
title="SDK"
|
|
367
|
+
badge="Integrate in your app"
|
|
368
|
+
badgeTone="muted"
|
|
369
|
+
active={method === "curl"}
|
|
370
|
+
onClick={() => setMethod("curl")}
|
|
371
|
+
/>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
{/* Per-tab content — 240px label column on the left,
|
|
375
|
+
stack of copy cards on the right. min-h keeps dialog height stable. */}
|
|
376
|
+
<div className="mt-6 min-h-[160px]">
|
|
377
|
+
{method === "cli" && (
|
|
378
|
+
<div className="flex gap-6">
|
|
379
|
+
<div className="flex w-[240px] shrink-0 flex-col gap-2">
|
|
380
|
+
<p className="text-sm font-medium leading-6 text-foreground">Link Project</p>
|
|
381
|
+
<p className="text-sm leading-6 text-muted-foreground">
|
|
382
|
+
Run this from your project directory. It writes{" "}
|
|
383
|
+
<code className="rounded bg-secondary px-1 py-0.5 font-mono text-[11px]">.polpo/polpo.json</code>{" "}
|
|
384
|
+
binding this folder to the cloud project.
|
|
385
|
+
</p>
|
|
386
|
+
</div>
|
|
387
|
+
<div className="flex-1 min-w-0">
|
|
388
|
+
{cliSnippet ? (
|
|
389
|
+
<CopyCard label="terminal command" value={cliSnippet} />
|
|
390
|
+
) : (
|
|
391
|
+
<div className="rounded border border-border bg-background p-6 text-center text-sm text-muted-foreground">
|
|
392
|
+
Open a project to get the exact link command.
|
|
393
|
+
</div>
|
|
394
|
+
)}
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
398
|
+
|
|
399
|
+
{method === "mcp" && (
|
|
400
|
+
<div className="flex flex-col gap-6">
|
|
401
|
+
{/* Skills vs MCP sub-toggle. Two very different shapes of
|
|
402
|
+
install — separated here instead of buried in copy. */}
|
|
403
|
+
<div className="inline-flex self-start border border-border overflow-hidden text-xs">
|
|
404
|
+
<button
|
|
405
|
+
type="button"
|
|
406
|
+
onClick={() => setCodingMode("skills")}
|
|
407
|
+
className={[
|
|
408
|
+
"px-3 py-1.5 font-medium transition-colors",
|
|
409
|
+
codingMode === "skills"
|
|
410
|
+
? "bg-foreground text-background"
|
|
411
|
+
: "bg-background text-muted-foreground hover:text-foreground",
|
|
412
|
+
].join(" ")}
|
|
413
|
+
aria-pressed={codingMode === "skills"}
|
|
414
|
+
>
|
|
415
|
+
Skills
|
|
416
|
+
</button>
|
|
417
|
+
<div className="w-px bg-border" />
|
|
418
|
+
<button
|
|
419
|
+
type="button"
|
|
420
|
+
onClick={() => setCodingMode("mcp")}
|
|
421
|
+
className={[
|
|
422
|
+
"px-3 py-1.5 font-medium transition-colors",
|
|
423
|
+
codingMode === "mcp"
|
|
424
|
+
? "bg-foreground text-background"
|
|
425
|
+
: "bg-background text-muted-foreground hover:text-foreground",
|
|
426
|
+
].join(" ")}
|
|
427
|
+
aria-pressed={codingMode === "mcp"}
|
|
428
|
+
>
|
|
429
|
+
MCP Server
|
|
430
|
+
</button>
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
{codingMode === "skills" ? (
|
|
434
|
+
<>
|
|
435
|
+
{/* Step 1 — install */}
|
|
436
|
+
<div className="flex gap-6">
|
|
437
|
+
<StepHeader
|
|
438
|
+
n={1}
|
|
439
|
+
last={false}
|
|
440
|
+
title="Install Polpo"
|
|
441
|
+
description="Run this command to install Polpo's skill pack in your coding agent so it knows how to work with your project."
|
|
442
|
+
/>
|
|
443
|
+
<div className="flex-1 min-w-0 flex flex-col gap-3">
|
|
444
|
+
<ClientPicker value={clients} onChange={setClients} />
|
|
445
|
+
<CopyCard label="Terminal Command" value={codingInstallSnippet} />
|
|
446
|
+
</div>
|
|
447
|
+
</div>
|
|
448
|
+
|
|
449
|
+
{/* Step 2 — verify */}
|
|
450
|
+
<div className="flex gap-6">
|
|
451
|
+
<StepHeader
|
|
452
|
+
n={2}
|
|
453
|
+
last
|
|
454
|
+
title="Verify Connection"
|
|
455
|
+
description="Send the prompt below to your AI coding agent to confirm the connection."
|
|
456
|
+
/>
|
|
457
|
+
<div className="flex-1 min-w-0">
|
|
458
|
+
<CopyCard label="prompt" value={codingVerifyPrompt} />
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
</>
|
|
462
|
+
) : (
|
|
463
|
+
<div className="flex gap-6">
|
|
464
|
+
<div className="flex w-[240px] shrink-0 flex-col gap-2">
|
|
465
|
+
<p className="text-sm font-medium leading-6 text-foreground">
|
|
466
|
+
MCP Server
|
|
467
|
+
</p>
|
|
468
|
+
<p className="text-sm leading-6 text-muted-foreground">
|
|
469
|
+
Connect your coding agent to Polpo's remote MCP
|
|
470
|
+
endpoint. No files land in your repo — tools are called
|
|
471
|
+
live over OAuth.
|
|
472
|
+
</p>
|
|
473
|
+
</div>
|
|
474
|
+
<div className="flex-1 min-w-0">
|
|
475
|
+
<McpInstallPanel />
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
)}
|
|
479
|
+
</div>
|
|
480
|
+
)}
|
|
481
|
+
|
|
482
|
+
{method === "api" && (
|
|
483
|
+
<div className="flex gap-6">
|
|
484
|
+
<div className="flex w-[240px] shrink-0 flex-col gap-2">
|
|
485
|
+
<p className="text-sm font-medium leading-6 text-foreground">API Keys</p>
|
|
486
|
+
<p className="text-sm leading-6 text-muted-foreground">
|
|
487
|
+
Drop these into your app's environment to connect from SDKs, HTTP clients, or your own backend.
|
|
488
|
+
</p>
|
|
489
|
+
</div>
|
|
490
|
+
<div className="flex-1 min-w-0 flex flex-col gap-3">
|
|
491
|
+
<CopyCard label="POLPO_URL" value={baseUrl} />
|
|
492
|
+
{generatedKey ? (
|
|
493
|
+
<>
|
|
494
|
+
<CopyCard label="POLPO_API_KEY" value={generatedKey} />
|
|
495
|
+
<p className="text-xs leading-5 text-amber-500">
|
|
496
|
+
Save this key now — it won't be shown again after you close this dialog.
|
|
497
|
+
</p>
|
|
498
|
+
</>
|
|
499
|
+
) : (
|
|
500
|
+
<div className="flex flex-col gap-3 rounded border border-border bg-background p-3">
|
|
501
|
+
<div className="flex items-center justify-between">
|
|
502
|
+
<span className="inline-flex h-5 items-center rounded bg-secondary px-2 text-[11px] font-medium text-muted-foreground">
|
|
503
|
+
POLPO_API_KEY
|
|
504
|
+
</span>
|
|
505
|
+
{existingKeys && existingKeys.length > 0 && (
|
|
506
|
+
<span className="text-[11px] text-muted-foreground">
|
|
507
|
+
{existingKeys.length} existing
|
|
508
|
+
</span>
|
|
509
|
+
)}
|
|
510
|
+
</div>
|
|
511
|
+
|
|
512
|
+
{/* Existing keys for this project — recognition aid so the
|
|
513
|
+
user can pick the right one from their password manager
|
|
514
|
+
instead of being forced to generate a new one. */}
|
|
515
|
+
{existingKeys && existingKeys.length > 0 && (
|
|
516
|
+
<ul className="flex flex-col gap-1.5 border-b border-border pb-3">
|
|
517
|
+
{existingKeys.slice(0, 5).map((k) => (
|
|
518
|
+
<li
|
|
519
|
+
key={k.id}
|
|
520
|
+
className="flex items-center justify-between gap-2 text-xs"
|
|
521
|
+
>
|
|
522
|
+
<span className="flex min-w-0 items-baseline gap-2">
|
|
523
|
+
<code className="font-mono text-foreground truncate">
|
|
524
|
+
{k.keyPrefix}…
|
|
525
|
+
</code>
|
|
526
|
+
<span className="text-muted-foreground truncate">
|
|
527
|
+
{k.name}
|
|
528
|
+
</span>
|
|
529
|
+
</span>
|
|
530
|
+
<span className="shrink-0 text-[11px] text-muted-foreground">
|
|
531
|
+
{k.lastUsedAt ? `used ${formatRelative(k.lastUsedAt)}` : "never used"}
|
|
532
|
+
</span>
|
|
533
|
+
</li>
|
|
534
|
+
))}
|
|
535
|
+
{existingKeys.length > 5 && (
|
|
536
|
+
<li className="text-[11px] text-muted-foreground">
|
|
537
|
+
+{existingKeys.length - 5} more…
|
|
538
|
+
</li>
|
|
539
|
+
)}
|
|
540
|
+
</ul>
|
|
541
|
+
)}
|
|
542
|
+
|
|
543
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
544
|
+
<button
|
|
545
|
+
type="button"
|
|
546
|
+
onClick={handleGenerateKey}
|
|
547
|
+
disabled={generating || !project}
|
|
548
|
+
className="rounded border border-foreground/20 bg-foreground/5 px-3 py-1.5 text-sm font-medium text-foreground hover:bg-foreground hover:text-background transition-colors disabled:opacity-50"
|
|
549
|
+
>
|
|
550
|
+
{generating ? "Generating…" : "Generate new key"}
|
|
551
|
+
</button>
|
|
552
|
+
<span className="text-xs text-muted-foreground">
|
|
553
|
+
{existingKeys && existingKeys.length > 0
|
|
554
|
+
? "or use one of the keys above (plaintext only shown at creation)"
|
|
555
|
+
: "Shown once."}
|
|
556
|
+
</span>
|
|
557
|
+
</div>
|
|
558
|
+
{keyError && (
|
|
559
|
+
<p className="text-xs text-red-500">{keyError}</p>
|
|
560
|
+
)}
|
|
561
|
+
</div>
|
|
562
|
+
)}
|
|
563
|
+
</div>
|
|
564
|
+
</div>
|
|
565
|
+
)}
|
|
566
|
+
|
|
567
|
+
{method === "curl" && (
|
|
568
|
+
<div className="flex gap-6">
|
|
569
|
+
<div className="flex w-[240px] shrink-0 flex-col gap-2">
|
|
570
|
+
<p className="text-sm font-medium leading-6 text-foreground">SDK</p>
|
|
571
|
+
<p className="text-sm leading-6 text-muted-foreground">
|
|
572
|
+
Integrate in your app. Set{" "}
|
|
573
|
+
<code className="rounded bg-secondary px-1 py-0.5 font-mono text-[11px]">POLPO_API_KEY</code>{" "}
|
|
574
|
+
in your environment, then paste the snippet.
|
|
575
|
+
</p>
|
|
576
|
+
{!generatedKey && (
|
|
577
|
+
<button
|
|
578
|
+
type="button"
|
|
579
|
+
onClick={() => setMethod("api")}
|
|
580
|
+
className="self-start text-xs font-medium text-amber-500 hover:underline underline-offset-4"
|
|
581
|
+
>
|
|
582
|
+
Generate an API key →
|
|
583
|
+
</button>
|
|
584
|
+
)}
|
|
585
|
+
</div>
|
|
586
|
+
<div className="flex-1 min-w-0">
|
|
587
|
+
<SdkSnippetPanel
|
|
588
|
+
baseUrl={baseUrl}
|
|
589
|
+
agents={agents}
|
|
590
|
+
defaultAgent={agent}
|
|
591
|
+
sdkKey={generatedKey ?? "$POLPO_API_KEY"}
|
|
592
|
+
/>
|
|
593
|
+
</div>
|
|
594
|
+
</div>
|
|
595
|
+
)}
|
|
596
|
+
</div>
|
|
597
|
+
|
|
598
|
+
<DialogFooter className="mt-2 flex items-center justify-between gap-3 sm:justify-between">
|
|
599
|
+
<p className="inline-flex items-center gap-1.5 text-sm text-muted-foreground flex-wrap">
|
|
600
|
+
<span>Need help?</span>
|
|
601
|
+
<a
|
|
602
|
+
href="https://discord.gg/polpo"
|
|
603
|
+
target="_blank"
|
|
604
|
+
rel="noopener noreferrer"
|
|
605
|
+
className="inline-flex items-center gap-1 font-medium text-[#5865F2] hover:text-[#7289f4] transition-colors"
|
|
606
|
+
>
|
|
607
|
+
<DiscordIcon className="h-4 w-4" />
|
|
608
|
+
Discord
|
|
609
|
+
</a>
|
|
610
|
+
<span className="text-muted-foreground/60">·</span>
|
|
611
|
+
<a
|
|
612
|
+
href="https://calendly.com/hello-polpo/polpo-demo"
|
|
613
|
+
target="_blank"
|
|
614
|
+
rel="noopener noreferrer"
|
|
615
|
+
className="font-medium text-foreground/80 hover:text-foreground transition-colors underline-offset-4 hover:underline"
|
|
616
|
+
>
|
|
617
|
+
Book a demo
|
|
618
|
+
</a>
|
|
619
|
+
</p>
|
|
620
|
+
<DialogClose className="rounded-md border border-border px-4 py-2 text-sm hover:border-foreground/30 transition-colors">
|
|
621
|
+
Close
|
|
622
|
+
</DialogClose>
|
|
623
|
+
</DialogFooter>
|
|
624
|
+
</DialogContent>
|
|
625
|
+
</Dialog>
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Header-mounted launcher. Reads the `[id]` URL segment from
|
|
631
|
+
* /projects/[id]/… to decide whether a project is in scope. Fetches the
|
|
632
|
+
* project row lazily on first open so we don't pay an extra round-trip
|
|
633
|
+
* on every dashboard page load.
|
|
634
|
+
*/
|
|
635
|
+
const PROJECT_ID_RE = /^\/projects\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/;
|
|
636
|
+
|
|
637
|
+
export function ConnectButton() {
|
|
638
|
+
const [open, setOpen] = useState(false);
|
|
639
|
+
const pathname = usePathname();
|
|
640
|
+
// Extract project id from ANY /projects/{uuid}/... route — more robust
|
|
641
|
+
// than useParams (which depends on the current route segment matching).
|
|
642
|
+
const projectId = pathname?.match(PROJECT_ID_RE)?.[1];
|
|
643
|
+
|
|
644
|
+
// Reuse TopHeader's React Query entry for the same key so we don't pay
|
|
645
|
+
// two cold starts for the same /v1/projects/{id} endpoint on a single
|
|
646
|
+
// page load. If TopHeader already resolved it, this is a cache hit;
|
|
647
|
+
// if not, whichever fires first populates the cache.
|
|
648
|
+
const { data: project } = useQuery({
|
|
649
|
+
queryKey: ["project", projectId],
|
|
650
|
+
queryFn: () =>
|
|
651
|
+
fetchControlPlane<Project>(`/v1/projects/${projectId}`),
|
|
652
|
+
enabled: !!projectId,
|
|
653
|
+
staleTime: 60_000,
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// Connect is a per-project surface — hide it when the user is outside a
|
|
657
|
+
// project context (listing, keys, billing, settings). No project = nothing
|
|
658
|
+
// to link/connect to.
|
|
659
|
+
if (!projectId) return null;
|
|
660
|
+
|
|
661
|
+
return (
|
|
662
|
+
<>
|
|
663
|
+
<button
|
|
664
|
+
type="button"
|
|
665
|
+
onClick={() => setOpen(true)}
|
|
666
|
+
data-testid="header-connect"
|
|
667
|
+
className="group inline-flex h-8 items-center gap-1.5 border border-border bg-card px-2.5 text-xs font-medium text-foreground hover:border-foreground/40 hover:bg-secondary hover:shadow-[0_0_0_3px_rgba(255,255,255,0.04)] transition-all duration-150"
|
|
668
|
+
>
|
|
669
|
+
<Plug className="h-3.5 w-3.5 text-muted-foreground group-hover:text-foreground transition-colors" strokeWidth={1.5} />
|
|
670
|
+
<span className="hidden sm:inline">Connect</span>
|
|
671
|
+
</button>
|
|
672
|
+
<ConnectDialog
|
|
673
|
+
open={open}
|
|
674
|
+
onOpenChange={setOpen}
|
|
675
|
+
project={project ?? undefined}
|
|
676
|
+
projectId={projectId}
|
|
677
|
+
/>
|
|
678
|
+
</>
|
|
679
|
+
);
|
|
680
|
+
}
|