@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,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'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
|
+
}
|