@nordsym/apiclaw 1.2.2 → 1.2.3

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 (77) hide show
  1. package/AGENTS.md +50 -33
  2. package/README.md +22 -12
  3. package/SOUL.md +60 -19
  4. package/STATUS.md +91 -169
  5. package/convex/_generated/api.d.ts +6 -0
  6. package/convex/directCall.ts +598 -0
  7. package/convex/providers.ts +341 -26
  8. package/convex/schema.ts +87 -0
  9. package/convex/usage.ts +260 -0
  10. package/convex/waitlist.ts +55 -0
  11. package/data/combined-02-26.json +22102 -0
  12. package/data/night-expansion-02-26-06-batch2.json +1898 -0
  13. package/data/night-expansion-02-26-06-batch3.json +1410 -0
  14. package/data/night-expansion-02-26-06.json +3146 -0
  15. package/data/night-expansion-02-26-full.json +9726 -0
  16. package/data/night-expansion-02-26-v2.json +330 -0
  17. package/data/night-expansion-02-26.json +171 -0
  18. package/dist/crypto.d.ts +7 -0
  19. package/dist/crypto.d.ts.map +1 -0
  20. package/dist/crypto.js +67 -0
  21. package/dist/crypto.js.map +1 -0
  22. package/dist/execute-dynamic.d.ts +116 -0
  23. package/dist/execute-dynamic.d.ts.map +1 -0
  24. package/dist/execute-dynamic.js +456 -0
  25. package/dist/execute-dynamic.js.map +1 -0
  26. package/dist/execute.d.ts +2 -1
  27. package/dist/execute.d.ts.map +1 -1
  28. package/dist/execute.js +35 -5
  29. package/dist/execute.js.map +1 -1
  30. package/dist/index.js +33 -4
  31. package/dist/index.js.map +1 -1
  32. package/dist/registry/apis.json +2081 -3
  33. package/docs/PRD-customer-key-passthrough.md +184 -0
  34. package/landing/public/badges/available-on-apiclaw.svg +14 -0
  35. package/landing/scripts/generate-stats.js +75 -4
  36. package/landing/src/app/admin/page.tsx +1 -1
  37. package/landing/src/app/api/auth/magic-link/route.ts +1 -1
  38. package/landing/src/app/api/auth/session/route.ts +1 -1
  39. package/landing/src/app/api/auth/verify/route.ts +1 -1
  40. package/landing/src/app/api/og/route.tsx +5 -3
  41. package/landing/src/app/docs/page.tsx +5 -4
  42. package/landing/src/app/earn/page.tsx +14 -11
  43. package/landing/src/app/globals.css +16 -15
  44. package/landing/src/app/layout.tsx +2 -2
  45. package/landing/src/app/page.tsx +425 -254
  46. package/landing/src/app/providers/dashboard/[apiId]/actions/[actionId]/edit/page.tsx +600 -0
  47. package/landing/src/app/providers/dashboard/[apiId]/actions/new/page.tsx +583 -0
  48. package/landing/src/app/providers/dashboard/[apiId]/actions/page.tsx +301 -0
  49. package/landing/src/app/providers/dashboard/[apiId]/direct-call/page.tsx +659 -0
  50. package/landing/src/app/providers/dashboard/[apiId]/page.tsx +381 -0
  51. package/landing/src/app/providers/dashboard/[apiId]/test/page.tsx +418 -0
  52. package/landing/src/app/providers/dashboard/layout.tsx +292 -0
  53. package/landing/src/app/providers/dashboard/page.tsx +353 -290
  54. package/landing/src/app/providers/register/page.tsx +87 -10
  55. package/landing/src/components/AiClientDropdown.tsx +85 -0
  56. package/landing/src/components/ConfigHelperModal.tsx +113 -0
  57. package/landing/src/components/HeroTabs.tsx +187 -0
  58. package/landing/src/components/ShareIntegrationModal.tsx +198 -0
  59. package/landing/src/hooks/useDashboardData.ts +53 -1
  60. package/landing/src/lib/apis.json +46554 -174
  61. package/landing/src/lib/convex-client.ts +22 -3
  62. package/landing/src/lib/stats.json +4 -4
  63. package/landing/tsconfig.tsbuildinfo +1 -1
  64. package/night-expansion-02-26-06-batch2.py +368 -0
  65. package/night-expansion-02-26-06-batch3.py +299 -0
  66. package/night-expansion-02-26-06.py +756 -0
  67. package/package.json +1 -1
  68. package/scripts/bulk-add-public-apis-v2.py +418 -0
  69. package/scripts/night-expansion-02-26-v2.py +296 -0
  70. package/scripts/night-expansion-02-26.py +890 -0
  71. package/scripts/seed-complete-api.js +181 -0
  72. package/scripts/seed-demo-api.sh +44 -0
  73. package/src/crypto.ts +75 -0
  74. package/src/execute-dynamic.ts +589 -0
  75. package/src/execute.ts +41 -5
  76. package/src/index.ts +38 -4
  77. package/src/registry/apis.json +2081 -3
