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,494 @@
1
+ "use client"
2
+
3
+ import { useState, useRef, useCallback } from "react"
4
+ import type { Agent, CronJob } from "@/lib/types"
5
+ import { AgentAvatar } from "@/components/AgentAvatar"
6
+
7
+ interface FeedViewProps {
8
+ agents: Agent[]
9
+ crons: CronJob[]
10
+ selectedId: string | null
11
+ onSelect: (agent: Agent) => void
12
+ }
13
+
14
+ type Filter = "all" | "error" | "ok"
15
+
16
+ const PILLS: { key: Filter; label: string; dotColor: string }[] = [
17
+ { key: "all", label: "All", dotColor: "var(--text-primary)" },
18
+ { key: "ok", label: "Healthy", dotColor: "var(--system-green)" },
19
+ { key: "error", label: "Errors", dotColor: "var(--system-red)" },
20
+ ]
21
+
22
+ function relativeTime(dateStr: string): string {
23
+ const now = Date.now()
24
+ const then = new Date(dateStr).getTime()
25
+ if (isNaN(then)) return dateStr
26
+ const diffMs = now - then
27
+ const mins = Math.floor(diffMs / 60000)
28
+ if (mins < 1) return "Just now"
29
+ if (mins < 60) return `${mins}m ago`
30
+ const hours = Math.floor(mins / 60)
31
+ if (hours < 24) return `${hours}h ago`
32
+ const days = Math.floor(hours / 24)
33
+ if (days === 1) return "Yesterday"
34
+ if (days < 7) return `${days}d ago`
35
+ return new Date(dateStr).toLocaleDateString()
36
+ }
37
+
38
+ function StatusBadge({ status }: { status: CronJob["status"] }) {
39
+ const bg =
40
+ status === "ok"
41
+ ? "color-mix(in srgb, var(--system-green) 15%, transparent)"
42
+ : status === "error"
43
+ ? "color-mix(in srgb, var(--system-red) 15%, transparent)"
44
+ : "var(--fill-tertiary)"
45
+ const color =
46
+ status === "ok"
47
+ ? "var(--system-green)"
48
+ : status === "error"
49
+ ? "var(--system-red)"
50
+ : "var(--text-tertiary)"
51
+ const label = status === "ok" ? "healthy" : status
52
+
53
+ return (
54
+ <span
55
+ style={{
56
+ fontSize: "var(--text-caption2)",
57
+ fontWeight: "var(--weight-semibold)",
58
+ color,
59
+ background: bg,
60
+ padding: "2px 8px",
61
+ borderRadius: 10,
62
+ textTransform: "uppercase",
63
+ letterSpacing: "0.02em",
64
+ }}
65
+ >
66
+ {label}
67
+ </span>
68
+ )
69
+ }
70
+
71
+ function StatCard({
72
+ value,
73
+ label,
74
+ color,
75
+ icon,
76
+ }: {
77
+ value: number
78
+ label: string
79
+ color: string
80
+ icon: React.ReactNode
81
+ }) {
82
+ return (
83
+ <div
84
+ style={{
85
+ flex: 1,
86
+ background: "var(--material-regular)",
87
+ border: "1px solid var(--separator)",
88
+ borderRadius: "var(--radius-md)",
89
+ padding: "var(--space-3) var(--space-4)",
90
+ display: "flex",
91
+ alignItems: "center",
92
+ gap: "var(--space-3)",
93
+ }}
94
+ >
95
+ <div
96
+ style={{
97
+ width: 36,
98
+ height: 36,
99
+ borderRadius: 10,
100
+ background: `color-mix(in srgb, ${color} 12%, transparent)`,
101
+ display: "flex",
102
+ alignItems: "center",
103
+ justifyContent: "center",
104
+ flexShrink: 0,
105
+ }}
106
+ >
107
+ {icon}
108
+ </div>
109
+ <div>
110
+ <div
111
+ style={{
112
+ fontSize: "var(--text-title3)",
113
+ fontWeight: "var(--weight-bold)",
114
+ color,
115
+ lineHeight: 1,
116
+ }}
117
+ >
118
+ {value}
119
+ </div>
120
+ <div
121
+ style={{
122
+ fontSize: "var(--text-caption2)",
123
+ color: "var(--text-tertiary)",
124
+ marginTop: 2,
125
+ }}
126
+ >
127
+ {label}
128
+ </div>
129
+ </div>
130
+ </div>
131
+ )
132
+ }
133
+
134
+ export function FeedView({ agents, crons, selectedId, onSelect }: FeedViewProps) {
135
+ const [filter, setFilter] = useState<Filter>("all")
136
+ const pillsRef = useRef<HTMLDivElement>(null)
137
+
138
+ const agentMap = new Map(agents.map((a) => [a.id, a]))
139
+
140
+ const counts = {
141
+ all: crons.length,
142
+ ok: crons.filter((c) => c.status === "ok").length,
143
+ error: crons.filter((c) => c.status === "error").length,
144
+ }
145
+ const idleCount = crons.filter((c) => c.status === "idle").length
146
+
147
+ const filtered = crons
148
+ .filter((c) => {
149
+ if (filter === "all") return true
150
+ return c.status === filter
151
+ })
152
+ .sort((a, b) => {
153
+ if (a.status === "error" && b.status !== "error") return -1
154
+ if (b.status === "error" && a.status !== "error") return 1
155
+ if (a.lastRun && b.lastRun) return new Date(b.lastRun).getTime() - new Date(a.lastRun).getTime()
156
+ if (a.lastRun && !b.lastRun) return -1
157
+ if (!a.lastRun && b.lastRun) return 1
158
+ return 0
159
+ })
160
+
161
+ const handlePillKeyDown = useCallback(
162
+ (e: React.KeyboardEvent) => {
163
+ if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return
164
+ e.preventDefault()
165
+ const pills = pillsRef.current?.querySelectorAll<HTMLButtonElement>('[role="tab"]')
166
+ if (!pills) return
167
+ const idx = Array.from(pills).findIndex((p) => p.getAttribute("aria-selected") === "true")
168
+ const next = e.key === "ArrowRight" ? (idx + 1) % pills.length : (idx - 1 + pills.length) % pills.length
169
+ pills[next].focus()
170
+ pills[next].click()
171
+ },
172
+ [],
173
+ )
174
+
175
+ return (
176
+ <div
177
+ className="h-full"
178
+ style={{
179
+ overflowY: "auto",
180
+ padding: "var(--space-6)",
181
+ paddingTop: 52,
182
+ }}
183
+ >
184
+ {/* Stat cards row */}
185
+ <div
186
+ style={{
187
+ display: "flex",
188
+ gap: "var(--space-3)",
189
+ marginBottom: "var(--space-5)",
190
+ }}
191
+ >
192
+ <StatCard
193
+ value={counts.all}
194
+ label="Total crons"
195
+ color="var(--text-primary)"
196
+ icon={
197
+ <svg width="18" height="18" viewBox="0 0 16 16" fill="none">
198
+ <circle cx="8" cy="8" r="6.5" stroke="var(--text-secondary)" strokeWidth="1.2" />
199
+ <polyline points="8 4.5 8 8 10.5 10" stroke="var(--text-secondary)" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
200
+ </svg>
201
+ }
202
+ />
203
+ <StatCard
204
+ value={counts.ok}
205
+ label="Healthy"
206
+ color="var(--system-green)"
207
+ icon={
208
+ <svg width="18" height="18" viewBox="0 0 16 16" fill="none">
209
+ <circle cx="8" cy="8" r="6.5" stroke="var(--system-green)" strokeWidth="1.2" />
210
+ <polyline points="5 8 7 10 11 6" stroke="var(--system-green)" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round" />
211
+ </svg>
212
+ }
213
+ />
214
+ <StatCard
215
+ value={counts.error}
216
+ label="Errors"
217
+ color={counts.error > 0 ? "var(--system-red)" : "var(--text-tertiary)"}
218
+ icon={
219
+ <svg width="18" height="18" viewBox="0 0 16 16" fill="none">
220
+ <circle cx="8" cy="8" r="6.5" stroke={counts.error > 0 ? "var(--system-red)" : "var(--text-tertiary)"} strokeWidth="1.2" />
221
+ <line x1="8" y1="5" x2="8" y2="8.5" stroke={counts.error > 0 ? "var(--system-red)" : "var(--text-tertiary)"} strokeWidth="1.3" strokeLinecap="round" />
222
+ <circle cx="8" cy="10.5" r="0.6" fill={counts.error > 0 ? "var(--system-red)" : "var(--text-tertiary)"} />
223
+ </svg>
224
+ }
225
+ />
226
+ <StatCard
227
+ value={idleCount}
228
+ label="Idle"
229
+ color="var(--text-tertiary)"
230
+ icon={
231
+ <svg width="18" height="18" viewBox="0 0 16 16" fill="none">
232
+ <circle cx="8" cy="8" r="6.5" stroke="var(--text-tertiary)" strokeWidth="1.2" />
233
+ <line x1="5.5" y1="8" x2="10.5" y2="8" stroke="var(--text-tertiary)" strokeWidth="1.3" strokeLinecap="round" />
234
+ </svg>
235
+ }
236
+ />
237
+ </div>
238
+
239
+ {/* Filter pills */}
240
+ <div
241
+ ref={pillsRef}
242
+ role="tablist"
243
+ onKeyDown={handlePillKeyDown}
244
+ style={{
245
+ display: "flex",
246
+ gap: "var(--space-2)",
247
+ marginBottom: "var(--space-4)",
248
+ }}
249
+ >
250
+ {PILLS.map((pill) => {
251
+ const isActive = filter === pill.key
252
+ return (
253
+ <button
254
+ key={pill.key}
255
+ role="tab"
256
+ aria-selected={isActive}
257
+ tabIndex={isActive ? 0 : -1}
258
+ onClick={() => setFilter(pill.key)}
259
+ className="focus-ring"
260
+ style={{
261
+ display: "flex",
262
+ alignItems: "center",
263
+ gap: "var(--space-2)",
264
+ borderRadius: 20,
265
+ padding: "6px 14px",
266
+ fontSize: "var(--text-footnote)",
267
+ fontWeight: "var(--weight-medium)",
268
+ border: "none",
269
+ cursor: "pointer",
270
+ transition: "all 200ms var(--ease-smooth)",
271
+ ...(isActive
272
+ ? {
273
+ background: "var(--accent-fill)",
274
+ color: "var(--accent)",
275
+ boxShadow: "0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent)",
276
+ }
277
+ : {
278
+ background: "var(--fill-secondary)",
279
+ color: "var(--text-primary)",
280
+ }),
281
+ }}
282
+ >
283
+ <span
284
+ style={{
285
+ width: 6,
286
+ height: 6,
287
+ borderRadius: "50%",
288
+ background: pill.dotColor,
289
+ display: "inline-block",
290
+ }}
291
+ className={pill.key === "error" && counts.error > 0 ? "animate-error-pulse" : ""}
292
+ />
293
+ <span>{pill.label}</span>
294
+ <span
295
+ style={{
296
+ fontWeight: "var(--weight-semibold)",
297
+ color: isActive ? "var(--accent)" : "var(--text-secondary)",
298
+ }}
299
+ >
300
+ {counts[pill.key]}
301
+ </span>
302
+ </button>
303
+ )
304
+ })}
305
+ </div>
306
+
307
+ {/* Feed entries */}
308
+ {filtered.length === 0 ? (
309
+ <div
310
+ style={{
311
+ textAlign: "center",
312
+ padding: "var(--space-16) var(--space-4)",
313
+ color: "var(--text-tertiary)",
314
+ }}
315
+ >
316
+ <svg
317
+ width="40"
318
+ height="40"
319
+ viewBox="0 0 16 16"
320
+ fill="none"
321
+ style={{ margin: "0 auto var(--space-3)" }}
322
+ >
323
+ <circle cx="8" cy="8" r="6.5" stroke="var(--fill-tertiary)" strokeWidth="1.2" />
324
+ <polyline points="8 4.5 8 8 10.5 10" stroke="var(--fill-tertiary)" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
325
+ </svg>
326
+ <div style={{ fontSize: "var(--text-body)", fontWeight: "var(--weight-medium)" }}>
327
+ {filter === "all" ? "No cron jobs configured" : `No ${filter} crons`}
328
+ </div>
329
+ <div style={{ fontSize: "var(--text-caption1)", marginTop: "var(--space-1)" }}>
330
+ {filter !== "all" ? "Try changing the filter" : "Crons will appear here once configured"}
331
+ </div>
332
+ </div>
333
+ ) : (
334
+ <div
335
+ style={{
336
+ background: "var(--bg-secondary)",
337
+ borderRadius: "var(--radius-lg)",
338
+ border: "1px solid var(--separator)",
339
+ overflow: "hidden",
340
+ }}
341
+ >
342
+ {filtered.map((cron, idx) => {
343
+ const agent = cron.agentId ? agentMap.get(cron.agentId) : null
344
+ const isSelected = agent ? selectedId === agent.id : false
345
+
346
+ return (
347
+ <button
348
+ key={cron.id}
349
+ className="hover-bg focus-ring"
350
+ onClick={() => agent && onSelect(agent)}
351
+ disabled={!agent}
352
+ style={{
353
+ display: "flex",
354
+ alignItems: "center",
355
+ gap: "var(--space-3)",
356
+ padding: "var(--space-3) var(--space-4)",
357
+ width: "100%",
358
+ background: isSelected
359
+ ? "var(--fill-secondary)"
360
+ : "transparent",
361
+ border: "none",
362
+ borderTop: idx > 0 ? "1px solid var(--separator)" : undefined,
363
+ cursor: agent ? "pointer" : "default",
364
+ textAlign: "left",
365
+ transition: "background 150ms var(--ease-smooth)",
366
+ }}
367
+ >
368
+ {/* Agent avatar */}
369
+ {agent ? (
370
+ <AgentAvatar
371
+ agent={agent}
372
+ size={34}
373
+ borderRadius={9}
374
+ style={{ border: `1px solid ${agent.color}30` }}
375
+ />
376
+ ) : (
377
+ <span
378
+ style={{
379
+ width: 34,
380
+ height: 34,
381
+ borderRadius: 9,
382
+ background: "var(--fill-tertiary)",
383
+ display: "flex",
384
+ alignItems: "center",
385
+ justifyContent: "center",
386
+ fontSize: 15,
387
+ flexShrink: 0,
388
+ }}
389
+ >
390
+ &#x2699;&#xFE0F;
391
+ </span>
392
+ )}
393
+
394
+ {/* Content */}
395
+ <div style={{ flex: 1, minWidth: 0 }}>
396
+ <div style={{ display: "flex", alignItems: "center", gap: "var(--space-2)" }}>
397
+ <span
398
+ style={{
399
+ fontSize: "var(--text-body)",
400
+ fontWeight: "var(--weight-semibold)",
401
+ color: "var(--text-primary)",
402
+ }}
403
+ >
404
+ {agent?.name ?? "Unknown"}
405
+ </span>
406
+ <span
407
+ style={{
408
+ fontSize: "var(--text-body)",
409
+ color: "var(--text-secondary)",
410
+ whiteSpace: "nowrap",
411
+ overflow: "hidden",
412
+ textOverflow: "ellipsis",
413
+ }}
414
+ >
415
+ {cron.name}
416
+ </span>
417
+ </div>
418
+ <div
419
+ style={{
420
+ display: "flex",
421
+ alignItems: "center",
422
+ gap: "var(--space-2)",
423
+ marginTop: 2,
424
+ }}
425
+ >
426
+ <span
427
+ style={{
428
+ fontSize: "var(--text-caption1)",
429
+ color: "var(--text-tertiary)",
430
+ }}
431
+ >
432
+ {cron.scheduleDescription}
433
+ </span>
434
+ {cron.lastRun && (
435
+ <>
436
+ <span style={{ color: "var(--text-quaternary)", fontSize: "var(--text-caption2)" }}>&middot;</span>
437
+ <span
438
+ style={{
439
+ fontSize: "var(--text-caption1)",
440
+ color: "var(--text-quaternary)",
441
+ }}
442
+ >
443
+ {relativeTime(cron.lastRun)}
444
+ </span>
445
+ </>
446
+ )}
447
+ </div>
448
+ {cron.lastError && cron.status === "error" && (
449
+ <div
450
+ style={{
451
+ fontSize: "var(--text-caption1)",
452
+ color: "var(--system-red)",
453
+ marginTop: 3,
454
+ whiteSpace: "nowrap",
455
+ overflow: "hidden",
456
+ textOverflow: "ellipsis",
457
+ }}
458
+ >
459
+ {cron.lastError}
460
+ </div>
461
+ )}
462
+ </div>
463
+
464
+ {/* Right: status badge + schedule */}
465
+ <div
466
+ style={{
467
+ display: "flex",
468
+ alignItems: "center",
469
+ gap: "var(--space-2)",
470
+ flexShrink: 0,
471
+ }}
472
+ >
473
+ <StatusBadge status={cron.status} />
474
+ <span
475
+ style={{
476
+ fontSize: "var(--text-caption1)",
477
+ fontFamily: "var(--font-mono)",
478
+ color: "var(--text-tertiary)",
479
+ background: "var(--fill-quaternary)",
480
+ padding: "2px 6px",
481
+ borderRadius: 4,
482
+ }}
483
+ >
484
+ {cron.schedule}
485
+ </span>
486
+ </div>
487
+ </button>
488
+ )
489
+ })}
490
+ </div>
491
+ )}
492
+ </div>
493
+ )
494
+ }