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