@chaaskit/client 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/dist/favicon.svg +11 -0
  2. package/dist/index.html +17 -0
  3. package/dist/lib/LoadingSkeletons-IcIC2JPq.js +132 -0
  4. package/dist/lib/LoadingSkeletons-IcIC2JPq.js.map +1 -0
  5. package/dist/lib/ServerThemeProvider-DNF0LAyk.js +42 -0
  6. package/dist/lib/ServerThemeProvider-DNF0LAyk.js.map +1 -0
  7. package/dist/lib/extensions.js +10 -0
  8. package/dist/lib/extensions.js.map +1 -0
  9. package/dist/lib/favicon.svg +11 -0
  10. package/dist/lib/index.js +74126 -0
  11. package/dist/lib/index.js.map +1 -0
  12. package/dist/lib/logo.svg +12 -0
  13. package/dist/lib/routes/AcceptInviteRoute.js +19 -0
  14. package/dist/lib/routes/AcceptInviteRoute.js.map +1 -0
  15. package/dist/lib/routes/AdminDashboardRoute.js +19 -0
  16. package/dist/lib/routes/AdminDashboardRoute.js.map +1 -0
  17. package/dist/lib/routes/AdminTeamRoute.js +19 -0
  18. package/dist/lib/routes/AdminTeamRoute.js.map +1 -0
  19. package/dist/lib/routes/AdminTeamsRoute.js +19 -0
  20. package/dist/lib/routes/AdminTeamsRoute.js.map +1 -0
  21. package/dist/lib/routes/AdminUsersRoute.js +19 -0
  22. package/dist/lib/routes/AdminUsersRoute.js.map +1 -0
  23. package/dist/lib/routes/ApiKeysRoute.js +19 -0
  24. package/dist/lib/routes/ApiKeysRoute.js.map +1 -0
  25. package/dist/lib/routes/AutomationsRoute.js +19 -0
  26. package/dist/lib/routes/AutomationsRoute.js.map +1 -0
  27. package/dist/lib/routes/ChatRoute.js +19 -0
  28. package/dist/lib/routes/ChatRoute.js.map +1 -0
  29. package/dist/lib/routes/DocumentsRoute.js +19 -0
  30. package/dist/lib/routes/DocumentsRoute.js.map +1 -0
  31. package/dist/lib/routes/OAuthConsentRoute.js +19 -0
  32. package/dist/lib/routes/OAuthConsentRoute.js.map +1 -0
  33. package/dist/lib/routes/PricingRoute.js +19 -0
  34. package/dist/lib/routes/PricingRoute.js.map +1 -0
  35. package/dist/lib/routes/PrivacyRoute.js +19 -0
  36. package/dist/lib/routes/PrivacyRoute.js.map +1 -0
  37. package/dist/lib/routes/TeamSettingsRoute.js +19 -0
  38. package/dist/lib/routes/TeamSettingsRoute.js.map +1 -0
  39. package/dist/lib/routes/TermsRoute.js +19 -0
  40. package/dist/lib/routes/TermsRoute.js.map +1 -0
  41. package/dist/lib/routes/VerifyEmailRoute.js +19 -0
  42. package/dist/lib/routes/VerifyEmailRoute.js.map +1 -0
  43. package/dist/lib/routes.js +79 -0
  44. package/dist/lib/routes.js.map +1 -0
  45. package/dist/lib/ssr-utils.js +29 -0
  46. package/dist/lib/ssr-utils.js.map +1 -0
  47. package/dist/lib/ssr.js +60 -0
  48. package/dist/lib/ssr.js.map +1 -0
  49. package/dist/lib/styles.css +2410 -0
  50. package/dist/lib/useExtensions-B5nX_8XD.js +155 -0
  51. package/dist/lib/useExtensions-B5nX_8XD.js.map +1 -0
  52. package/dist/logo.svg +12 -0
  53. package/package.json +84 -0
  54. package/src/components/AgentSelector.tsx +90 -0
  55. package/src/components/BranchModal.tsx +129 -0
  56. package/src/components/ClientOnly.tsx +27 -0
  57. package/src/components/ExportMenu.tsx +122 -0
  58. package/src/components/LoadingSkeletons.tsx +110 -0
  59. package/src/components/MCPCredentialsSection.tsx +309 -0
  60. package/src/components/MentionChip.tsx +149 -0
  61. package/src/components/MentionDropdown.tsx +175 -0
  62. package/src/components/MentionInput.tsx +293 -0
  63. package/src/components/MessageItem.tsx +300 -0
  64. package/src/components/MessageList.tsx +159 -0
  65. package/src/components/OAuthAppsSection.tsx +124 -0
  66. package/src/components/ProjectFolder.tsx +141 -0
  67. package/src/components/ProjectModal.tsx +296 -0
  68. package/src/components/SSRMessageList.tsx +153 -0
  69. package/src/components/SearchModal.tsx +173 -0
  70. package/src/components/SettingsModal.tsx +412 -0
  71. package/src/components/ShareModal.tsx +280 -0
  72. package/src/components/Sidebar.tsx +491 -0
  73. package/src/components/TeamSwitcher.tsx +273 -0
  74. package/src/components/ToolCallDisplay.tsx +473 -0
  75. package/src/components/ToolConfirmationModal.tsx +130 -0
  76. package/src/components/UsageChart.tsx +177 -0
  77. package/src/components/content/CodeBlock.tsx +69 -0
  78. package/src/components/content/MarkdownRenderer.tsx +64 -0
  79. package/src/components/content/SSRMarkdownRenderer.tsx +158 -0
  80. package/src/contexts/AuthContext.tsx +119 -0
  81. package/src/contexts/ConfigContext.tsx +214 -0
  82. package/src/contexts/ProjectContext.tsx +167 -0
  83. package/src/contexts/ServerConfigProvider.tsx +41 -0
  84. package/src/contexts/ServerThemeProvider.tsx +47 -0
  85. package/src/contexts/TeamContext.tsx +255 -0
  86. package/src/contexts/ThemeContext.tsx +113 -0
  87. package/src/extensions/index.ts +15 -0
  88. package/src/extensions/registry.ts +187 -0
  89. package/src/extensions/useExtensions.ts +52 -0
  90. package/src/hooks/useAppPath.ts +34 -0
  91. package/src/hooks/useBasePath.ts +13 -0
  92. package/src/hooks/useKeyboardShortcuts.ts +50 -0
  93. package/src/hooks/useMentionSearch.ts +106 -0
  94. package/src/index.tsx +116 -0
  95. package/src/layouts/MainLayout.tsx +98 -0
  96. package/src/pages/AcceptInvitePage.tsx +175 -0
  97. package/src/pages/AdminDashboardPage.tsx +362 -0
  98. package/src/pages/AdminTeamPage.tsx +304 -0
  99. package/src/pages/AdminTeamsPage.tsx +242 -0
  100. package/src/pages/AdminUsersPage.tsx +385 -0
  101. package/src/pages/ApiKeysPage.tsx +449 -0
  102. package/src/pages/ChatPage.tsx +310 -0
  103. package/src/pages/DocumentsPage.tsx +577 -0
  104. package/src/pages/LoginPage.tsx +232 -0
  105. package/src/pages/OAuthConsentPage.tsx +234 -0
  106. package/src/pages/PricingPage.tsx +314 -0
  107. package/src/pages/PrivacyPage.tsx +65 -0
  108. package/src/pages/RegisterPage.tsx +153 -0
  109. package/src/pages/ScheduledPromptsPage.tsx +702 -0
  110. package/src/pages/SharedThreadPage.tsx +116 -0
  111. package/src/pages/TeamSettingsPage.tsx +1085 -0
  112. package/src/pages/TermsPage.tsx +82 -0
  113. package/src/pages/VerifyEmailPage.tsx +202 -0
  114. package/src/routes/AcceptInviteRoute.tsx +24 -0
  115. package/src/routes/AdminDashboardRoute.tsx +24 -0
  116. package/src/routes/AdminTeamRoute.tsx +24 -0
  117. package/src/routes/AdminTeamsRoute.tsx +24 -0
  118. package/src/routes/AdminUsersRoute.tsx +24 -0
  119. package/src/routes/ApiKeysRoute.tsx +24 -0
  120. package/src/routes/AutomationsRoute.tsx +24 -0
  121. package/src/routes/ChatRoute.tsx +28 -0
  122. package/src/routes/DocumentsRoute.tsx +24 -0
  123. package/src/routes/OAuthConsentRoute.tsx +24 -0
  124. package/src/routes/PricingRoute.tsx +24 -0
  125. package/src/routes/PrivacyRoute.tsx +24 -0
  126. package/src/routes/TeamSettingsRoute.tsx +24 -0
  127. package/src/routes/TermsRoute.tsx +24 -0
  128. package/src/routes/VerifyEmailRoute.tsx +24 -0
  129. package/src/routes/index.ts +57 -0
  130. package/src/ssr-utils.tsx +84 -0
  131. package/src/ssr.ts +123 -0
  132. package/src/stores/chatStore.ts +670 -0
  133. package/src/styles/index.css +254 -0
  134. package/src/utils/api.ts +78 -0
  135. package/src/vite-env.d.ts +13 -0
