@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.
Files changed (135) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +44 -0
  3. package/app/(dashboard)/error.tsx +47 -0
  4. package/app/(dashboard)/layout.tsx +41 -0
  5. package/app/(dashboard)/loading.tsx +30 -0
  6. package/app/(dashboard)/projects/[id]/agents/[name]/memory/view.tsx +36 -0
  7. package/app/(dashboard)/projects/[id]/agents/[name]/models-view.tsx +30 -0
  8. package/app/(dashboard)/projects/[id]/agents/[name]/page.tsx +51 -0
  9. package/app/(dashboard)/projects/[id]/agents/[name]/prompt/view.tsx +211 -0
  10. package/app/(dashboard)/projects/[id]/agents/[name]/skills/view.tsx +84 -0
  11. package/app/(dashboard)/projects/[id]/agents/[name]/tools/view.tsx +525 -0
  12. package/app/(dashboard)/projects/[id]/agents/[name]/vault/view.tsx +61 -0
  13. package/app/(dashboard)/projects/[id]/agents/page.tsx +39 -0
  14. package/app/(dashboard)/projects/[id]/agents/view.tsx +283 -0
  15. package/app/(dashboard)/projects/[id]/layout.tsx +11 -0
  16. package/app/(dashboard)/projects/[id]/logs/page.tsx +27 -0
  17. package/app/(dashboard)/projects/[id]/logs/view.tsx +184 -0
  18. package/app/(dashboard)/projects/[id]/memory/page.tsx +40 -0
  19. package/app/(dashboard)/projects/[id]/memory/view.tsx +17 -0
  20. package/app/(dashboard)/projects/[id]/missions/[missionId]/page.tsx +88 -0
  21. package/app/(dashboard)/projects/[id]/missions/[missionId]/view.tsx +569 -0
  22. package/app/(dashboard)/projects/[id]/missions/page.tsx +32 -0
  23. package/app/(dashboard)/projects/[id]/missions/view.tsx +282 -0
  24. package/app/(dashboard)/projects/[id]/onboarding-checklist.tsx +359 -0
  25. package/app/(dashboard)/projects/[id]/page.tsx +68 -0
  26. package/app/(dashboard)/projects/[id]/playbooks/[name]/page.tsx +5 -0
  27. package/app/(dashboard)/projects/[id]/playbooks/[name]/view.tsx +433 -0
  28. package/app/(dashboard)/projects/[id]/playbooks/page.tsx +5 -0
  29. package/app/(dashboard)/projects/[id]/playbooks/view.tsx +222 -0
  30. package/app/(dashboard)/projects/[id]/schedules/page.tsx +44 -0
  31. package/app/(dashboard)/projects/[id]/schedules/view.tsx +385 -0
  32. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/page.tsx +55 -0
  33. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/raw-view.tsx +373 -0
  34. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/view.tsx +124 -0
  35. package/app/(dashboard)/projects/[id]/sessions/page.tsx +31 -0
  36. package/app/(dashboard)/projects/[id]/sessions/view.tsx +242 -0
  37. package/app/(dashboard)/projects/[id]/settings/page.tsx +52 -0
  38. package/app/(dashboard)/projects/[id]/skills/[name]/page.tsx +65 -0
  39. package/app/(dashboard)/projects/[id]/skills/[name]/view.tsx +227 -0
  40. package/app/(dashboard)/projects/[id]/skills/page.tsx +31 -0
  41. package/app/(dashboard)/projects/[id]/skills/view.tsx +243 -0
  42. package/app/(dashboard)/projects/[id]/storage/page.tsx +5 -0
  43. package/app/(dashboard)/projects/[id]/storage/view.tsx +385 -0
  44. package/app/(dashboard)/projects/[id]/tasks/[taskId]/activity/view.tsx +336 -0
  45. package/app/(dashboard)/projects/[id]/tasks/[taskId]/assessment/view.tsx +113 -0
  46. package/app/(dashboard)/projects/[id]/tasks/[taskId]/blueprint.ts +72 -0
  47. package/app/(dashboard)/projects/[id]/tasks/[taskId]/output/view.tsx +90 -0
  48. package/app/(dashboard)/projects/[id]/tasks/[taskId]/page.tsx +63 -0
  49. package/app/(dashboard)/projects/[id]/tasks/[taskId]/view.tsx +137 -0
  50. package/app/(dashboard)/projects/[id]/tasks/page.tsx +40 -0
  51. package/app/(dashboard)/projects/[id]/tasks/view.tsx +421 -0
  52. package/app/(dashboard)/projects/[id]/view.tsx +421 -0
  53. package/app/(dashboard)/projects/[id]/welcome-banner.tsx +156 -0
  54. package/app/(dashboard)/projects/projects-list.tsx +126 -0
  55. package/app/(playground)/error.tsx +42 -0
  56. package/app/(playground)/layout.tsx +27 -0
  57. package/app/(playground)/projects/[id]/playground/page.tsx +57 -0
  58. package/app/(playground)/projects/[id]/playground/view.tsx +167 -0
  59. package/app/(playground)/projects/[id]/playground-legacy/page.tsx +47 -0
  60. package/app/(playground)/projects/[id]/playground-legacy/skeleton.tsx +63 -0
  61. package/app/(playground)/projects/[id]/playground-legacy/view.tsx +237 -0
  62. package/app/favicon.ico +0 -0
  63. package/app/globals.css +385 -0
  64. package/app/icon.svg +6 -0
  65. package/app/layout.tsx +39 -0
  66. package/app/page.tsx +11 -0
  67. package/components/ai-elements/code-block.tsx +562 -0
  68. package/components/dashboard/agent-capabilities.tsx +326 -0
  69. package/components/dashboard/agent-identity-header.tsx +50 -0
  70. package/components/dashboard/agent-model-picker.tsx +195 -0
  71. package/components/dashboard/agent-model-selector.tsx +121 -0
  72. package/components/dashboard/agent-picker-card.tsx +46 -0
  73. package/components/dashboard/agent-studio.tsx +242 -0
  74. package/components/dashboard/agents-table.tsx +48 -0
  75. package/components/dashboard/breadcrumb.tsx +70 -0
  76. package/components/dashboard/builder-chat.tsx +418 -0
  77. package/components/dashboard/chat-shell.tsx +40 -0
  78. package/components/dashboard/client-picker.tsx +63 -0
  79. package/components/dashboard/command-snippet.tsx +77 -0
  80. package/components/dashboard/connect-dialog.tsx +680 -0
  81. package/components/dashboard/copy-button.tsx +45 -0
  82. package/components/dashboard/copy-card.tsx +48 -0
  83. package/components/dashboard/file-browser.tsx +161 -0
  84. package/components/dashboard/hint.tsx +22 -0
  85. package/components/dashboard/inference-mode.tsx +15 -0
  86. package/components/dashboard/manual-refresh-button.tsx +52 -0
  87. package/components/dashboard/markdown.tsx +31 -0
  88. package/components/dashboard/mcp-install-panel.tsx +196 -0
  89. package/components/dashboard/mission-graph.tsx +262 -0
  90. package/components/dashboard/nav-tabs.tsx +86 -0
  91. package/components/dashboard/polpo-chat.tsx +468 -0
  92. package/components/dashboard/project-copilot.tsx +253 -0
  93. package/components/dashboard/sdk-snippet-panel.tsx +194 -0
  94. package/components/dashboard/section-header.tsx +37 -0
  95. package/components/dashboard/settings-form.tsx +2477 -0
  96. package/components/dashboard/sidebar.tsx +385 -0
  97. package/components/dashboard/skeletons.tsx +1054 -0
  98. package/components/dashboard/skills-install-wizard.tsx +273 -0
  99. package/components/dashboard/swarm-runs.tsx +316 -0
  100. package/components/dashboard/tab-toggle.tsx +35 -0
  101. package/components/dashboard/task-studio.tsx +224 -0
  102. package/components/dashboard/top-header.tsx +203 -0
  103. package/components/dashboard/webhook-deliveries.tsx +234 -0
  104. package/components/icons/coding-agents.tsx +151 -0
  105. package/components/json-ld.tsx +8 -0
  106. package/components/providers.tsx +25 -0
  107. package/components/ui/badge.tsx +52 -0
  108. package/components/ui/button.tsx +60 -0
  109. package/components/ui/card.tsx +103 -0
  110. package/components/ui/chart.tsx +356 -0
  111. package/components/ui/command.tsx +196 -0
  112. package/components/ui/dialog.tsx +157 -0
  113. package/components/ui/input-group.tsx +158 -0
  114. package/components/ui/input.tsx +20 -0
  115. package/components/ui/multi-select.tsx +161 -0
  116. package/components/ui/popover.tsx +90 -0
  117. package/components/ui/select.tsx +201 -0
  118. package/components/ui/separator.tsx +25 -0
  119. package/components/ui/sheet.tsx +135 -0
  120. package/components/ui/skeleton.tsx +13 -0
  121. package/components/ui/tabs.tsx +56 -0
  122. package/components/ui/textarea.tsx +18 -0
  123. package/components/ui/tooltip.tsx +66 -0
  124. package/hooks/use-desktop-sidebar.tsx +50 -0
  125. package/hooks/use-mobile-sidebar.tsx +37 -0
  126. package/index.ts +14 -0
  127. package/lib/api.ts +194 -0
  128. package/lib/builder-context.ts +60 -0
  129. package/lib/data-client.ts +68 -0
  130. package/lib/get-query-client.ts +25 -0
  131. package/lib/polpo-client.tsx +49 -0
  132. package/lib/tool-catalog.ts +234 -0
  133. package/lib/use-event-catalog.ts +28 -0
  134. package/lib/utils.ts +6 -0
  135. 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
+ }