@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,483 @@
|
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useState } from "react";
|
|
2
|
+
import { Webhook, Plus, Trash2, X, Copy, Check } 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 { WebhookRoute, WebhooksResponse } 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 { usePageHeader } from "@/contexts/usePageHeader";
|
|
19
|
+
import { cn, themedBody } from "@/lib/utils";
|
|
20
|
+
|
|
21
|
+
interface CreatedWebhook {
|
|
22
|
+
url: string;
|
|
23
|
+
secret: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function CopyButton({ value }: { value: string }) {
|
|
27
|
+
const [copied, setCopied] = useState(false);
|
|
28
|
+
const handleCopy = useCallback(() => {
|
|
29
|
+
navigator.clipboard
|
|
30
|
+
.writeText(value)
|
|
31
|
+
.then(() => {
|
|
32
|
+
setCopied(true);
|
|
33
|
+
window.setTimeout(() => setCopied(false), 1500);
|
|
34
|
+
})
|
|
35
|
+
.catch(() => {});
|
|
36
|
+
}, [value]);
|
|
37
|
+
return (
|
|
38
|
+
<Button
|
|
39
|
+
ghost
|
|
40
|
+
size="icon"
|
|
41
|
+
title="Copy"
|
|
42
|
+
aria-label="Copy"
|
|
43
|
+
onClick={handleCopy}
|
|
44
|
+
className="text-muted-foreground hover:text-foreground"
|
|
45
|
+
>
|
|
46
|
+
{copied ? <Check /> : <Copy />}
|
|
47
|
+
</Button>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default function WebhooksPage() {
|
|
52
|
+
const [data, setData] = useState<WebhooksResponse | null>(null);
|
|
53
|
+
const [loading, setLoading] = useState(true);
|
|
54
|
+
const { toast, showToast } = useToast();
|
|
55
|
+
const { setEnd } = usePageHeader();
|
|
56
|
+
|
|
57
|
+
// New subscription modal state
|
|
58
|
+
const [createModalOpen, setCreateModalOpen] = useState(false);
|
|
59
|
+
const [name, setName] = useState("");
|
|
60
|
+
const [description, setDescription] = useState("");
|
|
61
|
+
const [events, setEvents] = useState("");
|
|
62
|
+
const [deliver, setDeliver] = useState("log");
|
|
63
|
+
const [deliverOnly, setDeliverOnly] = useState(false);
|
|
64
|
+
const [prompt, setPrompt] = useState("");
|
|
65
|
+
const [creating, setCreating] = useState(false);
|
|
66
|
+
const [created, setCreated] = useState<CreatedWebhook | null>(null);
|
|
67
|
+
|
|
68
|
+
const closeCreateModal = useCallback(() => {
|
|
69
|
+
setCreateModalOpen(false);
|
|
70
|
+
setCreated(null);
|
|
71
|
+
}, []);
|
|
72
|
+
const createModalRef = useModalBehavior({
|
|
73
|
+
open: createModalOpen,
|
|
74
|
+
onClose: closeCreateModal,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const enabled = data?.enabled ?? false;
|
|
78
|
+
const subscriptions = data?.subscriptions ?? [];
|
|
79
|
+
|
|
80
|
+
const loadWebhooks = useCallback(() => {
|
|
81
|
+
api
|
|
82
|
+
.getWebhooks()
|
|
83
|
+
.then(setData)
|
|
84
|
+
.catch(() => showToast("Failed to load webhooks", "error"))
|
|
85
|
+
.finally(() => setLoading(false));
|
|
86
|
+
}, [showToast]);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
loadWebhooks();
|
|
90
|
+
}, [loadWebhooks]);
|
|
91
|
+
|
|
92
|
+
const resetForm = useCallback(() => {
|
|
93
|
+
setName("");
|
|
94
|
+
setDescription("");
|
|
95
|
+
setEvents("");
|
|
96
|
+
setDeliver("log");
|
|
97
|
+
setDeliverOnly(false);
|
|
98
|
+
setPrompt("");
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
const handleCreate = async () => {
|
|
102
|
+
if (!name.trim()) {
|
|
103
|
+
showToast("Name required", "error");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
setCreating(true);
|
|
107
|
+
try {
|
|
108
|
+
const eventsList = events
|
|
109
|
+
.split(",")
|
|
110
|
+
.map((e) => e.trim())
|
|
111
|
+
.filter(Boolean);
|
|
112
|
+
const res = await api.createWebhook({
|
|
113
|
+
name: name.trim(),
|
|
114
|
+
description: description.trim() || undefined,
|
|
115
|
+
events: eventsList.length ? eventsList : undefined,
|
|
116
|
+
deliver,
|
|
117
|
+
deliver_only: deliverOnly,
|
|
118
|
+
prompt: prompt.trim() || undefined,
|
|
119
|
+
});
|
|
120
|
+
showToast("Created ✓", "success");
|
|
121
|
+
setCreated({ url: res.url, secret: res.secret ?? "" });
|
|
122
|
+
resetForm();
|
|
123
|
+
loadWebhooks();
|
|
124
|
+
} catch (e) {
|
|
125
|
+
showToast(`Failed to create: ${e}`, "error");
|
|
126
|
+
} finally {
|
|
127
|
+
setCreating(false);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const [togglingName, setTogglingName] = useState<string | null>(null);
|
|
132
|
+
|
|
133
|
+
const handleToggleEnabled = useCallback(
|
|
134
|
+
async (subName: string, nextEnabled: boolean) => {
|
|
135
|
+
setTogglingName(subName);
|
|
136
|
+
try {
|
|
137
|
+
await api.setWebhookEnabled(subName, nextEnabled);
|
|
138
|
+
showToast(
|
|
139
|
+
nextEnabled ? `Enabled: "${subName}"` : `Disabled: "${subName}"`,
|
|
140
|
+
"success",
|
|
141
|
+
);
|
|
142
|
+
loadWebhooks();
|
|
143
|
+
} catch (e) {
|
|
144
|
+
showToast(`Error: ${e}`, "error");
|
|
145
|
+
} finally {
|
|
146
|
+
setTogglingName(null);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
[loadWebhooks, showToast],
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const webhookDelete = useConfirmDelete({
|
|
153
|
+
onDelete: useCallback(
|
|
154
|
+
async (name: string) => {
|
|
155
|
+
try {
|
|
156
|
+
await api.deleteWebhook(name);
|
|
157
|
+
showToast(`Deleted: "${name}"`, "success");
|
|
158
|
+
loadWebhooks();
|
|
159
|
+
} catch (e) {
|
|
160
|
+
showToast(`Error: ${e}`, "error");
|
|
161
|
+
throw e;
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
[loadWebhooks, showToast],
|
|
165
|
+
),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Put "New subscription" button in page header
|
|
169
|
+
useLayoutEffect(() => {
|
|
170
|
+
setEnd(
|
|
171
|
+
<Button
|
|
172
|
+
className="uppercase"
|
|
173
|
+
size="sm"
|
|
174
|
+
disabled={!enabled}
|
|
175
|
+
prefix={<Plus />}
|
|
176
|
+
onClick={() => {
|
|
177
|
+
setCreated(null);
|
|
178
|
+
setCreateModalOpen(true);
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
New subscription
|
|
182
|
+
</Button>,
|
|
183
|
+
);
|
|
184
|
+
return () => {
|
|
185
|
+
setEnd(null);
|
|
186
|
+
};
|
|
187
|
+
}, [setEnd, enabled, loading]);
|
|
188
|
+
|
|
189
|
+
if (loading) {
|
|
190
|
+
return (
|
|
191
|
+
<div className="flex items-center justify-center py-24">
|
|
192
|
+
<Spinner className="text-2xl text-primary" />
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const pendingName = webhookDelete.pendingId ?? "";
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<div className="flex flex-col gap-6">
|
|
201
|
+
<Toast toast={toast} />
|
|
202
|
+
|
|
203
|
+
<DeleteConfirmDialog
|
|
204
|
+
open={webhookDelete.isOpen}
|
|
205
|
+
onCancel={webhookDelete.cancel}
|
|
206
|
+
onConfirm={webhookDelete.confirm}
|
|
207
|
+
title="Delete webhook"
|
|
208
|
+
description={
|
|
209
|
+
pendingName
|
|
210
|
+
? `"${pendingName}" — this will permanently remove this webhook subscription.`
|
|
211
|
+
: "This will permanently remove this webhook subscription."
|
|
212
|
+
}
|
|
213
|
+
loading={webhookDelete.isDeleting}
|
|
214
|
+
/>
|
|
215
|
+
|
|
216
|
+
{/* Create subscription modal */}
|
|
217
|
+
{createModalOpen && (
|
|
218
|
+
<div
|
|
219
|
+
ref={createModalRef}
|
|
220
|
+
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 backdrop-blur-sm p-4"
|
|
221
|
+
onClick={(e) => e.target === e.currentTarget && closeCreateModal()}
|
|
222
|
+
role="dialog"
|
|
223
|
+
aria-modal="true"
|
|
224
|
+
aria-labelledby="create-webhook-title"
|
|
225
|
+
>
|
|
226
|
+
<div className={cn(themedBody, "relative w-full max-w-lg border border-border bg-card shadow-2xl flex flex-col max-h-[90vh] overflow-y-auto")}>
|
|
227
|
+
<Button
|
|
228
|
+
ghost
|
|
229
|
+
size="icon"
|
|
230
|
+
onClick={closeCreateModal}
|
|
231
|
+
className="absolute right-2 top-2 text-muted-foreground hover:text-foreground"
|
|
232
|
+
aria-label="Close"
|
|
233
|
+
>
|
|
234
|
+
<X />
|
|
235
|
+
</Button>
|
|
236
|
+
|
|
237
|
+
<header className="p-5 pb-3 border-b border-border">
|
|
238
|
+
<h2
|
|
239
|
+
id="create-webhook-title"
|
|
240
|
+
className="font-mondwest text-display text-base tracking-wider"
|
|
241
|
+
>
|
|
242
|
+
New subscription
|
|
243
|
+
</h2>
|
|
244
|
+
</header>
|
|
245
|
+
|
|
246
|
+
{created ? (
|
|
247
|
+
<div className="p-5 grid gap-4">
|
|
248
|
+
<p className="text-sm text-muted-foreground">
|
|
249
|
+
Subscription created. Copy the secret now — it is only shown
|
|
250
|
+
once.
|
|
251
|
+
</p>
|
|
252
|
+
|
|
253
|
+
<div className="grid gap-2">
|
|
254
|
+
<Label>Webhook URL</Label>
|
|
255
|
+
<div className="flex items-center gap-2 border border-border bg-background/40 px-3 py-2">
|
|
256
|
+
<span className="flex-1 min-w-0 truncate font-mono text-xs">
|
|
257
|
+
{created.url}
|
|
258
|
+
</span>
|
|
259
|
+
<CopyButton value={created.url} />
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<div className="grid gap-2">
|
|
264
|
+
<Label>Secret (shown once)</Label>
|
|
265
|
+
<div className="flex items-center gap-2 border border-warning/40 bg-warning/10 px-3 py-2">
|
|
266
|
+
<span className="flex-1 min-w-0 truncate font-mono text-xs">
|
|
267
|
+
{created.secret}
|
|
268
|
+
</span>
|
|
269
|
+
<CopyButton value={created.secret} />
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<div className="flex justify-end">
|
|
274
|
+
<Button
|
|
275
|
+
className="uppercase"
|
|
276
|
+
size="sm"
|
|
277
|
+
onClick={closeCreateModal}
|
|
278
|
+
>
|
|
279
|
+
Done
|
|
280
|
+
</Button>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
) : (
|
|
284
|
+
<div className="p-5 grid gap-4">
|
|
285
|
+
<div className="grid gap-2">
|
|
286
|
+
<Label htmlFor="webhook-name">Name</Label>
|
|
287
|
+
<Input
|
|
288
|
+
id="webhook-name"
|
|
289
|
+
autoFocus
|
|
290
|
+
placeholder="e.g. github-push"
|
|
291
|
+
value={name}
|
|
292
|
+
onChange={(e) => setName(e.target.value)}
|
|
293
|
+
/>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<div className="grid gap-2">
|
|
297
|
+
<Label htmlFor="webhook-description">Description</Label>
|
|
298
|
+
<Input
|
|
299
|
+
id="webhook-description"
|
|
300
|
+
placeholder="What this webhook does (optional)"
|
|
301
|
+
value={description}
|
|
302
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
303
|
+
/>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<div className="grid gap-2">
|
|
307
|
+
<Label htmlFor="webhook-events">Events</Label>
|
|
308
|
+
<Input
|
|
309
|
+
id="webhook-events"
|
|
310
|
+
placeholder="comma-separated, leave empty for all"
|
|
311
|
+
value={events}
|
|
312
|
+
onChange={(e) => setEvents(e.target.value)}
|
|
313
|
+
/>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
317
|
+
<div className="grid gap-2">
|
|
318
|
+
<Label htmlFor="webhook-deliver">Deliver to</Label>
|
|
319
|
+
<Select
|
|
320
|
+
id="webhook-deliver"
|
|
321
|
+
value={deliver}
|
|
322
|
+
onValueChange={(v) => setDeliver(v)}
|
|
323
|
+
>
|
|
324
|
+
<SelectOption value="log">Log</SelectOption>
|
|
325
|
+
<SelectOption value="telegram">Telegram</SelectOption>
|
|
326
|
+
<SelectOption value="discord">Discord</SelectOption>
|
|
327
|
+
<SelectOption value="slack">Slack</SelectOption>
|
|
328
|
+
<SelectOption value="email">Email</SelectOption>
|
|
329
|
+
<SelectOption value="github_comment">
|
|
330
|
+
GitHub comment
|
|
331
|
+
</SelectOption>
|
|
332
|
+
</Select>
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
<div className="grid gap-2">
|
|
336
|
+
<Label htmlFor="webhook-deliver-only">Deliver only</Label>
|
|
337
|
+
<label className="flex items-center gap-2 text-sm text-muted-foreground h-9">
|
|
338
|
+
<input
|
|
339
|
+
id="webhook-deliver-only"
|
|
340
|
+
type="checkbox"
|
|
341
|
+
checked={deliverOnly}
|
|
342
|
+
onChange={(e) => setDeliverOnly(e.target.checked)}
|
|
343
|
+
/>
|
|
344
|
+
Skip the agent, deliver payload directly
|
|
345
|
+
</label>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
|
|
349
|
+
<div className="grid gap-2">
|
|
350
|
+
<Label htmlFor="webhook-prompt">Prompt</Label>
|
|
351
|
+
<textarea
|
|
352
|
+
id="webhook-prompt"
|
|
353
|
+
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"
|
|
354
|
+
placeholder="Instructions for the agent when this webhook fires (optional)"
|
|
355
|
+
value={prompt}
|
|
356
|
+
onChange={(e) => setPrompt(e.target.value)}
|
|
357
|
+
/>
|
|
358
|
+
</div>
|
|
359
|
+
|
|
360
|
+
<div className="flex justify-end">
|
|
361
|
+
<Button
|
|
362
|
+
className="uppercase"
|
|
363
|
+
size="sm"
|
|
364
|
+
onClick={handleCreate}
|
|
365
|
+
disabled={creating}
|
|
366
|
+
prefix={creating ? <Spinner /> : undefined}
|
|
367
|
+
>
|
|
368
|
+
{creating ? "Creating…" : "Create"}
|
|
369
|
+
</Button>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
)}
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
)}
|
|
376
|
+
|
|
377
|
+
{!enabled && (
|
|
378
|
+
<Card>
|
|
379
|
+
<CardContent className="py-6 flex items-start gap-3 text-sm">
|
|
380
|
+
<Webhook className="h-5 w-5 shrink-0 text-warning" />
|
|
381
|
+
<div className="flex flex-col gap-1">
|
|
382
|
+
<span className="font-medium">Webhook platform disabled</span>
|
|
383
|
+
<span className="text-muted-foreground">
|
|
384
|
+
The webhook platform must be enabled in your messaging settings
|
|
385
|
+
before you can create subscriptions. Enable it, then return to
|
|
386
|
+
this page.
|
|
387
|
+
</span>
|
|
388
|
+
</div>
|
|
389
|
+
</CardContent>
|
|
390
|
+
</Card>
|
|
391
|
+
)}
|
|
392
|
+
|
|
393
|
+
<div className="flex flex-col gap-3">
|
|
394
|
+
<H2
|
|
395
|
+
variant="sm"
|
|
396
|
+
className="flex items-center gap-2 text-muted-foreground"
|
|
397
|
+
>
|
|
398
|
+
<Webhook className="h-4 w-4" />
|
|
399
|
+
Subscriptions ({subscriptions.length})
|
|
400
|
+
</H2>
|
|
401
|
+
|
|
402
|
+
<p className="text-xs text-muted-foreground -mt-1">
|
|
403
|
+
Disabled webhooks reject incoming events; the gateway hot-reloads
|
|
404
|
+
changes (no restart needed).
|
|
405
|
+
</p>
|
|
406
|
+
|
|
407
|
+
{subscriptions.length === 0 && (
|
|
408
|
+
<Card>
|
|
409
|
+
<CardContent className="py-8 text-center text-sm text-muted-foreground">
|
|
410
|
+
No webhook subscriptions yet.
|
|
411
|
+
</CardContent>
|
|
412
|
+
</Card>
|
|
413
|
+
)}
|
|
414
|
+
|
|
415
|
+
{subscriptions.map((sub: WebhookRoute) => (
|
|
416
|
+
<Card key={sub.name}>
|
|
417
|
+
<CardContent className="flex items-start gap-4 py-4">
|
|
418
|
+
<div className={cn("flex-1 min-w-0", !sub.enabled && "opacity-60")}>
|
|
419
|
+
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
|
420
|
+
<span className="font-medium text-sm truncate">
|
|
421
|
+
{sub.name}
|
|
422
|
+
</span>
|
|
423
|
+
<Badge tone="outline">{sub.deliver}</Badge>
|
|
424
|
+
{sub.deliver_only && (
|
|
425
|
+
<Badge tone="secondary">deliver only</Badge>
|
|
426
|
+
)}
|
|
427
|
+
{!sub.enabled && <Badge tone="warning">disabled</Badge>}
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
{sub.description && (
|
|
431
|
+
<p className="text-xs text-muted-foreground mb-2">
|
|
432
|
+
{sub.description}
|
|
433
|
+
</p>
|
|
434
|
+
)}
|
|
435
|
+
|
|
436
|
+
<div className="flex items-center gap-1 flex-wrap mb-2">
|
|
437
|
+
{sub.events.length === 0 ? (
|
|
438
|
+
<Badge tone="secondary">(all)</Badge>
|
|
439
|
+
) : (
|
|
440
|
+
sub.events.map((evt) => (
|
|
441
|
+
<Badge key={evt} tone="secondary">
|
|
442
|
+
{evt}
|
|
443
|
+
</Badge>
|
|
444
|
+
))
|
|
445
|
+
)}
|
|
446
|
+
</div>
|
|
447
|
+
|
|
448
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
449
|
+
<span className="flex-1 min-w-0 truncate font-mono">
|
|
450
|
+
{sub.url}
|
|
451
|
+
</span>
|
|
452
|
+
<CopyButton value={sub.url} />
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
|
|
456
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
457
|
+
<Button
|
|
458
|
+
ghost
|
|
459
|
+
size="sm"
|
|
460
|
+
className="uppercase"
|
|
461
|
+
disabled={togglingName === sub.name}
|
|
462
|
+
onClick={() => handleToggleEnabled(sub.name, !sub.enabled)}
|
|
463
|
+
>
|
|
464
|
+
{sub.enabled ? "Disable" : "Enable"}
|
|
465
|
+
</Button>
|
|
466
|
+
<Button
|
|
467
|
+
ghost
|
|
468
|
+
destructive
|
|
469
|
+
size="icon"
|
|
470
|
+
title="Delete"
|
|
471
|
+
aria-label="Delete"
|
|
472
|
+
onClick={() => webhookDelete.requestDelete(sub.name)}
|
|
473
|
+
>
|
|
474
|
+
<Trash2 />
|
|
475
|
+
</Button>
|
|
476
|
+
</div>
|
|
477
|
+
</CardContent>
|
|
478
|
+
</Card>
|
|
479
|
+
))}
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
);
|
|
483
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
import { Spinner } from "@nastechai/ui/ui/components/spinner";
|
|
3
|
+
import {
|
|
4
|
+
getPluginComponent,
|
|
5
|
+
getPluginLoadError,
|
|
6
|
+
onPluginRegistered,
|
|
7
|
+
} from "./registry";
|
|
8
|
+
import { useI18n } from "@/i18n";
|
|
9
|
+
import { cn } from "@/lib/utils";
|
|
10
|
+
import type { Translations } from "@/i18n/types";
|
|
11
|
+
|
|
12
|
+
/** Renders a plugin tab once its bundle has called `register()`. */
|
|
13
|
+
export function PluginPage({ name }: { name: string }) {
|
|
14
|
+
const { t } = useI18n();
|
|
15
|
+
// Subscribe in render (via useSyncExternalStore) so we never miss
|
|
16
|
+
// `register()` if the script loads before a useEffect would run.
|
|
17
|
+
const Component = useSyncExternalStore(
|
|
18
|
+
(onChange) => onPluginRegistered(onChange),
|
|
19
|
+
() => getPluginComponent(name) ?? null,
|
|
20
|
+
() => null,
|
|
21
|
+
);
|
|
22
|
+
const loadError = useSyncExternalStore(
|
|
23
|
+
(onChange) => onPluginRegistered(onChange),
|
|
24
|
+
() => getPluginLoadError(name) ?? null,
|
|
25
|
+
() => null,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (Component) {
|
|
29
|
+
return <Component />;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (loadError) {
|
|
33
|
+
const message = formatPluginError(loadError, t);
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
className={cn(
|
|
37
|
+
"max-w-lg p-4",
|
|
38
|
+
"font-mondwest text-sm tracking-[0.08em] text-text-secondary",
|
|
39
|
+
)}
|
|
40
|
+
role="alert"
|
|
41
|
+
>
|
|
42
|
+
{message}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
className={cn(
|
|
50
|
+
"flex items-center gap-2 p-4",
|
|
51
|
+
"font-mondwest text-sm tracking-[0.1em] text-text-tertiary",
|
|
52
|
+
)}
|
|
53
|
+
>
|
|
54
|
+
<Spinner className="shrink-0" />
|
|
55
|
+
<span>{t.common.loading}</span>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function formatPluginError(code: string, t: Translations): string {
|
|
61
|
+
if (code === "LOAD_FAILED") return t.common.pluginLoadFailed;
|
|
62
|
+
if (code === "NO_REGISTER") return t.common.pluginNotRegistered;
|
|
63
|
+
return code;
|
|
64
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { exposePluginSDK, getPluginComponent, onPluginRegistered, getRegisteredCount } from "./registry";
|
|
2
|
+
export { PluginPage } from "./PluginPage";
|
|
3
|
+
export { usePlugins } from "./usePlugins";
|
|
4
|
+
export { PluginSlot, KNOWN_SLOT_NAMES, registerSlot, getSlotEntries, onSlotRegistered, unregisterPluginSlots } from "./slots";
|
|
5
|
+
export type { KnownSlotName } from "./slots";
|
|
6
|
+
export type { PluginManifest, RegisteredPlugin } from "./types";
|