@polpo-ai/dashboard 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +44 -0
  3. package/app/(dashboard)/error.tsx +47 -0
  4. package/app/(dashboard)/layout.tsx +41 -0
  5. package/app/(dashboard)/loading.tsx +30 -0
  6. package/app/(dashboard)/projects/[id]/agents/[name]/memory/view.tsx +36 -0
  7. package/app/(dashboard)/projects/[id]/agents/[name]/models-view.tsx +30 -0
  8. package/app/(dashboard)/projects/[id]/agents/[name]/page.tsx +51 -0
  9. package/app/(dashboard)/projects/[id]/agents/[name]/prompt/view.tsx +211 -0
  10. package/app/(dashboard)/projects/[id]/agents/[name]/skills/view.tsx +84 -0
  11. package/app/(dashboard)/projects/[id]/agents/[name]/tools/view.tsx +525 -0
  12. package/app/(dashboard)/projects/[id]/agents/[name]/vault/view.tsx +61 -0
  13. package/app/(dashboard)/projects/[id]/agents/page.tsx +39 -0
  14. package/app/(dashboard)/projects/[id]/agents/view.tsx +283 -0
  15. package/app/(dashboard)/projects/[id]/layout.tsx +11 -0
  16. package/app/(dashboard)/projects/[id]/logs/page.tsx +27 -0
  17. package/app/(dashboard)/projects/[id]/logs/view.tsx +184 -0
  18. package/app/(dashboard)/projects/[id]/memory/page.tsx +40 -0
  19. package/app/(dashboard)/projects/[id]/memory/view.tsx +17 -0
  20. package/app/(dashboard)/projects/[id]/missions/[missionId]/page.tsx +88 -0
  21. package/app/(dashboard)/projects/[id]/missions/[missionId]/view.tsx +569 -0
  22. package/app/(dashboard)/projects/[id]/missions/page.tsx +32 -0
  23. package/app/(dashboard)/projects/[id]/missions/view.tsx +282 -0
  24. package/app/(dashboard)/projects/[id]/onboarding-checklist.tsx +359 -0
  25. package/app/(dashboard)/projects/[id]/page.tsx +68 -0
  26. package/app/(dashboard)/projects/[id]/playbooks/[name]/page.tsx +5 -0
  27. package/app/(dashboard)/projects/[id]/playbooks/[name]/view.tsx +433 -0
  28. package/app/(dashboard)/projects/[id]/playbooks/page.tsx +5 -0
  29. package/app/(dashboard)/projects/[id]/playbooks/view.tsx +222 -0
  30. package/app/(dashboard)/projects/[id]/schedules/page.tsx +44 -0
  31. package/app/(dashboard)/projects/[id]/schedules/view.tsx +385 -0
  32. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/page.tsx +55 -0
  33. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/raw-view.tsx +373 -0
  34. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/view.tsx +124 -0
  35. package/app/(dashboard)/projects/[id]/sessions/page.tsx +31 -0
  36. package/app/(dashboard)/projects/[id]/sessions/view.tsx +242 -0
  37. package/app/(dashboard)/projects/[id]/settings/page.tsx +52 -0
  38. package/app/(dashboard)/projects/[id]/skills/[name]/page.tsx +65 -0
  39. package/app/(dashboard)/projects/[id]/skills/[name]/view.tsx +227 -0
  40. package/app/(dashboard)/projects/[id]/skills/page.tsx +31 -0
  41. package/app/(dashboard)/projects/[id]/skills/view.tsx +243 -0
  42. package/app/(dashboard)/projects/[id]/storage/page.tsx +5 -0
  43. package/app/(dashboard)/projects/[id]/storage/view.tsx +385 -0
  44. package/app/(dashboard)/projects/[id]/tasks/[taskId]/activity/view.tsx +336 -0
  45. package/app/(dashboard)/projects/[id]/tasks/[taskId]/assessment/view.tsx +113 -0
  46. package/app/(dashboard)/projects/[id]/tasks/[taskId]/blueprint.ts +72 -0
  47. package/app/(dashboard)/projects/[id]/tasks/[taskId]/output/view.tsx +90 -0
  48. package/app/(dashboard)/projects/[id]/tasks/[taskId]/page.tsx +63 -0
  49. package/app/(dashboard)/projects/[id]/tasks/[taskId]/view.tsx +137 -0
  50. package/app/(dashboard)/projects/[id]/tasks/page.tsx +40 -0
  51. package/app/(dashboard)/projects/[id]/tasks/view.tsx +421 -0
  52. package/app/(dashboard)/projects/[id]/view.tsx +421 -0
  53. package/app/(dashboard)/projects/[id]/welcome-banner.tsx +156 -0
  54. package/app/(dashboard)/projects/projects-list.tsx +126 -0
  55. package/app/(playground)/error.tsx +42 -0
  56. package/app/(playground)/layout.tsx +27 -0
  57. package/app/(playground)/projects/[id]/playground/page.tsx +57 -0
  58. package/app/(playground)/projects/[id]/playground/view.tsx +167 -0
  59. package/app/(playground)/projects/[id]/playground-legacy/page.tsx +47 -0
  60. package/app/(playground)/projects/[id]/playground-legacy/skeleton.tsx +63 -0
  61. package/app/(playground)/projects/[id]/playground-legacy/view.tsx +237 -0
  62. package/app/favicon.ico +0 -0
  63. package/app/globals.css +385 -0
  64. package/app/icon.svg +6 -0
  65. package/app/layout.tsx +39 -0
  66. package/app/page.tsx +11 -0
  67. package/components/ai-elements/code-block.tsx +562 -0
  68. package/components/dashboard/agent-capabilities.tsx +326 -0
  69. package/components/dashboard/agent-identity-header.tsx +50 -0
  70. package/components/dashboard/agent-model-picker.tsx +195 -0
  71. package/components/dashboard/agent-model-selector.tsx +121 -0
  72. package/components/dashboard/agent-picker-card.tsx +46 -0
  73. package/components/dashboard/agent-studio.tsx +242 -0
  74. package/components/dashboard/agents-table.tsx +48 -0
  75. package/components/dashboard/breadcrumb.tsx +70 -0
  76. package/components/dashboard/builder-chat.tsx +418 -0
  77. package/components/dashboard/chat-shell.tsx +40 -0
  78. package/components/dashboard/client-picker.tsx +63 -0
  79. package/components/dashboard/command-snippet.tsx +77 -0
  80. package/components/dashboard/connect-dialog.tsx +680 -0
  81. package/components/dashboard/copy-button.tsx +45 -0
  82. package/components/dashboard/copy-card.tsx +48 -0
  83. package/components/dashboard/file-browser.tsx +161 -0
  84. package/components/dashboard/hint.tsx +22 -0
  85. package/components/dashboard/inference-mode.tsx +15 -0
  86. package/components/dashboard/manual-refresh-button.tsx +52 -0
  87. package/components/dashboard/markdown.tsx +31 -0
  88. package/components/dashboard/mcp-install-panel.tsx +196 -0
  89. package/components/dashboard/mission-graph.tsx +262 -0
  90. package/components/dashboard/nav-tabs.tsx +86 -0
  91. package/components/dashboard/polpo-chat.tsx +468 -0
  92. package/components/dashboard/project-copilot.tsx +253 -0
  93. package/components/dashboard/sdk-snippet-panel.tsx +194 -0
  94. package/components/dashboard/section-header.tsx +37 -0
  95. package/components/dashboard/settings-form.tsx +2477 -0
  96. package/components/dashboard/sidebar.tsx +385 -0
  97. package/components/dashboard/skeletons.tsx +1054 -0
  98. package/components/dashboard/skills-install-wizard.tsx +273 -0
  99. package/components/dashboard/swarm-runs.tsx +316 -0
  100. package/components/dashboard/tab-toggle.tsx +35 -0
  101. package/components/dashboard/task-studio.tsx +224 -0
  102. package/components/dashboard/top-header.tsx +203 -0
  103. package/components/dashboard/webhook-deliveries.tsx +234 -0
  104. package/components/icons/coding-agents.tsx +151 -0
  105. package/components/json-ld.tsx +8 -0
  106. package/components/providers.tsx +25 -0
  107. package/components/ui/badge.tsx +52 -0
  108. package/components/ui/button.tsx +60 -0
  109. package/components/ui/card.tsx +103 -0
  110. package/components/ui/chart.tsx +356 -0
  111. package/components/ui/command.tsx +196 -0
  112. package/components/ui/dialog.tsx +157 -0
  113. package/components/ui/input-group.tsx +158 -0
  114. package/components/ui/input.tsx +20 -0
  115. package/components/ui/multi-select.tsx +161 -0
  116. package/components/ui/popover.tsx +90 -0
  117. package/components/ui/select.tsx +201 -0
  118. package/components/ui/separator.tsx +25 -0
  119. package/components/ui/sheet.tsx +135 -0
  120. package/components/ui/skeleton.tsx +13 -0
  121. package/components/ui/tabs.tsx +56 -0
  122. package/components/ui/textarea.tsx +18 -0
  123. package/components/ui/tooltip.tsx +66 -0
  124. package/hooks/use-desktop-sidebar.tsx +50 -0
  125. package/hooks/use-mobile-sidebar.tsx +37 -0
  126. package/index.ts +14 -0
  127. package/lib/api.ts +194 -0
  128. package/lib/builder-context.ts +60 -0
  129. package/lib/data-client.ts +68 -0
  130. package/lib/get-query-client.ts +25 -0
  131. package/lib/polpo-client.tsx +49 -0
  132. package/lib/tool-catalog.ts +234 -0
  133. package/lib/use-event-catalog.ts +28 -0
  134. package/lib/utils.ts +6 -0
  135. package/package.json +99 -0
