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,70 @@
1
+ 'use client'
2
+
3
+ import { COLUMNS } from '@/lib/kanban/types'
4
+ import type { KanbanTicket, TicketStatus } from '@/lib/kanban/types'
5
+ import type { KanbanStore } from '@/lib/kanban/store'
6
+ import { getTicketsByStatus } from '@/lib/kanban/store'
7
+ import type { Agent } from '@/lib/types'
8
+ import { KanbanColumn } from './KanbanColumn'
9
+ import { TicketCard } from './TicketCard'
10
+
11
+ interface KanbanBoardProps {
12
+ tickets: KanbanStore
13
+ agents: Agent[]
14
+ onTicketClick: (ticket: KanbanTicket) => void
15
+ onMoveTicket: (ticketId: string, status: TicketStatus) => void
16
+ onCreateTicket: () => void
17
+ isWorking?: (ticketId: string) => boolean
18
+ filterAgentId?: string | null
19
+ }
20
+
21
+ export function KanbanBoard({
22
+ tickets,
23
+ agents,
24
+ onTicketClick,
25
+ onMoveTicket,
26
+ onCreateTicket,
27
+ isWorking,
28
+ filterAgentId,
29
+ }: KanbanBoardProps) {
30
+ return (
31
+ <div
32
+ style={{
33
+ display: 'flex',
34
+ gap: 'var(--space-3)',
35
+ height: '100%',
36
+ overflowX: 'auto',
37
+ overflowY: 'hidden',
38
+ padding: 'var(--space-2) 0',
39
+ WebkitOverflowScrolling: 'touch',
40
+ }}
41
+ >
42
+ {COLUMNS.map((column) => {
43
+ const allColumnTickets = getTicketsByStatus(tickets, column.id)
44
+ const columnTickets = filterAgentId
45
+ ? allColumnTickets.filter((t) => t.assigneeId === filterAgentId)
46
+ : allColumnTickets
47
+
48
+ return (
49
+ <KanbanColumn
50
+ key={column.id}
51
+ column={column}
52
+ tickets={columnTickets}
53
+ agents={agents}
54
+ onTicketClick={onTicketClick}
55
+ onDrop={onMoveTicket}
56
+ onCreateTicket={column.id === 'backlog' ? onCreateTicket : undefined}
57
+ renderTicket={(ticket) => (
58
+ <TicketCard
59
+ ticket={ticket}
60
+ agent={agents.find((a) => a.id === ticket.assigneeId) ?? null}
61
+ onClick={() => onTicketClick(ticket)}
62
+ isWorking={isWorking?.(ticket.id)}
63
+ />
64
+ )}
65
+ />
66
+ )
67
+ })}
68
+ </div>
69
+ )
70
+ }
@@ -0,0 +1,166 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { Plus } from 'lucide-react'
5
+ import type { KanbanColumn as KanbanColumnType, KanbanTicket, TicketStatus } from '@/lib/kanban/types'
6
+ import type { Agent } from '@/lib/types'
7
+
8
+ interface KanbanColumnProps {
9
+ column: KanbanColumnType
10
+ tickets: KanbanTicket[]
11
+ agents: Agent[]
12
+ onTicketClick: (ticket: KanbanTicket) => void
13
+ onDrop: (ticketId: string, status: TicketStatus) => void
14
+ onCreateTicket?: () => void
15
+ renderTicket: (ticket: KanbanTicket) => React.ReactNode
16
+ }
17
+
18
+ export function KanbanColumn({
19
+ column,
20
+ tickets,
21
+ agents,
22
+ onTicketClick,
23
+ onDrop,
24
+ onCreateTicket,
25
+ renderTicket,
26
+ }: KanbanColumnProps) {
27
+ const [isDragOver, setIsDragOver] = useState(false)
28
+
29
+ function handleDragOver(e: React.DragEvent) {
30
+ e.preventDefault()
31
+ e.dataTransfer.dropEffect = 'move'
32
+ setIsDragOver(true)
33
+ }
34
+
35
+ function handleDragLeave(e: React.DragEvent) {
36
+ // Only set false when leaving the column itself, not a child
37
+ if (!e.currentTarget.contains(e.relatedTarget as Node)) {
38
+ setIsDragOver(false)
39
+ }
40
+ }
41
+
42
+ function handleDrop(e: React.DragEvent) {
43
+ e.preventDefault()
44
+ setIsDragOver(false)
45
+ const ticketId = e.dataTransfer.getData('text/plain')
46
+ if (ticketId) {
47
+ onDrop(ticketId, column.id)
48
+ }
49
+ }
50
+
51
+ return (
52
+ <div
53
+ onDragOver={handleDragOver}
54
+ onDragLeave={handleDragLeave}
55
+ onDrop={handleDrop}
56
+ style={{
57
+ display: 'flex',
58
+ flexDirection: 'column',
59
+ minWidth: 280,
60
+ maxWidth: 320,
61
+ flex: '1 0 280px',
62
+ height: '100%',
63
+ borderRadius: 'var(--radius-lg)',
64
+ background: isDragOver ? 'var(--fill-secondary)' : 'var(--fill-tertiary)',
65
+ border: isDragOver
66
+ ? '2px dashed var(--accent)'
67
+ : '2px dashed transparent',
68
+ transition: 'background 200ms var(--ease-smooth), border-color 200ms var(--ease-smooth)',
69
+ }}
70
+ >
71
+ {/* Column header */}
72
+ <div
73
+ style={{
74
+ display: 'flex',
75
+ alignItems: 'center',
76
+ justifyContent: 'space-between',
77
+ padding: 'var(--space-3) var(--space-4)',
78
+ flexShrink: 0,
79
+ }}
80
+ >
81
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-2)' }}>
82
+ <span
83
+ style={{
84
+ fontSize: 'var(--text-footnote)',
85
+ fontWeight: 'var(--weight-semibold)',
86
+ color: 'var(--text-primary)',
87
+ letterSpacing: '-0.01em',
88
+ }}
89
+ >
90
+ {column.title}
91
+ </span>
92
+ <span
93
+ style={{
94
+ fontSize: 'var(--text-caption2)',
95
+ fontWeight: 'var(--weight-medium)',
96
+ color: 'var(--text-tertiary)',
97
+ background: 'var(--fill-secondary)',
98
+ borderRadius: 'var(--radius-sm)',
99
+ padding: '1px 6px',
100
+ minWidth: 20,
101
+ textAlign: 'center',
102
+ }}
103
+ >
104
+ {tickets.length}
105
+ </span>
106
+ </div>
107
+
108
+ {column.id === 'backlog' && onCreateTicket && (
109
+ <button
110
+ onClick={onCreateTicket}
111
+ className="focus-ring hover-bg"
112
+ aria-label="Create new ticket"
113
+ style={{
114
+ display: 'flex',
115
+ alignItems: 'center',
116
+ justifyContent: 'center',
117
+ width: 24,
118
+ height: 24,
119
+ borderRadius: 'var(--radius-sm)',
120
+ border: 'none',
121
+ background: 'transparent',
122
+ color: 'var(--text-secondary)',
123
+ cursor: 'pointer',
124
+ padding: 0,
125
+ transition: 'color 150ms var(--ease-smooth)',
126
+ }}
127
+ >
128
+ <Plus size={16} />
129
+ </button>
130
+ )}
131
+ </div>
132
+
133
+ {/* Scrollable ticket area */}
134
+ <div
135
+ style={{
136
+ flex: 1,
137
+ overflowY: 'auto',
138
+ padding: '0 var(--space-2) var(--space-2)',
139
+ display: 'flex',
140
+ flexDirection: 'column',
141
+ gap: 'var(--space-2)',
142
+ }}
143
+ >
144
+ {tickets.map((ticket) => (
145
+ <div key={ticket.id}>
146
+ {renderTicket(ticket)}
147
+ </div>
148
+ ))}
149
+
150
+ {/* Empty state */}
151
+ {tickets.length === 0 && (
152
+ <div
153
+ style={{
154
+ padding: 'var(--space-8) var(--space-4)',
155
+ textAlign: 'center',
156
+ fontSize: 'var(--text-caption1)',
157
+ color: 'var(--text-tertiary)',
158
+ }}
159
+ >
160
+ No tickets
161
+ </div>
162
+ )}
163
+ </div>
164
+ </div>
165
+ )
166
+ }
@@ -0,0 +1,245 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import type { Agent } from '@/lib/types'
5
+ import {
6
+ KanbanTicket,
7
+ PRIORITY_COLORS,
8
+ ROLE_LABELS,
9
+ } from '@/lib/kanban/types'
10
+ import { AgentAvatar } from '@/components/AgentAvatar'
11
+
12
+ const PRIORITY_LABELS: Record<string, string> = {
13
+ low: 'Low',
14
+ medium: 'Med',
15
+ high: 'High',
16
+ }
17
+
18
+ function relativeTime(ts: number): string {
19
+ const diff = Date.now() - ts
20
+ const mins = Math.floor(diff / 60000)
21
+ if (mins < 1) return 'just now'
22
+ if (mins < 60) return `${mins}m ago`
23
+ const hrs = Math.floor(mins / 60)
24
+ if (hrs < 24) return `${hrs}h ago`
25
+ const days = Math.floor(hrs / 24)
26
+ if (days < 30) return `${days}d ago`
27
+ return `${Math.floor(days / 30)}mo ago`
28
+ }
29
+
30
+ interface TicketCardProps {
31
+ ticket: KanbanTicket
32
+ agent: Agent | null
33
+ onClick: () => void
34
+ isWorking?: boolean
35
+ }
36
+
37
+ export function TicketCard({ ticket, agent, onClick, isWorking }: TicketCardProps) {
38
+ const [isDragging, setIsDragging] = useState(false)
39
+
40
+ function handleDragStart(e: React.DragEvent<HTMLDivElement>) {
41
+ e.dataTransfer.setData('text/plain', ticket.id)
42
+ e.dataTransfer.effectAllowed = 'move'
43
+ setIsDragging(true)
44
+ }
45
+
46
+ function handleDragEnd() {
47
+ setIsDragging(false)
48
+ }
49
+
50
+ return (
51
+ <div
52
+ draggable
53
+ onDragStart={handleDragStart}
54
+ onDragEnd={handleDragEnd}
55
+ onClick={onClick}
56
+ className="hover-lift focus-ring"
57
+ role="button"
58
+ tabIndex={0}
59
+ onKeyDown={(e) => {
60
+ if (e.key === 'Enter' || e.key === ' ') {
61
+ e.preventDefault()
62
+ onClick()
63
+ }
64
+ }}
65
+ style={{
66
+ background: 'var(--material-regular)',
67
+ borderRadius: 'var(--radius-md)',
68
+ padding: 'var(--space-3)',
69
+ cursor: isDragging ? 'grabbing' : 'grab',
70
+ opacity: isDragging ? 0.6 : 1,
71
+ border: '1px solid var(--separator)',
72
+ borderLeft: agent ? `3px solid ${agent.color}` : '1px solid var(--separator)',
73
+ display: 'flex',
74
+ flexDirection: 'column',
75
+ gap: 'var(--space-2)',
76
+ userSelect: 'none',
77
+ transition: 'opacity 150ms var(--ease-smooth)',
78
+ }}
79
+ >
80
+ {/* Agent row */}
81
+ {agent && (
82
+ <div
83
+ style={{
84
+ display: 'flex',
85
+ alignItems: 'center',
86
+ gap: 'var(--space-2)',
87
+ }}
88
+ >
89
+ <AgentAvatar agent={agent} size={28} borderRadius={8} />
90
+ <span
91
+ style={{
92
+ fontSize: 'var(--text-caption1)',
93
+ fontWeight: 600,
94
+ color: 'var(--text-secondary)',
95
+ lineHeight: 1.2,
96
+ }}
97
+ >
98
+ {agent.name}
99
+ </span>
100
+ </div>
101
+ )}
102
+
103
+ {/* Priority + Title */}
104
+ <div
105
+ style={{
106
+ display: 'flex',
107
+ alignItems: 'flex-start',
108
+ gap: 'var(--space-2)',
109
+ }}
110
+ >
111
+ <span
112
+ style={{
113
+ display: 'inline-flex',
114
+ alignItems: 'center',
115
+ gap: 3,
116
+ fontSize: 'var(--text-caption2)',
117
+ fontWeight: 600,
118
+ color: PRIORITY_COLORS[ticket.priority],
119
+ flexShrink: 0,
120
+ marginTop: 2,
121
+ }}
122
+ >
123
+ <span
124
+ style={{
125
+ width: 6,
126
+ height: 6,
127
+ borderRadius: '50%',
128
+ background: PRIORITY_COLORS[ticket.priority],
129
+ }}
130
+ />
131
+ {PRIORITY_LABELS[ticket.priority]}
132
+ </span>
133
+ <span
134
+ style={{
135
+ fontSize: 'var(--text-footnote)',
136
+ fontWeight: 'var(--weight-semibold)',
137
+ color: 'var(--text-primary)',
138
+ lineHeight: 1.3,
139
+ display: '-webkit-box',
140
+ WebkitLineClamp: 2,
141
+ WebkitBoxOrient: 'vertical',
142
+ overflow: 'hidden',
143
+ wordBreak: 'break-word',
144
+ }}
145
+ >
146
+ {ticket.title}
147
+ </span>
148
+ </div>
149
+
150
+ {/* Description preview */}
151
+ {ticket.description && (
152
+ <div
153
+ style={{
154
+ fontSize: 'var(--text-caption2)',
155
+ color: 'var(--text-tertiary)',
156
+ lineHeight: 1.4,
157
+ display: '-webkit-box',
158
+ WebkitLineClamp: 2,
159
+ WebkitBoxOrient: 'vertical',
160
+ overflow: 'hidden',
161
+ wordBreak: 'break-word',
162
+ }}
163
+ >
164
+ {ticket.description}
165
+ </div>
166
+ )}
167
+
168
+ {/* Bottom row: role badge + assignee + timestamp */}
169
+ <div
170
+ style={{
171
+ display: 'flex',
172
+ alignItems: 'center',
173
+ gap: 'var(--space-2)',
174
+ flexWrap: 'wrap',
175
+ }}
176
+ >
177
+ {ticket.assigneeRole && (
178
+ <span
179
+ style={{
180
+ fontSize: 'var(--text-caption2)',
181
+ fontWeight: 'var(--weight-medium)',
182
+ color: 'var(--text-secondary)',
183
+ background: 'var(--fill-tertiary)',
184
+ borderRadius: 'var(--radius-sm)',
185
+ padding: '1px var(--space-2)',
186
+ lineHeight: 1.5,
187
+ }}
188
+ >
189
+ {ROLE_LABELS[ticket.assigneeRole]}
190
+ </span>
191
+ )}
192
+
193
+ <span
194
+ style={{
195
+ fontSize: 'var(--text-caption2)',
196
+ color: 'var(--text-quaternary)',
197
+ marginLeft: 'auto',
198
+ }}
199
+ title={new Date(ticket.createdAt).toLocaleString()}
200
+ >
201
+ {relativeTime(ticket.createdAt)}
202
+ </span>
203
+ </div>
204
+
205
+ {/* Work state indicators */}
206
+ {(ticket.workState === 'working' || isWorking) && (
207
+ <div
208
+ style={{
209
+ display: 'flex',
210
+ alignItems: 'center',
211
+ gap: 'var(--space-1)',
212
+ fontSize: 'var(--text-caption2)',
213
+ fontWeight: 600,
214
+ color: 'var(--system-orange)',
215
+ animation: 'pulse 2s ease-in-out infinite',
216
+ }}
217
+ >
218
+ <span style={{
219
+ width: 6,
220
+ height: 6,
221
+ borderRadius: '50%',
222
+ background: 'var(--system-orange)',
223
+ animation: 'pulse 2s ease-in-out infinite',
224
+ }} />
225
+ Working...
226
+ </div>
227
+ )}
228
+
229
+ {ticket.workState === 'failed' && (
230
+ <div
231
+ style={{
232
+ fontSize: 'var(--text-caption2)',
233
+ fontWeight: 600,
234
+ color: 'var(--system-red)',
235
+ background: 'color-mix(in srgb, var(--system-red) 10%, transparent)',
236
+ borderRadius: 'var(--radius-sm)',
237
+ padding: '1px var(--space-2)',
238
+ }}
239
+ >
240
+ Failed
241
+ </div>
242
+ )}
243
+ </div>
244
+ )
245
+ }