@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,237 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import Image from "next/image";
|
|
6
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
7
|
+
import { PolpoProvider } from "@polpo-ai/react";
|
|
8
|
+
import { Chat } from "@polpo-ai/chat";
|
|
9
|
+
import type { AgentConfig } from "@polpo-ai/core";
|
|
10
|
+
import { ArrowLeft, Plus } from "lucide-react";
|
|
11
|
+
import { AgentModelSelector } from "#/components/dashboard/agent-model-selector";
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
projectId: string;
|
|
15
|
+
apiUrl: string;
|
|
16
|
+
projectName: string;
|
|
17
|
+
initialAgents: AgentConfig[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Standalone playground surface — rendered inside `(playground)/layout.tsx`,
|
|
22
|
+
* which strips the project sidebar and dashboard chrome. Opened in a new
|
|
23
|
+
* browser tab so the user clearly sees this is an isolated chat sandbox,
|
|
24
|
+
* not an embedded panel inside the project.
|
|
25
|
+
*/
|
|
26
|
+
export default function PlaygroundView({
|
|
27
|
+
projectId,
|
|
28
|
+
apiUrl,
|
|
29
|
+
projectName,
|
|
30
|
+
initialAgents,
|
|
31
|
+
}: Props) {
|
|
32
|
+
const baseUrl = `${apiUrl}/v1/projects/${projectId}/data`;
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
const [selectedAgent, setSelectedAgent] = useState<string | undefined>(
|
|
36
|
+
initialAgents[0]?.name,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Resume an existing chat across reloads + make the URL shareable.
|
|
40
|
+
// The Chat component creates a fresh session when `sessionId` is
|
|
41
|
+
// omitted, so without this the playground silently leaked a new
|
|
42
|
+
// session every refresh. `router.replace` (no `push`) is intentional:
|
|
43
|
+
// the back button shouldn't accumulate one entry per stream chunk.
|
|
44
|
+
const router = useRouter();
|
|
45
|
+
const searchParams = useSearchParams();
|
|
46
|
+
const sessionParam = searchParams.get("session") ?? undefined;
|
|
47
|
+
|
|
48
|
+
// Capture sessionParam only at the initial render. After that we
|
|
49
|
+
// never re-pass a new sessionId prop to <Chat>, because:
|
|
50
|
+
// 1. useChat already keeps the session alive internally — its
|
|
51
|
+
// sessionIdRef gets set from the response x-session-id during
|
|
52
|
+
// the first stream (line 92 of use-chat.js). The second
|
|
53
|
+
// sendMessage call reads that ref and sends the right header.
|
|
54
|
+
// 2. Re-passing sessionId to <Chat> would change its prop, which
|
|
55
|
+
// triggers useChat's effect at line 22 to call
|
|
56
|
+
// client.getSessionMessages(id) → setMessages(server's view),
|
|
57
|
+
// causing a visible skeleton flash right after onFinish.
|
|
58
|
+
//
|
|
59
|
+
// Refresh / share-link works because on a fresh page mount this ref
|
|
60
|
+
// captures whatever ?session= is in the URL at that moment.
|
|
61
|
+
const initialSessionIdRef = useRef<string | undefined>(sessionParam);
|
|
62
|
+
|
|
63
|
+
// Park the new id mid-stream, commit it to the URL on onFinish. We
|
|
64
|
+
// never put it back into a React prop or state — only the URL — so
|
|
65
|
+
// <Chat> doesn't see anything change.
|
|
66
|
+
const pendingSessionIdRef = useRef<string | null>(null);
|
|
67
|
+
|
|
68
|
+
const commitSession = useRef<(id: string) => void>(() => {});
|
|
69
|
+
commitSession.current = (id: string) => {
|
|
70
|
+
const current = searchParams.get("session");
|
|
71
|
+
if (current === id) return;
|
|
72
|
+
const params = new URLSearchParams(Array.from(searchParams.entries()));
|
|
73
|
+
params.set("session", id);
|
|
74
|
+
router.replace(`?${params.toString()}`, { scroll: false });
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const handleSessionCreated = useCallback((id: string) => {
|
|
78
|
+
pendingSessionIdRef.current = id;
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
const handleFinish = useCallback(() => {
|
|
82
|
+
const id = pendingSessionIdRef.current;
|
|
83
|
+
if (id) {
|
|
84
|
+
pendingSessionIdRef.current = null;
|
|
85
|
+
commitSession.current(id);
|
|
86
|
+
}
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
// cookieFetch stays minimal — we don't read x-session-id here anymore
|
|
90
|
+
// since the SDK already exposes it via Chat's onSessionCreated. Empty
|
|
91
|
+
// deps keep the fetch identity stable so PolpoProvider doesn't
|
|
92
|
+
// rebuild the client on every render.
|
|
93
|
+
const cookieFetch = useMemo<typeof globalThis.fetch>(
|
|
94
|
+
() =>
|
|
95
|
+
((input, init) =>
|
|
96
|
+
globalThis.fetch(input, {
|
|
97
|
+
...init,
|
|
98
|
+
credentials: "include",
|
|
99
|
+
})) as typeof globalThis.fetch,
|
|
100
|
+
[],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Switching agent unmounts the Chat (via `key`) — keeping the old
|
|
104
|
+
// session id in the URL would resume the wrong agent's history.
|
|
105
|
+
// Clear the param when the user picks a different agent so a fresh
|
|
106
|
+
// session is created on the next message. We also reset the
|
|
107
|
+
// initial-session ref so the re-mounted Chat starts fresh.
|
|
108
|
+
const handleSelectAgent = useCallback(
|
|
109
|
+
(name: string) => {
|
|
110
|
+
setSelectedAgent(name);
|
|
111
|
+
initialSessionIdRef.current = undefined;
|
|
112
|
+
pendingSessionIdRef.current = null;
|
|
113
|
+
if (searchParams.get("session")) {
|
|
114
|
+
const params = new URLSearchParams(Array.from(searchParams.entries()));
|
|
115
|
+
params.delete("session");
|
|
116
|
+
const qs = params.toString();
|
|
117
|
+
router.replace(qs ? `?${qs}` : "?", { scroll: false });
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
[router, searchParams],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<>
|
|
125
|
+
<TopBar
|
|
126
|
+
projectId={projectId}
|
|
127
|
+
projectName={projectName}
|
|
128
|
+
agents={initialAgents}
|
|
129
|
+
selectedAgent={selectedAgent}
|
|
130
|
+
onSelectAgent={handleSelectAgent}
|
|
131
|
+
/>
|
|
132
|
+
|
|
133
|
+
<main className="flex flex-1 flex-col overflow-hidden">
|
|
134
|
+
{initialAgents.length === 0 ? (
|
|
135
|
+
<div className="flex flex-1 items-center justify-center px-4 py-8">
|
|
136
|
+
<EmptyState projectId={projectId} />
|
|
137
|
+
</div>
|
|
138
|
+
) : (
|
|
139
|
+
<PolpoProvider
|
|
140
|
+
baseUrl={baseUrl}
|
|
141
|
+
apiPrefix="/v1"
|
|
142
|
+
fetch={cookieFetch}
|
|
143
|
+
autoConnect={false}
|
|
144
|
+
>
|
|
145
|
+
<Chat
|
|
146
|
+
key={selectedAgent}
|
|
147
|
+
sessionId={initialSessionIdRef.current}
|
|
148
|
+
agent={selectedAgent}
|
|
149
|
+
onSessionCreated={handleSessionCreated}
|
|
150
|
+
onFinish={handleFinish}
|
|
151
|
+
className="flex-1"
|
|
152
|
+
/>
|
|
153
|
+
</PolpoProvider>
|
|
154
|
+
)}
|
|
155
|
+
</main>
|
|
156
|
+
</>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function TopBar({
|
|
161
|
+
projectId,
|
|
162
|
+
projectName,
|
|
163
|
+
agents,
|
|
164
|
+
selectedAgent,
|
|
165
|
+
onSelectAgent,
|
|
166
|
+
}: {
|
|
167
|
+
projectId: string;
|
|
168
|
+
projectName: string;
|
|
169
|
+
agents: AgentConfig[];
|
|
170
|
+
selectedAgent: string | undefined;
|
|
171
|
+
onSelectAgent: (name: string) => void;
|
|
172
|
+
}) {
|
|
173
|
+
return (
|
|
174
|
+
<header className="flex h-14 shrink-0 items-center justify-between gap-4 border-b border-border bg-card px-4">
|
|
175
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
176
|
+
<Link
|
|
177
|
+
href={`/projects/${projectId}`}
|
|
178
|
+
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"
|
|
179
|
+
>
|
|
180
|
+
<ArrowLeft className="h-3.5 w-3.5" />
|
|
181
|
+
Back to dashboard
|
|
182
|
+
</Link>
|
|
183
|
+
|
|
184
|
+
<div className="hidden md:block h-5 w-px bg-border" />
|
|
185
|
+
|
|
186
|
+
<div className="hidden md:flex items-center gap-2 min-w-0">
|
|
187
|
+
<Image
|
|
188
|
+
src="/polpo-logo.svg"
|
|
189
|
+
alt="Polpo"
|
|
190
|
+
width={72}
|
|
191
|
+
height={16}
|
|
192
|
+
priority
|
|
193
|
+
className="h-4 w-auto opacity-70"
|
|
194
|
+
/>
|
|
195
|
+
<span className="text-xs uppercase tracking-wide text-muted-foreground">
|
|
196
|
+
Playground
|
|
197
|
+
</span>
|
|
198
|
+
<span className="text-muted-foreground/50">·</span>
|
|
199
|
+
<span className="truncate text-sm font-medium text-foreground/80 font-mono">
|
|
200
|
+
{projectName}
|
|
201
|
+
</span>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
{agents.length > 0 && (
|
|
206
|
+
<AgentModelSelector
|
|
207
|
+
agents={agents}
|
|
208
|
+
selected={selectedAgent}
|
|
209
|
+
onSelect={onSelectAgent}
|
|
210
|
+
/>
|
|
211
|
+
)}
|
|
212
|
+
</header>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function EmptyState({ projectId }: { projectId: string }) {
|
|
217
|
+
return (
|
|
218
|
+
<div className="flex flex-col items-center justify-center gap-4 border border-dashed border-border p-12 text-center max-w-md">
|
|
219
|
+
<div className="flex h-10 w-10 items-center justify-center border border-border">
|
|
220
|
+
<Plus className="h-5 w-5 text-muted-foreground" />
|
|
221
|
+
</div>
|
|
222
|
+
<div>
|
|
223
|
+
<p className="text-sm font-medium">No agents yet</p>
|
|
224
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
225
|
+
Create an agent from the dashboard, or deploy one from your CLI with{" "}
|
|
226
|
+
<span className="font-mono">polpo deploy</span>.
|
|
227
|
+
</p>
|
|
228
|
+
</div>
|
|
229
|
+
<Link
|
|
230
|
+
href={`/projects/${projectId}/agents`}
|
|
231
|
+
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"
|
|
232
|
+
>
|
|
233
|
+
Go to Agents
|
|
234
|
+
</Link>
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
}
|
package/app/favicon.ico
ADDED
|
Binary file
|
package/app/globals.css
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
@import "shadcn/tailwind.css";
|
|
4
|
+
|
|
5
|
+
/* Tailwind 4 doesn't scan node_modules by default. Without these
|
|
6
|
+
* @source directives, classes used only inside @polpo-ai/chat (like
|
|
7
|
+
* `bg-current` on the typing dots) never make it into the generated
|
|
8
|
+
* CSS — the dots are rendered transparent and invisible. */
|
|
9
|
+
@source "../node_modules/@polpo-ai/chat/dist/**/*.{js,mjs}";
|
|
10
|
+
@source "../node_modules/@polpo-ai/react/dist/**/*.{js,mjs}";
|
|
11
|
+
/* Same reason for the lumea-labs surface used by playground v2. */
|
|
12
|
+
@source "../node_modules/@lumea-labs/chat/dist/**/*.{js,mjs}";
|
|
13
|
+
@source "../node_modules/@lumea-labs/chat-polpo/dist/**/*.{js,mjs}";
|
|
14
|
+
@source "../node_modules/@lumea-labs/tool-calls/dist/**/*.{js,mjs}";
|
|
15
|
+
@source "../node_modules/@lumea-labs/llm-polpo/dist/**/*.{js,mjs}";
|
|
16
|
+
@source "../node_modules/@lumea-labs/file-manager/dist/**/*.{js,mjs}";
|
|
17
|
+
@source "../node_modules/@lumea-labs/orchestrator/dist/**/*.{js,mjs}";
|
|
18
|
+
@source "../node_modules/@lumea-labs/file-manager-polpo/dist/**/*.{js,mjs}";
|
|
19
|
+
@import "../styles/feature-cards.css";
|
|
20
|
+
@import "../styles/machine-mode.css";
|
|
21
|
+
|
|
22
|
+
@plugin "@tailwindcss/typography";
|
|
23
|
+
|
|
24
|
+
/* Copilot panel — tighten the chat gutters (the chat package hardcodes
|
|
25
|
+
* px-6) so more fits in the narrow docked panel. Unlayered rule → wins
|
|
26
|
+
* over the layered Tailwind utility without !important. Scoped to the
|
|
27
|
+
* copilot wrapper so the playground / Studio chat is unaffected. */
|
|
28
|
+
.copilot-chat-dense .px-6 {
|
|
29
|
+
padding-left: 0.875rem;
|
|
30
|
+
padding-right: 0.875rem;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@custom-variant dark (&:is(.dark *));
|
|
34
|
+
|
|
35
|
+
@theme inline {
|
|
36
|
+
--color-background: var(--background);
|
|
37
|
+
--color-foreground: var(--foreground);
|
|
38
|
+
--font-sans: "Geist", "Geist Fallback", ui-sans-serif, system-ui, sans-serif;
|
|
39
|
+
--font-mono: "Geist Mono", "Geist Mono Fallback", ui-monospace, monospace;
|
|
40
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
41
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
42
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
43
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
44
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
45
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
46
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
47
|
+
--color-sidebar: var(--sidebar);
|
|
48
|
+
--color-chart-5: var(--chart-5);
|
|
49
|
+
--color-chart-4: var(--chart-4);
|
|
50
|
+
--color-chart-3: var(--chart-3);
|
|
51
|
+
--color-chart-2: var(--chart-2);
|
|
52
|
+
--color-chart-1: var(--chart-1);
|
|
53
|
+
--color-ring: var(--ring);
|
|
54
|
+
--color-input: var(--input);
|
|
55
|
+
--color-border: var(--border);
|
|
56
|
+
--color-destructive: var(--destructive);
|
|
57
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
58
|
+
--color-accent: var(--accent);
|
|
59
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
60
|
+
--color-muted: var(--muted);
|
|
61
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
62
|
+
--color-secondary: var(--secondary);
|
|
63
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
64
|
+
--color-primary: var(--primary);
|
|
65
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
66
|
+
--color-popover: var(--popover);
|
|
67
|
+
--color-card-foreground: var(--card-foreground);
|
|
68
|
+
--color-card: var(--card);
|
|
69
|
+
--color-brand: var(--brand);
|
|
70
|
+
--color-brand-foreground: var(--brand-foreground);
|
|
71
|
+
/* Polpo design-system tokens consumed by @lumea-labs/chat (the
|
|
72
|
+
* `bg-p-*`, `text-p-*`, `border-p-*` Tailwind classes). Mapped onto
|
|
73
|
+
* our existing shadcn vars so they render in our theme without us
|
|
74
|
+
* having to ship a second palette. */
|
|
75
|
+
--color-p-bg: var(--background);
|
|
76
|
+
--color-p-surface: var(--card);
|
|
77
|
+
--color-p-warm: var(--secondary);
|
|
78
|
+
--color-p-line: var(--border);
|
|
79
|
+
--color-p-ink: var(--foreground);
|
|
80
|
+
--color-p-ink-2: var(--foreground);
|
|
81
|
+
--color-p-ink-3: var(--muted-foreground);
|
|
82
|
+
--color-p-accent: var(--brand);
|
|
83
|
+
--color-p-accent-light: var(--accent);
|
|
84
|
+
--radius-sm: calc(var(--radius) * 0.6);
|
|
85
|
+
--radius-md: calc(var(--radius) * 0.8);
|
|
86
|
+
--radius-lg: var(--radius);
|
|
87
|
+
--radius-xl: calc(var(--radius) * 1.4);
|
|
88
|
+
--radius-2xl: calc(var(--radius) * 1.8);
|
|
89
|
+
--radius-3xl: calc(var(--radius) * 2.2);
|
|
90
|
+
--radius-4xl: calc(var(--radius) * 2.6);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
:root {
|
|
94
|
+
--background: #ffffff;
|
|
95
|
+
--foreground: #171717;
|
|
96
|
+
--card: #ffffff;
|
|
97
|
+
--card-foreground: #171717;
|
|
98
|
+
--popover: #ffffff;
|
|
99
|
+
--popover-foreground: #171717;
|
|
100
|
+
--primary: #171717;
|
|
101
|
+
--primary-foreground: #fafafa;
|
|
102
|
+
--secondary: #f5f5f5;
|
|
103
|
+
--secondary-foreground: #171717;
|
|
104
|
+
--muted: #f5f5f5;
|
|
105
|
+
--muted-foreground: #737373;
|
|
106
|
+
--accent: #f5f5f5;
|
|
107
|
+
--accent-foreground: #171717;
|
|
108
|
+
--destructive: #dc2626;
|
|
109
|
+
--border: #e5e5e5;
|
|
110
|
+
--input: #e5e5e5;
|
|
111
|
+
--ring: #171717;
|
|
112
|
+
--radius: 0.5rem;
|
|
113
|
+
--brand: #3ecf8e;
|
|
114
|
+
--brand-foreground: #0d1117;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.dark {
|
|
118
|
+
/* Stone-family dark surface hierarchy — background deepest, card
|
|
119
|
+
raised a clear step, secondary on top for active/hover. Warmer
|
|
120
|
+
neutral than the prior blue-grey set; reads less "tech cold" and
|
|
121
|
+
more "editorial" without shouting.
|
|
122
|
+
Values cross-reference Tailwind stone-950 / 900 / 800 so the whole
|
|
123
|
+
palette stays coherent with utility classes if we ever need them. */
|
|
124
|
+
--background: #111111; /* lifted from neutral-950 for softer contrast */
|
|
125
|
+
--foreground: #ededed;
|
|
126
|
+
--card: #1a1a1a; /* +9pt over bg — subtle pop without gap */
|
|
127
|
+
--card-foreground: #ededed;
|
|
128
|
+
--popover: #1a1a1a;
|
|
129
|
+
--popover-foreground: #ededed;
|
|
130
|
+
--primary: #ededed;
|
|
131
|
+
--primary-foreground: #111111;
|
|
132
|
+
--secondary: #262626; /* neutral-800 — active row, hover */
|
|
133
|
+
--secondary-foreground: #ededed;
|
|
134
|
+
--muted: #1f1f1f;
|
|
135
|
+
--muted-foreground: #a3a3a3; /* neutral-400 — readable secondary text */
|
|
136
|
+
--accent: #262626;
|
|
137
|
+
--accent-foreground: #ededed;
|
|
138
|
+
--destructive: #f85149;
|
|
139
|
+
--border: #2e2e2e; /* softer divider */
|
|
140
|
+
--input: #2e2e2e;
|
|
141
|
+
--ring: #ededed;
|
|
142
|
+
--brand: #3ecf8e;
|
|
143
|
+
--brand-foreground: #1a1a1a;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* sugar-high token colors — GitHub Dark (primer/github-syntax-dark) */
|
|
147
|
+
:root {
|
|
148
|
+
--sh-class: #c8e1ff;
|
|
149
|
+
--sh-identifier: #ededed;
|
|
150
|
+
--sh-keyword: #ea4a5a;
|
|
151
|
+
--sh-string: #79b8ff;
|
|
152
|
+
--sh-property: #c8e1ff;
|
|
153
|
+
--sh-entity: #b392f0;
|
|
154
|
+
--sh-jsxliterals: #b392f0;
|
|
155
|
+
--sh-sign: #ededed;
|
|
156
|
+
--sh-comment: #959da5;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@layer base {
|
|
160
|
+
* {
|
|
161
|
+
@apply border-border outline-ring/50;
|
|
162
|
+
}
|
|
163
|
+
body {
|
|
164
|
+
@apply bg-background text-foreground;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* Fine cross-hatch grid */
|
|
169
|
+
.bg-grid-fine {
|
|
170
|
+
background-image:
|
|
171
|
+
linear-gradient(to right, var(--border) 0.5px, transparent 0.5px),
|
|
172
|
+
linear-gradient(to bottom, var(--border) 0.5px, transparent 0.5px);
|
|
173
|
+
background-size: 48px 48px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* Dot pattern */
|
|
177
|
+
.bg-dots {
|
|
178
|
+
background-image: radial-gradient(circle, var(--border) 1px, transparent 1px);
|
|
179
|
+
background-size: 20px 20px;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* Diagonal lines */
|
|
183
|
+
.bg-diag {
|
|
184
|
+
background-image: repeating-linear-gradient(
|
|
185
|
+
-45deg,
|
|
186
|
+
transparent,
|
|
187
|
+
transparent 28px,
|
|
188
|
+
var(--border) 28px,
|
|
189
|
+
var(--border) 28.5px
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* Terminal cursor */
|
|
194
|
+
@keyframes blink {
|
|
195
|
+
0%, 100% { opacity: 1; }
|
|
196
|
+
50% { opacity: 0; }
|
|
197
|
+
}
|
|
198
|
+
.cursor-blink {
|
|
199
|
+
animation: blink 1s step-end infinite;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* Hide scrollbar for horizontal tab scroll */
|
|
203
|
+
.scrollbar-none {
|
|
204
|
+
-ms-overflow-style: none;
|
|
205
|
+
scrollbar-width: none;
|
|
206
|
+
}
|
|
207
|
+
.scrollbar-none::-webkit-scrollbar {
|
|
208
|
+
display: none;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* Brand-wide thin scrollbars — applied globally so every scrollable
|
|
212
|
+
container (including third-party surfaces like @lumea-labs/chat's
|
|
213
|
+
message list and popovers) matches the dashboard's neo-brutalist
|
|
214
|
+
identity: sharp edges (no border-radius), no shadow, theme-aware
|
|
215
|
+
border/muted-foreground tokens. The `*` selector is harmless on
|
|
216
|
+
non-scrollable elements — webkit pseudos are no-ops there. */
|
|
217
|
+
* {
|
|
218
|
+
scrollbar-width: thin;
|
|
219
|
+
scrollbar-color: var(--border) transparent;
|
|
220
|
+
}
|
|
221
|
+
*::-webkit-scrollbar {
|
|
222
|
+
width: 5px;
|
|
223
|
+
height: 5px;
|
|
224
|
+
}
|
|
225
|
+
*::-webkit-scrollbar-track {
|
|
226
|
+
background: transparent;
|
|
227
|
+
}
|
|
228
|
+
*::-webkit-scrollbar-thumb {
|
|
229
|
+
background: var(--border);
|
|
230
|
+
border-radius: 0;
|
|
231
|
+
transition: background 120ms ease;
|
|
232
|
+
}
|
|
233
|
+
*::-webkit-scrollbar-thumb:hover {
|
|
234
|
+
background: var(--muted-foreground);
|
|
235
|
+
}
|
|
236
|
+
*::-webkit-scrollbar-corner {
|
|
237
|
+
background: transparent;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* Opt-in utilities kept for backwards compatibility — both now alias
|
|
241
|
+
the global behaviour above. Existing call-sites need no changes. */
|
|
242
|
+
.scrollbar-thin,
|
|
243
|
+
.scrollbar-thin-nested {
|
|
244
|
+
/* inherits everything from `*` rules above */
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* Neo-brutalist brand scope — flattens rounded corners on third-party
|
|
248
|
+
surfaces (e.g. @lumea-labs/orchestrator's SwarmStatusBar, RunList,
|
|
249
|
+
RunRow) when they're rendered inside a `.polpo-sharp` wrapper.
|
|
250
|
+
Excludes `rounded-full` so circular status dots / monogram avatars
|
|
251
|
+
stay round, which is the intended affordance there. */
|
|
252
|
+
.polpo-sharp .rounded-xl,
|
|
253
|
+
.polpo-sharp .rounded-lg,
|
|
254
|
+
.polpo-sharp .rounded-md,
|
|
255
|
+
.polpo-sharp .rounded {
|
|
256
|
+
border-radius: 0 !important;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* Infinite horizontal scroll for logo strips */
|
|
260
|
+
@keyframes scroll {
|
|
261
|
+
0% { transform: translateX(0); }
|
|
262
|
+
100% { transform: translateX(-50%); }
|
|
263
|
+
}
|
|
264
|
+
.animate-scroll {
|
|
265
|
+
animation: scroll 50s linear infinite;
|
|
266
|
+
}
|
|
267
|
+
.animate-scroll:hover {
|
|
268
|
+
animation-play-state: paused;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
@keyframes scroll-reverse {
|
|
272
|
+
0% { transform: translateX(-50%); }
|
|
273
|
+
100% { transform: translateX(0); }
|
|
274
|
+
}
|
|
275
|
+
.animate-scroll-reverse {
|
|
276
|
+
animation: scroll-reverse 60s linear infinite;
|
|
277
|
+
}
|
|
278
|
+
.animate-scroll-reverse:hover {
|
|
279
|
+
animation-play-state: paused;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.animate-marquee {
|
|
283
|
+
animation: scroll 80s linear infinite;
|
|
284
|
+
}
|
|
285
|
+
.animate-marquee:hover {
|
|
286
|
+
animation-play-state: paused;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* Infinite vertical scroll for stacked logo/feature columns.
|
|
290
|
+
Pair with a column whose cells are duplicated once (two identical halves)
|
|
291
|
+
so translateY(-50%) loops seamlessly. */
|
|
292
|
+
@keyframes scroll-y {
|
|
293
|
+
0% { transform: translateY(0); }
|
|
294
|
+
100% { transform: translateY(-50%); }
|
|
295
|
+
}
|
|
296
|
+
.animate-scroll-y {
|
|
297
|
+
animation: scroll-y var(--scroll-y-duration, 24s) linear infinite;
|
|
298
|
+
}
|
|
299
|
+
@keyframes scroll-y-reverse {
|
|
300
|
+
0% { transform: translateY(-50%); }
|
|
301
|
+
100% { transform: translateY(0); }
|
|
302
|
+
}
|
|
303
|
+
.animate-scroll-y-reverse {
|
|
304
|
+
animation: scroll-y-reverse var(--scroll-y-duration, 24s) linear infinite;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* Orbit — slow continuous rotation for the model-provider ring.
|
|
308
|
+
The ring spins one way; each logo counter-spins (reverse) to stay upright. */
|
|
309
|
+
@keyframes orbit-spin {
|
|
310
|
+
to { transform: rotate(360deg); }
|
|
311
|
+
}
|
|
312
|
+
.animate-orbit {
|
|
313
|
+
animation: orbit-spin 44s linear infinite;
|
|
314
|
+
}
|
|
315
|
+
.animate-orbit-reverse {
|
|
316
|
+
animation: orbit-spin 44s linear infinite reverse;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/* Dot flow — staggered fade pulses create a "stream of light" effect
|
|
320
|
+
when applied to a row of dots with negative animation-delay per dot.
|
|
321
|
+
Forward flow: delays run -100ms, -200ms, -300ms...
|
|
322
|
+
Reverse flow: delays run -1000ms, -900ms, -800ms... */
|
|
323
|
+
@keyframes dot-flow {
|
|
324
|
+
0%, 60%, 100% { opacity: 0.18; transform: scale(0.85); }
|
|
325
|
+
25% { opacity: 1; transform: scale(1); }
|
|
326
|
+
}
|
|
327
|
+
.animate-dot-flow {
|
|
328
|
+
animation: dot-flow 1s linear infinite;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/* Skeleton shimmer — horizontal gradient sweep over muted background.
|
|
332
|
+
Pair with `bg-muted` + overflow-hidden parent. */
|
|
333
|
+
@keyframes shimmer {
|
|
334
|
+
0% { transform: translateX(-100%); }
|
|
335
|
+
100% { transform: translateX(100%); }
|
|
336
|
+
}
|
|
337
|
+
.skeleton-bar {
|
|
338
|
+
position: relative;
|
|
339
|
+
overflow: hidden;
|
|
340
|
+
background: var(--muted);
|
|
341
|
+
}
|
|
342
|
+
.skeleton-bar::after {
|
|
343
|
+
content: "";
|
|
344
|
+
position: absolute;
|
|
345
|
+
inset: 0;
|
|
346
|
+
background: linear-gradient(
|
|
347
|
+
90deg,
|
|
348
|
+
transparent,
|
|
349
|
+
color-mix(in oklab, var(--foreground) 8%, transparent),
|
|
350
|
+
transparent
|
|
351
|
+
);
|
|
352
|
+
animation: shimmer 1.8s linear infinite;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
html {
|
|
356
|
+
scroll-behavior: smooth;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
/* Blog Streamdown code blocks — squared style */
|
|
361
|
+
.blog-content [data-streamdown="code-block"] {
|
|
362
|
+
border: 1px solid hsl(var(--border));
|
|
363
|
+
border-radius: 0;
|
|
364
|
+
margin: 1.5rem 0;
|
|
365
|
+
overflow: hidden;
|
|
366
|
+
}
|
|
367
|
+
.blog-content [data-streamdown="code-block"] pre {
|
|
368
|
+
border-radius: 0;
|
|
369
|
+
margin: 0;
|
|
370
|
+
}
|
|
371
|
+
.blog-content [data-streamdown="inline-code"] {
|
|
372
|
+
font-family: var(--font-mono);
|
|
373
|
+
font-size: 0.875em;
|
|
374
|
+
background: hsl(var(--card));
|
|
375
|
+
border: 1px solid hsl(var(--border));
|
|
376
|
+
padding: 0.15em 0.4em;
|
|
377
|
+
border-radius: 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* Blog code blocks — squared corners */
|
|
381
|
+
.blog-code-block [data-element="code-block"],
|
|
382
|
+
.blog-code-block pre,
|
|
383
|
+
.blog-code-block [class*="rounded"] {
|
|
384
|
+
border-radius: 0 !important;
|
|
385
|
+
}
|
package/app/icon.svg
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
+
<rect width="512" height="512" fill="#0A0A0A"/>
|
|
4
|
+
<rect x="104" y="104" width="140" height="140" fill="#FAFAFA"/>
|
|
5
|
+
<rect x="268" y="268" width="140" height="140" fill="#FAFAFA"/>
|
|
6
|
+
</svg>
|
package/app/layout.tsx
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Plus_Jakarta_Sans, JetBrains_Mono } from "next/font/google";
|
|
3
|
+
import { ThemeProvider } from "next-themes";
|
|
4
|
+
import { Toaster } from "sonner";
|
|
5
|
+
import "./globals.css";
|
|
6
|
+
|
|
7
|
+
const sans = Plus_Jakarta_Sans({
|
|
8
|
+
variable: "--font-sans",
|
|
9
|
+
subsets: ["latin"],
|
|
10
|
+
weight: ["400", "500", "600", "700", "800"],
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const mono = JetBrains_Mono({
|
|
14
|
+
variable: "--font-mono",
|
|
15
|
+
subsets: ["latin"],
|
|
16
|
+
weight: ["400", "500"],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const metadata: Metadata = {
|
|
20
|
+
title: "Polpo",
|
|
21
|
+
description: "Self-hosted Polpo dashboard.",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default function RootLayout({
|
|
25
|
+
children,
|
|
26
|
+
}: Readonly<{
|
|
27
|
+
children: React.ReactNode;
|
|
28
|
+
}>) {
|
|
29
|
+
return (
|
|
30
|
+
<html lang="en" data-scroll-behavior="smooth" suppressHydrationWarning>
|
|
31
|
+
<body className={`${sans.variable} ${mono.variable} font-sans antialiased`}>
|
|
32
|
+
<ThemeProvider attribute="class" defaultTheme="dark" disableTransitionOnChange>
|
|
33
|
+
{children}
|
|
34
|
+
<Toaster position="top-right" richColors closeButton />
|
|
35
|
+
</ThemeProvider>
|
|
36
|
+
</body>
|
|
37
|
+
</html>
|
|
38
|
+
);
|
|
39
|
+
}
|
package/app/page.tsx
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { redirect } from "next/navigation";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Single-tenant self-host root. There is no marketing landing or multi-project
|
|
5
|
+
* list here — that lives only in the cloud. Send `/` straight into the local
|
|
6
|
+
* project's dashboard. `local` is a placeholder project id; the data client is
|
|
7
|
+
* single-tenant and ignores it.
|
|
8
|
+
*/
|
|
9
|
+
export default function Home() {
|
|
10
|
+
redirect("/projects/local/agents");
|
|
11
|
+
}
|