@@ -0,0 +1,702 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { Link, Navigate } from 'react-router';
3
+ import {
4
+ Clock,
5
+ Plus,
6
+ Trash2,
7
+ Play,
8
+ Pencil,
9
+ Check,
10
+ X,
11
+ Loader2,
12
+ ToggleLeft,
13
+ ToggleRight,
14
+ MessageSquare,
15
+ ExternalLink,
16
+ } from 'lucide-react';
17
+ import { useConfig, useConfigLoaded } from '../contexts/ConfigContext';
18
+ import { useAuth } from '../contexts/AuthContext';
19
+ import { useTeam } from '../contexts/TeamContext';
20
+ import { useAppPath } from '../hooks/useAppPath';
21
+ import { api } from '../utils/api';
22
+ import type {
23
+ ScheduledPromptSummary,
24
+ ScheduledPromptDetail,
25
+ CreateScheduledPromptRequest,
26
+ UpdateScheduledPromptRequest,
27
+ } from '@chaaskit/shared';
28
+
29
+ const SCHEDULE_PRESETS = [
30
+ { label: 'Every morning (9 AM)', cron: '0 9 * * *' },
31
+ { label: 'Every evening (6 PM)', cron: '0 18 * * *' },
32
+ { label: 'Weekdays at 9 AM', cron: '0 9 * * 1-5' },
33
+ { label: 'Every Monday', cron: '0 9 * * 1' },
34
+ { label: 'First of month', cron: '0 9 1 * *' },
35
+ { label: 'Every hour', cron: '0 * * * *' },
36
+ ];
37
+
38
+ interface Agent {
39
+ id: string;
40
+ name: string;
41
+ isDefault?: boolean;
42
+ }
43
+
44
+ export default function ScheduledPromptsPage() {
45
+ const config = useConfig();
46
+ const configLoaded = useConfigLoaded();
47
+ const { user } = useAuth();
48
+ const { teams, currentTeam } = useTeam();
49
+ const appPath = useAppPath();
50
+
51
+ const [prompts, setPrompts] = useState<ScheduledPromptSummary[]>([]);
52
+ const [isLoading, setIsLoading] = useState(true);
53
+ const [error, setError] = useState('');
54
+
55
+ // Limits info
56
+ const [limits, setLimits] = useState<{
57
+ context: 'personal' | 'team' | 'all';
58
+ current: number;
59
+ max: number;
60
+ } | null>(null);
61
+
62
+ const [agents, setAgents] = useState<Agent[]>([]);
63
+
64
+ const [showModal, setShowModal] = useState(false);
65
+ const [editingPrompt, setEditingPrompt] = useState<ScheduledPromptDetail | null>(null);
66
+ const [isSaving, setIsSaving] = useState(false);
67
+
68
+ // Form state
69
+ const [formName, setFormName] = useState('');
70
+ const [formPrompt, setFormPrompt] = useState('');
71
+ const [formAgentId, setFormAgentId] = useState('');
72
+ const [formSchedule, setFormSchedule] = useState('0 9 * * *');
73
+ const [formSchedulePreset, setFormSchedulePreset] = useState('0 9 * * *');
74
+ const [formIsCustomSchedule, setFormIsCustomSchedule] = useState(false);
75
+ const [formTimezone, setFormTimezone] = useState('UTC');
76
+ const [formNotifySlack, setFormNotifySlack] = useState(true);
77
+ const [formNotifyEmail, setFormNotifyEmail] = useState(false);
78
+ const [formEmailRecipients, setFormEmailRecipients] = useState('');
79
+ const [formTeamId, setFormTeamId] = useState('');
80
+
81
+ const [deletingId, setDeletingId] = useState<string | null>(null);
82
+ const [runningId, setRunningId] = useState<string | null>(null);
83
+ const [togglingId, setTogglingId] = useState<string | null>(null);
84
+
85
+ const featureName = config.scheduledPrompts?.featureName || 'Scheduled Prompts';
86
+ const teamsEnabled = config.teams?.enabled ?? false;
87
+ const slackEnabled = config.slack?.enabled ?? false;
88
+ const emailEnabled = config.email?.enabled ?? false;
89
+
90
+ // Wait for config to load before checking if feature is enabled
91
+ if (!configLoaded) {
92
+ return (
93
+ <div className="min-h-screen bg-background flex items-center justify-center">
94
+ <Loader2 className="h-6 w-6 animate-spin text-primary" />
95
+ </div>
96
+ );
97
+ }
98
+
99
+ // Check if feature is enabled
100
+ if (!config.scheduledPrompts?.enabled) {
101
+ return <Navigate to={appPath('/')} replace />;
102
+ }
103
+
104
+ // Load prompts and agents
105
+ useEffect(() => {
106
+ loadPrompts();
107
+ loadAgents();
108
+ }, [currentTeam]);
109
+
110
+ async function loadPrompts() {
111
+ setIsLoading(true);
112
+ try {
113
+ // If a team is selected, filter by team; otherwise filter to personal prompts
114
+ const params = currentTeam ? `?teamId=${currentTeam.id}` : '?personal=true';
115
+ const res = await api.get<{
116
+ prompts: ScheduledPromptSummary[];
117
+ limits: { context: 'personal' | 'team' | 'all'; current: number; max: number };
118
+ }>(`/api/scheduled-prompts${params}`);
119
+ setPrompts(res.prompts);
120
+ setLimits(res.limits);
121
+ } catch (err) {
122
+ setError(err instanceof Error ? err.message : 'Failed to load scheduled prompts');
123
+ } finally {
124
+ setIsLoading(false);
125
+ }
126
+ }
127
+
128
+ async function loadAgents() {
129
+ try {
130
+ const res = await api.get<{ agents: Agent[] }>('/api/agents');
131
+ setAgents(res.agents);
132
+ } catch {
133
+ // Ignore agent loading errors
134
+ }
135
+ }
136
+
137
+ function openCreateModal() {
138
+ setEditingPrompt(null);
139
+ setFormName('');
140
+ setFormPrompt('');
141
+ setFormAgentId('');
142
+ setFormSchedule('0 9 * * *');
143
+ setFormSchedulePreset('0 9 * * *');
144
+ setFormIsCustomSchedule(false);
145
+ setFormTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
146
+ setFormNotifySlack(true);
147
+ setFormNotifyEmail(false);
148
+ setFormEmailRecipients('');
149
+ setFormTeamId(currentTeam?.id || '');
150
+ setShowModal(true);
151
+ }
152
+
153
+ async function openEditModal(id: string) {
154
+ try {
155
+ const res = await api.get<{ prompt: ScheduledPromptDetail }>(`/api/scheduled-prompts/${id}`);
156
+ const prompt = res.prompt;
157
+ setEditingPrompt(prompt);
158
+ setFormName(prompt.name);
159
+ setFormPrompt(prompt.prompt);
160
+ setFormAgentId(prompt.agentId || '');
161
+ setFormSchedule(prompt.schedule);
162
+ setFormSchedulePreset(SCHEDULE_PRESETS.find((p) => p.cron === prompt.schedule)?.cron || '');
163
+ setFormIsCustomSchedule(!SCHEDULE_PRESETS.find((p) => p.cron === prompt.schedule));
164
+ setFormTimezone(prompt.timezone);
165
+ setFormNotifySlack(prompt.notifySlack);
166
+ setFormNotifyEmail(prompt.notifyEmail);
167
+ setFormEmailRecipients(prompt.emailRecipients.join(', '));
168
+ setFormTeamId(prompt.teamId || '');
169
+ setShowModal(true);
170
+ } catch (err) {
171
+ setError(err instanceof Error ? err.message : 'Failed to load prompt');
172
+ }
173
+ }
174
+
175
+ async function handleSave(e: React.FormEvent) {
176
+ e.preventDefault();
177
+ setIsSaving(true);
178
+ setError('');
179
+
180
+ const emailRecipients = formEmailRecipients
181
+ .split(',')
182
+ .map((e) => e.trim())
183
+ .filter((e) => e);
184
+
185
+ try {
186
+ if (editingPrompt) {
187
+ const data: UpdateScheduledPromptRequest = {
188
+ name: formName,
189
+ prompt: formPrompt,
190
+ agentId: formAgentId || null,
191
+ schedule: formSchedule,
192
+ timezone: formTimezone,
193
+ notifySlack: formNotifySlack,
194
+ notifyEmail: formNotifyEmail,
195
+ emailRecipients,
196
+ };
197
+
198
+ await api.put(`/api/scheduled-prompts/${editingPrompt.id}`, data);
199
+ } else {
200
+ const data: CreateScheduledPromptRequest = {
201
+ name: formName,
202
+ prompt: formPrompt,
203
+ agentId: formAgentId || undefined,
204
+ schedule: formSchedule,
205
+ timezone: formTimezone,
206
+ notifySlack: formNotifySlack,
207
+ notifyEmail: formNotifyEmail,
208
+ emailRecipients,
209
+ teamId: formTeamId || undefined,
210
+ };
211
+
212
+ await api.post('/api/scheduled-prompts', data);
213
+ }
214
+
215
+ setShowModal(false);
216
+ await loadPrompts();
217
+ } catch (err) {
218
+ setError(err instanceof Error ? err.message : 'Failed to save');
219
+ } finally {
220
+ setIsSaving(false);
221
+ }
222
+ }
223
+
224
+ async function handleDelete(id: string) {
225
+ if (!confirm('Are you sure you want to delete this scheduled prompt?')) {
226
+ return;
227
+ }
228
+
229
+ setDeletingId(id);
230
+ try {
231
+ await api.delete(`/api/scheduled-prompts/${id}`);
232
+ // Reload to get updated counts
233
+ await loadPrompts();
234
+ } catch (err) {
235
+ setError(err instanceof Error ? err.message : 'Failed to delete');
236
+ } finally {
237
+ setDeletingId(null);
238
+ }
239
+ }
240
+
241
+ async function handleRun(id: string) {
242
+ setRunningId(id);
243
+ try {
244
+ await api.post(`/api/scheduled-prompts/${id}/run`, {});
245
+ // Show a brief success message
246
+ setError('');
247
+ alert('Run triggered successfully. Check the thread for results.');
248
+ } catch (err) {
249
+ setError(err instanceof Error ? err.message : 'Failed to trigger run');
250
+ } finally {
251
+ setRunningId(null);
252
+ }
253
+ }
254
+
255
+ async function handleToggle(id: string, currentEnabled: boolean) {
256
+ setTogglingId(id);
257
+ try {
258
+ await api.put(`/api/scheduled-prompts/${id}`, { enabled: !currentEnabled });
259
+ setPrompts((prev) =>
260
+ prev.map((p) => (p.id === id ? { ...p, enabled: !currentEnabled } : p))
261
+ );
262
+ } catch (err) {
263
+ setError(err instanceof Error ? err.message : 'Failed to toggle');
264
+ } finally {
265
+ setTogglingId(null);
266
+ }
267
+ }
268
+
269
+ function formatSchedule(schedule: string): string {
270
+ const preset = SCHEDULE_PRESETS.find((p) => p.cron === schedule);
271
+ return preset?.label || schedule;
272
+ }
273
+
274
+ function formatDate(dateString: string | null): string {
275
+ if (!dateString) return 'Never';
276
+ const date = new Date(dateString);
277
+ return date.toLocaleString(undefined, {
278
+ month: 'short',
279
+ day: 'numeric',
280
+ hour: 'numeric',
281
+ minute: '2-digit',
282
+ });
283
+ }
284
+
285
+ return (
286
+ <div className="min-h-screen bg-background p-4 sm:p-8">
287
+ <div className="mx-auto max-w-4xl">
288
+ {/* Header */}
289
+ <div className="flex items-center justify-between mb-6 sm:mb-8">
290
+ <div className="flex items-center gap-3">
291
+ <Clock size={24} className="text-primary" />
292
+ <h1 className="text-xl sm:text-2xl font-bold text-text-primary">
293
+ {featureName}
294
+ </h1>
295
+ </div>
296
+ <Link
297
+ to={appPath('/')}
298
+ className="flex items-center justify-center rounded-lg p-2 text-text-muted hover:text-text-primary hover:bg-background-secondary"
299
+ aria-label="Close"
300
+ >
301
+ <X size={20} />
302
+ </Link>
303
+ </div>
304
+
305
+ {/* Description and Limits */}
306
+ <div className="mb-6">
307
+ <p className="text-sm text-text-secondary mb-2">
308
+ Create prompts that run automatically on a schedule. Results are saved to a thread and notifications sent to Slack or email.
309
+ </p>
310
+ {limits && limits.max > 0 && (
311
+ <div className="flex items-center gap-2 text-sm">
312
+ <span className="text-text-muted">
313
+ {currentTeam ? currentTeam.name : 'Personal'}:
314
+ </span>
315
+ <span className={`font-medium ${limits.current >= limits.max ? 'text-error' : 'text-text-primary'}`}>
316
+ {limits.current} of {limits.max}
317
+ </span>
318
+ <span className="text-text-muted">
319
+ {featureName.toLowerCase()} used
320
+ </span>
321
+ </div>
322
+ )}
323
+ {limits && limits.max === 0 && (
324
+ <div className="text-sm text-text-muted">
325
+ {currentTeam ? currentTeam.name : 'Personal'}: {featureName} not available on this plan
326
+ </div>
327
+ )}
328
+ </div>
329
+
330
+ {/* Error */}
331
+ {error && (
332
+ <div className="mb-6 rounded-lg bg-error/10 p-4 text-sm text-error flex items-center justify-between">
333
+ <span>{error}</span>
334
+ <button onClick={() => setError('')} className="text-error hover:text-error/80">
335
+ <X size={16} />
336
+ </button>
337
+ </div>
338
+ )}
339
+
340
+ {/* Create Button */}
341
+ <div className="mb-6">
342
+ <button
343
+ onClick={openCreateModal}
344
+ disabled={limits !== null && limits.current >= limits.max}
345
+ className="flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-50 disabled:cursor-not-allowed"
346
+ title={limits !== null && limits.current >= limits.max ? 'Plan limit reached' : undefined}
347
+ >
348
+ <Plus size={16} />
349
+ Create {featureName.replace(/s$/, '')}
350
+ </button>
351
+ </div>
352
+
353
+ {/* Prompts List */}
354
+ {isLoading ? (
355
+ <div className="flex items-center justify-center py-12">
356
+ <Loader2 className="h-6 w-6 animate-spin text-primary" />
357
+ </div>
358
+ ) : prompts.length === 0 ? (
359
+ <div className="rounded-lg border border-border bg-background-secondary p-8 text-center">
360
+ <Clock size={48} className="mx-auto mb-4 text-text-muted" />
361
+ <h3 className="text-lg font-medium text-text-primary mb-2">
362
+ No {currentTeam ? `${currentTeam.name} ` : 'Personal '}{featureName}
363
+ </h3>
364
+ <p className="text-sm text-text-secondary mb-4">
365
+ {limits && limits.max === 0
366
+ ? `${featureName} are not available on ${currentTeam ? "this team's" : 'your'} current plan.`
367
+ : `Create your first ${currentTeam ? 'team ' : ''}scheduled prompt to automate tasks.`}
368
+ </p>
369
+ </div>
370
+ ) : (
371
+ <div className="space-y-3">
372
+ {prompts.map((prompt) => (
373
+ <div
374
+ key={prompt.id}
375
+ className={`rounded-lg border bg-background-secondary p-4 ${
376
+ prompt.enabled ? 'border-border' : 'border-border/50 opacity-60'
377
+ }`}
378
+ >
379
+ <div className="flex flex-col sm:flex-row sm:items-start gap-3">
380
+ <div className="flex-1 min-w-0">
381
+ <div className="flex items-center gap-2 mb-1">
382
+ <span className="font-medium text-text-primary truncate">
383
+ {prompt.name}
384
+ </span>
385
+ <span className="text-xs px-2 py-0.5 rounded-full bg-background text-text-muted">
386
+ {prompt.agentName}
387
+ </span>
388
+ </div>
389
+ <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-text-muted">
390
+ <span>{formatSchedule(prompt.schedule)}</span>
391
+ {prompt.lastRunAt && (
392
+ <span>
393
+ Last: {formatDate(prompt.lastRunAt)}
394
+ {prompt.lastRunStatus && (
395
+ <span
396
+ className={
397
+ prompt.lastRunStatus === 'success'
398
+ ? 'text-success ml-1'
399
+ : 'text-error ml-1'
400
+ }
401
+ >
402
+ ({prompt.lastRunStatus})
403
+ </span>
404
+ )}
405
+ </span>
406
+ )}
407
+ {prompt.enabled && prompt.nextRunAt && (
408
+ <span className="text-primary">
409
+ Next: {formatDate(prompt.nextRunAt)}
410
+ </span>
411
+ )}
412
+ <span>{prompt.runCount} runs</span>
413
+ </div>
414
+ </div>
415
+
416
+ <div className="flex items-center gap-1">
417
+ {/* Toggle */}
418
+ <button
419
+ onClick={() => handleToggle(prompt.id, prompt.enabled)}
420
+ disabled={togglingId === prompt.id}
421
+ className="p-2 rounded-lg hover:bg-background text-text-muted"
422
+ title={prompt.enabled ? 'Disable' : 'Enable'}
423
+ >
424
+ {togglingId === prompt.id ? (
425
+ <Loader2 size={18} className="animate-spin" />
426
+ ) : prompt.enabled ? (
427
+ <ToggleRight size={18} className="text-success" />
428
+ ) : (
429
+ <ToggleLeft size={18} />
430
+ )}
431
+ </button>
432
+
433
+ {/* Run Now */}
434
+ <button
435
+ onClick={() => handleRun(prompt.id)}
436
+ disabled={runningId === prompt.id}
437
+ className="p-2 rounded-lg hover:bg-background text-text-muted hover:text-primary"
438
+ title="Run now"
439
+ >
440
+ {runningId === prompt.id ? (
441
+ <Loader2 size={18} className="animate-spin" />
442
+ ) : (
443
+ <Play size={18} />
444
+ )}
445
+ </button>
446
+
447
+ {/* View Thread */}
448
+ <Link
449
+ to={`/chat/${prompt.id}`}
450
+ className="p-2 rounded-lg hover:bg-background text-text-muted hover:text-primary"
451
+ title="View thread"
452
+ >
453
+ <MessageSquare size={18} />
454
+ </Link>
455
+
456
+ {/* Edit */}
457
+ <button
458
+ onClick={() => openEditModal(prompt.id)}
459
+ className="p-2 rounded-lg hover:bg-background text-text-muted hover:text-primary"
460
+ title="Edit"
461
+ >
462
+ <Pencil size={18} />
463
+ </button>
464
+
465
+ {/* Delete */}
466
+ <button
467
+ onClick={() => handleDelete(prompt.id)}
468
+ disabled={deletingId === prompt.id}
469
+ className="p-2 rounded-lg hover:bg-error/10 text-text-muted hover:text-error"
470
+ title="Delete"
471
+ >
472
+ {deletingId === prompt.id ? (
473
+ <Loader2 size={18} className="animate-spin" />
474
+ ) : (
475
+ <Trash2 size={18} />
476
+ )}
477
+ </button>
478
+ </div>
479
+ </div>
480
+ </div>
481
+ ))}
482
+ </div>
483
+ )}
484
+
485
+ {/* Create/Edit Modal */}
486
+ {showModal && (
487
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
488
+ <div className="absolute inset-0 bg-black/50" onClick={() => setShowModal(false)} />
489
+ <div className="relative w-full max-w-lg max-h-[90vh] overflow-y-auto rounded-2xl bg-background p-6">
490
+ <h2 className="text-lg font-semibold text-text-primary mb-4">
491
+ {editingPrompt ? 'Edit' : 'Create'} {featureName.replace(/s$/, '')}
492
+ </h2>
493
+ <form onSubmit={handleSave}>
494
+ <div className="space-y-4">
495
+ {/* Name */}
496
+ <div>
497
+ <label className="block text-sm font-medium text-text-primary mb-2">
498
+ Name *
499
+ </label>
500
+ <input
501
+ type="text"
502
+ value={formName}
503
+ onChange={(e) => setFormName(e.target.value)}
504
+ placeholder="e.g., Daily Summary"
505
+ required
506
+ className="w-full rounded-lg border border-input-border bg-input-background px-3 py-2 text-text-primary placeholder-text-muted focus:border-primary focus:outline-none"
507
+ />
508
+ </div>
509
+
510
+ {/* Prompt */}
511
+ <div>
512
+ <label className="block text-sm font-medium text-text-primary mb-2">
513
+ Prompt *
514
+ </label>
515
+ <textarea
516
+ value={formPrompt}
517
+ onChange={(e) => setFormPrompt(e.target.value)}
518
+ placeholder="Enter the prompt to run..."
519
+ required
520
+ rows={4}
521
+ className="w-full rounded-lg border border-input-border bg-input-background px-3 py-2 text-text-primary placeholder-text-muted focus:border-primary focus:outline-none resize-y"
522
+ />
523
+ </div>
524
+
525
+ {/* Agent */}
526
+ {agents.length > 1 && (
527
+ <div>
528
+ <label className="block text-sm font-medium text-text-primary mb-2">
529
+ Agent
530
+ </label>
531
+ <select
532
+ value={formAgentId}
533
+ onChange={(e) => setFormAgentId(e.target.value)}
534
+ className="w-full rounded-lg border border-input-border bg-input-background px-3 py-2 text-text-primary focus:border-primary focus:outline-none"
535
+ >
536
+ <option value="">Default</option>
537
+ {agents.map((agent) => (
538
+ <option key={agent.id} value={agent.id}>
539
+ {agent.name}
540
+ </option>
541
+ ))}
542
+ </select>
543
+ </div>
544
+ )}
545
+
546
+ {/* Schedule */}
547
+ <div>
548
+ <label className="block text-sm font-medium text-text-primary mb-2">
549
+ Schedule *
550
+ </label>
551
+ {!formIsCustomSchedule ? (
552
+ <>
553
+ <select
554
+ value={formSchedulePreset}
555
+ onChange={(e) => {
556
+ if (e.target.value === 'custom') {
557
+ setFormIsCustomSchedule(true);
558
+ } else {
559
+ setFormSchedulePreset(e.target.value);
560
+ setFormSchedule(e.target.value);
561
+ }
562
+ }}
563
+ className="w-full rounded-lg border border-input-border bg-input-background px-3 py-2 text-text-primary focus:border-primary focus:outline-none"
564
+ >
565
+ {SCHEDULE_PRESETS.map((preset) => (
566
+ <option key={preset.cron} value={preset.cron}>
567
+ {preset.label}
568
+ </option>
569
+ ))}
570
+ <option value="custom">Custom cron...</option>
571
+ </select>
572
+ </>
573
+ ) : (
574
+ <div className="flex gap-2">
575
+ <input
576
+ type="text"
577
+ value={formSchedule}
578
+ onChange={(e) => setFormSchedule(e.target.value)}
579
+ placeholder="0 9 * * *"
580
+ required
581
+ className="flex-1 rounded-lg border border-input-border bg-input-background px-3 py-2 text-text-primary font-mono placeholder-text-muted focus:border-primary focus:outline-none"
582
+ />
583
+ <button
584
+ type="button"
585
+ onClick={() => {
586
+ setFormIsCustomSchedule(false);
587
+ setFormSchedule(formSchedulePreset);
588
+ }}
589
+ className="px-3 py-2 text-sm text-text-muted hover:text-text-primary"
590
+ >
591
+ Presets
592
+ </button>
593
+ </div>
594
+ )}
595
+ <p className="mt-1 text-xs text-text-muted">
596
+ Uses cron syntax. Example: 0 9 * * * = 9 AM daily
597
+ </p>
598
+ </div>
599
+
600
+ {/* Timezone */}
601
+ <div>
602
+ <label className="block text-sm font-medium text-text-primary mb-2">
603
+ Timezone
604
+ </label>
605
+ <input
606
+ type="text"
607
+ value={formTimezone}
608
+ onChange={(e) => setFormTimezone(e.target.value)}
609
+ placeholder="UTC"
610
+ className="w-full rounded-lg border border-input-border bg-input-background px-3 py-2 text-text-primary placeholder-text-muted focus:border-primary focus:outline-none"
611
+ />
612
+ </div>
613
+
614
+ {/* Notifications */}
615
+ <div>
616
+ <label className="block text-sm font-medium text-text-primary mb-2">
617
+ Notifications
618
+ </label>
619
+ <div className="space-y-2">
620
+ {slackEnabled && (
621
+ <label className="flex items-center gap-2 cursor-pointer">
622
+ <input
623
+ type="checkbox"
624
+ checked={formNotifySlack}
625
+ onChange={(e) => setFormNotifySlack(e.target.checked)}
626
+ className="rounded border-input-border"
627
+ />
628
+ <span className="text-sm text-text-secondary">Slack</span>
629
+ </label>
630
+ )}
631
+ {emailEnabled && (
632
+ <>
633
+ <label className="flex items-center gap-2 cursor-pointer">
634
+ <input
635
+ type="checkbox"
636
+ checked={formNotifyEmail}
637
+ onChange={(e) => setFormNotifyEmail(e.target.checked)}
638
+ className="rounded border-input-border"
639
+ />
640
+ <span className="text-sm text-text-secondary">Email</span>
641
+ </label>
642
+ {formNotifyEmail && (
643
+ <input
644
+ type="text"
645
+ value={formEmailRecipients}
646
+ onChange={(e) => setFormEmailRecipients(e.target.value)}
647
+ placeholder="email1@example.com, email2@example.com"
648
+ className="w-full rounded-lg border border-input-border bg-input-background px-3 py-2 text-sm text-text-primary placeholder-text-muted focus:border-primary focus:outline-none"
649
+ />
650
+ )}
651
+ </>
652
+ )}
653
+ </div>
654
+ </div>
655
+
656
+ {/* Team (for new prompts) */}
657
+ {teamsEnabled && !editingPrompt && teams.length > 0 && (
658
+ <div>
659
+ <label className="block text-sm font-medium text-text-primary mb-2">
660
+ Owner
661
+ </label>
662
+ <select
663
+ value={formTeamId}
664
+ onChange={(e) => setFormTeamId(e.target.value)}
665
+ className="w-full rounded-lg border border-input-border bg-input-background px-3 py-2 text-text-primary focus:border-primary focus:outline-none"
666
+ >
667
+ <option value="">Personal</option>
668
+ {teams.map((team) => (
669
+ <option key={team.id} value={team.id}>
670
+ {team.name}
671
+ </option>
672
+ ))}
673
+ </select>
674
+ </div>
675
+ )}
676
+ </div>
677
+
678
+ <div className="mt-6 flex justify-end gap-3">
679
+ <button
680
+ type="button"
681
+ onClick={() => setShowModal(false)}
682
+ className="rounded-lg px-4 py-2 text-sm text-text-secondary hover:bg-background-secondary"
683
+ >
684
+ Cancel
685
+ </button>
686
+ <button
687
+ type="submit"
688
+ disabled={isSaving}
689
+ className="flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-50"
690
+ >
691
+ {isSaving && <Loader2 size={14} className="animate-spin" />}
692
+ {editingPrompt ? 'Save' : 'Create'}
693
+ </button>
694
+ </div>
695
+ </form>
696
+ </div>
697
+ </div>
698
+ )}
699
+ </div>
700
+ </div>
701
+ );
702
+ }