clawport-ui 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 (132) hide show
  1. package/.env.example +35 -0
  2. package/BRANDING.md +131 -0
  3. package/CLAUDE.md +252 -0
  4. package/README.md +262 -0
  5. package/SETUP.md +337 -0
  6. package/app/agents/[id]/page.tsx +727 -0
  7. package/app/api/agents/route.ts +12 -0
  8. package/app/api/chat/[id]/route.ts +139 -0
  9. package/app/api/cron-runs/route.ts +13 -0
  10. package/app/api/crons/route.ts +12 -0
  11. package/app/api/kanban/chat/[id]/route.ts +119 -0
  12. package/app/api/kanban/chat-history/[ticketId]/route.ts +36 -0
  13. package/app/api/memory/route.ts +12 -0
  14. package/app/api/transcribe/route.ts +37 -0
  15. package/app/api/tts/route.ts +42 -0
  16. package/app/chat/[id]/page.tsx +10 -0
  17. package/app/chat/page.tsx +200 -0
  18. package/app/crons/page.tsx +870 -0
  19. package/app/docs/page.tsx +399 -0
  20. package/app/favicon.ico +0 -0
  21. package/app/globals.css +692 -0
  22. package/app/kanban/page.tsx +327 -0
  23. package/app/layout.tsx +45 -0
  24. package/app/memory/page.tsx +685 -0
  25. package/app/page.tsx +817 -0
  26. package/app/providers.tsx +37 -0
  27. package/app/settings/page.tsx +901 -0
  28. package/app/settings-provider.tsx +209 -0
  29. package/components/AgentAvatar.tsx +54 -0
  30. package/components/AgentNode.tsx +122 -0
  31. package/components/Breadcrumbs.tsx +126 -0
  32. package/components/DynamicFavicon.tsx +62 -0
  33. package/components/ErrorState.tsx +97 -0
  34. package/components/FeedView.tsx +494 -0
  35. package/components/GlobalSearch.tsx +571 -0
  36. package/components/GridView.tsx +532 -0
  37. package/components/ManorMap.tsx +157 -0
  38. package/components/MobileSidebar.tsx +251 -0
  39. package/components/NavLinks.tsx +271 -0
  40. package/components/OnboardingWizard.tsx +1067 -0
  41. package/components/Sidebar.tsx +115 -0
  42. package/components/ThemeToggle.tsx +108 -0
  43. package/components/chat/AgentList.tsx +537 -0
  44. package/components/chat/ConversationView.tsx +1047 -0
  45. package/components/chat/FileAttachment.tsx +140 -0
  46. package/components/chat/MediaPreview.tsx +111 -0
  47. package/components/chat/VoiceMessage.tsx +139 -0
  48. package/components/crons/PipelineGraph.tsx +327 -0
  49. package/components/crons/WeeklySchedule.tsx +630 -0
  50. package/components/docs/AgentsSection.tsx +209 -0
  51. package/components/docs/ApiReferenceSection.tsx +256 -0
  52. package/components/docs/ArchitectureSection.tsx +221 -0
  53. package/components/docs/ComponentsSection.tsx +253 -0
  54. package/components/docs/CronSystemSection.tsx +235 -0
  55. package/components/docs/DocSection.tsx +346 -0
  56. package/components/docs/GettingStartedSection.tsx +169 -0
  57. package/components/docs/ThemingSection.tsx +257 -0
  58. package/components/docs/TroubleshootingSection.tsx +200 -0
  59. package/components/kanban/AgentPicker.tsx +321 -0
  60. package/components/kanban/CreateTicketModal.tsx +333 -0
  61. package/components/kanban/KanbanBoard.tsx +70 -0
  62. package/components/kanban/KanbanColumn.tsx +166 -0
  63. package/components/kanban/TicketCard.tsx +245 -0
  64. package/components/kanban/TicketDetailPanel.tsx +850 -0
  65. package/components/ui/badge.tsx +48 -0
  66. package/components/ui/button.tsx +64 -0
  67. package/components/ui/card.tsx +92 -0
  68. package/components/ui/dialog.tsx +158 -0
  69. package/components/ui/scroll-area.tsx +58 -0
  70. package/components/ui/separator.tsx +28 -0
  71. package/components/ui/skeleton.tsx +27 -0
  72. package/components/ui/tabs.tsx +91 -0
  73. package/components/ui/tooltip.tsx +57 -0
  74. package/components.json +23 -0
  75. package/docs/API.md +648 -0
  76. package/docs/COMPONENTS.md +1059 -0
  77. package/docs/THEMING.md +795 -0
  78. package/lib/agents-registry.ts +35 -0
  79. package/lib/agents.json +282 -0
  80. package/lib/agents.test.ts +367 -0
  81. package/lib/agents.ts +32 -0
  82. package/lib/anthropic.test.ts +422 -0
  83. package/lib/anthropic.ts +220 -0
  84. package/lib/api-error.ts +16 -0
  85. package/lib/audio-recorder.test.ts +72 -0
  86. package/lib/audio-recorder.ts +169 -0
  87. package/lib/conversations.test.ts +331 -0
  88. package/lib/conversations.ts +117 -0
  89. package/lib/cron-pipelines.test.ts +69 -0
  90. package/lib/cron-pipelines.ts +58 -0
  91. package/lib/cron-runs.test.ts +118 -0
  92. package/lib/cron-runs.ts +67 -0
  93. package/lib/cron-utils.test.ts +222 -0
  94. package/lib/cron-utils.ts +160 -0
  95. package/lib/crons.test.ts +502 -0
  96. package/lib/crons.ts +114 -0
  97. package/lib/env.test.ts +44 -0
  98. package/lib/env.ts +14 -0
  99. package/lib/kanban/automation.test.ts +245 -0
  100. package/lib/kanban/automation.ts +143 -0
  101. package/lib/kanban/chat-store.test.ts +149 -0
  102. package/lib/kanban/chat-store.ts +81 -0
  103. package/lib/kanban/store.test.ts +238 -0
  104. package/lib/kanban/store.ts +98 -0
  105. package/lib/kanban/types.ts +50 -0
  106. package/lib/kanban/useAgentWork.ts +78 -0
  107. package/lib/memory.ts +45 -0
  108. package/lib/multimodal.test.ts +219 -0
  109. package/lib/multimodal.ts +68 -0
  110. package/lib/pipeline.integration.test.ts +343 -0
  111. package/lib/sanitize.ts +194 -0
  112. package/lib/settings.test.ts +137 -0
  113. package/lib/settings.ts +94 -0
  114. package/lib/styles.ts +24 -0
  115. package/lib/themes.ts +9 -0
  116. package/lib/transcribe.test.ts +141 -0
  117. package/lib/transcribe.ts +111 -0
  118. package/lib/types.ts +66 -0
  119. package/lib/utils.ts +6 -0
  120. package/lib/validation.test.ts +132 -0
  121. package/lib/validation.ts +80 -0
  122. package/next.config.ts +7 -0
  123. package/package.json +56 -0
  124. package/postcss.config.mjs +7 -0
  125. package/public/file.svg +1 -0
  126. package/public/globe.svg +1 -0
  127. package/public/next.svg +1 -0
  128. package/public/vercel.svg +1 -0
  129. package/public/window.svg +1 -0
  130. package/scripts/setup.mjs +215 -0
  131. package/tsconfig.json +34 -0
  132. package/vitest.config.ts +17 -0