@@ -0,0 +1,262 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import {
5
+ ReactFlow,
6
+ Background,
7
+ BackgroundVariant,
8
+ Controls,
9
+ MarkerType,
10
+ type Node,
11
+ type Edge,
12
+ type NodeProps,
13
+ Position,
14
+ useNodesState,
15
+ useEdgesState,
16
+ Handle,
17
+ } from "@xyflow/react";
18
+ import "@xyflow/react/dist/style.css";
19
+ import type { Task } from "@polpo-ai/core";
20
+ import Link from "next/link";
21
+
22
+ // ── Status config ──
23
+
24
+ const STATUS_CONFIG: Record<string, { bg: string; border: string; dot: string; text: string }> = {
25
+ done: { bg: "bg-emerald-500/5", border: "border-emerald-500/40", dot: "bg-emerald-500", text: "text-emerald-500" },
26
+ completed: { bg: "bg-emerald-500/5", border: "border-emerald-500/40", dot: "bg-emerald-500", text: "text-emerald-500" },
27
+ in_progress: { bg: "bg-blue-500/5", border: "border-blue-500/40", dot: "bg-blue-500", text: "text-blue-500" },
28
+ review: { bg: "bg-amber-500/5", border: "border-amber-500/40", dot: "bg-amber-500", text: "text-amber-500" },
29
+ pending: { bg: "bg-card/90", border: "border-border", dot: "bg-muted-foreground/30", text: "text-muted-foreground" },
30
+ assigned: { bg: "bg-card/90", border: "border-border", dot: "bg-muted-foreground/50", text: "text-muted-foreground" },
31
+ draft: { bg: "bg-card/90", border: "border-border/60", dot: "bg-muted-foreground/20", text: "text-muted-foreground/60" },
32
+ failed: { bg: "bg-red-500/5", border: "border-red-500/40", dot: "bg-red-500", text: "text-red-500" },
33
+ awaiting_approval: { bg: "bg-amber-500/5", border: "border-amber-500/40", dot: "bg-amber-500", text: "text-amber-500" },
34
+ };
35
+
36
+ const DEFAULT_CFG = { bg: "bg-card/90", border: "border-border", dot: "bg-muted-foreground/30", text: "text-muted-foreground" };
37
+
38
+ // ── Task Node ──
39
+
40
+ interface TaskNodeData {
41
+ task: Task;
42
+ index: number;
43
+ projectId: string;
44
+ readonly?: boolean;
45
+ [key: string]: unknown;
46
+ }
47
+
48
+ function TaskNode({ data }: NodeProps<Node<TaskNodeData>>) {
49
+ const { task, index, projectId, readonly: isReadonly } = data;
50
+ const cfg = STATUS_CONFIG[task.status] ?? DEFAULT_CFG;
51
+ const duration = task.result?.duration != null ? `${Math.round(task.result.duration / 1000)}s` : null;
52
+
53
+ return (
54
+ <>
55
+ <Handle type="target" position={Position.Top} className="!bg-muted-foreground/40 !border-none !w-2 !h-2" />
56
+ <div className={`rounded-xl border-2 ${cfg.border} ${cfg.bg} px-4 py-3 min-w-[220px] max-w-[280px] shadow-sm backdrop-blur-sm`}>
57
+ {/* Header */}
58
+ <div className="flex items-center gap-2 mb-1.5">
59
+ <span className="text-[10px] font-mono text-muted-foreground/60">#{index + 1}</span>
60
+ <span className={`h-2.5 w-2.5 rounded-full ${cfg.dot} ${task.status === "in_progress" ? "animate-pulse" : ""}`} />
61
+ <span className={`text-[10px] font-medium ${cfg.text}`}>{task.status.replace(/_/g, " ")}</span>
62
+ {task.phase && task.status === "in_progress" && (
63
+ <span className="text-[9px] text-muted-foreground/50">({task.phase})</span>
64
+ )}
65
+ {duration && (
66
+ <span className="text-[9px] text-muted-foreground ml-auto font-mono">{duration}</span>
67
+ )}
68
+ </div>
69
+
70
+ {/* Title */}
71
+ {isReadonly ? (
72
+ <span className="text-xs font-semibold leading-tight line-clamp-2 block">{task.title}</span>
73
+ ) : (
74
+ <Link
75
+ href={`/projects/${projectId}/tasks/${task.id}`}
76
+ className="text-xs font-semibold leading-tight line-clamp-2 hover:underline underline-offset-2 block"
77
+ onClick={(e) => e.stopPropagation()}
78
+ >
79
+ {task.title}
80
+ </Link>
81
+ )}
82
+
83
+ {/* Footer */}
84
+ <div className="mt-2 flex items-center gap-2">
85
+ <span className="rounded bg-secondary/80 px-1.5 py-0.5 text-[9px] font-mono text-muted-foreground">
86
+ {task.assignTo}
87
+ </span>
88
+ {task.dependsOn && task.dependsOn.length > 0 && (
89
+ <span className="text-[9px] text-muted-foreground/50">
90
+ {task.dependsOn.length} dep{task.dependsOn.length > 1 ? "s" : ""}
91
+ </span>
92
+ )}
93
+ {task.result?.assessment?.globalScore != null && (
94
+ <span className={`text-[9px] font-medium ml-auto ${task.result.assessment.globalScore >= 0.7 ? "text-emerald-500" : "text-red-400"}`}>
95
+ {Math.round(task.result.assessment.globalScore * 100)}%
96
+ </span>
97
+ )}
98
+ </div>
99
+ </div>
100
+ <Handle type="source" position={Position.Bottom} className="!bg-muted-foreground/40 !border-none !w-2 !h-2" />
101
+ </>
102
+ );
103
+ }
104
+
105
+ const nodeTypes = { task: TaskNode };
106
+
107
+ // ── Layout ──
108
+
109
+ const NODE_WIDTH = 240;
110
+ const NODE_HEIGHT = 120;
111
+ const H_GAP = 60;
112
+ const V_GAP = 80;
113
+
114
+ function layoutNodes(tasks: Task[], projectId: string): { nodes: Node<TaskNodeData>[]; edges: Edge[] } {
115
+ const taskMap = new Map(tasks.map(t => [t.id, t]));
116
+
117
+ // Compute depth per task
118
+ const depths = new Map<string, number>();
119
+
120
+ function getDepth(taskId: string, visited = new Set<string>()): number {
121
+ if (depths.has(taskId)) return depths.get(taskId)!;
122
+ if (visited.has(taskId)) return 0;
123
+ visited.add(taskId);
124
+
125
+ const task = taskMap.get(taskId);
126
+ if (!task?.dependsOn?.length) {
127
+ depths.set(taskId, 0);
128
+ return 0;
129
+ }
130
+
131
+ const maxParent = Math.max(...task.dependsOn.map(d => getDepth(d, visited)));
132
+ const depth = maxParent + 1;
133
+ depths.set(taskId, depth);
134
+ return depth;
135
+ }
136
+
137
+ for (const task of tasks) getDepth(task.id);
138
+
139
+ // Group by depth
140
+ const layers = new Map<number, Task[]>();
141
+ for (const task of tasks) {
142
+ const d = depths.get(task.id) ?? 0;
143
+ if (!layers.has(d)) layers.set(d, []);
144
+ layers.get(d)!.push(task);
145
+ }
146
+
147
+ const maxDepth = Math.max(...layers.keys(), 0);
148
+ const nodes: Node<TaskNodeData>[] = [];
149
+
150
+ // Track task index globally
151
+ let globalIndex = 0;
152
+
153
+ for (let depth = 0; depth <= maxDepth; depth++) {
154
+ const layerTasks = layers.get(depth) ?? [];
155
+ const totalWidth = layerTasks.length * NODE_WIDTH + (layerTasks.length - 1) * H_GAP;
156
+ const startX = -totalWidth / 2;
157
+
158
+ layerTasks.forEach((task, i) => {
159
+ nodes.push({
160
+ id: task.id,
161
+ type: "task",
162
+ position: {
163
+ x: startX + i * (NODE_WIDTH + H_GAP),
164
+ y: depth * (NODE_HEIGHT + V_GAP),
165
+ },
166
+ data: { task, index: globalIndex++, projectId },
167
+ });
168
+ });
169
+ }
170
+
171
+ // Edges with arrows
172
+ const edges: Edge[] = [];
173
+ for (const task of tasks) {
174
+ if (!task.dependsOn) continue;
175
+ for (const depId of task.dependsOn) {
176
+ const sourceTask = taskMap.get(depId);
177
+ if (!sourceTask) continue;
178
+
179
+ const isDone = sourceTask.status === "done";
180
+ const isFailed = sourceTask.status === "failed";
181
+ const isActive = sourceTask.status === "in_progress";
182
+
183
+ const edgeColor = "#888";
184
+
185
+ edges.push({
186
+ id: `${depId}->${task.id}`,
187
+ source: depId,
188
+ target: task.id,
189
+ type: "smoothstep",
190
+ animated: isActive,
191
+ markerEnd: {
192
+ type: MarkerType.ArrowClosed,
193
+ width: 14,
194
+ height: 14,
195
+ color: edgeColor,
196
+ },
197
+ style: {
198
+ stroke: edgeColor,
199
+ strokeWidth: 1.5,
200
+ strokeDasharray: isActive ? undefined : "6 4",
201
+ },
202
+ });
203
+ }
204
+ }
205
+
206
+ return { nodes, edges };
207
+ }
208
+
209
+ // ── Component ──
210
+
211
+ interface MissionGraphProps {
212
+ tasks: Task[];
213
+ projectId: string;
214
+ readonly?: boolean;
215
+ }
216
+
217
+ export function MissionGraph({ tasks, projectId, readonly = false }: MissionGraphProps) {
218
+ const { nodes: initialNodes, edges: initialEdges } = useMemo(() => {
219
+ const layout = layoutNodes(tasks, projectId);
220
+ if (readonly) {
221
+ for (const node of layout.nodes) {
222
+ node.data.readonly = true;
223
+ }
224
+ }
225
+ return layout;
226
+ }, [tasks, projectId, readonly]);
227
+
228
+ const [nodes, , onNodesChange] = useNodesState(initialNodes);
229
+ const [edges, , onEdgesChange] = useEdgesState(initialEdges);
230
+
231
+ return (
232
+ <div className="h-[600px] w-full rounded-xl border border-border overflow-hidden">
233
+ <ReactFlow
234
+ nodes={nodes}
235
+ edges={edges}
236
+ onNodesChange={onNodesChange}
237
+ onEdgesChange={onEdgesChange}
238
+ nodeTypes={nodeTypes}
239
+ fitView
240
+ fitViewOptions={{ padding: 0.3 }}
241
+ minZoom={0.2}
242
+ maxZoom={2}
243
+ proOptions={{ hideAttribution: true }}
244
+ nodesDraggable
245
+ nodesConnectable={false}
246
+ defaultEdgeOptions={{ type: "smoothstep" }}
247
+ style={{ background: "hsl(var(--card))" }}
248
+ >
249
+ <Background
250
+ variant={BackgroundVariant.Dots}
251
+ gap={20}
252
+ size={1.5}
253
+ color="#555"
254
+ />
255
+ <Controls
256
+ showInteractive={false}
257
+ className="!bg-card !border-border !shadow-none [&>button]:!bg-card [&>button]:!border-border [&>button]:!text-muted-foreground [&>button:hover]:!bg-secondary"
258
+ />
259
+ </ReactFlow>
260
+ </div>
261
+ );
262
+ }
@@ -0,0 +1,86 @@
1
+ "use client";
2
+
3
+ import { usePathname } from "next/navigation";
4
+ import Link from "next/link";
5
+ import type { LucideIcon } from "lucide-react";
6
+
7
+ interface Tab {
8
+ label: string;
9
+ href: string;
10
+ icon?: LucideIcon;
11
+ }
12
+
13
+ export function NavTabs({
14
+ basePath,
15
+ tabs,
16
+ className,
17
+ orientation = "horizontal",
18
+ }: {
19
+ basePath: string;
20
+ tabs: Tab[];
21
+ className?: string;
22
+ orientation?: "horizontal" | "vertical";
23
+ }) {
24
+ const pathname = usePathname();
25
+ const vertical = orientation === "vertical";
26
+
27
+ return (
28
+ <div
29
+ data-testid="nav-tabs"
30
+ className={
31
+ vertical
32
+ ? `flex flex-col gap-0.5 ${className ?? ""}`
33
+ : `flex gap-1 border-b border-border overflow-x-auto scrollbar-none ${className ?? ""}`
34
+ }
35
+ >
36
+ {tabs.map((tab) => {
37
+ const href = `${basePath}${tab.href}`;
38
+ const active = tab.href === ""
39
+ ? pathname === basePath
40
+ : pathname.startsWith(href);
41
+
42
+ if (vertical) {
43
+ const Icon = tab.icon;
44
+ return (
45
+ <Link
46
+ key={tab.label}
47
+ href={href}
48
+ data-testid={`tab-${tab.label.toLowerCase().replace(/\s+/g, "-")}`}
49
+ className={`relative flex items-center gap-2.5 border-l-2 px-3 py-1.5 text-sm transition-colors ${
50
+ active
51
+ ? "border-foreground bg-foreground/5 font-medium text-foreground"
52
+ : "border-transparent text-muted-foreground hover:border-border hover:text-foreground"
53
+ }`}
54
+ >
55
+ {Icon && (
56
+ <Icon
57
+ className={`h-4 w-4 shrink-0 ${active ? "text-foreground" : "text-muted-foreground/60"}`}
58
+ strokeWidth={1.5}
59
+ />
60
+ )}
61
+ {tab.label}
62
+ </Link>
63
+ );
64
+ }
65
+
66
+ return (
67
+ <Link
68
+ key={tab.label}
69
+ href={href}
70
+ data-testid={`tab-${tab.label.toLowerCase().replace(/\s+/g, "-")}`}
71
+ className={`relative px-3 py-2 text-sm transition-colors whitespace-nowrap ${
72
+ active
73
+ ? "text-foreground font-medium"
74
+ : "text-muted-foreground hover:text-foreground"
75
+ }`}
76
+ >
77
+ {tab.label}
78
+ {active && (
79
+ <span className="absolute bottom-0 left-0 right-0 h-[2px] bg-foreground" />
80
+ )}
81
+ </Link>
82
+ );
83
+ })}
84
+ </div>
85
+ );
86
+ }