@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,126 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import Link from "next/link";
5
+ import { LayoutGrid, List } from "lucide-react";
6
+ import type { Project } from "#/lib/api";
7
+
8
+ function timeAgo(date: string) {
9
+ const diff = Date.now() - new Date(date).getTime();
10
+ const days = Math.floor(diff / 86400000);
11
+ if (days > 0) return `${days}d ago`;
12
+ const hours = Math.floor(diff / 3600000);
13
+ if (hours > 0) return `${hours}h ago`;
14
+ return "just now";
15
+ }
16
+
17
+ function StatusDot({ status }: { status: string }) {
18
+ return (
19
+ <span className={`h-2 w-2 rounded-full shrink-0 ${
20
+ status === "active" ? "bg-brand" : "bg-muted-foreground/30"
21
+ }`} />
22
+ );
23
+ }
24
+
25
+ function GridView({ projects }: { projects: Project[] }) {
26
+ return (
27
+ <div data-testid="projects-grid" className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
28
+ {projects.map((project) => (
29
+ <Link
30
+ key={project.id}
31
+ href={`/projects/${project.id}`}
32
+ data-testid={`project-card-${project.slug}`}
33
+ className="group border border-border bg-card p-5 transition-colors hover:border-foreground/20"
34
+ >
35
+ <div className="flex items-center justify-between">
36
+ <h3 className="text-sm font-semibold">{project.name}</h3>
37
+ <StatusDot status={project.status} />
38
+ </div>
39
+ <p className="mt-1 font-mono text-xs text-muted-foreground">
40
+ {project.slug}
41
+ </p>
42
+ <div className="mt-4 flex items-center justify-between text-[11px] text-muted-foreground">
43
+ <span>Updated {timeAgo(project.updatedAt)}</span>
44
+ <span className="font-mono">{project.status}</span>
45
+ </div>
46
+ </Link>
47
+ ))}
48
+ </div>
49
+ );
50
+ }
51
+
52
+ function TableView({ projects }: { projects: Project[] }) {
53
+ return (
54
+ <div data-testid="projects-table" className="mt-8 border border-border overflow-hidden overflow-x-auto">
55
+ <table className="w-full text-sm min-w-[500px]">
56
+ <thead>
57
+ <tr className="border-b border-border bg-secondary/50">
58
+ <th className="px-4 py-2.5 text-left text-xs font-medium text-muted-foreground">Name</th>
59
+ <th className="px-4 py-2.5 text-left text-xs font-medium text-muted-foreground">Slug</th>
60
+ <th className="px-4 py-2.5 text-left text-xs font-medium text-muted-foreground">Status</th>
61
+ <th className="px-4 py-2.5 text-left text-xs font-medium text-muted-foreground">Updated</th>
62
+ </tr>
63
+ </thead>
64
+ <tbody>
65
+ {projects.map((project) => (
66
+ <tr key={project.id} className="border-b border-border last:border-0">
67
+ <td className="px-4 py-3">
68
+ <Link href={`/projects/${project.id}`} className="font-medium hover:text-brand transition-colors">
69
+ {project.name}
70
+ </Link>
71
+ </td>
72
+ <td className="px-4 py-3 font-mono text-xs text-muted-foreground">{project.slug}</td>
73
+ <td className="px-4 py-3">
74
+ <div className="flex items-center gap-2">
75
+ <StatusDot status={project.status} />
76
+ <span className="text-xs text-muted-foreground">{project.status}</span>
77
+ </div>
78
+ </td>
79
+ <td className="px-4 py-3 text-xs text-muted-foreground">{timeAgo(project.updatedAt)}</td>
80
+ </tr>
81
+ ))}
82
+ </tbody>
83
+ </table>
84
+ </div>
85
+ );
86
+ }
87
+
88
+ export function ProjectsList({ projects }: { projects: Project[] }) {
89
+ const [view, setView] = useState<"grid" | "table">("grid");
90
+
91
+ if (projects.length === 0) {
92
+ return (
93
+ <div data-testid="projects-empty" className="mt-8 border border-border p-12 text-center">
94
+ <p className="text-sm text-muted-foreground">No projects yet.</p>
95
+ <p className="mt-1 text-xs text-muted-foreground">Create your first project to get started.</p>
96
+ </div>
97
+ );
98
+ }
99
+
100
+ return (
101
+ <div>
102
+ <div className="mt-6 flex items-center justify-between">
103
+ <p className="text-xs text-muted-foreground font-mono uppercase tracking-wider">
104
+ {projects.length} project{projects.length !== 1 ? "s" : ""}
105
+ </p>
106
+ <div className="flex items-center border border-border">
107
+ <button
108
+ onClick={() => setView("grid")}
109
+ className={`p-1.5 transition-colors ${view === "grid" ? "bg-secondary text-foreground" : "text-muted-foreground/40 hover:text-muted-foreground"}`}
110
+ aria-label="Grid view"
111
+ >
112
+ <LayoutGrid className="h-3.5 w-3.5" />
113
+ </button>
114
+ <button
115
+ onClick={() => setView("table")}
116
+ className={`p-1.5 transition-colors ${view === "table" ? "bg-secondary text-foreground" : "text-muted-foreground/40 hover:text-muted-foreground"}`}
117
+ aria-label="Table view"
118
+ >
119
+ <List className="h-3.5 w-3.5" />
120
+ </button>
121
+ </div>
122
+ </div>
123
+ {view === "grid" ? <GridView projects={projects} /> : <TableView projects={projects} />}
124
+ </div>
125
+ );
126
+ }
@@ -0,0 +1,42 @@
1
+ "use client";
2
+
3
+ /** Error boundary for the playground group — see app/(dashboard)/error.tsx. */
4
+ export default function PlaygroundError({
5
+ error,
6
+ reset,
7
+ }: {
8
+ error: Error & { digest?: string };
9
+ reset: () => void;
10
+ }) {
11
+ return (
12
+ <div className="flex min-h-[60vh] items-center justify-center p-6">
13
+ <div className="w-full max-w-md border border-border bg-card p-8 text-center">
14
+ <h2 className="text-base font-semibold text-foreground">Something went wrong</h2>
15
+ <p className="mt-2 text-sm text-muted-foreground">
16
+ We couldn&apos;t reach the control plane. This is usually temporary —
17
+ give it a moment and retry.
18
+ </p>
19
+ {error?.digest ? (
20
+ <p className="mt-3 font-mono text-[11px] text-muted-foreground/50">
21
+ ref: {error.digest}
22
+ </p>
23
+ ) : null}
24
+ <div className="mt-6 flex items-center justify-center gap-3">
25
+ <button
26
+ type="button"
27
+ onClick={() => reset()}
28
+ className="border border-foreground bg-foreground px-4 py-2 text-sm font-medium text-background transition-colors hover:bg-foreground/90"
29
+ >
30
+ Try again
31
+ </button>
32
+ <a
33
+ href="/login"
34
+ className="border border-border px-4 py-2 text-sm font-medium text-foreground transition-colors hover:border-foreground/40"
35
+ >
36
+ Sign in
37
+ </a>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,27 @@
1
+ import { redirect } from "next/navigation";
2
+ import { Providers } from "#/components/providers";
3
+ import { getOrgs } from "#/lib/api";
4
+
5
+ /**
6
+ * Isolated layout for the Playground tab. No project sidebar — this is a
7
+ * standalone surface (opened in a new browser tab) so end-users / builders
8
+ * can clearly see they're chatting with an agent, not poking around the
9
+ * dashboard. The PlaygroundFrame inside the page renders its own top bar
10
+ * with a "Back to dashboard" link.
11
+ */
12
+ export default async function PlaygroundLayout({
13
+ children,
14
+ }: {
15
+ children: React.ReactNode;
16
+ }) {
17
+ const orgs = await getOrgs();
18
+ if (orgs.length === 0) redirect("/onboarding");
19
+
20
+ return (
21
+ <Providers>
22
+ <div className="flex h-screen flex-col overflow-hidden bg-background">
23
+ {children}
24
+ </div>
25
+ </Providers>
26
+ );
27
+ }
@@ -0,0 +1,57 @@
1
+ import { Suspense } from "react";
2
+ import { api, dataApi } from "#/lib/api";
3
+ import type { Project } from "#/lib/api";
4
+ import type { AgentConfig } from "@polpo-ai/core";
5
+ import PlaygroundView from "./view";
6
+ import { PlaygroundSkeleton } from "../playground-legacy/skeleton";
7
+
8
+ const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
9
+
10
+ async function PlaygroundData({
11
+ id,
12
+ initialAgent,
13
+ }: {
14
+ id: string;
15
+ initialAgent?: string;
16
+ }) {
17
+ let project: Project | null = null;
18
+ let agents: AgentConfig[] = [];
19
+
20
+ try {
21
+ const [projectRes, agentsRes] = await Promise.all([
22
+ api<Project>(`/v1/projects/${id}`).catch(() => null),
23
+ dataApi<{ ok: boolean; data: AgentConfig[] }>(id, "/v1/agents").catch(
24
+ () => ({ ok: false, data: [] as AgentConfig[] }),
25
+ ),
26
+ ]);
27
+ project = projectRes;
28
+ agents = agentsRes.data ?? [];
29
+ } catch {}
30
+
31
+ return (
32
+ <PlaygroundView
33
+ projectId={id}
34
+ apiUrl={API_URL}
35
+ projectName={project?.name ?? "Your project"}
36
+ initialAgents={agents}
37
+ initialAgent={initialAgent}
38
+ />
39
+ );
40
+ }
41
+
42
+ export default async function PlaygroundPage({
43
+ params,
44
+ searchParams,
45
+ }: {
46
+ params: Promise<{ id: string }>;
47
+ searchParams: Promise<{ agent?: string }>;
48
+ }) {
49
+ const { id } = await params;
50
+ const { agent } = await searchParams;
51
+
52
+ return (
53
+ <Suspense fallback={<PlaygroundSkeleton />}>
54
+ <PlaygroundData id={id} initialAgent={agent} />
55
+ </Suspense>
56
+ );
57
+ }
@@ -0,0 +1,167 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Playground — chat surface backed by `@lumea-labs/chat` +
5
+ * `@lumea-labs/chat-polpo`. The chat composition itself lives in the
6
+ * shared `<PolpoChat>` (components/dashboard/polpo-chat.tsx) so the
7
+ * Playground and the Agent Builder are guaranteed identical.
8
+ *
9
+ * This view owns only the surrounding chrome: the top bar (agent
10
+ * selector + Files button) and the no-agents empty state.
11
+ */
12
+
13
+ import { useState } from "react";
14
+ import Link from "next/link";
15
+ import Image from "next/image";
16
+ import type { AgentConfig } from "@polpo-ai/core";
17
+ import { ArrowLeft, FolderOpen, Plus } from "lucide-react";
18
+ import { AgentModelSelector } from "#/components/dashboard/agent-model-selector";
19
+ import { PolpoChat } from "#/components/dashboard/polpo-chat";
20
+
21
+ interface Props {
22
+ projectId: string;
23
+ apiUrl: string;
24
+ projectName: string;
25
+ initialAgents: AgentConfig[];
26
+ /** Agent to preselect (from ?agent=). Falls back to the first agent. */
27
+ initialAgent?: string;
28
+ }
29
+
30
+ export default function PlaygroundView({
31
+ projectId,
32
+ apiUrl,
33
+ projectName,
34
+ initialAgents,
35
+ initialAgent,
36
+ }: Props) {
37
+ const baseUrl = `${apiUrl}/v1/projects/${projectId}/data`;
38
+
39
+ const [selectedAgent, setSelectedAgent] = useState<string | undefined>(
40
+ (initialAgent && initialAgents.some((a) => a.name === initialAgent)
41
+ ? initialAgent
42
+ : undefined) ?? initialAgents[0]?.name,
43
+ );
44
+ const [filesOpen, setFilesOpen] = useState(false);
45
+
46
+ return (
47
+ <>
48
+ <TopBar
49
+ projectId={projectId}
50
+ projectName={projectName}
51
+ agents={initialAgents}
52
+ selectedAgent={selectedAgent}
53
+ onSelectAgent={setSelectedAgent}
54
+ onOpenFiles={() => setFilesOpen(true)}
55
+ />
56
+
57
+ <main className="flex flex-1 flex-col overflow-hidden">
58
+ {initialAgents.length === 0 ? (
59
+ <div className="flex flex-1 items-center justify-center px-4 py-8">
60
+ <EmptyState projectId={projectId} />
61
+ </div>
62
+ ) : (
63
+ <PolpoChat
64
+ baseUrl={baseUrl}
65
+ agent={selectedAgent}
66
+ filesOpen={filesOpen}
67
+ onFilesOpenChange={setFilesOpen}
68
+ />
69
+ )}
70
+ </main>
71
+ </>
72
+ );
73
+ }
74
+
75
+ function TopBar({
76
+ projectId,
77
+ projectName,
78
+ agents,
79
+ selectedAgent,
80
+ onSelectAgent,
81
+ onOpenFiles,
82
+ }: {
83
+ projectId: string;
84
+ projectName: string;
85
+ agents: AgentConfig[];
86
+ selectedAgent: string | undefined;
87
+ onSelectAgent: (name: string) => void;
88
+ onOpenFiles: () => void;
89
+ }) {
90
+ return (
91
+ <header className="flex h-14 shrink-0 items-center justify-between gap-4 border-b border-border bg-card px-4">
92
+ <div className="flex items-center gap-3 min-w-0">
93
+ <Link
94
+ href={`/projects/${projectId}`}
95
+ className="inline-flex items-center gap-1.5 rounded border border-border px-2.5 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-foreground/30 hover:text-foreground"
96
+ >
97
+ <ArrowLeft className="h-3.5 w-3.5" />
98
+ Back to dashboard
99
+ </Link>
100
+
101
+ <div className="hidden md:block h-5 w-px bg-border" />
102
+
103
+ <div className="hidden md:flex items-center gap-2 min-w-0">
104
+ <Image
105
+ src="/polpo-logo.svg"
106
+ alt="Polpo"
107
+ width={72}
108
+ height={16}
109
+ priority
110
+ className="h-4 w-auto opacity-70"
111
+ />
112
+ <span className="text-xs uppercase tracking-wide text-muted-foreground">
113
+ Playground
114
+ </span>
115
+ <span className="text-muted-foreground/50">·</span>
116
+ <span className="truncate text-sm font-medium text-foreground/80 font-mono">
117
+ {projectName}
118
+ </span>
119
+ </div>
120
+ </div>
121
+
122
+ <div className="flex items-center gap-2">
123
+ <button
124
+ type="button"
125
+ onClick={onOpenFiles}
126
+ className="inline-flex h-8 items-center gap-1.5 rounded border border-border bg-card px-2.5 text-xs font-medium text-muted-foreground transition-colors hover:border-foreground/30 hover:text-foreground"
127
+ aria-label="Open files"
128
+ title="Open files"
129
+ >
130
+ <FolderOpen className="h-3.5 w-3.5" />
131
+ <span className="hidden sm:inline">Files</span>
132
+ </button>
133
+
134
+ {agents.length > 0 && (
135
+ <AgentModelSelector
136
+ agents={agents}
137
+ selected={selectedAgent}
138
+ onSelect={onSelectAgent}
139
+ />
140
+ )}
141
+ </div>
142
+ </header>
143
+ );
144
+ }
145
+
146
+ function EmptyState({ projectId }: { projectId: string }) {
147
+ return (
148
+ <div className="flex flex-col items-center justify-center gap-4 border border-dashed border-border p-12 text-center max-w-md">
149
+ <div className="flex h-10 w-10 items-center justify-center border border-border">
150
+ <Plus className="h-5 w-5 text-muted-foreground" />
151
+ </div>
152
+ <div>
153
+ <p className="text-sm font-medium">No agents yet</p>
154
+ <p className="mt-1 text-xs text-muted-foreground">
155
+ Create an agent from the dashboard, or deploy one from your CLI with{" "}
156
+ <span className="font-mono">polpo deploy</span>.
157
+ </p>
158
+ </div>
159
+ <Link
160
+ href={`/projects/${projectId}/agents`}
161
+ className="mt-2 bg-foreground text-background px-3 py-1.5 text-xs font-medium transition-all hover:bg-brand hover:text-brand-foreground"
162
+ >
163
+ Go to Agents
164
+ </Link>
165
+ </div>
166
+ );
167
+ }
@@ -0,0 +1,47 @@
1
+ import { Suspense } from "react";
2
+ import { api, dataApi } from "#/lib/api";
3
+ import type { Project } from "#/lib/api";
4
+ import type { AgentConfig } from "@polpo-ai/core";
5
+ import PlaygroundView from "./view";
6
+ import { PlaygroundSkeleton } from "./skeleton";
7
+
8
+ const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
9
+
10
+ async function PlaygroundData({ id }: { id: string }) {
11
+ let project: Project | null = null;
12
+ let agents: AgentConfig[] = [];
13
+
14
+ try {
15
+ const [projectRes, agentsRes] = await Promise.all([
16
+ api<Project>(`/v1/projects/${id}`).catch(() => null),
17
+ dataApi<{ ok: boolean; data: AgentConfig[] }>(id, "/v1/agents").catch(
18
+ () => ({ ok: false, data: [] as AgentConfig[] }),
19
+ ),
20
+ ]);
21
+ project = projectRes;
22
+ agents = agentsRes.data ?? [];
23
+ } catch {}
24
+
25
+ return (
26
+ <PlaygroundView
27
+ projectId={id}
28
+ apiUrl={API_URL}
29
+ projectName={project?.name ?? "Your project"}
30
+ initialAgents={agents}
31
+ />
32
+ );
33
+ }
34
+
35
+ export default async function PlaygroundPage({
36
+ params,
37
+ }: {
38
+ params: Promise<{ id: string }>;
39
+ }) {
40
+ const { id } = await params;
41
+
42
+ return (
43
+ <Suspense fallback={<PlaygroundSkeleton />}>
44
+ <PlaygroundData id={id} />
45
+ </Suspense>
46
+ );
47
+ }
@@ -0,0 +1,63 @@
1
+ import { Skeleton } from "#/components/ui/skeleton";
2
+
3
+ export function PlaygroundSkeleton() {
4
+ return (
5
+ <>
6
+ {/* Top bar — mirrors view.tsx: back button + brand row + selector */}
7
+ <div className="flex h-14 shrink-0 items-center justify-between gap-4 border-b border-border bg-card px-4">
8
+ <div className="flex items-center gap-3">
9
+ <Skeleton className="h-7 w-32" />
10
+ <div className="hidden md:block h-5 w-px bg-border" />
11
+ <div className="hidden md:flex items-center gap-2">
12
+ <Skeleton className="h-4 w-16 opacity-60" />
13
+ <Skeleton className="h-3 w-20 opacity-60" />
14
+ </div>
15
+ </div>
16
+ <div className="flex items-center gap-3 border border-border bg-card px-3 py-2">
17
+ <Skeleton className="size-6" />
18
+ <div className="space-y-1.5">
19
+ <Skeleton className="h-3.5 w-20" />
20
+ <Skeleton className="h-2.5 w-28 opacity-60" />
21
+ </div>
22
+ <Skeleton className="ml-2 size-3.5" />
23
+ </div>
24
+ </div>
25
+
26
+ {/* Chat pane fills the rest of the viewport. */}
27
+ <main className="flex flex-1 flex-col overflow-hidden">
28
+ <div className="flex-1 space-y-6 px-6 py-8">
29
+ <MessageSkeleton role="user" />
30
+ <MessageSkeleton role="assistant" />
31
+ <MessageSkeleton role="user" short />
32
+ <MessageSkeleton role="assistant" />
33
+ </div>
34
+
35
+ <div className="px-6 pb-4 pt-3">
36
+ <Skeleton className="h-11 w-full rounded-2xl" />
37
+ </div>
38
+ </main>
39
+ </>
40
+ );
41
+ }
42
+
43
+ function MessageSkeleton({
44
+ role,
45
+ short,
46
+ }: {
47
+ role: "user" | "assistant";
48
+ short?: boolean;
49
+ }) {
50
+ const isUser = role === "user";
51
+ return (
52
+ <div className={`flex ${isUser ? "justify-end" : "justify-start"}`}>
53
+ <div className={`max-w-[70%] space-y-2 ${isUser ? "text-right" : ""}`}>
54
+ <Skeleton
55
+ className={`h-3 ${isUser ? "ml-auto w-16" : "w-20"} opacity-60`}
56
+ />
57
+ <Skeleton className={`h-4 ${short ? "w-32" : "w-full min-w-[280px]"}`} />
58
+ {!short && <Skeleton className="h-4 w-4/5" />}
59
+ {!short && !isUser && <Skeleton className="h-4 w-3/5" />}
60
+ </div>
61
+ </div>
62
+ );
63
+ }