@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,253 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CopilotLayout — wraps the dashboard main content and docks a global,
|
|
5
|
+
* context-aware Builder panel BESIDE it (push, not overlay): when open the
|
|
6
|
+
* content shrinks and the panel takes its place on the right.
|
|
7
|
+
*
|
|
8
|
+
* The Meta Agent is the same one the Agent Studio uses; here it picks up the
|
|
9
|
+
* page CONTEXT (current agent / skill / section) from the route so it acts
|
|
10
|
+
* on what you're looking at.
|
|
11
|
+
*
|
|
12
|
+
* The panel is ALWAYS mounted (it collapses by WIDTH, never unmounts), so
|
|
13
|
+
* the conversation survives open/close and navigation between project pages.
|
|
14
|
+
* It's keyed by projectId so switching projects starts a fresh session.
|
|
15
|
+
*
|
|
16
|
+
* Phase 1: additive. It coexists with the Studio's docked builder;
|
|
17
|
+
* convergence (one surface) is a later decision.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
createContext,
|
|
22
|
+
useCallback,
|
|
23
|
+
useContext,
|
|
24
|
+
useEffect,
|
|
25
|
+
useMemo,
|
|
26
|
+
useRef,
|
|
27
|
+
useState,
|
|
28
|
+
type ReactNode,
|
|
29
|
+
} from "react";
|
|
30
|
+
import { useRouter } from "next/navigation";
|
|
31
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
32
|
+
import { SquarePen, X } from "lucide-react";
|
|
33
|
+
import { BuilderChat } from "#/components/dashboard/builder-chat";
|
|
34
|
+
import { useBuilderContext, type NavigateTarget } from "#/lib/builder-context";
|
|
35
|
+
import { fetchControlPlane } from "#/lib/data-client";
|
|
36
|
+
|
|
37
|
+
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
|
|
38
|
+
|
|
39
|
+
/** localStorage key for persisted copilot UI preferences. */
|
|
40
|
+
const COPILOT_PREFS_KEY = "polpo.copilot.prefs";
|
|
41
|
+
|
|
42
|
+
interface CopilotContextValue {
|
|
43
|
+
open: boolean;
|
|
44
|
+
setOpen: (open: boolean) => void;
|
|
45
|
+
toggle: () => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const CopilotContext = createContext<CopilotContextValue | null>(null);
|
|
49
|
+
|
|
50
|
+
/** Open/close the global Copilot panel from anywhere under the dashboard
|
|
51
|
+
* layout (e.g. the Agent Studio "Edit" button). Returns null outside it. */
|
|
52
|
+
export function useCopilot(): CopilotContextValue | null {
|
|
53
|
+
return useContext(CopilotContext);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The Polpo mark — two offset squares (no wordmark). Inherits color. */
|
|
57
|
+
function PolpoMark({ className = "" }: { className?: string }) {
|
|
58
|
+
return (
|
|
59
|
+
<span className={`relative inline-block ${className}`} aria-hidden>
|
|
60
|
+
<span className="absolute left-0 top-0 h-[45%] w-[45%] bg-current" />
|
|
61
|
+
<span className="absolute bottom-0 right-0 h-[45%] w-[45%] bg-current" />
|
|
62
|
+
</span>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function CopilotLayout({ children }: { children: ReactNode }) {
|
|
67
|
+
const router = useRouter();
|
|
68
|
+
const queryClient = useQueryClient();
|
|
69
|
+
const [open, setOpen] = useState(false);
|
|
70
|
+
// Bumping this remounts <BuilderChat>, resetting its in-memory state.
|
|
71
|
+
const [sessionNonce, setSessionNonce] = useState(0);
|
|
72
|
+
const { projectId, context } = useBuilderContext();
|
|
73
|
+
|
|
74
|
+
// Persist UI preferences (currently: was the panel open) across reloads.
|
|
75
|
+
const prefsHydrated = useRef(false);
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (typeof window === "undefined") return;
|
|
78
|
+
try {
|
|
79
|
+
const raw = window.localStorage.getItem(COPILOT_PREFS_KEY);
|
|
80
|
+
if (raw) {
|
|
81
|
+
const prefs = JSON.parse(raw);
|
|
82
|
+
if (typeof prefs?.open === "boolean") setOpen(prefs.open);
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
/* ignore */
|
|
86
|
+
}
|
|
87
|
+
}, []);
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (typeof window === "undefined") return;
|
|
90
|
+
if (!prefsHydrated.current) {
|
|
91
|
+
prefsHydrated.current = true;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
window.localStorage.setItem(COPILOT_PREFS_KEY, JSON.stringify({ open }));
|
|
96
|
+
} catch {
|
|
97
|
+
/* ignore */
|
|
98
|
+
}
|
|
99
|
+
}, [open]);
|
|
100
|
+
|
|
101
|
+
const copilotCtx = useMemo<CopilotContextValue>(
|
|
102
|
+
() => ({ open, setOpen, toggle: () => setOpen((v) => !v) }),
|
|
103
|
+
[open],
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Clear the locally-persisted conversation and start fresh.
|
|
107
|
+
const clearConversation = useCallback(() => {
|
|
108
|
+
if (projectId && typeof window !== "undefined") {
|
|
109
|
+
try {
|
|
110
|
+
window.localStorage.removeItem(`polpo.builder.${projectId}`);
|
|
111
|
+
} catch {
|
|
112
|
+
/* ignore */
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
setSessionNonce((n) => n + 1);
|
|
116
|
+
}, [projectId]);
|
|
117
|
+
|
|
118
|
+
// When the Meta Agent mutates something, refresh only the relevant data:
|
|
119
|
+
// router.refresh() re-fetches server components (e.g. the Studio agent
|
|
120
|
+
// detail), and we invalidate the matching client queries so lists update
|
|
121
|
+
// live too. Keyed by which tool ran.
|
|
122
|
+
const handleMutation = useCallback(
|
|
123
|
+
(tool: string) => {
|
|
124
|
+
router.refresh();
|
|
125
|
+
const inv = (key: unknown[]) => queryClient.invalidateQueries({ queryKey: key });
|
|
126
|
+
// Tool name is `polpo_<resource>_<action>` — invalidate that resource's
|
|
127
|
+
// queries, plus the cross-referenced ones (team ↔ agent membership).
|
|
128
|
+
const resource = tool.split("_")[1] ?? "";
|
|
129
|
+
inv([resource, projectId]);
|
|
130
|
+
if (resource === "agents") inv(["teams", projectId]);
|
|
131
|
+
if (resource === "skills" || resource === "teams") inv(["agents", projectId]);
|
|
132
|
+
},
|
|
133
|
+
[router, queryClient, projectId],
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// Client-side navigation requested by the Meta Agent (navigate tool):
|
|
137
|
+
// build the URL and push it. The panel is global, so it stays open and
|
|
138
|
+
// its context auto-updates to the new page.
|
|
139
|
+
const handleNavigate = useCallback(
|
|
140
|
+
(t: NavigateTarget) => {
|
|
141
|
+
if (!projectId) return;
|
|
142
|
+
let url = `/projects/${projectId}`;
|
|
143
|
+
if (t.section && t.section !== "overview") url += `/${t.section}`;
|
|
144
|
+
if (t.name) url += `/${encodeURIComponent(t.name)}`;
|
|
145
|
+
if (t.tab) url += `?tab=${encodeURIComponent(t.tab)}`;
|
|
146
|
+
router.push(url);
|
|
147
|
+
},
|
|
148
|
+
[router, projectId],
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// Only meaningful inside a project (the Meta Agent operates on one).
|
|
152
|
+
const show = !!projectId;
|
|
153
|
+
|
|
154
|
+
// Project name for the context line — shares the ["project", id] query
|
|
155
|
+
// with sidebar/top-header (cache hit, no extra fetch).
|
|
156
|
+
const { data: project } = useQuery({
|
|
157
|
+
queryKey: ["project", projectId],
|
|
158
|
+
queryFn: () =>
|
|
159
|
+
fetchControlPlane<{ name: string; orgId: string }>(`/v1/projects/${projectId}`),
|
|
160
|
+
enabled: show,
|
|
161
|
+
staleTime: 60_000,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Dynamic, human explanation of what the Meta Agent is scoped to —
|
|
165
|
+
// always names the specific thing (which agent / skill / project).
|
|
166
|
+
const projectName = project?.name ?? "this project";
|
|
167
|
+
const contextDetail =
|
|
168
|
+
context.type === "agent"
|
|
169
|
+
? `Agent ${context.agentName}`
|
|
170
|
+
: context.type === "skill"
|
|
171
|
+
? `Skill ${context.skill}`
|
|
172
|
+
: context.type === "section"
|
|
173
|
+
? context.label
|
|
174
|
+
: "Overview";
|
|
175
|
+
const contextExplain = `${contextDetail} · ${projectName}`;
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<CopilotContext.Provider value={copilotCtx}>
|
|
179
|
+
<div className="flex min-h-0 flex-1 overflow-hidden">
|
|
180
|
+
<main className="min-w-0 flex-1 overflow-y-auto">{children}</main>
|
|
181
|
+
|
|
182
|
+
{show && (
|
|
183
|
+
<>
|
|
184
|
+
{/* Launcher — Intercom-style, bottom-right; only visible when closed. */}
|
|
185
|
+
{!open && (
|
|
186
|
+
<button
|
|
187
|
+
type="button"
|
|
188
|
+
onClick={() => setOpen(true)}
|
|
189
|
+
className="group fixed bottom-5 right-5 z-40 inline-flex items-center gap-2.5 border border-border bg-card px-3 py-2.5 shadow-lg transition-colors hover:border-foreground/40 hover:bg-card"
|
|
190
|
+
>
|
|
191
|
+
<PolpoMark className="h-3.5 w-3.5 text-foreground" />
|
|
192
|
+
<span className="text-xs font-semibold text-foreground">Polpo AI</span>
|
|
193
|
+
</button>
|
|
194
|
+
)}
|
|
195
|
+
|
|
196
|
+
{/* Panel — docked beside content; collapses by width, stays mounted. */}
|
|
197
|
+
<aside
|
|
198
|
+
className={`shrink-0 overflow-hidden border-l border-border transition-[width] duration-200 ease-out ${
|
|
199
|
+
open ? "w-[500px]" : "w-0"
|
|
200
|
+
}`}
|
|
201
|
+
>
|
|
202
|
+
<div className="flex h-full w-[500px] flex-col overflow-hidden">
|
|
203
|
+
{/* Header — title + dynamic context on one row. The context
|
|
204
|
+
names the specific thing the agent acts on. */}
|
|
205
|
+
<div className="flex shrink-0 items-center gap-2 border-b border-border px-4 py-3">
|
|
206
|
+
<PolpoMark className="h-3.5 w-3.5 shrink-0 text-foreground" />
|
|
207
|
+
<span className="shrink-0 text-sm font-semibold text-foreground">Polpo AI</span>
|
|
208
|
+
<span className="ml-1 flex min-w-0 items-center gap-1.5 border-l border-border pl-2">
|
|
209
|
+
<span className="shrink-0 font-mono text-[10px] font-bold uppercase tracking-[0.14em] text-muted-foreground/55">
|
|
210
|
+
Context
|
|
211
|
+
</span>
|
|
212
|
+
<span className="min-w-0 truncate text-xs text-foreground" title={contextExplain}>
|
|
213
|
+
{contextExplain}
|
|
214
|
+
</span>
|
|
215
|
+
</span>
|
|
216
|
+
<button
|
|
217
|
+
type="button"
|
|
218
|
+
onClick={clearConversation}
|
|
219
|
+
aria-label="New conversation"
|
|
220
|
+
title="New conversation"
|
|
221
|
+
className="ml-auto shrink-0 text-muted-foreground transition-colors hover:text-foreground"
|
|
222
|
+
>
|
|
223
|
+
<SquarePen className="h-4 w-4" strokeWidth={1.5} />
|
|
224
|
+
</button>
|
|
225
|
+
<button
|
|
226
|
+
type="button"
|
|
227
|
+
onClick={() => setOpen(false)}
|
|
228
|
+
aria-label="Close"
|
|
229
|
+
className="shrink-0 text-muted-foreground transition-colors hover:text-foreground"
|
|
230
|
+
>
|
|
231
|
+
<X className="h-4 w-4" strokeWidth={1.5} />
|
|
232
|
+
</button>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<div className="copilot-chat-dense flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
236
|
+
<BuilderChat
|
|
237
|
+
key={`${projectId ?? "none"}:${sessionNonce}`}
|
|
238
|
+
projectId={projectId as string}
|
|
239
|
+
apiUrl={API_URL}
|
|
240
|
+
context={context}
|
|
241
|
+
onMutation={handleMutation}
|
|
242
|
+
onNavigate={handleNavigate}
|
|
243
|
+
persistKey={`polpo.builder.${projectId}`}
|
|
244
|
+
/>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
</aside>
|
|
248
|
+
</>
|
|
249
|
+
)}
|
|
250
|
+
</div>
|
|
251
|
+
</CopilotContext.Provider>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo, useState } from "react";
|
|
4
|
+
import { CopyCard } from "#/components/dashboard/copy-card";
|
|
5
|
+
import { AgentPickerCard } from "#/components/dashboard/agent-picker-card";
|
|
6
|
+
import { TabToggle } from "#/components/dashboard/tab-toggle";
|
|
7
|
+
import { AgentModelSelector } from "#/components/dashboard/agent-model-selector";
|
|
8
|
+
import { PolpoProvider } from "@polpo-ai/react";
|
|
9
|
+
import { Chat } from "@polpo-ai/chat";
|
|
10
|
+
import type { AgentConfig } from "@polpo-ai/core";
|
|
11
|
+
|
|
12
|
+
type SnippetTab = "curl" | "ts" | "playground";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* SDK snippet panel — agent picker + language toggle (curl / TS /
|
|
16
|
+
* Playground) + copy-able code snippet. Shared between ConnectDialog
|
|
17
|
+
* SDK tab and OnboardingChecklist "Chat with your agent" step.
|
|
18
|
+
*
|
|
19
|
+
* When `projectId` and `apiUrl` are provided, a third "Playground"
|
|
20
|
+
* tab appears that renders the same <Chat> UI against the session-
|
|
21
|
+
* authenticated proxy — the user can fire a real chat completion
|
|
22
|
+
* without leaving the panel.
|
|
23
|
+
*/
|
|
24
|
+
export function SdkSnippetPanel({
|
|
25
|
+
baseUrl,
|
|
26
|
+
agents,
|
|
27
|
+
agentConfigs,
|
|
28
|
+
defaultAgent,
|
|
29
|
+
sdkKey = "$POLPO_API_KEY",
|
|
30
|
+
compact = false,
|
|
31
|
+
projectId,
|
|
32
|
+
apiUrl,
|
|
33
|
+
}: {
|
|
34
|
+
baseUrl: string;
|
|
35
|
+
agents: string[] | null;
|
|
36
|
+
/** Full agent configs — enables the Playground tab to show model info
|
|
37
|
+
* next to the agent name. When omitted, Playground shows name only. */
|
|
38
|
+
agentConfigs?: AgentConfig[];
|
|
39
|
+
defaultAgent: string;
|
|
40
|
+
sdkKey?: string;
|
|
41
|
+
compact?: boolean;
|
|
42
|
+
projectId?: string;
|
|
43
|
+
apiUrl?: string;
|
|
44
|
+
}) {
|
|
45
|
+
const [agent, setAgent] = useState(defaultAgent);
|
|
46
|
+
const canUsePlayground = !!projectId && !!apiUrl;
|
|
47
|
+
const [lang, setLang] = useState<SnippetTab>(
|
|
48
|
+
canUsePlayground ? "playground" : "curl",
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// If `sdkKey` looks like a real key (starts with sk_), inline it in the
|
|
52
|
+
// snippet so a freshly-generated key is immediately copy-pasteable.
|
|
53
|
+
// Otherwise fall back to the $POLPO_API_KEY env-var placeholder.
|
|
54
|
+
const isRealKey = sdkKey.startsWith("sk_");
|
|
55
|
+
const tsKeyExpr = isRealKey ? `"${sdkKey}"` : "process.env.POLPO_API_KEY!";
|
|
56
|
+
const curlAuthKey = isRealKey ? sdkKey : "$POLPO_API_KEY";
|
|
57
|
+
|
|
58
|
+
const tsSnippet = `import { PolpoClient } from "@polpo-ai/sdk";
|
|
59
|
+
|
|
60
|
+
const polpo = new PolpoClient({
|
|
61
|
+
apiKey: ${tsKeyExpr},
|
|
62
|
+
baseUrl: "${baseUrl}",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const res = await polpo.chatCompletions({
|
|
66
|
+
model: "polpo",
|
|
67
|
+
agent: "${agent}",
|
|
68
|
+
stream: true,
|
|
69
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
70
|
+
});`;
|
|
71
|
+
|
|
72
|
+
const curlPreamble = isRealKey
|
|
73
|
+
? ""
|
|
74
|
+
: `# Load your API key from .env.local (created by polpo link)
|
|
75
|
+
export POLPO_API_KEY=$(grep POLPO_API_KEY .env.local | cut -d= -f2)
|
|
76
|
+
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
const curlSnippet = `${curlPreamble}curl ${baseUrl}/v1/chat/completions \\
|
|
80
|
+
-H "Authorization: Bearer ${curlAuthKey}" \\
|
|
81
|
+
-H "Content-Type: application/json" \\
|
|
82
|
+
-d '{
|
|
83
|
+
"model": "polpo",
|
|
84
|
+
"agent": "${agent}",
|
|
85
|
+
"stream": true,
|
|
86
|
+
"messages": [{"role":"user","content":"Hello"}]
|
|
87
|
+
}'`;
|
|
88
|
+
|
|
89
|
+
const options: { value: SnippetTab; label: string }[] = [
|
|
90
|
+
...(canUsePlayground
|
|
91
|
+
? [{ value: "playground" as const, label: "Playground" }]
|
|
92
|
+
: []),
|
|
93
|
+
{ value: "curl", label: "curl" },
|
|
94
|
+
{ value: "ts", label: "TypeScript" },
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className="flex flex-col gap-3">
|
|
99
|
+
<TabToggle value={lang} onChange={setLang} options={options} />
|
|
100
|
+
|
|
101
|
+
{!compact && lang !== "playground" && (
|
|
102
|
+
<AgentPickerCard
|
|
103
|
+
agents={agents}
|
|
104
|
+
agent={agent}
|
|
105
|
+
onChange={setAgent}
|
|
106
|
+
show={agents !== null && agents.length > 0}
|
|
107
|
+
/>
|
|
108
|
+
)}
|
|
109
|
+
|
|
110
|
+
{lang === "playground" && canUsePlayground && (
|
|
111
|
+
<InlinePlayground
|
|
112
|
+
projectId={projectId!}
|
|
113
|
+
apiUrl={apiUrl!}
|
|
114
|
+
agent={agent}
|
|
115
|
+
onAgentChange={setAgent}
|
|
116
|
+
agentConfigs={agentConfigs}
|
|
117
|
+
/>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{lang !== "playground" && (
|
|
121
|
+
<CopyCard
|
|
122
|
+
label={lang === "ts" ? "@polpo-ai/sdk" : "curl"}
|
|
123
|
+
value={lang === "ts" ? tsSnippet : curlSnippet}
|
|
124
|
+
lang={lang}
|
|
125
|
+
/>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* ------------------------------------------------------------------ */
|
|
132
|
+
/* Inline playground — same wiring as the /playground page, but */
|
|
133
|
+
/* constrained to a small panel for use inside onboarding / dialogs. */
|
|
134
|
+
/* ------------------------------------------------------------------ */
|
|
135
|
+
|
|
136
|
+
function InlinePlayground({
|
|
137
|
+
projectId,
|
|
138
|
+
apiUrl,
|
|
139
|
+
agent,
|
|
140
|
+
onAgentChange,
|
|
141
|
+
agentConfigs,
|
|
142
|
+
}: {
|
|
143
|
+
projectId: string;
|
|
144
|
+
apiUrl: string;
|
|
145
|
+
agent: string;
|
|
146
|
+
onAgentChange: (name: string) => void;
|
|
147
|
+
agentConfigs?: AgentConfig[];
|
|
148
|
+
}) {
|
|
149
|
+
const baseUrl = `${apiUrl}/v1/projects/${projectId}/data`;
|
|
150
|
+
const [sessionState, setSessionState] = useState<{ agent: string; id: string } | null>(null);
|
|
151
|
+
const sessionId = sessionState?.agent === agent ? sessionState.id : undefined;
|
|
152
|
+
const setCurrentSessionId = useCallback((id: string) => setSessionState({ agent, id }), [agent]);
|
|
153
|
+
|
|
154
|
+
const cookieFetch = useMemo<typeof globalThis.fetch>(
|
|
155
|
+
() =>
|
|
156
|
+
(async (input, init) => {
|
|
157
|
+
const res = await globalThis.fetch(input, {
|
|
158
|
+
...init,
|
|
159
|
+
credentials: "include",
|
|
160
|
+
});
|
|
161
|
+
const id = res.headers.get("x-session-id");
|
|
162
|
+
if (id) setCurrentSessionId(id);
|
|
163
|
+
return res;
|
|
164
|
+
}) as typeof globalThis.fetch,
|
|
165
|
+
[setCurrentSessionId],
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div className="flex flex-col gap-2">
|
|
170
|
+
{agentConfigs && agentConfigs.length > 0 && (
|
|
171
|
+
<div className="flex items-center justify-between">
|
|
172
|
+
<span className="text-[10px] font-mono uppercase tracking-[0.18em] text-muted-foreground/60">
|
|
173
|
+
Chatting with
|
|
174
|
+
</span>
|
|
175
|
+
<AgentModelSelector
|
|
176
|
+
agents={agentConfigs}
|
|
177
|
+
selected={agent}
|
|
178
|
+
onSelect={onAgentChange}
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
<div className="flex h-[420px] flex-col border border-border bg-card">
|
|
183
|
+
<PolpoProvider
|
|
184
|
+
baseUrl={baseUrl}
|
|
185
|
+
apiPrefix="/v1"
|
|
186
|
+
fetch={cookieFetch}
|
|
187
|
+
autoConnect={false}
|
|
188
|
+
>
|
|
189
|
+
<Chat key={agent} sessionId={sessionId} agent={agent} onSessionCreated={setCurrentSessionId} className="flex-1" />
|
|
190
|
+
</PolpoProvider>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared section header for the agent tab pages (Capabilities, Tools,
|
|
5
|
+
* Skills, …) so every internal section reads the same: a title with an
|
|
6
|
+
* optional count, and a one-line muted description. Keeps the tabs
|
|
7
|
+
* visually coherent.
|
|
8
|
+
*/
|
|
9
|
+
export function SectionHeader({
|
|
10
|
+
title,
|
|
11
|
+
count,
|
|
12
|
+
description,
|
|
13
|
+
}: {
|
|
14
|
+
/** Optional — omit when a tab already labels the section (avoids a
|
|
15
|
+
* redundant heading). Description-only is rendered then. */
|
|
16
|
+
title?: string;
|
|
17
|
+
count?: number;
|
|
18
|
+
description?: ReactNode;
|
|
19
|
+
}) {
|
|
20
|
+
return (
|
|
21
|
+
<div>
|
|
22
|
+
{title && (
|
|
23
|
+
<h3 className="text-sm font-semibold">
|
|
24
|
+
{title}
|
|
25
|
+
{count !== undefined && (
|
|
26
|
+
<span className="ml-1 font-mono text-muted-foreground/60">({count})</span>
|
|
27
|
+
)}
|
|
28
|
+
</h3>
|
|
29
|
+
)}
|
|
30
|
+
{description && (
|
|
31
|
+
<p className={`text-xs text-muted-foreground ${title ? "mt-1" : ""}`}>
|
|
32
|
+
{description}
|
|
33
|
+
</p>
|
|
34
|
+
)}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|