@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.
- package/dist/favicon.svg +11 -0
- package/dist/index.html +17 -0
- package/dist/lib/LoadingSkeletons-IcIC2JPq.js +132 -0
- package/dist/lib/LoadingSkeletons-IcIC2JPq.js.map +1 -0
- package/dist/lib/ServerThemeProvider-DNF0LAyk.js +42 -0
- package/dist/lib/ServerThemeProvider-DNF0LAyk.js.map +1 -0
- package/dist/lib/extensions.js +10 -0
- package/dist/lib/extensions.js.map +1 -0
- package/dist/lib/favicon.svg +11 -0
- package/dist/lib/index.js +74126 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/logo.svg +12 -0
- package/dist/lib/routes/AcceptInviteRoute.js +19 -0
- package/dist/lib/routes/AcceptInviteRoute.js.map +1 -0
- package/dist/lib/routes/AdminDashboardRoute.js +19 -0
- package/dist/lib/routes/AdminDashboardRoute.js.map +1 -0
- package/dist/lib/routes/AdminTeamRoute.js +19 -0
- package/dist/lib/routes/AdminTeamRoute.js.map +1 -0
- package/dist/lib/routes/AdminTeamsRoute.js +19 -0
- package/dist/lib/routes/AdminTeamsRoute.js.map +1 -0
- package/dist/lib/routes/AdminUsersRoute.js +19 -0
- package/dist/lib/routes/AdminUsersRoute.js.map +1 -0
- package/dist/lib/routes/ApiKeysRoute.js +19 -0
- package/dist/lib/routes/ApiKeysRoute.js.map +1 -0
- package/dist/lib/routes/AutomationsRoute.js +19 -0
- package/dist/lib/routes/AutomationsRoute.js.map +1 -0
- package/dist/lib/routes/ChatRoute.js +19 -0
- package/dist/lib/routes/ChatRoute.js.map +1 -0
- package/dist/lib/routes/DocumentsRoute.js +19 -0
- package/dist/lib/routes/DocumentsRoute.js.map +1 -0
- package/dist/lib/routes/OAuthConsentRoute.js +19 -0
- package/dist/lib/routes/OAuthConsentRoute.js.map +1 -0
- package/dist/lib/routes/PricingRoute.js +19 -0
- package/dist/lib/routes/PricingRoute.js.map +1 -0
- package/dist/lib/routes/PrivacyRoute.js +19 -0
- package/dist/lib/routes/PrivacyRoute.js.map +1 -0
- package/dist/lib/routes/TeamSettingsRoute.js +19 -0
- package/dist/lib/routes/TeamSettingsRoute.js.map +1 -0
- package/dist/lib/routes/TermsRoute.js +19 -0
- package/dist/lib/routes/TermsRoute.js.map +1 -0
- package/dist/lib/routes/VerifyEmailRoute.js +19 -0
- package/dist/lib/routes/VerifyEmailRoute.js.map +1 -0
- package/dist/lib/routes.js +79 -0
- package/dist/lib/routes.js.map +1 -0
- package/dist/lib/ssr-utils.js +29 -0
- package/dist/lib/ssr-utils.js.map +1 -0
- package/dist/lib/ssr.js +60 -0
- package/dist/lib/ssr.js.map +1 -0
- package/dist/lib/styles.css +2410 -0
- package/dist/lib/useExtensions-B5nX_8XD.js +155 -0
- package/dist/lib/useExtensions-B5nX_8XD.js.map +1 -0
- package/dist/logo.svg +12 -0
- package/package.json +84 -0
- package/src/components/AgentSelector.tsx +90 -0
- package/src/components/BranchModal.tsx +129 -0
- package/src/components/ClientOnly.tsx +27 -0
- package/src/components/ExportMenu.tsx +122 -0
- package/src/components/LoadingSkeletons.tsx +110 -0
- package/src/components/MCPCredentialsSection.tsx +309 -0
- package/src/components/MentionChip.tsx +149 -0
- package/src/components/MentionDropdown.tsx +175 -0
- package/src/components/MentionInput.tsx +293 -0
- package/src/components/MessageItem.tsx +300 -0
- package/src/components/MessageList.tsx +159 -0
- package/src/components/OAuthAppsSection.tsx +124 -0
- package/src/components/ProjectFolder.tsx +141 -0
- package/src/components/ProjectModal.tsx +296 -0
- package/src/components/SSRMessageList.tsx +153 -0
- package/src/components/SearchModal.tsx +173 -0
- package/src/components/SettingsModal.tsx +412 -0
- package/src/components/ShareModal.tsx +280 -0
- package/src/components/Sidebar.tsx +491 -0
- package/src/components/TeamSwitcher.tsx +273 -0
- package/src/components/ToolCallDisplay.tsx +473 -0
- package/src/components/ToolConfirmationModal.tsx +130 -0
- package/src/components/UsageChart.tsx +177 -0
- package/src/components/content/CodeBlock.tsx +69 -0
- package/src/components/content/MarkdownRenderer.tsx +64 -0
- package/src/components/content/SSRMarkdownRenderer.tsx +158 -0
- package/src/contexts/AuthContext.tsx +119 -0
- package/src/contexts/ConfigContext.tsx +214 -0
- package/src/contexts/ProjectContext.tsx +167 -0
- package/src/contexts/ServerConfigProvider.tsx +41 -0
- package/src/contexts/ServerThemeProvider.tsx +47 -0
- package/src/contexts/TeamContext.tsx +255 -0
- package/src/contexts/ThemeContext.tsx +113 -0
- package/src/extensions/index.ts +15 -0
- package/src/extensions/registry.ts +187 -0
- package/src/extensions/useExtensions.ts +52 -0
- package/src/hooks/useAppPath.ts +34 -0
- package/src/hooks/useBasePath.ts +13 -0
- package/src/hooks/useKeyboardShortcuts.ts +50 -0
- package/src/hooks/useMentionSearch.ts +106 -0
- package/src/index.tsx +116 -0
- package/src/layouts/MainLayout.tsx +98 -0
- package/src/pages/AcceptInvitePage.tsx +175 -0
- package/src/pages/AdminDashboardPage.tsx +362 -0
- package/src/pages/AdminTeamPage.tsx +304 -0
- package/src/pages/AdminTeamsPage.tsx +242 -0
- package/src/pages/AdminUsersPage.tsx +385 -0
- package/src/pages/ApiKeysPage.tsx +449 -0
- package/src/pages/ChatPage.tsx +310 -0
- package/src/pages/DocumentsPage.tsx +577 -0
- package/src/pages/LoginPage.tsx +232 -0
- package/src/pages/OAuthConsentPage.tsx +234 -0
- package/src/pages/PricingPage.tsx +314 -0
- package/src/pages/PrivacyPage.tsx +65 -0
- package/src/pages/RegisterPage.tsx +153 -0
- package/src/pages/ScheduledPromptsPage.tsx +702 -0
- package/src/pages/SharedThreadPage.tsx +116 -0
- package/src/pages/TeamSettingsPage.tsx +1085 -0
- package/src/pages/TermsPage.tsx +82 -0
- package/src/pages/VerifyEmailPage.tsx +202 -0
- package/src/routes/AcceptInviteRoute.tsx +24 -0
- package/src/routes/AdminDashboardRoute.tsx +24 -0
- package/src/routes/AdminTeamRoute.tsx +24 -0
- package/src/routes/AdminTeamsRoute.tsx +24 -0
- package/src/routes/AdminUsersRoute.tsx +24 -0
- package/src/routes/ApiKeysRoute.tsx +24 -0
- package/src/routes/AutomationsRoute.tsx +24 -0
- package/src/routes/ChatRoute.tsx +28 -0
- package/src/routes/DocumentsRoute.tsx +24 -0
- package/src/routes/OAuthConsentRoute.tsx +24 -0
- package/src/routes/PricingRoute.tsx +24 -0
- package/src/routes/PrivacyRoute.tsx +24 -0
- package/src/routes/TeamSettingsRoute.tsx +24 -0
- package/src/routes/TermsRoute.tsx +24 -0
- package/src/routes/VerifyEmailRoute.tsx +24 -0
- package/src/routes/index.ts +57 -0
- package/src/ssr-utils.tsx +84 -0
- package/src/ssr.ts +123 -0
- package/src/stores/chatStore.ts +670 -0
- package/src/styles/index.css +254 -0
- package/src/utils/api.ts +78 -0
- 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
|
+
}
|