@@ -0,0 +1,583 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { useParams, useRouter } from "next/navigation";
5
+ import Link from "next/link";
6
+ import {
7
+ ArrowLeft,
8
+ Save,
9
+ Loader2,
10
+ AlertCircle,
11
+ Plus,
12
+ Trash2,
13
+ Check,
14
+ } from "lucide-react";
15
+
16
+ interface ActionParam {
17
+ name: string;
18
+ type: "string" | "number" | "boolean" | "object";
19
+ required: boolean;
20
+ description: string;
21
+ in: "body" | "query" | "path";
22
+ }
23
+
24
+ interface ResponseMapping {
25
+ name: string;
26
+ path: string;
27
+ }
28
+
29
+ interface ActionFormData {
30
+ name: string;
31
+ displayName: string;
32
+ description: string;
33
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
34
+ path: string;
35
+ params: ActionParam[];
36
+ responseMapping: ResponseMapping[];
37
+ enabled: boolean;
38
+ }
39
+
40
+ const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || 'https://adventurous-avocet-799.convex.cloud';
41
+
42
+ const METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"] as const;
43
+ const PARAM_TYPES = ["string", "number", "boolean", "object"] as const;
44
+ const PARAM_LOCATIONS = ["body", "query", "path"] as const;
45
+
46
+ export default function NewActionPage() {
47
+ const params = useParams();
48
+ const router = useRouter();
49
+ const apiId = params.apiId as string;
50
+
51
+ const [isLoading, setIsLoading] = useState(true);
52
+ const [isSaving, setIsSaving] = useState(false);
53
+ const [error, setError] = useState<string | null>(null);
54
+ const [success, setSuccess] = useState<string | null>(null);
55
+ const [directCallId, setDirectCallId] = useState<string | null>(null);
56
+
57
+ const [formData, setFormData] = useState<ActionFormData>({
58
+ name: "",
59
+ displayName: "",
60
+ description: "",
61
+ method: "GET",
62
+ path: "",
63
+ params: [],
64
+ responseMapping: [],
65
+ enabled: true,
66
+ });
67
+
68
+ useEffect(() => {
69
+ const loadDirectCallConfig = async () => {
70
+ const token = localStorage.getItem("apiclaw_session");
71
+ if (!token) {
72
+ router.push("/providers/dashboard/login");
73
+ return;
74
+ }
75
+
76
+ try {
77
+ const configRes = await fetch(`${CONVEX_URL}/api/query`, {
78
+ method: 'POST',
79
+ headers: { 'Content-Type': 'application/json' },
80
+ body: JSON.stringify({
81
+ path: 'directCall:getDirectCallConfigByApiId',
82
+ args: { apiId }
83
+ })
84
+ });
85
+ const configData = await configRes.json();
86
+
87
+ if (!configData || !configData._id) {
88
+ setError("Direct Call not configured. Please set up Direct Call first.");
89
+ return;
90
+ }
91
+
92
+ setDirectCallId(configData._id);
93
+ } catch (err) {
94
+ console.error("Failed to load config:", err);
95
+ setError(err instanceof Error ? err.message : "Failed to load configuration");
96
+ } finally {
97
+ setIsLoading(false);
98
+ }
99
+ };
100
+
101
+ loadDirectCallConfig();
102
+ }, [apiId, router]);
103
+
104
+ const updateField = <K extends keyof ActionFormData>(
105
+ field: K,
106
+ value: ActionFormData[K]
107
+ ) => {
108
+ setFormData((prev) => ({ ...prev, [field]: value }));
109
+ setError(null);
110
+ setSuccess(null);
111
+ };
112
+
113
+ const generateSlug = (displayName: string) => {
114
+ return displayName
115
+ .toLowerCase()
116
+ .replace(/[^a-z0-9]+/g, '-')
117
+ .replace(/^-|-$/g, '');
118
+ };
119
+
120
+ const handleDisplayNameChange = (value: string) => {
121
+ updateField("displayName", value);
122
+ // Auto-generate slug if name is empty or was auto-generated
123
+ if (!formData.name || formData.name === generateSlug(formData.displayName)) {
124
+ updateField("name", generateSlug(value));
125
+ }
126
+ };
127
+
128
+ // Param management
129
+ const addParam = () => {
130
+ setFormData((prev) => ({
131
+ ...prev,
132
+ params: [
133
+ ...prev.params,
134
+ { name: "", type: "string", required: false, description: "", in: "body" }
135
+ ]
136
+ }));
137
+ };
138
+
139
+ const updateParam = (index: number, field: keyof ActionParam, value: string | boolean) => {
140
+ setFormData((prev) => ({
141
+ ...prev,
142
+ params: prev.params.map((p, i) =>
143
+ i === index ? { ...p, [field]: value } : p
144
+ )
145
+ }));
146
+ };
147
+
148
+ const removeParam = (index: number) => {
149
+ setFormData((prev) => ({
150
+ ...prev,
151
+ params: prev.params.filter((_, i) => i !== index)
152
+ }));
153
+ };
154
+
155
+ // Response mapping management
156
+ const addResponseMapping = () => {
157
+ setFormData((prev) => ({
158
+ ...prev,
159
+ responseMapping: [
160
+ ...prev.responseMapping,
161
+ { name: "", path: "" }
162
+ ]
163
+ }));
164
+ };
165
+
166
+ const updateResponseMapping = (index: number, field: keyof ResponseMapping, value: string) => {
167
+ setFormData((prev) => ({
168
+ ...prev,
169
+ responseMapping: prev.responseMapping.map((r, i) =>
170
+ i === index ? { ...r, [field]: value } : r
171
+ )
172
+ }));
173
+ };
174
+
175
+ const removeResponseMapping = (index: number) => {
176
+ setFormData((prev) => ({
177
+ ...prev,
178
+ responseMapping: prev.responseMapping.filter((_, i) => i !== index)
179
+ }));
180
+ };
181
+
182
+ const handleSave = async () => {
183
+ // Validation
184
+ if (!formData.name.trim()) {
185
+ setError("Action name is required");
186
+ return;
187
+ }
188
+ if (!formData.displayName.trim()) {
189
+ setError("Display name is required");
190
+ return;
191
+ }
192
+ if (!formData.path.trim()) {
193
+ setError("Path is required");
194
+ return;
195
+ }
196
+ if (!directCallId) {
197
+ setError("Direct Call configuration not found");
198
+ return;
199
+ }
200
+
201
+ // Validate params have names
202
+ const invalidParams = formData.params.filter(p => !p.name.trim());
203
+ if (invalidParams.length > 0) {
204
+ setError("All parameters must have a name");
205
+ return;
206
+ }
207
+
208
+ setIsSaving(true);
209
+ setError(null);
210
+
211
+ try {
212
+ await fetch(`${CONVEX_URL}/api/mutation`, {
213
+ method: 'POST',
214
+ headers: { 'Content-Type': 'application/json' },
215
+ body: JSON.stringify({
216
+ path: 'directCall:saveAction',
217
+ args: {
218
+ directCallId,
219
+ action: {
220
+ name: formData.name,
221
+ displayName: formData.displayName,
222
+ description: formData.description,
223
+ method: formData.method,
224
+ path: formData.path,
225
+ params: formData.params,
226
+ responseMapping: formData.responseMapping,
227
+ enabled: formData.enabled,
228
+ }
229
+ }
230
+ })
231
+ });
232
+
233
+ setSuccess("Action created successfully!");
234
+ setTimeout(() => {
235
+ router.push(`/providers/dashboard/${apiId}/actions`);
236
+ }, 1000);
237
+ } catch (err) {
238
+ console.error("Save failed:", err);
239
+ setError(err instanceof Error ? err.message : "Failed to save action");
240
+ } finally {
241
+ setIsSaving(false);
242
+ }
243
+ };
244
+
245
+ const getMethodColor = (method: string) => {
246
+ switch (method) {
247
+ case 'GET': return 'border-green-500 bg-green-500/20 text-green-500';
248
+ case 'POST': return 'border-blue-500 bg-blue-500/20 text-blue-500';
249
+ case 'PUT': return 'border-yellow-500 bg-yellow-500/20 text-yellow-600';
250
+ case 'PATCH': return 'border-orange-500 bg-orange-500/20 text-orange-500';
251
+ case 'DELETE': return 'border-red-500 bg-red-500/20 text-red-500';
252
+ default: return 'border-gray-500 bg-gray-500/20 text-gray-500';
253
+ }
254
+ };
255
+
256
+ if (isLoading) {
257
+ return (
258
+ <div className="flex items-center justify-center py-24">
259
+ <Loader2 className="w-8 h-8 text-accent animate-spin" />
260
+ </div>
261
+ );
262
+ }
263
+
264
+ if (error && !directCallId) {
265
+ return (
266
+ <div className="text-center py-24">
267
+ <AlertCircle className="w-16 h-16 text-yellow-500 mx-auto mb-4" />
268
+ <h1 className="text-2xl font-bold mb-2">Setup Required</h1>
269
+ <p className="text-text-muted mb-6">{error}</p>
270
+ <Link href={`/providers/dashboard/${apiId}/direct-call`} className="btn-primary">
271
+ Set Up Direct Call
272
+ </Link>
273
+ </div>
274
+ );
275
+ }
276
+
277
+ return (
278
+ <div className="max-w-3xl space-y-8">
279
+ {/* Header */}
280
+ <div className="flex items-center gap-4">
281
+ <Link
282
+ href={`/providers/dashboard/${apiId}/actions`}
283
+ className="p-2 rounded-lg hover:bg-surface transition"
284
+ >
285
+ <ArrowLeft className="w-5 h-5" />
286
+ </Link>
287
+ <div>
288
+ <h1 className="text-2xl font-bold">New Action</h1>
289
+ <p className="text-text-muted">Define a new endpoint for agents to call</p>
290
+ </div>
291
+ </div>
292
+
293
+ {/* Basic Info */}
294
+ <div className="rounded-2xl border border-border bg-surface-elevated p-6 space-y-4">
295
+ <h2 className="font-semibold">Basic Information</h2>
296
+
297
+ <div>
298
+ <label className="block text-sm font-medium mb-2">Display Name *</label>
299
+ <input
300
+ type="text"
301
+ value={formData.displayName}
302
+ onChange={(e) => handleDisplayNameChange(e.target.value)}
303
+ placeholder="Create Invoice"
304
+ className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
305
+ />
306
+ <p className="mt-1 text-xs text-text-muted">Human-readable name shown to users</p>
307
+ </div>
308
+
309
+ <div>
310
+ <label className="block text-sm font-medium mb-2">Action Name (slug) *</label>
311
+ <input
312
+ type="text"
313
+ value={formData.name}
314
+ onChange={(e) => updateField("name", e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '-'))}
315
+ placeholder="create-invoice"
316
+ className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition font-mono"
317
+ />
318
+ <p className="mt-1 text-xs text-text-muted">Unique identifier used in API calls</p>
319
+ </div>
320
+
321
+ <div>
322
+ <label className="block text-sm font-medium mb-2">Description</label>
323
+ <textarea
324
+ value={formData.description}
325
+ onChange={(e) => updateField("description", e.target.value)}
326
+ placeholder="Creates a new invoice in the system..."
327
+ rows={3}
328
+ className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition resize-none"
329
+ />
330
+ </div>
331
+
332
+ <div className="flex items-center gap-3">
333
+ <input
334
+ type="checkbox"
335
+ id="enabled"
336
+ checked={formData.enabled}
337
+ onChange={(e) => updateField("enabled", e.target.checked)}
338
+ className="w-5 h-5 rounded border-border text-accent focus:ring-accent"
339
+ />
340
+ <label htmlFor="enabled" className="text-sm font-medium">
341
+ Enabled (agents can call this action)
342
+ </label>
343
+ </div>
344
+ </div>
345
+
346
+ {/* Endpoint */}
347
+ <div className="rounded-2xl border border-border bg-surface-elevated p-6 space-y-4">
348
+ <h2 className="font-semibold">Endpoint</h2>
349
+
350
+ <div>
351
+ <label className="block text-sm font-medium mb-2">HTTP Method *</label>
352
+ <div className="flex flex-wrap gap-2">
353
+ {METHODS.map((method) => (
354
+ <button
355
+ key={method}
356
+ onClick={() => updateField("method", method)}
357
+ className={`px-4 py-2 rounded-lg border transition font-bold text-sm ${
358
+ formData.method === method
359
+ ? getMethodColor(method)
360
+ : 'border-border hover:border-accent/50'
361
+ }`}
362
+ >
363
+ {method}
364
+ </button>
365
+ ))}
366
+ </div>
367
+ </div>
368
+
369
+ <div>
370
+ <label className="block text-sm font-medium mb-2">Path *</label>
371
+ <input
372
+ type="text"
373
+ value={formData.path}
374
+ onChange={(e) => updateField("path", e.target.value)}
375
+ placeholder="/invoices or /invoices/{id}"
376
+ className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition font-mono"
377
+ />
378
+ <p className="mt-1 text-xs text-text-muted">
379
+ Use {"{param}"} for path parameters, e.g. /users/{"{id}"}/orders
380
+ </p>
381
+ </div>
382
+ </div>
383
+
384
+ {/* Parameters */}
385
+ <div className="rounded-2xl border border-border bg-surface-elevated p-6 space-y-4">
386
+ <div className="flex items-center justify-between">
387
+ <h2 className="font-semibold">Parameters</h2>
388
+ <button
389
+ onClick={addParam}
390
+ className="btn-secondary !py-2 !px-3 text-sm"
391
+ >
392
+ <Plus className="w-4 h-4" />
393
+ Add Parameter
394
+ </button>
395
+ </div>
396
+
397
+ {formData.params.length === 0 ? (
398
+ <p className="text-sm text-text-muted py-4 text-center">
399
+ No parameters defined. Click "Add Parameter" to add one.
400
+ </p>
401
+ ) : (
402
+ <div className="space-y-4">
403
+ {formData.params.map((param, index) => (
404
+ <div
405
+ key={index}
406
+ className="p-4 rounded-xl bg-surface border border-border space-y-3"
407
+ >
408
+ <div className="flex items-start justify-between gap-4">
409
+ <div className="flex-1 grid grid-cols-2 gap-3">
410
+ <div>
411
+ <label className="block text-xs font-medium mb-1">Name *</label>
412
+ <input
413
+ type="text"
414
+ value={param.name}
415
+ onChange={(e) => updateParam(index, "name", e.target.value)}
416
+ placeholder="customer_id"
417
+ className="w-full px-3 py-2 rounded-lg bg-surface-elevated border border-border focus:border-accent focus:outline-none transition text-sm font-mono"
418
+ />
419
+ </div>
420
+ <div>
421
+ <label className="block text-xs font-medium mb-1">Type</label>
422
+ <select
423
+ value={param.type}
424
+ onChange={(e) => updateParam(index, "type", e.target.value)}
425
+ className="w-full px-3 py-2 rounded-lg bg-surface-elevated border border-border focus:border-accent focus:outline-none transition text-sm"
426
+ >
427
+ {PARAM_TYPES.map((type) => (
428
+ <option key={type} value={type}>{type}</option>
429
+ ))}
430
+ </select>
431
+ </div>
432
+ <div>
433
+ <label className="block text-xs font-medium mb-1">Location</label>
434
+ <select
435
+ value={param.in}
436
+ onChange={(e) => updateParam(index, "in", e.target.value)}
437
+ className="w-full px-3 py-2 rounded-lg bg-surface-elevated border border-border focus:border-accent focus:outline-none transition text-sm"
438
+ >
439
+ {PARAM_LOCATIONS.map((loc) => (
440
+ <option key={loc} value={loc}>{loc}</option>
441
+ ))}
442
+ </select>
443
+ </div>
444
+ <div className="flex items-end pb-1">
445
+ <label className="flex items-center gap-2 text-sm">
446
+ <input
447
+ type="checkbox"
448
+ checked={param.required}
449
+ onChange={(e) => updateParam(index, "required", e.target.checked)}
450
+ className="w-4 h-4 rounded border-border text-accent focus:ring-accent"
451
+ />
452
+ Required
453
+ </label>
454
+ </div>
455
+ </div>
456
+ <button
457
+ onClick={() => removeParam(index)}
458
+ className="p-2 rounded-lg hover:bg-red-500/20 text-text-muted hover:text-red-500 transition mt-5"
459
+ >
460
+ <Trash2 className="w-4 h-4" />
461
+ </button>
462
+ </div>
463
+ <div>
464
+ <label className="block text-xs font-medium mb-1">Description</label>
465
+ <input
466
+ type="text"
467
+ value={param.description}
468
+ onChange={(e) => updateParam(index, "description", e.target.value)}
469
+ placeholder="The unique identifier of the customer"
470
+ className="w-full px-3 py-2 rounded-lg bg-surface-elevated border border-border focus:border-accent focus:outline-none transition text-sm"
471
+ />
472
+ </div>
473
+ </div>
474
+ ))}
475
+ </div>
476
+ )}
477
+ </div>
478
+
479
+ {/* Response Mapping */}
480
+ <div className="rounded-2xl border border-border bg-surface-elevated p-6 space-y-4">
481
+ <div className="flex items-center justify-between">
482
+ <div>
483
+ <h2 className="font-semibold">Response Mapping</h2>
484
+ <p className="text-xs text-text-muted mt-1">Extract specific fields from the API response</p>
485
+ </div>
486
+ <button
487
+ onClick={addResponseMapping}
488
+ className="btn-secondary !py-2 !px-3 text-sm"
489
+ >
490
+ <Plus className="w-4 h-4" />
491
+ Add Mapping
492
+ </button>
493
+ </div>
494
+
495
+ {formData.responseMapping.length === 0 ? (
496
+ <p className="text-sm text-text-muted py-4 text-center">
497
+ No response mappings defined. The full response will be returned.
498
+ </p>
499
+ ) : (
500
+ <div className="space-y-3">
501
+ {formData.responseMapping.map((mapping, index) => (
502
+ <div
503
+ key={index}
504
+ className="flex items-center gap-3 p-3 rounded-xl bg-surface border border-border"
505
+ >
506
+ <div className="flex-1 grid grid-cols-2 gap-3">
507
+ <div>
508
+ <label className="block text-xs font-medium mb-1">Field Name</label>
509
+ <input
510
+ type="text"
511
+ value={mapping.name}
512
+ onChange={(e) => updateResponseMapping(index, "name", e.target.value)}
513
+ placeholder="invoiceId"
514
+ className="w-full px-3 py-2 rounded-lg bg-surface-elevated border border-border focus:border-accent focus:outline-none transition text-sm font-mono"
515
+ />
516
+ </div>
517
+ <div>
518
+ <label className="block text-xs font-medium mb-1">JSONPath</label>
519
+ <input
520
+ type="text"
521
+ value={mapping.path}
522
+ onChange={(e) => updateResponseMapping(index, "path", e.target.value)}
523
+ placeholder="$.data.id"
524
+ className="w-full px-3 py-2 rounded-lg bg-surface-elevated border border-border focus:border-accent focus:outline-none transition text-sm font-mono"
525
+ />
526
+ </div>
527
+ </div>
528
+ <button
529
+ onClick={() => removeResponseMapping(index)}
530
+ className="p-2 rounded-lg hover:bg-red-500/20 text-text-muted hover:text-red-500 transition"
531
+ >
532
+ <Trash2 className="w-4 h-4" />
533
+ </button>
534
+ </div>
535
+ ))}
536
+ </div>
537
+ )}
538
+ </div>
539
+
540
+ {/* Error/Success Messages */}
541
+ {error && (
542
+ <div className="p-4 rounded-xl bg-red-500/10 border border-red-500/30 flex items-center gap-3 text-red-500">
543
+ <AlertCircle className="w-5 h-5 flex-shrink-0" />
544
+ <span className="text-sm">{error}</span>
545
+ </div>
546
+ )}
547
+
548
+ {success && (
549
+ <div className="p-4 rounded-xl bg-green-500/10 border border-green-500/30 flex items-center gap-3 text-green-500">
550
+ <Check className="w-5 h-5 flex-shrink-0" />
551
+ <span className="text-sm">{success}</span>
552
+ </div>
553
+ )}
554
+
555
+ {/* Save Button */}
556
+ <div className="flex items-center justify-between pt-4 border-t border-border">
557
+ <Link
558
+ href={`/providers/dashboard/${apiId}/actions`}
559
+ className="btn-secondary !py-2.5"
560
+ >
561
+ Cancel
562
+ </Link>
563
+ <button
564
+ onClick={handleSave}
565
+ disabled={isSaving}
566
+ className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed"
567
+ >
568
+ {isSaving ? (
569
+ <>
570
+ <Loader2 className="w-5 h-5 animate-spin" />
571
+ Creating...
572
+ </>
573
+ ) : (
574
+ <>
575
+ <Save className="w-5 h-5" />
576
+ Create Action
577
+ </>
578
+ )}
579
+ </button>
580
+ </div>
581
+ </div>
582
+ );
583
+ }