@nastechai/agent 0.16.0 → 0.17.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/eslint.config.js +23 -0
- package/index.html +24 -0
- package/package.json +54 -26
- package/package.json.bak +89 -0
- package/package.json.pub +88 -0
- package/src/App.tsx +1173 -0
- package/src/components/AuthWidget.tsx +150 -0
- package/src/components/AutoField.tsx +206 -0
- package/src/components/Backdrop.tsx +93 -0
- package/src/components/ChatSidebar.tsx +394 -0
- package/src/components/DeleteConfirmDialog.tsx +40 -0
- package/src/components/LanguageSwitcher.tsx +186 -0
- package/src/components/Markdown.tsx +383 -0
- package/src/components/ModelInfoCard.tsx +112 -0
- package/src/components/ModelPickerDialog.tsx +470 -0
- package/src/components/OAuthLoginModal.tsx +374 -0
- package/src/components/OAuthProvidersCard.tsx +287 -0
- package/src/components/PlatformsCard.tsx +97 -0
- package/src/components/ScheduleBuilder.tsx +273 -0
- package/src/components/SidebarFooter.tsx +42 -0
- package/src/components/SidebarStatusStrip.tsx +72 -0
- package/src/components/SlashPopover.tsx +171 -0
- package/src/components/ThemeSwitcher.tsx +243 -0
- package/src/components/ToolCall.tsx +228 -0
- package/src/components/ToolsetConfigDrawer.tsx +448 -0
- package/src/contexts/PageHeaderProvider.tsx +139 -0
- package/src/contexts/SystemActions.tsx +120 -0
- package/src/contexts/page-header-context.ts +12 -0
- package/src/contexts/system-actions-context.ts +18 -0
- package/src/contexts/usePageHeader.ts +10 -0
- package/src/contexts/useSystemActions.ts +15 -0
- package/src/hooks/useModalBehavior.ts +44 -0
- package/src/hooks/useSidebarStatus.ts +27 -0
- package/src/i18n/af.ts +702 -0
- package/src/i18n/context.tsx +123 -0
- package/src/i18n/de.ts +701 -0
- package/src/i18n/en.ts +708 -0
- package/src/i18n/es.ts +701 -0
- package/src/i18n/fr.ts +701 -0
- package/src/i18n/ga.ts +702 -0
- package/src/i18n/hu.ts +702 -0
- package/src/i18n/index.ts +2 -0
- package/src/i18n/it.ts +701 -0
- package/src/i18n/ja.ts +702 -0
- package/src/i18n/ko.ts +702 -0
- package/src/i18n/pt.ts +702 -0
- package/src/i18n/ru.ts +702 -0
- package/src/i18n/tr.ts +702 -0
- package/src/i18n/types.ts +710 -0
- package/src/i18n/uk.ts +702 -0
- package/src/i18n/zh-hant.ts +702 -0
- package/src/i18n/zh.ts +698 -0
- package/src/index.css +274 -0
- package/src/lib/api.ts +1585 -0
- package/src/lib/dashboard-flags.ts +15 -0
- package/src/lib/format.ts +9 -0
- package/src/lib/fuzzy.ts +192 -0
- package/src/lib/gatewayClient.ts +253 -0
- package/src/lib/nested.ts +23 -0
- package/src/lib/resolve-page-title.ts +41 -0
- package/src/lib/schedule.ts +382 -0
- package/src/lib/slashExec.ts +163 -0
- package/src/lib/utils.ts +35 -0
- package/src/main.tsx +25 -0
- package/src/pages/AnalyticsPage.tsx +601 -0
- package/src/pages/ChannelsPage.tsx +772 -0
- package/src/pages/ChatPage.tsx +889 -0
- package/src/pages/ConfigPage.tsx +660 -0
- package/src/pages/CronPage.tsx +524 -0
- package/src/pages/DocsPage.tsx +69 -0
- package/src/pages/EnvPage.tsx +918 -0
- package/src/pages/LogsPage.tsx +246 -0
- package/src/pages/McpPage.tsx +757 -0
- package/src/pages/ModelsPage.tsx +994 -0
- package/src/pages/PairingPage.tsx +276 -0
- package/src/pages/PluginsPage.tsx +580 -0
- package/src/pages/ProfilesPage.tsx +559 -0
- package/src/pages/SessionsPage.tsx +936 -0
- package/src/pages/SkillsPage.tsx +557 -0
- package/src/pages/SystemPage.tsx +1259 -0
- package/src/pages/WebhooksPage.tsx +483 -0
- package/src/plugins/PluginPage.tsx +64 -0
- package/src/plugins/index.ts +6 -0
- package/src/plugins/registry.ts +151 -0
- package/src/plugins/sdk.d.ts +160 -0
- package/src/plugins/slots.ts +199 -0
- package/src/plugins/types.ts +37 -0
- package/src/plugins/usePlugins.ts +133 -0
- package/src/themes/context.tsx +443 -0
- package/src/themes/fonts.ts +160 -0
- package/src/themes/index.ts +3 -0
- package/src/themes/presets.ts +477 -0
- package/src/themes/types.ts +187 -0
- package/tsconfig.app.json +34 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +124 -0
- package/vite.config.ts.timestamp-1780999102396-af6b77b30ebd8.mjs +105 -0
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useState } from "react";
|
|
2
|
+
import { Clock, Pause, Play, Trash2, X, Zap } from "lucide-react";
|
|
3
|
+
import { Badge } from "@nastechai/ui/ui/components/badge";
|
|
4
|
+
import { Button } from "@nastechai/ui/ui/components/button";
|
|
5
|
+
import { Select, SelectOption } from "@nastechai/ui/ui/components/select";
|
|
6
|
+
import { Spinner } from "@nastechai/ui/ui/components/spinner";
|
|
7
|
+
import { H2 } from "@nastechai/ui/ui/components/typography/h2";
|
|
8
|
+
import { api } from "@/lib/api";
|
|
9
|
+
import type { CronJob, ProfileInfo } from "@/lib/api";
|
|
10
|
+
import { DeleteConfirmDialog } from "@/components/DeleteConfirmDialog";
|
|
11
|
+
import { useToast } from "@nastechai/ui/hooks/use-toast";
|
|
12
|
+
import { useConfirmDelete } from "@nastechai/ui/hooks/use-confirm-delete";
|
|
13
|
+
import { useModalBehavior } from "@/hooks/useModalBehavior";
|
|
14
|
+
import { Toast } from "@nastechai/ui/ui/components/toast";
|
|
15
|
+
import { Card, CardContent } from "@nastechai/ui/ui/components/card";
|
|
16
|
+
import { Input } from "@nastechai/ui/ui/components/input";
|
|
17
|
+
import { Label } from "@nastechai/ui/ui/components/label";
|
|
18
|
+
import { useI18n } from "@/i18n";
|
|
19
|
+
import { usePageHeader } from "@/contexts/usePageHeader";
|
|
20
|
+
import { PluginSlot } from "@/plugins";
|
|
21
|
+
import { cn, themedBody } from "@/lib/utils";
|
|
22
|
+
|
|
23
|
+
function formatTime(iso?: string | null): string {
|
|
24
|
+
if (!iso) return "—";
|
|
25
|
+
const d = new Date(iso);
|
|
26
|
+
return d.toLocaleString();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function asText(value: unknown): string {
|
|
30
|
+
return typeof value === "string" ? value : "";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function truncateText(value: string, maxLength: number): string {
|
|
34
|
+
return value.length > maxLength
|
|
35
|
+
? value.slice(0, maxLength) + "..."
|
|
36
|
+
: value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getJobPrompt(job: CronJob): string {
|
|
40
|
+
return asText(job.prompt);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getJobName(job: CronJob): string {
|
|
44
|
+
return asText(job.name).trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getJobTitle(job: CronJob): string {
|
|
48
|
+
const name = getJobName(job);
|
|
49
|
+
if (name) return name;
|
|
50
|
+
|
|
51
|
+
const prompt = getJobPrompt(job);
|
|
52
|
+
if (prompt) return truncateText(prompt, 60);
|
|
53
|
+
|
|
54
|
+
const script = asText(job.script);
|
|
55
|
+
if (script) return truncateText(script, 60);
|
|
56
|
+
|
|
57
|
+
return job.id || "Cron job";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getJobScheduleDisplay(job: CronJob): string {
|
|
61
|
+
return (
|
|
62
|
+
asText(job.schedule_display) ||
|
|
63
|
+
asText(job.schedule?.display) ||
|
|
64
|
+
asText(job.schedule?.expr) ||
|
|
65
|
+
"—"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getJobState(job: CronJob): string {
|
|
70
|
+
return asText(job.state) || (job.enabled === false ? "disabled" : "scheduled");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getJobProfile(job: CronJob): string {
|
|
74
|
+
return asText(job.profile) || asText(job.profile_name) || "default";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getJobKey(job: CronJob): string {
|
|
78
|
+
return `${getJobProfile(job)}:${job.id}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function splitJobKey(key: string): { profile: string; id: string } {
|
|
82
|
+
const idx = key.indexOf(":");
|
|
83
|
+
if (idx === -1) return { profile: "default", id: key };
|
|
84
|
+
return { profile: key.slice(0, idx) || "default", id: key.slice(idx + 1) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function profileLabel(profile: string): string {
|
|
88
|
+
return profile === "default" ? "default" : profile;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const STATUS_TONE: Record<string, "success" | "warning" | "destructive"> = {
|
|
92
|
+
enabled: "success",
|
|
93
|
+
scheduled: "success",
|
|
94
|
+
paused: "warning",
|
|
95
|
+
error: "destructive",
|
|
96
|
+
completed: "destructive",
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export default function CronPage() {
|
|
100
|
+
const [jobs, setJobs] = useState<CronJob[]>([]);
|
|
101
|
+
const [profiles, setProfiles] = useState<ProfileInfo[]>([]);
|
|
102
|
+
const [selectedProfile, setSelectedProfile] = useState("all");
|
|
103
|
+
const [loading, setLoading] = useState(true);
|
|
104
|
+
const { toast, showToast } = useToast();
|
|
105
|
+
const { t } = useI18n();
|
|
106
|
+
const { setEnd } = usePageHeader();
|
|
107
|
+
|
|
108
|
+
// New job modal state
|
|
109
|
+
const [createModalOpen, setCreateModalOpen] = useState(false);
|
|
110
|
+
const [prompt, setPrompt] = useState("");
|
|
111
|
+
const [schedule, setSchedule] = useState("");
|
|
112
|
+
const [name, setName] = useState("");
|
|
113
|
+
const closeCreateModal = useCallback(() => setCreateModalOpen(false), []);
|
|
114
|
+
const createModalRef = useModalBehavior({
|
|
115
|
+
open: createModalOpen,
|
|
116
|
+
onClose: closeCreateModal,
|
|
117
|
+
});
|
|
118
|
+
const [deliver, setDeliver] = useState("local");
|
|
119
|
+
const [creating, setCreating] = useState(false);
|
|
120
|
+
const createProfile = selectedProfile === "all" ? "default" : selectedProfile;
|
|
121
|
+
|
|
122
|
+
const loadJobs = useCallback(() => {
|
|
123
|
+
api
|
|
124
|
+
.getCronJobs(selectedProfile)
|
|
125
|
+
.then(setJobs)
|
|
126
|
+
.catch(() => showToast(t.common.loading, "error"))
|
|
127
|
+
.finally(() => setLoading(false));
|
|
128
|
+
}, [selectedProfile, showToast, t.common.loading]);
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
api
|
|
132
|
+
.getProfiles()
|
|
133
|
+
.then((res) => setProfiles(res.profiles))
|
|
134
|
+
.catch(() => setProfiles([]));
|
|
135
|
+
}, []);
|
|
136
|
+
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
loadJobs();
|
|
139
|
+
}, [loadJobs]);
|
|
140
|
+
|
|
141
|
+
const handleCreate = async () => {
|
|
142
|
+
if (!prompt.trim() || !schedule.trim()) {
|
|
143
|
+
showToast(`${t.cron.prompt} & ${t.cron.schedule} required`, "error");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
setCreating(true);
|
|
147
|
+
try {
|
|
148
|
+
await api.createCronJob(
|
|
149
|
+
{
|
|
150
|
+
prompt: prompt.trim(),
|
|
151
|
+
schedule: schedule.trim(),
|
|
152
|
+
name: name.trim() || undefined,
|
|
153
|
+
deliver,
|
|
154
|
+
},
|
|
155
|
+
createProfile,
|
|
156
|
+
);
|
|
157
|
+
showToast(t.common.create + " ✓", "success");
|
|
158
|
+
setPrompt("");
|
|
159
|
+
setSchedule("");
|
|
160
|
+
setName("");
|
|
161
|
+
setDeliver("local");
|
|
162
|
+
setCreateModalOpen(false);
|
|
163
|
+
loadJobs();
|
|
164
|
+
} catch (e) {
|
|
165
|
+
showToast(`${t.config.failedToSave}: ${e}`, "error");
|
|
166
|
+
} finally {
|
|
167
|
+
setCreating(false);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const handlePauseResume = async (job: CronJob) => {
|
|
172
|
+
try {
|
|
173
|
+
const isPaused = getJobState(job) === "paused";
|
|
174
|
+
const profile = getJobProfile(job);
|
|
175
|
+
if (isPaused) {
|
|
176
|
+
await api.resumeCronJob(job.id, profile);
|
|
177
|
+
showToast(
|
|
178
|
+
`${t.cron.resume}: "${truncateText(getJobTitle(job), 30)}"`,
|
|
179
|
+
"success",
|
|
180
|
+
);
|
|
181
|
+
} else {
|
|
182
|
+
await api.pauseCronJob(job.id, profile);
|
|
183
|
+
showToast(
|
|
184
|
+
`${t.cron.pause}: "${truncateText(getJobTitle(job), 30)}"`,
|
|
185
|
+
"success",
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
loadJobs();
|
|
189
|
+
} catch (e) {
|
|
190
|
+
showToast(`${t.status.error}: ${e}`, "error");
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const handleTrigger = async (job: CronJob) => {
|
|
195
|
+
try {
|
|
196
|
+
await api.triggerCronJob(job.id, getJobProfile(job));
|
|
197
|
+
showToast(
|
|
198
|
+
`${t.cron.triggerNow}: "${truncateText(getJobTitle(job), 30)}"`,
|
|
199
|
+
"success",
|
|
200
|
+
);
|
|
201
|
+
loadJobs();
|
|
202
|
+
} catch (e) {
|
|
203
|
+
showToast(`${t.status.error}: ${e}`, "error");
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const jobDelete = useConfirmDelete({
|
|
208
|
+
onDelete: useCallback(
|
|
209
|
+
async (key: string) => {
|
|
210
|
+
const { profile, id } = splitJobKey(key);
|
|
211
|
+
const job = jobs.find((j) => getJobKey(j) === key);
|
|
212
|
+
try {
|
|
213
|
+
await api.deleteCronJob(id, profile);
|
|
214
|
+
showToast(
|
|
215
|
+
`${t.common.delete}: "${job ? truncateText(getJobTitle(job), 30) : id}"`,
|
|
216
|
+
"success",
|
|
217
|
+
);
|
|
218
|
+
loadJobs();
|
|
219
|
+
} catch (e) {
|
|
220
|
+
showToast(`${t.status.error}: ${e}`, "error");
|
|
221
|
+
throw e;
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
[jobs, loadJobs, showToast, t.common.delete, t.status.error],
|
|
225
|
+
),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Put "Create" button in page header
|
|
229
|
+
useLayoutEffect(() => {
|
|
230
|
+
setEnd(
|
|
231
|
+
<Button
|
|
232
|
+
className="uppercase"
|
|
233
|
+
size="sm"
|
|
234
|
+
onClick={() => setCreateModalOpen(true)}
|
|
235
|
+
>
|
|
236
|
+
{t.common.create}
|
|
237
|
+
</Button>,
|
|
238
|
+
);
|
|
239
|
+
return () => {
|
|
240
|
+
setEnd(null);
|
|
241
|
+
};
|
|
242
|
+
}, [setEnd, t.common.create, loading]);
|
|
243
|
+
|
|
244
|
+
if (loading) {
|
|
245
|
+
return (
|
|
246
|
+
<div className="flex items-center justify-center py-24">
|
|
247
|
+
<Spinner className="text-2xl text-primary" />
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const pendingJob = jobDelete.pendingId
|
|
253
|
+
? jobs.find((j) => getJobKey(j) === jobDelete.pendingId)
|
|
254
|
+
: null;
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<div className="flex flex-col gap-6">
|
|
258
|
+
<PluginSlot name="cron:top" />
|
|
259
|
+
<Toast toast={toast} />
|
|
260
|
+
|
|
261
|
+
<DeleteConfirmDialog
|
|
262
|
+
open={jobDelete.isOpen}
|
|
263
|
+
onCancel={jobDelete.cancel}
|
|
264
|
+
onConfirm={jobDelete.confirm}
|
|
265
|
+
title={t.cron.confirmDeleteTitle}
|
|
266
|
+
description={
|
|
267
|
+
pendingJob
|
|
268
|
+
? `"${truncateText(getJobTitle(pendingJob), 40)}" — ${
|
|
269
|
+
t.cron.confirmDeleteMessage
|
|
270
|
+
}`
|
|
271
|
+
: t.cron.confirmDeleteMessage
|
|
272
|
+
}
|
|
273
|
+
loading={jobDelete.isDeleting}
|
|
274
|
+
/>
|
|
275
|
+
|
|
276
|
+
{/* Create job modal */}
|
|
277
|
+
{createModalOpen && (
|
|
278
|
+
<div
|
|
279
|
+
ref={createModalRef}
|
|
280
|
+
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 backdrop-blur-sm p-4"
|
|
281
|
+
onClick={(e) => e.target === e.currentTarget && setCreateModalOpen(false)}
|
|
282
|
+
role="dialog"
|
|
283
|
+
aria-modal="true"
|
|
284
|
+
aria-labelledby="create-cron-title"
|
|
285
|
+
>
|
|
286
|
+
<div className={cn(themedBody, "relative w-full max-w-lg border border-border bg-card shadow-2xl flex flex-col")}>
|
|
287
|
+
<Button
|
|
288
|
+
ghost
|
|
289
|
+
size="icon"
|
|
290
|
+
onClick={() => setCreateModalOpen(false)}
|
|
291
|
+
className="absolute right-2 top-2 text-muted-foreground hover:text-foreground"
|
|
292
|
+
aria-label="Close"
|
|
293
|
+
>
|
|
294
|
+
<X />
|
|
295
|
+
</Button>
|
|
296
|
+
|
|
297
|
+
<header className="p-5 pb-3 border-b border-border">
|
|
298
|
+
<h2
|
|
299
|
+
id="create-cron-title"
|
|
300
|
+
className="font-mondwest text-display text-base tracking-wider"
|
|
301
|
+
>
|
|
302
|
+
{t.cron.newJob}
|
|
303
|
+
</h2>
|
|
304
|
+
</header>
|
|
305
|
+
|
|
306
|
+
<div className="p-5 grid gap-4">
|
|
307
|
+
<div className="grid gap-2">
|
|
308
|
+
<Label htmlFor="cron-profile">Profile</Label>
|
|
309
|
+
<Select
|
|
310
|
+
id="cron-profile"
|
|
311
|
+
value={createProfile}
|
|
312
|
+
onValueChange={(v) => setSelectedProfile(v)}
|
|
313
|
+
>
|
|
314
|
+
{profiles.map((profile) => (
|
|
315
|
+
<SelectOption key={profile.name} value={profile.name}>
|
|
316
|
+
{profileLabel(profile.name)}
|
|
317
|
+
</SelectOption>
|
|
318
|
+
))}
|
|
319
|
+
</Select>
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<div className="grid gap-2">
|
|
323
|
+
<Label htmlFor="cron-name">{t.cron.nameOptional}</Label>
|
|
324
|
+
<Input
|
|
325
|
+
id="cron-name"
|
|
326
|
+
autoFocus
|
|
327
|
+
placeholder={t.cron.namePlaceholder}
|
|
328
|
+
value={name}
|
|
329
|
+
onChange={(e) => setName(e.target.value)}
|
|
330
|
+
/>
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
<div className="grid gap-2">
|
|
334
|
+
<Label htmlFor="cron-prompt">{t.cron.prompt}</Label>
|
|
335
|
+
<textarea
|
|
336
|
+
id="cron-prompt"
|
|
337
|
+
className="flex min-h-[80px] w-full border border-border bg-background/40 px-3 py-2 text-sm font-courier shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-foreground/30 focus-visible:border-foreground/25"
|
|
338
|
+
placeholder={t.cron.promptPlaceholder}
|
|
339
|
+
value={prompt}
|
|
340
|
+
onChange={(e) => setPrompt(e.target.value)}
|
|
341
|
+
/>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
345
|
+
<div className="grid gap-2">
|
|
346
|
+
<Label htmlFor="cron-schedule">{t.cron.schedule}</Label>
|
|
347
|
+
<Input
|
|
348
|
+
id="cron-schedule"
|
|
349
|
+
placeholder={t.cron.schedulePlaceholder}
|
|
350
|
+
value={schedule}
|
|
351
|
+
onChange={(e) => setSchedule(e.target.value)}
|
|
352
|
+
/>
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
<div className="grid gap-2">
|
|
356
|
+
<Label htmlFor="cron-deliver">{t.cron.deliverTo}</Label>
|
|
357
|
+
<Select
|
|
358
|
+
id="cron-deliver"
|
|
359
|
+
value={deliver}
|
|
360
|
+
onValueChange={(v) => setDeliver(v)}
|
|
361
|
+
>
|
|
362
|
+
<SelectOption value="local">
|
|
363
|
+
{t.cron.delivery.local}
|
|
364
|
+
</SelectOption>
|
|
365
|
+
<SelectOption value="telegram">
|
|
366
|
+
{t.cron.delivery.telegram}
|
|
367
|
+
</SelectOption>
|
|
368
|
+
<SelectOption value="discord">
|
|
369
|
+
{t.cron.delivery.discord}
|
|
370
|
+
</SelectOption>
|
|
371
|
+
<SelectOption value="slack">
|
|
372
|
+
{t.cron.delivery.slack}
|
|
373
|
+
</SelectOption>
|
|
374
|
+
<SelectOption value="email">
|
|
375
|
+
{t.cron.delivery.email}
|
|
376
|
+
</SelectOption>
|
|
377
|
+
</Select>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
<div className="flex justify-end">
|
|
382
|
+
<Button
|
|
383
|
+
className="uppercase"
|
|
384
|
+
size="sm"
|
|
385
|
+
onClick={handleCreate}
|
|
386
|
+
disabled={creating}
|
|
387
|
+
prefix={creating ? <Spinner /> : undefined}
|
|
388
|
+
>
|
|
389
|
+
{creating ? t.common.creating : t.common.create}
|
|
390
|
+
</Button>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
)}
|
|
396
|
+
|
|
397
|
+
<div className="flex flex-col gap-3">
|
|
398
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
|
399
|
+
<H2
|
|
400
|
+
variant="sm"
|
|
401
|
+
className="flex items-center gap-2 text-muted-foreground"
|
|
402
|
+
>
|
|
403
|
+
<Clock className="h-4 w-4" />
|
|
404
|
+
{t.cron.scheduledJobs} ({jobs.length})
|
|
405
|
+
</H2>
|
|
406
|
+
|
|
407
|
+
<div className="grid gap-1 min-w-[220px]">
|
|
408
|
+
<Label htmlFor="cron-profile-filter">Profile</Label>
|
|
409
|
+
<Select
|
|
410
|
+
id="cron-profile-filter"
|
|
411
|
+
value={selectedProfile}
|
|
412
|
+
onValueChange={(v) => setSelectedProfile(v)}
|
|
413
|
+
>
|
|
414
|
+
<SelectOption value="all">All profiles</SelectOption>
|
|
415
|
+
{profiles.map((profile) => (
|
|
416
|
+
<SelectOption key={profile.name} value={profile.name}>
|
|
417
|
+
{profileLabel(profile.name)}
|
|
418
|
+
</SelectOption>
|
|
419
|
+
))}
|
|
420
|
+
</Select>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
{jobs.length === 0 && (
|
|
425
|
+
<Card>
|
|
426
|
+
<CardContent className="py-8 text-center text-sm text-muted-foreground">
|
|
427
|
+
{t.cron.noJobs}
|
|
428
|
+
</CardContent>
|
|
429
|
+
</Card>
|
|
430
|
+
)}
|
|
431
|
+
|
|
432
|
+
{jobs.map((job) => {
|
|
433
|
+
const state = getJobState(job);
|
|
434
|
+
const promptText = getJobPrompt(job);
|
|
435
|
+
const title = getJobTitle(job);
|
|
436
|
+
const hasName = Boolean(getJobName(job));
|
|
437
|
+
const deliver = asText(job.deliver);
|
|
438
|
+
const profile = getJobProfile(job);
|
|
439
|
+
const jobKey = getJobKey(job);
|
|
440
|
+
|
|
441
|
+
return (
|
|
442
|
+
<Card key={jobKey}>
|
|
443
|
+
<CardContent className="flex items-start gap-4 py-4">
|
|
444
|
+
<div className="flex-1 min-w-0">
|
|
445
|
+
<div className="flex items-center gap-2 mb-1">
|
|
446
|
+
<span className="font-medium text-sm truncate">
|
|
447
|
+
{title}
|
|
448
|
+
</span>
|
|
449
|
+
<Badge tone={STATUS_TONE[state] ?? "secondary"}>
|
|
450
|
+
{state}
|
|
451
|
+
</Badge>
|
|
452
|
+
<Badge tone="outline">{profileLabel(profile)}</Badge>
|
|
453
|
+
{deliver && deliver !== "local" && (
|
|
454
|
+
<Badge tone="outline">{deliver}</Badge>
|
|
455
|
+
)}
|
|
456
|
+
</div>
|
|
457
|
+
{hasName && promptText && (
|
|
458
|
+
<p className="text-xs text-muted-foreground truncate mb-1">
|
|
459
|
+
{truncateText(promptText, 100)}
|
|
460
|
+
</p>
|
|
461
|
+
)}
|
|
462
|
+
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
|
463
|
+
<span className="font-mono">{getJobScheduleDisplay(job)}</span>
|
|
464
|
+
<span>
|
|
465
|
+
{t.cron.last}: {formatTime(job.last_run_at)}
|
|
466
|
+
</span>
|
|
467
|
+
<span>
|
|
468
|
+
{t.cron.next}: {formatTime(job.next_run_at)}
|
|
469
|
+
</span>
|
|
470
|
+
</div>
|
|
471
|
+
{job.last_error && (
|
|
472
|
+
<p className="text-xs text-destructive mt-1">
|
|
473
|
+
{job.last_error}
|
|
474
|
+
</p>
|
|
475
|
+
)}
|
|
476
|
+
</div>
|
|
477
|
+
|
|
478
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
479
|
+
<Button
|
|
480
|
+
ghost
|
|
481
|
+
size="icon"
|
|
482
|
+
title={state === "paused" ? t.cron.resume : t.cron.pause}
|
|
483
|
+
aria-label={
|
|
484
|
+
state === "paused" ? t.cron.resume : t.cron.pause
|
|
485
|
+
}
|
|
486
|
+
onClick={() => handlePauseResume(job)}
|
|
487
|
+
className={
|
|
488
|
+
state === "paused" ? "text-success" : "text-warning"
|
|
489
|
+
}
|
|
490
|
+
>
|
|
491
|
+
{state === "paused" ? <Play /> : <Pause />}
|
|
492
|
+
</Button>
|
|
493
|
+
|
|
494
|
+
<Button
|
|
495
|
+
ghost
|
|
496
|
+
size="icon"
|
|
497
|
+
title={t.cron.triggerNow}
|
|
498
|
+
aria-label={t.cron.triggerNow}
|
|
499
|
+
onClick={() => handleTrigger(job)}
|
|
500
|
+
>
|
|
501
|
+
<Zap />
|
|
502
|
+
</Button>
|
|
503
|
+
|
|
504
|
+
<Button
|
|
505
|
+
ghost
|
|
506
|
+
destructive
|
|
507
|
+
size="icon"
|
|
508
|
+
title={t.common.delete}
|
|
509
|
+
aria-label={t.common.delete}
|
|
510
|
+
onClick={() => jobDelete.requestDelete(jobKey)}
|
|
511
|
+
>
|
|
512
|
+
<Trash2 />
|
|
513
|
+
</Button>
|
|
514
|
+
</div>
|
|
515
|
+
</CardContent>
|
|
516
|
+
</Card>
|
|
517
|
+
);
|
|
518
|
+
})}
|
|
519
|
+
</div>
|
|
520
|
+
|
|
521
|
+
<PluginSlot name="cron:bottom" />
|
|
522
|
+
</div>
|
|
523
|
+
);
|
|
524
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useLayoutEffect } from "react";
|
|
2
|
+
import { ExternalLink } from "lucide-react";
|
|
3
|
+
import { useI18n } from "@/i18n";
|
|
4
|
+
import { usePageHeader } from "@/contexts/usePageHeader";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import { PluginSlot } from "@/plugins";
|
|
7
|
+
|
|
8
|
+
export const NASTECH_DOCS_URL = "https://docs.nastech-agent.workers.dev/docs/";
|
|
9
|
+
|
|
10
|
+
const DS_BUTTON_OUTLINED_LINK_CN = cn(
|
|
11
|
+
"group relative inline-grid grid-cols-[auto_1fr_auto] items-center",
|
|
12
|
+
"px-[.9em_.75em] py-[1.25em] gap-2",
|
|
13
|
+
"leading-0 font-bold tracking-[0.2em] uppercase",
|
|
14
|
+
"text-midground bg-transparent shadow-midground",
|
|
15
|
+
"shadow-[inset_-1px_-1px_0_0_#00000080,inset_1px_1px_0_0_#ffffff80]",
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
export default function DocsPage() {
|
|
19
|
+
const { t } = useI18n();
|
|
20
|
+
const { setEnd } = usePageHeader();
|
|
21
|
+
|
|
22
|
+
useLayoutEffect(() => {
|
|
23
|
+
setEnd(
|
|
24
|
+
<a
|
|
25
|
+
href={NASTECH_DOCS_URL}
|
|
26
|
+
target="_blank"
|
|
27
|
+
rel="noopener noreferrer"
|
|
28
|
+
className={DS_BUTTON_OUTLINED_LINK_CN}
|
|
29
|
+
>
|
|
30
|
+
<ExternalLink className="size-3.5" />
|
|
31
|
+
{t.app.openDocumentation}
|
|
32
|
+
</a>,
|
|
33
|
+
);
|
|
34
|
+
return () => {
|
|
35
|
+
setEnd(null);
|
|
36
|
+
};
|
|
37
|
+
}, [setEnd, t]);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
className={cn(
|
|
42
|
+
"flex min-h-0 w-full min-w-0 flex-1 flex-col",
|
|
43
|
+
"pt-1 sm:pt-2",
|
|
44
|
+
)}
|
|
45
|
+
>
|
|
46
|
+
<PluginSlot name="docs:top" />
|
|
47
|
+
<iframe
|
|
48
|
+
title={t.app.nav.documentation}
|
|
49
|
+
src={NASTECH_DOCS_URL}
|
|
50
|
+
className={cn(
|
|
51
|
+
"min-h-0 w-full min-w-0 flex-1",
|
|
52
|
+
"rounded-sm border border-current/20",
|
|
53
|
+
// Docusaurus paints over a transparent <html> / <body> and
|
|
54
|
+
// relies on the browser's canvas color (light by default) to
|
|
55
|
+
// fill the viewport. Inheriting the dashboard's dark color
|
|
56
|
+
// scheme makes that canvas dark, so the docs body text — which
|
|
57
|
+
// is tuned for a light canvas — becomes near-invisible. Force a
|
|
58
|
+
// light color scheme + white background on the iframe element so
|
|
59
|
+
// the docs render cleanly regardless of the active dashboard
|
|
60
|
+
// theme or the user's prefers-color-scheme.
|
|
61
|
+
"[color-scheme:light] bg-white",
|
|
62
|
+
)}
|
|
63
|
+
sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
|
|
64
|
+
referrerPolicy="no-referrer-when-downgrade"
|
|
65
|
+
/>
|
|
66
|
+
<PluginSlot name="docs:bottom" />
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|