@@ -0,0 +1,327 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState, useCallback } from 'react'
4
+ import type { Agent } from '@/lib/types'
5
+ import type { KanbanTicket, TicketStatus, TicketPriority, TeamRole } from '@/lib/kanban/types'
6
+ import {
7
+ loadTickets,
8
+ saveTickets,
9
+ createTicket,
10
+ updateTicket,
11
+ moveTicket,
12
+ deleteTicket,
13
+ type KanbanStore,
14
+ } from '@/lib/kanban/store'
15
+ import { useAgentWork } from '@/lib/kanban/useAgentWork'
16
+ import { Plus } from 'lucide-react'
17
+ import { KanbanBoard } from '@/components/kanban/KanbanBoard'
18
+ import { CreateTicketModal } from '@/components/kanban/CreateTicketModal'
19
+ import { TicketDetailPanel } from '@/components/kanban/TicketDetailPanel'
20
+ import { AgentAvatar } from '@/components/AgentAvatar'
21
+ import { ErrorState } from '@/components/ErrorState'
22
+ import { Skeleton } from '@/components/ui/skeleton'
23
+
24
+ export default function KanbanPage() {
25
+ const [tickets, setTickets] = useState<KanbanStore>({})
26
+ const [agents, setAgents] = useState<Agent[]>([])
27
+ const [loading, setLoading] = useState(true)
28
+ const [error, setError] = useState<string | null>(null)
29
+ const [createOpen, setCreateOpen] = useState(false)
30
+ const [selectedTicket, setSelectedTicket] = useState<KanbanTicket | null>(null)
31
+ const [filterAgentId, setFilterAgentId] = useState<string | null>(null)
32
+
33
+ const loadData = useCallback(() => {
34
+ setLoading(true)
35
+ setError(null)
36
+
37
+ // Load tickets from localStorage
38
+ const stored = loadTickets()
39
+ setTickets(stored)
40
+
41
+ // Load agents from API
42
+ fetch('/api/agents')
43
+ .then((r) => {
44
+ if (!r.ok) throw new Error('Failed to fetch agents')
45
+ return r.json()
46
+ })
47
+ .then((a: Agent[]) => setAgents(a))
48
+ .catch((e) => setError(e.message))
49
+ .finally(() => setLoading(false))
50
+ }, [])
51
+
52
+ useEffect(() => {
53
+ loadData()
54
+ }, [loadData])
55
+
56
+ // Persist tickets whenever they change
57
+ useEffect(() => {
58
+ if (!loading) {
59
+ saveTickets(tickets)
60
+ }
61
+ }, [tickets, loading])
62
+
63
+ // Keep selectedTicket in sync with store
64
+ useEffect(() => {
65
+ if (selectedTicket && tickets[selectedTicket.id]) {
66
+ const current = tickets[selectedTicket.id]
67
+ if (current.updatedAt !== selectedTicket.updatedAt) {
68
+ setSelectedTicket(current)
69
+ }
70
+ }
71
+ }, [tickets, selectedTicket])
72
+
73
+ function handleCreateTicket(data: {
74
+ title: string
75
+ description: string
76
+ priority: TicketPriority
77
+ assigneeId: string | null
78
+ assigneeRole: TeamRole | null
79
+ }) {
80
+ setTickets((prev) =>
81
+ createTicket(prev, {
82
+ ...data,
83
+ status: 'backlog',
84
+ }),
85
+ )
86
+ }
87
+
88
+ function handleMoveTicket(ticketId: string, status: TicketStatus) {
89
+ setTickets((prev) => moveTicket(prev, ticketId, status))
90
+ }
91
+
92
+ function handleDeleteTicket(ticketId: string) {
93
+ setTickets((prev) => deleteTicket(prev, ticketId))
94
+ setSelectedTicket(null)
95
+ }
96
+
97
+ const handleUpdateTicket = useCallback(
98
+ (ticketId: string, updates: Partial<KanbanTicket>) => {
99
+ setTickets((prev) => updateTicket(prev, ticketId, updates))
100
+ },
101
+ [],
102
+ )
103
+
104
+ const { isWorking } = useAgentWork({
105
+ tickets,
106
+ onUpdateTicket: handleUpdateTicket,
107
+ })
108
+
109
+ function handleRetryWork(ticketId: string) {
110
+ setTickets((prev) =>
111
+ updateTicket(prev, ticketId, {
112
+ status: 'todo',
113
+ workState: 'idle',
114
+ workError: null,
115
+ workStartedAt: null,
116
+ }),
117
+ )
118
+ }
119
+
120
+ function handleTicketClick(ticket: KanbanTicket) {
121
+ setSelectedTicket(ticket)
122
+ }
123
+
124
+ if (error) {
125
+ return <ErrorState message={error} onRetry={loadData} />
126
+ }
127
+
128
+ const selectedAgent = selectedTicket?.assigneeId
129
+ ? agents.find((a) => a.id === selectedTicket.assigneeId) ?? null
130
+ : null
131
+
132
+ const ticketCount = Object.keys(tickets).length
133
+
134
+ // Agents that have at least one ticket assigned
135
+ const assignedAgentIds = new Set(
136
+ Object.values(tickets)
137
+ .map((t) => t.assigneeId)
138
+ .filter(Boolean),
139
+ )
140
+ const assignedAgents = agents.filter((a) => assignedAgentIds.has(a.id))
141
+
142
+ return (
143
+ <div className="flex h-full relative" style={{ background: 'var(--bg)' }}>
144
+ {/* Board area */}
145
+ <div className="flex-1 h-full flex flex-col" style={{ minWidth: 0 }}>
146
+ {/* Header */}
147
+ <div
148
+ style={{
149
+ padding: 'var(--space-4) var(--space-5)',
150
+ display: 'flex',
151
+ alignItems: 'center',
152
+ justifyContent: 'space-between',
153
+ flexShrink: 0,
154
+ borderBottom: '1px solid var(--separator)',
155
+ }}
156
+ >
157
+ <div>
158
+ <h1
159
+ style={{
160
+ fontSize: 'var(--text-title2)',
161
+ fontWeight: 'var(--weight-bold)',
162
+ color: 'var(--text-primary)',
163
+ margin: 0,
164
+ letterSpacing: '-0.3px',
165
+ }}
166
+ >
167
+ Kanban Board
168
+ </h1>
169
+ <p
170
+ style={{
171
+ fontSize: 'var(--text-caption1)',
172
+ color: 'var(--text-tertiary)',
173
+ margin: '2px 0 0',
174
+ }}
175
+ >
176
+ {ticketCount} ticket{ticketCount !== 1 ? 's' : ''}
177
+ </p>
178
+ </div>
179
+
180
+ <button
181
+ onClick={() => setCreateOpen(true)}
182
+ className="btn-primary focus-ring btn-scale"
183
+ style={{
184
+ borderRadius: 'var(--radius-md)',
185
+ padding: '8px 16px',
186
+ fontSize: 'var(--text-footnote)',
187
+ fontWeight: 'var(--weight-semibold)',
188
+ border: 'none',
189
+ display: 'flex',
190
+ alignItems: 'center',
191
+ gap: 'var(--space-2)',
192
+ }}
193
+ >
194
+ <Plus size={16} />
195
+ New Ticket
196
+ </button>
197
+ </div>
198
+
199
+ {/* Agent filter bar */}
200
+ {assignedAgents.length > 0 && (
201
+ <div
202
+ style={{
203
+ display: 'flex',
204
+ alignItems: 'center',
205
+ gap: 'var(--space-2)',
206
+ padding: 'var(--space-2) var(--space-5)',
207
+ overflowX: 'auto',
208
+ flexShrink: 0,
209
+ }}
210
+ >
211
+ <button
212
+ onClick={() => setFilterAgentId(null)}
213
+ className="focus-ring"
214
+ style={{
215
+ display: 'flex',
216
+ alignItems: 'center',
217
+ gap: 'var(--space-1)',
218
+ padding: '4px 12px',
219
+ borderRadius: 'var(--radius-full)',
220
+ border: 'none',
221
+ fontSize: 'var(--text-caption1)',
222
+ fontWeight: 600,
223
+ cursor: 'pointer',
224
+ background: filterAgentId === null ? 'var(--accent)' : 'var(--fill-tertiary)',
225
+ color: filterAgentId === null ? 'white' : 'var(--text-secondary)',
226
+ flexShrink: 0,
227
+ }}
228
+ >
229
+ All
230
+ </button>
231
+ {assignedAgents.map((agent) => (
232
+ <button
233
+ key={agent.id}
234
+ onClick={() =>
235
+ setFilterAgentId(filterAgentId === agent.id ? null : agent.id)
236
+ }
237
+ className="focus-ring"
238
+ style={{
239
+ display: 'flex',
240
+ alignItems: 'center',
241
+ gap: 'var(--space-1)',
242
+ padding: '4px 12px 4px 4px',
243
+ borderRadius: 'var(--radius-full)',
244
+ border: 'none',
245
+ fontSize: 'var(--text-caption1)',
246
+ fontWeight: 600,
247
+ cursor: 'pointer',
248
+ background:
249
+ filterAgentId === agent.id
250
+ ? `${agent.color}30`
251
+ : 'var(--fill-tertiary)',
252
+ color:
253
+ filterAgentId === agent.id
254
+ ? agent.color
255
+ : 'var(--text-secondary)',
256
+ flexShrink: 0,
257
+ }}
258
+ >
259
+ <AgentAvatar agent={agent} size={20} borderRadius={10} />
260
+ {agent.name}
261
+ </button>
262
+ ))}
263
+ </div>
264
+ )}
265
+
266
+ {/* Board */}
267
+ <div style={{ flex: 1, padding: '0 var(--space-3)', minHeight: 0 }}>
268
+ {loading ? (
269
+ <div
270
+ className="flex gap-3 h-full"
271
+ style={{ padding: 'var(--space-4) 0' }}
272
+ >
273
+ {[1, 2, 3, 4, 5].map((i) => (
274
+ <div key={i} style={{ flex: '1 0 200px' }}>
275
+ <Skeleton
276
+ width="100%"
277
+ height="100%"
278
+ style={{ borderRadius: 'var(--radius-lg)' }}
279
+ />
280
+ </div>
281
+ ))}
282
+ </div>
283
+ ) : (
284
+ <KanbanBoard
285
+ tickets={tickets}
286
+ agents={agents}
287
+ onTicketClick={handleTicketClick}
288
+ onMoveTicket={handleMoveTicket}
289
+ onCreateTicket={() => setCreateOpen(true)}
290
+ isWorking={isWorking}
291
+ filterAgentId={filterAgentId}
292
+ />
293
+ )}
294
+ </div>
295
+ </div>
296
+
297
+ {/* Mobile backdrop */}
298
+ {selectedTicket && (
299
+ <div
300
+ className="fixed inset-0 z-30 md:hidden"
301
+ style={{ background: 'rgba(0,0,0,0.5)' }}
302
+ onClick={() => setSelectedTicket(null)}
303
+ />
304
+ )}
305
+
306
+ {/* Detail panel */}
307
+ {selectedTicket && (
308
+ <TicketDetailPanel
309
+ ticket={selectedTicket}
310
+ agent={selectedAgent}
311
+ onClose={() => setSelectedTicket(null)}
312
+ onStatusChange={(status) => handleMoveTicket(selectedTicket.id, status)}
313
+ onDelete={() => handleDeleteTicket(selectedTicket.id)}
314
+ onRetryWork={() => handleRetryWork(selectedTicket.id)}
315
+ />
316
+ )}
317
+
318
+ {/* Create ticket modal */}
319
+ <CreateTicketModal
320
+ open={createOpen}
321
+ onOpenChange={setCreateOpen}
322
+ agents={agents}
323
+ onSubmit={handleCreateTicket}
324
+ />
325
+ </div>
326
+ )
327
+ }
package/app/layout.tsx ADDED
@@ -0,0 +1,45 @@
1
+ import type { Metadata } from 'next';
2
+ import './globals.css';
3
+ import { ThemeProvider } from './providers';
4
+ import { SettingsProvider } from './settings-provider';
5
+ import { Sidebar } from '@/components/Sidebar';
6
+ import { DynamicFavicon } from '@/components/DynamicFavicon';
7
+ import { OnboardingWizard } from '@/components/OnboardingWizard';
8
+
9
+ export const metadata: Metadata = {
10
+ title: 'ClawPort -- Command Centre',
11
+ description: 'AI Agent Management Dashboard',
12
+ };
13
+
14
+ export default function RootLayout({
15
+ children,
16
+ }: {
17
+ children: React.ReactNode;
18
+ }) {
19
+ return (
20
+ <html lang="en" data-theme="dark" suppressHydrationWarning>
21
+ <body>
22
+ <ThemeProvider>
23
+ <SettingsProvider>
24
+ <DynamicFavicon />
25
+ <OnboardingWizard />
26
+ <div
27
+ className="flex h-screen overflow-hidden"
28
+ style={{ background: 'var(--bg)' }}
29
+ >
30
+ {/* Client-side shell handles both desktop sidebar + mobile */}
31
+ <Sidebar />
32
+
33
+ {/* Main content */}
34
+ <main className="flex-1 overflow-hidden relative">
35
+ {/* Mobile spacer for fixed header */}
36
+ <div className="md:hidden" style={{ height: '48px', flexShrink: 0 }} />
37
+ {children}
38
+ </main>
39
+ </div>
40
+ </SettingsProvider>
41
+ </ThemeProvider>
42
+ </body>
43
+ </html>
44
+ );
45
+ }