@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
package/LICENSE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright 2026-present Lumea Labs
|
|
2
|
+
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License.
|
|
5
|
+
You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
See the License for the specific language governing permissions and
|
|
13
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# @polpo-ai/dashboard
|
|
2
|
+
|
|
3
|
+
The self-hosted Polpo dashboard — the single-instance debugger UI for one Polpo
|
|
4
|
+
server: agents, tasks, missions, sessions, memory, skills, playbooks, schedules,
|
|
5
|
+
storage, logs and a chat playground.
|
|
6
|
+
|
|
7
|
+
It talks to your Polpo server through the public SDK (`@polpo-ai/sdk`). There is
|
|
8
|
+
no auth, billing, org or onboarding here — those are cloud-only. One server, one
|
|
9
|
+
dashboard.
|
|
10
|
+
|
|
11
|
+
## Run it
|
|
12
|
+
|
|
13
|
+
You need a running Polpo server (the data plane). Then:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cp .env.example .env.local # point NEXT_PUBLIC_API_URL at your server
|
|
17
|
+
pnpm install
|
|
18
|
+
pnpm dev # http://localhost:3000
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`/` redirects to `/projects/local/agents`. `local` is a placeholder project id —
|
|
22
|
+
single-tenant self-host has one project, and the data client ignores the id.
|
|
23
|
+
|
|
24
|
+
## Config
|
|
25
|
+
|
|
26
|
+
| Env var | Default | What |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| `NEXT_PUBLIC_API_URL` | `http://localhost:4000` | Your Polpo server base URL |
|
|
29
|
+
| `NEXT_PUBLIC_POLPO_API_PREFIX` | `/api/v1` | API root the server mounts under (the standalone server serves at `/api/v1`) |
|
|
30
|
+
| `NEXT_PUBLIC_POLPO_API_KEY` | — | `sk_...` key for the server (optional if it runs without auth) |
|
|
31
|
+
|
|
32
|
+
## How the data layer works
|
|
33
|
+
|
|
34
|
+
Views address the data plane by path (`/v1/agents`, `/v1/tasks`, …). `lib/data-client.ts`
|
|
35
|
+
drives a single `PolpoClient` from `@polpo-ai/sdk` (transport, auth, errors) and
|
|
36
|
+
re-wraps the result in the `{ ok, data }` envelope the views expect. Swapping the
|
|
37
|
+
server is just `NEXT_PUBLIC_API_URL`.
|
|
38
|
+
|
|
39
|
+
## Not wired yet
|
|
40
|
+
|
|
41
|
+
A few shell/settings surfaces still reach for cloud-only control-plane endpoints
|
|
42
|
+
(project name, BYOK keys, billing). These are gated to single-tenant stubs so the
|
|
43
|
+
dashboard runs; see the `TODO` in `lib/data-client.ts`. BYOK will be re-pointed to
|
|
44
|
+
the data-plane `vault` route.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Error boundary for the dashboard group. A transient backend failure
|
|
5
|
+
* (control-plane 5xx / network / DB unreachable) now surfaces here instead
|
|
6
|
+
* of being swallowed into "no orgs → /onboarding". Auth failures (401) are
|
|
7
|
+
* handled upstream by `getOrgs` (redirect to /login).
|
|
8
|
+
*/
|
|
9
|
+
export default function DashboardError({
|
|
10
|
+
error,
|
|
11
|
+
reset,
|
|
12
|
+
}: {
|
|
13
|
+
error: Error & { digest?: string };
|
|
14
|
+
reset: () => void;
|
|
15
|
+
}) {
|
|
16
|
+
return (
|
|
17
|
+
<div className="flex min-h-[60vh] items-center justify-center p-6">
|
|
18
|
+
<div className="w-full max-w-md border border-border bg-card p-8 text-center">
|
|
19
|
+
<h2 className="text-base font-semibold text-foreground">Something went wrong</h2>
|
|
20
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
21
|
+
We couldn't reach the control plane. This is usually temporary —
|
|
22
|
+
give it a moment and retry.
|
|
23
|
+
</p>
|
|
24
|
+
{error?.digest ? (
|
|
25
|
+
<p className="mt-3 font-mono text-[11px] text-muted-foreground/50">
|
|
26
|
+
ref: {error.digest}
|
|
27
|
+
</p>
|
|
28
|
+
) : null}
|
|
29
|
+
<div className="mt-6 flex items-center justify-center gap-3">
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
onClick={() => reset()}
|
|
33
|
+
className="border border-foreground bg-foreground px-4 py-2 text-sm font-medium text-background transition-colors hover:bg-foreground/90"
|
|
34
|
+
>
|
|
35
|
+
Try again
|
|
36
|
+
</button>
|
|
37
|
+
<a
|
|
38
|
+
href="/login"
|
|
39
|
+
className="border border-border px-4 py-2 text-sm font-medium text-foreground transition-colors hover:border-foreground/40"
|
|
40
|
+
>
|
|
41
|
+
Sign in
|
|
42
|
+
</a>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Sidebar, MobileSidebar, MobileHeader } from "#/components/dashboard/sidebar";
|
|
2
|
+
import { TopHeader } from "#/components/dashboard/top-header";
|
|
3
|
+
import { Providers } from "#/components/providers";
|
|
4
|
+
import { MobileSidebarProvider } from "#/hooks/use-mobile-sidebar";
|
|
5
|
+
import { DesktopSidebarProvider } from "#/hooks/use-desktop-sidebar";
|
|
6
|
+
import { CopilotLayout } from "#/components/dashboard/project-copilot";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Single-tenant self-host shell. No Better Auth session, no org list, no
|
|
10
|
+
* onboarding gate — those live only in the cloud build. One local instance,
|
|
11
|
+
* straight into the dashboard chrome.
|
|
12
|
+
*/
|
|
13
|
+
export default function DashboardLayout({
|
|
14
|
+
children,
|
|
15
|
+
}: {
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
}) {
|
|
18
|
+
return (
|
|
19
|
+
<Providers>
|
|
20
|
+
<MobileSidebarProvider>
|
|
21
|
+
<DesktopSidebarProvider>
|
|
22
|
+
<div className="flex h-screen flex-col overflow-hidden">
|
|
23
|
+
<div className="hidden md:block">
|
|
24
|
+
<TopHeader />
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div className="flex flex-1 overflow-hidden">
|
|
28
|
+
<Sidebar />
|
|
29
|
+
<MobileSidebar />
|
|
30
|
+
|
|
31
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
32
|
+
<MobileHeader />
|
|
33
|
+
<CopilotLayout>{children}</CopilotLayout>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</DesktopSidebarProvider>
|
|
38
|
+
</MobileSidebarProvider>
|
|
39
|
+
</Providers>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export default function DashboardLoading() {
|
|
2
|
+
return (
|
|
3
|
+
<div className="px-4 py-6 md:px-8 md:py-8">
|
|
4
|
+
<div className="space-y-4">
|
|
5
|
+
{/* Header skeleton */}
|
|
6
|
+
<div className="h-8 w-48 animate-pulse rounded bg-muted" />
|
|
7
|
+
<div className="h-4 w-80 animate-pulse rounded bg-muted/60" />
|
|
8
|
+
|
|
9
|
+
{/* Content skeleton */}
|
|
10
|
+
<div className="mt-8 grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
11
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
12
|
+
<div
|
|
13
|
+
key={i}
|
|
14
|
+
className="h-24 animate-pulse rounded-lg border border-border bg-card"
|
|
15
|
+
/>
|
|
16
|
+
))}
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div className="mt-8 space-y-3">
|
|
20
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
21
|
+
<div
|
|
22
|
+
key={i}
|
|
23
|
+
className="h-12 animate-pulse rounded border border-border bg-card"
|
|
24
|
+
/>
|
|
25
|
+
))}
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { Markdown } from "#/components/dashboard/markdown";
|
|
3
|
+
import { CopyButton } from "#/components/dashboard/copy-button";
|
|
4
|
+
import { Hint } from "#/components/dashboard/hint";
|
|
5
|
+
|
|
6
|
+
export default function AgentMemoryView({ id, content }: { id: string; content: string }) {
|
|
7
|
+
return (
|
|
8
|
+
<div>
|
|
9
|
+
<div className="mb-4">
|
|
10
|
+
<Hint action={content ? <CopyButton value={content} label="Copy" className="shrink-0" /> : undefined}>
|
|
11
|
+
Private memory this agent accumulates across sessions. It also inherits the
|
|
12
|
+
shared project memory.
|
|
13
|
+
</Hint>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
{content ? (
|
|
17
|
+
<div data-testid="agent-memory-content" className="border border-border bg-card p-6">
|
|
18
|
+
<Markdown content={content} />
|
|
19
|
+
</div>
|
|
20
|
+
) : (
|
|
21
|
+
<div className="border border-border p-8 text-center text-sm text-muted-foreground">
|
|
22
|
+
No memory yet. Memory is created automatically when the agent works.
|
|
23
|
+
</div>
|
|
24
|
+
)}
|
|
25
|
+
|
|
26
|
+
<div className="mt-4">
|
|
27
|
+
<Link
|
|
28
|
+
href={`/projects/${id}/memory`}
|
|
29
|
+
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
30
|
+
>
|
|
31
|
+
View shared project memory →
|
|
32
|
+
</Link>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AgentConfig } from "@polpo-ai/core";
|
|
2
|
+
import { AgentCapabilities } from "#/components/dashboard/agent-capabilities";
|
|
3
|
+
|
|
4
|
+
/** Models tab — the per-capability model grid (one row per modality). */
|
|
5
|
+
export default function AgentModelsView({
|
|
6
|
+
agent,
|
|
7
|
+
projectId,
|
|
8
|
+
agentName,
|
|
9
|
+
}: {
|
|
10
|
+
agent: AgentConfig | null;
|
|
11
|
+
projectId: string;
|
|
12
|
+
agentName: string;
|
|
13
|
+
}) {
|
|
14
|
+
if (!agent) {
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
data-testid="agent-not-found"
|
|
18
|
+
className="mt-4 border border-border p-8 text-center text-sm text-muted-foreground"
|
|
19
|
+
>
|
|
20
|
+
Agent not found.
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<section className="pt-2">
|
|
27
|
+
<AgentCapabilities agent={agent} projectId={projectId} agentName={agentName} />
|
|
28
|
+
</section>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Suspense } from "react";
|
|
2
|
+
import { dataApi, getAgent } from "#/lib/api";
|
|
3
|
+
import { AgentProfileSkeleton } from "#/components/dashboard/skeletons";
|
|
4
|
+
import { AgentStudio, type VaultEntry } from "#/components/dashboard/agent-studio";
|
|
5
|
+
|
|
6
|
+
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
|
|
7
|
+
|
|
8
|
+
async function AgentData({ id, name }: { id: string; name: string }) {
|
|
9
|
+
// The agent detail IS the Studio (chat canvas). Fetch everything its
|
|
10
|
+
// tabs need (agent + memory + vault) up front, server-side.
|
|
11
|
+
const [agent, memoryContent, vaultEntries] = await Promise.all([
|
|
12
|
+
getAgent(id, name),
|
|
13
|
+
dataApi<{ ok: boolean; data: { content: string } }>(
|
|
14
|
+
id,
|
|
15
|
+
`/v1/memory/agent/${encodeURIComponent(name)}`,
|
|
16
|
+
)
|
|
17
|
+
.then((r) => r.data?.content ?? "")
|
|
18
|
+
.catch(() => ""),
|
|
19
|
+
dataApi<{ ok: boolean; data: VaultEntry[] }>(
|
|
20
|
+
id,
|
|
21
|
+
`/v1/vault/entries/${encodeURIComponent(name)}`,
|
|
22
|
+
)
|
|
23
|
+
.then((r) => r.data ?? [])
|
|
24
|
+
.catch(() => [] as VaultEntry[]),
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<AgentStudio
|
|
29
|
+
projectId={id}
|
|
30
|
+
apiUrl={API_URL}
|
|
31
|
+
agent={agent}
|
|
32
|
+
memoryContent={memoryContent}
|
|
33
|
+
vaultEntries={vaultEntries}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default async function AgentDetailPage({
|
|
39
|
+
params,
|
|
40
|
+
}: {
|
|
41
|
+
params: Promise<{ id: string; name: string }>;
|
|
42
|
+
}) {
|
|
43
|
+
const { id, name } = await params;
|
|
44
|
+
const decodedName = decodeURIComponent(name);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Suspense fallback={<AgentProfileSkeleton />}>
|
|
48
|
+
<AgentData id={id} name={decodedName} />
|
|
49
|
+
</Suspense>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Eye } from "lucide-react";
|
|
5
|
+
import type { AgentConfig } from "@polpo-ai/core";
|
|
6
|
+
import { Markdown } from "#/components/dashboard/markdown";
|
|
7
|
+
import { CopyButton } from "#/components/dashboard/copy-button";
|
|
8
|
+
import { Hint } from "#/components/dashboard/hint";
|
|
9
|
+
import {
|
|
10
|
+
Dialog,
|
|
11
|
+
DialogContent,
|
|
12
|
+
DialogDescription,
|
|
13
|
+
DialogHeader,
|
|
14
|
+
DialogTitle,
|
|
15
|
+
} from "#/components/ui/dialog";
|
|
16
|
+
|
|
17
|
+
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
|
|
18
|
+
|
|
19
|
+
/** Rough token estimate — ~4 chars/token, good enough for a budget hint. */
|
|
20
|
+
const approxTokens = (s: string) => Math.round(s.length / 4);
|
|
21
|
+
|
|
22
|
+
export default function AgentPromptView({
|
|
23
|
+
agent,
|
|
24
|
+
projectId,
|
|
25
|
+
agentName,
|
|
26
|
+
}: {
|
|
27
|
+
agent: AgentConfig | null;
|
|
28
|
+
projectId: string;
|
|
29
|
+
agentName: string;
|
|
30
|
+
}) {
|
|
31
|
+
if (!agent) {
|
|
32
|
+
return (
|
|
33
|
+
<div className="mt-4 border border-border p-8 text-center text-sm text-muted-foreground">
|
|
34
|
+
Agent not found.
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const instructions = agent.systemPrompt ?? "";
|
|
40
|
+
const chars = instructions.length;
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div>
|
|
44
|
+
<Hint action={<PreviewButton projectId={projectId} agentName={agentName} />}>
|
|
45
|
+
Extra instructions added on top of the agent's base prompt — they shape
|
|
46
|
+
tone and rules without replacing Polpo's defaults. Hit Preview for the
|
|
47
|
+
full composed prompt.
|
|
48
|
+
</Hint>
|
|
49
|
+
|
|
50
|
+
{instructions ? (
|
|
51
|
+
<>
|
|
52
|
+
<div className="mt-2 flex items-center gap-2">
|
|
53
|
+
<span className="inline-flex items-center gap-1 border border-border bg-secondary/40 px-2 py-0.5 font-mono text-[11px] font-medium text-foreground">
|
|
54
|
+
{chars.toLocaleString()}
|
|
55
|
+
<span className="text-muted-foreground">chars</span>
|
|
56
|
+
</span>
|
|
57
|
+
<span className="inline-flex items-center gap-1 border border-border bg-secondary/40 px-2 py-0.5 font-mono text-[11px] font-medium text-foreground">
|
|
58
|
+
~{approxTokens(instructions).toLocaleString()}
|
|
59
|
+
<span className="text-muted-foreground">tokens</span>
|
|
60
|
+
</span>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="mt-3 border border-border bg-card p-6">
|
|
63
|
+
<Markdown content={instructions} />
|
|
64
|
+
</div>
|
|
65
|
+
</>
|
|
66
|
+
) : (
|
|
67
|
+
<div className="mt-3 border border-border p-8 text-center">
|
|
68
|
+
<p className="text-sm text-muted-foreground">No extra instructions configured.</p>
|
|
69
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
70
|
+
This agent runs on the default base prompt. Preview it to see what that includes.
|
|
71
|
+
</p>
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Loading state for the prompt preview — a sharp skeleton that mimics the
|
|
80
|
+
* composed prompt materialising (metadata bar + paragraph blocks of
|
|
81
|
+
* varying width), in keeping with the rest of the surface.
|
|
82
|
+
*/
|
|
83
|
+
function PromptSkeleton() {
|
|
84
|
+
// Width pattern per "paragraph" — last line of each block is shorter,
|
|
85
|
+
// so it reads as real prose rather than a uniform block.
|
|
86
|
+
const blocks = [
|
|
87
|
+
["w-1/3"],
|
|
88
|
+
["w-full", "w-full", "w-11/12", "w-2/3"],
|
|
89
|
+
["w-full", "w-5/6", "w-full", "w-1/2"],
|
|
90
|
+
["w-3/4", "w-full", "w-4/5"],
|
|
91
|
+
];
|
|
92
|
+
let line = 0;
|
|
93
|
+
return (
|
|
94
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
95
|
+
<div className="flex items-center justify-between gap-4 border-y border-border py-2">
|
|
96
|
+
<span className="h-3 w-40 animate-pulse bg-muted" />
|
|
97
|
+
<span className="h-6 w-16 animate-pulse bg-muted" />
|
|
98
|
+
</div>
|
|
99
|
+
<div className="min-h-0 flex-1 space-y-5 overflow-hidden bg-card p-4">
|
|
100
|
+
{blocks.map((widths, b) => (
|
|
101
|
+
<div key={b} className="space-y-2">
|
|
102
|
+
{widths.map((w) => {
|
|
103
|
+
const delay = (line++ % 6) * 90;
|
|
104
|
+
return (
|
|
105
|
+
<div
|
|
106
|
+
key={w + line}
|
|
107
|
+
className={`h-3 ${w} animate-pulse bg-muted`}
|
|
108
|
+
style={{ animationDelay: `${delay}ms` }}
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
})}
|
|
112
|
+
</div>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
<p className="border-t border-border pt-2 font-mono text-[11px] text-muted-foreground/60">
|
|
116
|
+
Composing the full prompt…
|
|
117
|
+
</p>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Preview — fetches the FULLY composed system prompt from the data plane
|
|
124
|
+
* (`GET /v1/agents/:name/prompt`), which runs the same `buildAgentSystemPrompt`
|
|
125
|
+
* + skill-loading the runtime uses. Shows the exact text sent to the model.
|
|
126
|
+
*/
|
|
127
|
+
function PreviewButton({
|
|
128
|
+
projectId,
|
|
129
|
+
agentName,
|
|
130
|
+
}: {
|
|
131
|
+
projectId: string;
|
|
132
|
+
agentName: string;
|
|
133
|
+
}) {
|
|
134
|
+
const [open, setOpen] = useState(false);
|
|
135
|
+
const [loading, setLoading] = useState(false);
|
|
136
|
+
const [prompt, setPrompt] = useState<string | null>(null);
|
|
137
|
+
const [error, setError] = useState<string | null>(null);
|
|
138
|
+
|
|
139
|
+
async function load() {
|
|
140
|
+
setOpen(true);
|
|
141
|
+
setLoading(true);
|
|
142
|
+
setError(null);
|
|
143
|
+
setPrompt(null);
|
|
144
|
+
try {
|
|
145
|
+
const res = await fetch(
|
|
146
|
+
`${API_URL}/v1/projects/${projectId}/data/v1/agents/${encodeURIComponent(agentName)}/prompt`,
|
|
147
|
+
{ credentials: "include" },
|
|
148
|
+
);
|
|
149
|
+
const body = await res.json().catch(() => ({}));
|
|
150
|
+
if (!res.ok || !body.ok) {
|
|
151
|
+
setError(body.error ?? `Failed to compose prompt (${res.status})`);
|
|
152
|
+
} else {
|
|
153
|
+
setPrompt(body.data?.prompt ?? "");
|
|
154
|
+
}
|
|
155
|
+
} catch (err) {
|
|
156
|
+
setError((err as Error).message ?? "Failed to compose prompt");
|
|
157
|
+
}
|
|
158
|
+
setLoading(false);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<>
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
onClick={() => void load()}
|
|
166
|
+
className="inline-flex shrink-0 items-center gap-1.5 border border-border bg-card px-2.5 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-foreground/30 hover:text-foreground"
|
|
167
|
+
>
|
|
168
|
+
<Eye className="h-3.5 w-3.5" strokeWidth={1.5} />
|
|
169
|
+
Preview
|
|
170
|
+
</button>
|
|
171
|
+
|
|
172
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
173
|
+
<DialogContent className="flex max-h-[85vh] flex-col overflow-hidden sm:max-w-3xl">
|
|
174
|
+
<DialogHeader>
|
|
175
|
+
<DialogTitle>Composed system prompt</DialogTitle>
|
|
176
|
+
<DialogDescription>
|
|
177
|
+
The complete prompt the platform sends to the model — base prompt,
|
|
178
|
+
identity, your instructions and loaded skills, exactly as the
|
|
179
|
+
runtime composes it.
|
|
180
|
+
</DialogDescription>
|
|
181
|
+
</DialogHeader>
|
|
182
|
+
|
|
183
|
+
{loading ? (
|
|
184
|
+
<PromptSkeleton />
|
|
185
|
+
) : error ? (
|
|
186
|
+
<p className="py-6 text-sm text-destructive">{error}</p>
|
|
187
|
+
) : prompt !== null ? (
|
|
188
|
+
<>
|
|
189
|
+
<div className="flex items-center justify-between gap-4 border-y border-border py-2">
|
|
190
|
+
<div className="flex items-center gap-2">
|
|
191
|
+
<span className="inline-flex items-center gap-1 border border-border bg-secondary/40 px-2 py-0.5 font-mono text-[11px] font-medium text-foreground">
|
|
192
|
+
{prompt.length.toLocaleString()}
|
|
193
|
+
<span className="text-muted-foreground">chars</span>
|
|
194
|
+
</span>
|
|
195
|
+
<span className="inline-flex items-center gap-1 border border-border bg-secondary/40 px-2 py-0.5 font-mono text-[11px] font-medium text-foreground">
|
|
196
|
+
~{approxTokens(prompt).toLocaleString()}
|
|
197
|
+
<span className="text-muted-foreground">tokens</span>
|
|
198
|
+
</span>
|
|
199
|
+
</div>
|
|
200
|
+
<CopyButton value={prompt} label="Copy" className="shrink-0" />
|
|
201
|
+
</div>
|
|
202
|
+
<div className="min-h-0 flex-1 overflow-auto bg-card p-4">
|
|
203
|
+
<Markdown content={prompt} />
|
|
204
|
+
</div>
|
|
205
|
+
</>
|
|
206
|
+
) : null}
|
|
207
|
+
</DialogContent>
|
|
208
|
+
</Dialog>
|
|
209
|
+
</>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import type { AgentConfig } from "@polpo-ai/core";
|
|
3
|
+
import { BookMarked, ExternalLink } from "lucide-react";
|
|
4
|
+
import { SectionHeader } from "#/components/dashboard/section-header";
|
|
5
|
+
|
|
6
|
+
export default function AgentSkillsView({
|
|
7
|
+
agent,
|
|
8
|
+
projectId,
|
|
9
|
+
}: {
|
|
10
|
+
agent: AgentConfig | null;
|
|
11
|
+
projectId: string;
|
|
12
|
+
}) {
|
|
13
|
+
if (!agent) {
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
data-testid="agent-not-found"
|
|
17
|
+
className="border border-border p-8 text-center text-sm text-muted-foreground"
|
|
18
|
+
>
|
|
19
|
+
Agent not found.
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const skills = agent.skills ?? [];
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div>
|
|
28
|
+
<SectionHeader
|
|
29
|
+
description={
|
|
30
|
+
<>
|
|
31
|
+
Installed skills extend this agent with reusable procedures. Manage
|
|
32
|
+
them with <span className="font-mono">polpo install</span> or in the
|
|
33
|
+
agent config <span className="font-mono">skills</span> field.
|
|
34
|
+
</>
|
|
35
|
+
}
|
|
36
|
+
/>
|
|
37
|
+
|
|
38
|
+
{skills.length === 0 ? (
|
|
39
|
+
<div className="mt-4 border border-dashed border-border p-8 text-center">
|
|
40
|
+
<BookMarked
|
|
41
|
+
className="mx-auto h-5 w-5 text-muted-foreground/40"
|
|
42
|
+
strokeWidth={1.5}
|
|
43
|
+
/>
|
|
44
|
+
<p className="mt-2 text-sm text-muted-foreground">No skills installed.</p>
|
|
45
|
+
<p className="mt-1 text-xs text-muted-foreground/60">
|
|
46
|
+
Browse the catalog at{" "}
|
|
47
|
+
<a
|
|
48
|
+
href="https://docs.polpo.sh/docs/agents/skills"
|
|
49
|
+
target="_blank"
|
|
50
|
+
rel="noopener noreferrer"
|
|
51
|
+
className="underline decoration-dotted underline-offset-2 transition-colors hover:text-foreground"
|
|
52
|
+
>
|
|
53
|
+
docs.polpo.sh
|
|
54
|
+
</a>
|
|
55
|
+
.
|
|
56
|
+
</p>
|
|
57
|
+
</div>
|
|
58
|
+
) : (
|
|
59
|
+
<div className="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
60
|
+
{skills.map((skill) => (
|
|
61
|
+
<Link
|
|
62
|
+
key={skill}
|
|
63
|
+
href={`/projects/${projectId}/skills/${encodeURIComponent(skill)}`}
|
|
64
|
+
target="_blank"
|
|
65
|
+
rel="noopener noreferrer"
|
|
66
|
+
className="group flex items-center gap-2.5 border border-border bg-card p-3 transition-colors hover:border-foreground/30"
|
|
67
|
+
>
|
|
68
|
+
<span className="grid size-7 shrink-0 place-items-center bg-secondary text-muted-foreground">
|
|
69
|
+
<BookMarked className="h-3.5 w-3.5" strokeWidth={1.5} />
|
|
70
|
+
</span>
|
|
71
|
+
<span className="min-w-0 flex-1 truncate font-mono text-sm text-foreground">
|
|
72
|
+
{skill}
|
|
73
|
+
</span>
|
|
74
|
+
<ExternalLink
|
|
75
|
+
className="h-3.5 w-3.5 shrink-0 text-muted-foreground/40 transition-colors group-hover:text-foreground"
|
|
76
|
+
strokeWidth={1.5}
|
|
77
|
+
/>
|
|
78
|
+
</Link>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|