PluginProbe
WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services / 8.8.1
WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services v8.8.1
8.8.1 8.8.0 8.7.9 8.7.8 8.7.7 8.7.6 8.7.5 8.7.4 8.7.3 8.7.2 8.7.1 8.7.0 8.6.9 8.6.8 8.6.7 8.6.6 8.6.5 8.6.4 8.6.2 8.6.1 8.6.0 8.5.9 8.5.8 8.5.7 8.5.6 All 537 releases
chatbot / addons / automator / src / components / NodeConfigModal.jsx

NodeConfigModal.jsx in WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services 8.8.1, at addons/automator/src/components/NodeConfigModal.jsx

682 lines 32.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 "use client";
2
3 import React, { useState, useEffect } from "react";
4 import { ChevronRight, Database, Settings, Activity, Search, ArrowLeft, Lock } from "lucide-react";
5 import Modal from "./ui/Modal";
6
7 export default function NodeConfigModal({ node, isOpen, onClose, onUpdateNode }) {
8 const [step, setStep] = useState(1);
9 const [search, setSearch] = useState("");
10 const [selectedAction, setSelectedAction] = useState(null);
11 const [fieldValues, setFieldValues] = useState({});
12 const [fieldOptions, setFieldOptions] = useState({});
13 const [loadingOptions, setLoadingOptions] = useState({});
14 const [isTesting, setIsTesting] = useState(false);
15 const [testResponse, setTestResponse] = useState(null);
16
17 const [credentials, setCredentials] = useState([]);
18 const [isLoadingCredentials, setIsLoadingCredentials] = useState(false);
19 const [isAddingCredential, setIsAddingCredential] = useState(false);
20 const [newCredential, setNewCredential] = useState({ name: "", data: {} });
21 const [isSavingCredential, setIsSavingCredential] = useState(false);
22
23 useEffect(() => {
24 if (isOpen) {
25 setStep(1);
26 setSearch("");
27 setTestResponse(null);
28 if (node?.data?.actionId) {
29 // Find action in metadata based on node.data.actionId
30 let foundAction = null;
31 if (node.appData?.categories) {
32 node.appData.categories.forEach(cat => {
33 const act = cat.actions.find(a => a.id === node.data.actionId);
34 if (act) foundAction = act;
35 });
36 } else if (node.appData?.actions) {
37 foundAction = node.appData.actions.find(a => a.id === node.data.actionId);
38 }
39 setSelectedAction(foundAction);
40 setFieldValues(node.data?.config || {});
41 } else {
42 setSelectedAction(null);
43 setFieldValues({});
44 }
45 }
46 }, [isOpen, node]);
47
48 useEffect(() => {
49 if (isOpen && node?.appData?.id) {
50 const fetchCredentials = async () => {
51 setIsLoadingCredentials(true);
52 try {
53 const response = await fetch(`${window.wpbotAutomator.apiUrl}/credentials?app_id=${node.appData.id}`, {
54 headers: { 'X-WP-Nonce': window.wpbotAutomator.nonce },
55 });
56 const data = await response.json();
57 if (Array.isArray(data)) setCredentials(data);
58 } catch (error) {
59 console.error("Failed to fetch credentials:", error);
60 } finally {
61 setIsLoadingCredentials(false);
62 }
63 };
64
65 setIsAddingCredential(false);
66 setNewCredential({ name: "", data: {} });
67 fetchCredentials();
68 }
69 }, [isOpen, node?.appData?.id]);
70
71 const handleSaveCredential = async () => {
72 try {
73 if (!newCredential.name) {
74 alert("Validation Error: Please enter a typical Name for this credential.");
75 return;
76 }
77
78 setIsSavingCredential(true);
79 const payload = {
80 name: newCredential.name,
81 app_id: node.appData.id,
82 data: newCredential.data
83 };
84
85 const response = await fetch(`${window.wpbotAutomator.apiUrl}/credentials`, {
86 method: "POST",
87 headers: {
88 'Content-Type': 'application/json',
89 'X-WP-Nonce': window.wpbotAutomator.nonce
90 },
91 body: JSON.stringify(payload)
92 });
93
94 const result = await response.json();
95
96 if (result.success) {
97 setCredentials(prev => [...prev, result.credential]);
98 if (selectedAction) {
99 const credField = selectedAction.fields?.find(f => f.type === 'credential');
100 if (credField) handleFieldChange(credField.id, result.credential.id);
101 }
102 setIsAddingCredential(false);
103 setNewCredential({ name: "", data: {} });
104 alert("Credential saved successfully!");
105 } else {
106 alert("Failed to save credential on server: " + (result.message || JSON.stringify(result)));
107 }
108 } catch (error) {
109 console.error("Error saving credential:", error);
110 alert("Error saving credential. Check your browser console: " + error.message);
111 } finally {
112 setIsSavingCredential(false);
113 }
114 };
115
116 useEffect(() => {
117 if (selectedAction?.fields) {
118 selectedAction.fields.forEach(field => {
119 if (field.type === 'select' && field.optionsUrl && !fieldOptions[field.id]) {
120 fetchOptions(field);
121 }
122 });
123 }
124 }, [selectedAction]);
125
126 const fetchOptions = async (field) => {
127 setLoadingOptions(prev => ({ ...prev, [field.id]: true }));
128 try {
129 const baseUrl = window.wpbotAutomator.apiUrl.replace(/\/$/, '');
130 const response = await fetch(`${baseUrl}${field.optionsUrl}`, {
131 headers: {
132 'X-WP-Nonce': window.wpbotAutomator.nonce,
133 },
134 });
135 const data = await response.json();
136 setFieldOptions(prev => ({ ...prev, [field.id]: Array.isArray(data) ? data : [] }));
137 } catch (error) {
138 console.error(`Error fetching options for ${field.id}:`, error);
139 } finally {
140 setLoadingOptions(prev => ({ ...prev, [field.id]: false }));
141 }
142 };
143
144 if (!node) return null;
145
146 const handleSelectAction = (action) => {
147 setSelectedAction(action);
148 setStep(2);
149 };
150
151 const handleSave = () => {
152 onUpdateNode({
153 ...node,
154 subtitle: selectedAction ? selectedAction.name : "Configure action",
155 data: {
156 ...node.data,
157 actionId: selectedAction?.id,
158 config: fieldValues
159 }
160 });
161 onClose();
162 };
163
164 const handleFieldChange = (fieldId, value) => {
165 setFieldValues(prev => ({ ...prev, [fieldId]: value }));
166 };
167
168 const handleTestRun = async () => {
169 setIsTesting(true);
170 setTestResponse(null);
171 try {
172 const response = await fetch(`${window.wpbotAutomator.apiUrl}/test-action`, {
173 method: 'POST',
174 headers: {
175 'Content-Type': 'application/json',
176 'X-WP-Nonce': window.wpbotAutomator.nonce,
177 },
178 body: JSON.stringify({
179 appId: node.appData.id,
180 actionId: selectedAction.id,
181 config: fieldValues,
182 }),
183 });
184 const data = await response.json();
185 setTestResponse(data);
186 } catch (error) {
187 console.error("Test Run error:", error);
188 setTestResponse({ success: false, message: "Request failed. Check your connection or URL." });
189 } finally {
190 setIsTesting(false);
191 }
192 };
193
194 const renderStep1 = () => {
195 const categories = node.appData?.categories || (node.appData?.actions ? [{ id: "main", name: "Actions", actions: node.appData.actions }] : []);
196
197 return (
198 <div className="space-y-6">
199 <div className="relative">
200 <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
201 <input
202 placeholder="Search actions..."
203 className="w-full pl-10 bg-input border border-border rounded-lg py-2.5 text-sm outline-none focus:ring-2 focus:ring-primary/20 transition-all"
204 value={search}
205 onChange={(e) => setSearch(e.target.value)}
206 />
207 </div>
208
209 <div className="space-y-8 max-h-[50vh] overflow-y-auto pr-2 custom-scrollbar">
210 {categories.map((category) => {
211 const filteredActions = category.actions.filter(a =>
212 a.name.toLowerCase().includes(search.toLowerCase())
213 );
214
215 if (filteredActions.length === 0) return null;
216
217 return (
218 <div key={category.id} className="space-y-3">
219 <h4 className="text-xs font-bold text-muted-foreground uppercase tracking-wider px-1">
220 {category.name}
221 </h4>
222 <div className="grid grid-cols-1 gap-2">
223 {filteredActions.map((action) => {
224 const isLocked = action.isPro && !window.wpbotAutomator.isProActive;
225
226 return (
227 <button
228 key={action.id}
229 onClick={() => !isLocked && handleSelectAction(action)}
230 className={`flex items-center justify-between p-3 rounded-xl border transition-all text-left group ${isLocked ? "opacity-75 cursor-not-allowed bg-sidebar-accent/30" :
231 selectedAction?.id === action.id
232 ? "bg-primary/10 border-primary text-primary shadow-sm"
233 : "bg-card border-border hover:border-muted-foreground/30 hover:shadow-sm"
234 }`}
235 >
236 <div className="flex items-center gap-3">
237 <div className={`w-8 h-8 rounded-lg flex items-center justify-center text-xs font-bold ${selectedAction?.id === action.id ? "bg-primary text-white" : "bg-sidebar-accent text-muted-foreground group-hover:bg-primary/20 group-hover:text-primary"
238 } transition-colors`}>
239 {isLocked ? <Lock className="w-4 h-4" /> : <Activity className="w-4 h-4" />}
240 </div>
241 <span className="text-sm font-medium">{action.name}</span>
242 {isLocked && (
243 <span className="text-[10px] px-2 py-0.5 bg-amber-500/10 text-amber-500 rounded-full font-bold ml-2">PRO</span>
244 )}
245 {(action.isPro && window.wpbotAutomator.isProActive) && (
246 <span className="text-[10px] px-2 py-0.5 bg-green-500/10 text-green-600 rounded-full font-bold ml-2">PRO</span>
247 )}
248 </div>
249 <ChevronRight className={`w-4 h-4 transition-transform ${selectedAction?.id === action.id ? "translate-x-1" : "text-muted-foreground group-hover:translate-x-1"}`} />
250 </button>
251 );
252 })}
253 </div>
254 </div>
255 );
256 })}
257 </div>
258 </div>
259 );
260 };
261
262 // Renders one labeled input per column of the selected table.
263 // fieldOptions[tableIdField] is the /tables array already fetched for the table select.
264 const TableColumnsMapper = ({ field }) => {
265 const tableId = fieldValues[field.tableIdField] || '';
266 const allTables = fieldOptions[field.tableIdField] || [];
267 const selected = allTables.find(t => String(t.id) === String(tableId));
268 const columns = selected?.columns || [];
269
270 let current = {};
271 try { current = JSON.parse(fieldValues[field.id] || '{}') || {}; } catch {}
272
273 const update = (slug, val) => {
274 const next = { ...current, [slug]: val };
275 handleFieldChange(field.id, JSON.stringify(next));
276 };
277
278 if (!tableId) return (
279 <div className="p-3 rounded-lg bg-muted/40 border border-border text-xs text-muted-foreground text-center">
280 Select a table above to see its fields.
281 </div>
282 );
283
284 if (columns.length === 0) return (
285 <div className="p-3 rounded-lg bg-muted/40 border border-border text-xs text-muted-foreground">
286 {allTables.length === 0 ? 'Loading table columns…' : 'This table has no columns.'}
287 </div>
288 );
289
290 return (
291 <div className="space-y-2.5">
292 {columns.map(col => (
293 <div key={col.slug} className="flex items-center gap-2.5">
294 <div className="w-28 shrink-0">
295 <p className="text-xs font-semibold text-foreground truncate" title={col.name}>
296 {col.name}{col.required && <span className="text-rose-500 ml-0.5">*</span>}
297 </p>
298 <p className="text-[10px] text-muted-foreground/60">{col.type}</p>
299 </div>
300 <input
301 type="text"
302 className="flex-1 bg-input border border-border rounded-lg py-2 px-3 text-xs outline-none focus:ring-2 focus:ring-primary/20 transition-all"
303 placeholder={`{${col.slug}} or a fixed value`}
304 value={current[col.slug] || ''}
305 onChange={e => update(col.slug, e.target.value)}
306 />
307 </div>
308 ))}
309 <p className="text-[10px] text-muted-foreground pl-1 pt-0.5">
310 Use <code className="bg-muted px-1 py-0.5 rounded font-mono">{'{token}'}</code> to map values from the trigger.
311 </p>
312 </div>
313 );
314 };
315
316 const KeyValueField = ({ values, onChange, placeholder }) => {
317 const pairs = values ? (typeof values === 'string' ? JSON.parse(values) : values) : [{ key: '', value: '' }];
318
319 const updatePair = (index, k, v) => {
320 const newPairs = [...pairs];
321 newPairs[index] = { ...newPairs[index], key: k, value: v };
322 onChange(newPairs);
323 };
324
325 const addPair = () => {
326 onChange([...pairs, { key: '', value: '' }]);
327 };
328
329 const removePair = (index) => {
330 if (pairs.length === 1) {
331 onChange([{ key: '', value: '' }]);
332 } else {
333 const newPairs = pairs.filter((_, i) => i !== index);
334 onChange(newPairs);
335 }
336 };
337
338 return (
339 <div className="space-y-2">
340 {pairs.map((pair, index) => (
341 <div key={index} className="flex gap-2">
342 <input
343 className="flex-1 bg-input border border-border rounded-lg py-2 px-3 text-xs outline-none focus:ring-2 focus:ring-primary/20"
344 placeholder="Key"
345 value={pair.key}
346 onChange={(e) => updatePair(index, e.target.value, pair.value)}
347 />
348 <input
349 className="flex-1 bg-input border border-border rounded-lg py-2 px-3 text-xs outline-none focus:ring-2 focus:ring-primary/20"
350 placeholder="Value"
351 value={pair.value}
352 onChange={(e) => updatePair(index, pair.key, e.target.value)}
353 />
354 <button
355 onClick={() => removePair(index)}
356 className="p-2 text-rose-500 hover:bg-rose-50 rounded-lg transition-colors"
357 >
358 <ChevronRight className="w-4 h-4 rotate-45" />
359 </button>
360 </div>
361 ))}
362 <button
363 onClick={addPair}
364 className="flex items-center gap-2 text-xs font-bold text-primary hover:text-primary/80 transition-colors pl-1"
365 >
366 + {placeholder || "Add Pair"}
367 </button>
368 </div>
369 );
370 };
371
372 const renderStep2 = () => (
373 <div className="space-y-6 animate-in slide-in-from-right-4 duration-300">
374 <div className="flex items-center gap-3 p-4 bg-primary/5 border border-primary/10 rounded-xl">
375 <div className="w-10 h-10 rounded-xl bg-primary flex items-center justify-center text-white shadow-lg shadow-primary/20">
376 <Activity className="w-5 h-5" />
377 </div>
378 <div>
379 <h4 className="text-sm font-bold text-foreground leading-tight">{selectedAction?.name}</h4>
380 <p className="text-xs text-muted-foreground">Configure the details for this action</p>
381 <p>{selectedAction?.description}</p>
382 </div>
383 </div>
384
385 {node.type === 'trigger' && selectedAction?.id === 'incoming_webhook' && (
386 <div className="p-4 bg-violet-500/5 border border-violet-500/10 rounded-xl space-y-2">
387 <label className="text-xs font-bold text-violet-600 uppercase tracking-wider">Your Webhook URL</label>
388 <div className="flex gap-2">
389 <input
390 readOnly
391 className="flex-1 bg-white/50 border border-violet-500/20 rounded-lg py-2 px-3 text-xs font-mono text-violet-700 outline-none"
392 value={`${window.location.origin}/wp-json/wpbot-automator/v1/webhook/${node.id.replace('node-', '')}`}
393 />
394 <button
395 onClick={() => navigator.clipboard.writeText(`${window.location.origin}/wp-json/wpbot-automator/v1/webhook/${node.id.replace('node-', '')}`)}
396 className="px-3 py-2 bg-violet-600 text-white rounded-lg text-xs font-bold hover:bg-violet-700 transition-colors"
397 >
398 Copy
399 </button>
400 </div>
401 <p className="text-[10px] text-muted-foreground italic">Use this URL to send GET or POST requests to trigger this workflow.</p>
402 </div>
403 )}
404
405 {node.type === 'trigger' && selectedAction?.id === 'facebook_lead_ads' && (
406 <div className="p-4 bg-blue-500/5 border border-blue-500/10 rounded-xl space-y-2">
407 <label className="text-xs font-bold text-blue-600 uppercase tracking-wider">Your Facebook Webhook URL</label>
408 <div className="flex gap-2">
409 <input
410 readOnly
411 className="flex-1 bg-white/50 border border-blue-500/20 rounded-lg py-2 px-3 text-xs font-mono text-blue-700 outline-none"
412 value={`${window.location.origin}/wp-json/wpbot-automator/v1/facebook-lead-ads/${node.id.replace('node-', '')}`}
413 />
414 <button
415 onClick={() => navigator.clipboard.writeText(`${window.location.origin}/wp-json/wpbot-automator/v1/facebook-lead-ads/${node.id.replace('node-', '')}`)}
416 className="px-3 py-2 bg-blue-600 text-white rounded-lg text-xs font-bold hover:bg-blue-700 transition-colors"
417 >
418 Copy
419 </button>
420 </div>
421 <p className="text-[10px] text-muted-foreground italic">Use this URL in your Facebook Developer App configuration.</p>
422 </div>
423 )}
424
425 <div className="space-y-4">
426 <label className="flex items-center gap-2 text-xs font-bold text-muted-foreground uppercase tracking-wider">
427 <Settings className="w-4 h-4" />
428 Settings & Options
429 </label>
430
431 {selectedAction?.fields ? (
432 <div className="space-y-4">
433 {selectedAction.fields.map(field => {
434 const fieldLocked = field.isPro && !window.wpbotAutomator?.isProActive;
435 return (
436 <div key={field.id} className="space-y-1.5">
437 <label className="text-xs font-bold text-muted-foreground uppercase tracking-wider pl-1 pb-2 flex items-center gap-2">
438 {field.name}
439 {field.isPro && (
440 <span className="text-[10px] font-bold uppercase tracking-wide bg-amber-100 text-amber-700 border border-amber-300 rounded px-1.5 py-0.5">PRO</span>
441 )}
442 </label>
443 {fieldLocked ? (
444 <div className="space-y-1">
445 <div className="w-full flex items-center gap-2 bg-muted/40 border border-border rounded-lg py-2.5 px-3 cursor-not-allowed select-none">
446 <Lock className="w-4 h-4 text-amber-500 shrink-0" />
447 <span className="text-sm text-muted-foreground">{field.placeholder || 'Pro feature'}</span>
448 </div>
449 <p className="text-xs text-muted-foreground pl-1">
450 <a href="https://wpbot.pro" target="_blank" rel="noopener noreferrer" className="text-amber-600 underline hover:text-amber-800">Upgrade to Pro</a> to unlock this field.
451 </p>
452 </div>
453 ) : field.type === 'credential' ? (
454 <div className="space-y-3">
455 {!isAddingCredential ? (
456 <div className="flex gap-2 items-center">
457 <select
458 className="flex-1 bg-input border border-border rounded-lg py-2.5 px-3 text-sm outline-none focus:ring-2 focus:ring-primary/20 appearance-none cursor-pointer"
459 value={fieldValues[field.id] || ""}
460 onChange={(e) => handleFieldChange(field.id, e.target.value)}
461 disabled={isLoadingCredentials}
462 >
463 <option value="">{isLoadingCredentials ? "Loading credentials..." : "Select a credential..."}</option>
464 {credentials.map(c => (
465 <option key={c.id} value={c.id}>{c.name}</option>
466 ))}
467 </select>
468 <button
469 type="button"
470 onClick={() => setIsAddingCredential(true)}
471 className="px-4 py-2.5 bg-sidebar-accent text-foreground font-semibold rounded-lg text-sm hover:bg-sidebar-accent/80 transition-colors border shadow-sm shrink-0"
472 >
473 Set up credential
474 </button>
475 </div>
476 ) : (
477 <div className="p-4 bg-muted/30 border border-border rounded-xl space-y-4 shadow-sm">
478 <div className="flex items-center justify-between pb-2 border-b border-border">
479 <h4 className="text-sm font-bold flex items-center gap-2"><Lock className="w-4 h-4 text-primary" /> Add New Credential</h4>
480 <button type="button" onClick={() => setIsAddingCredential(false)} className="text-muted-foreground hover:text-foreground text-xs font-semibold px-2 py-1 rounded bg-black/5 dark:bg-white/5 transition-colors">Cancel</button>
481 </div>
482 <div className="space-y-4">
483 <div>
484 <label className="text-xs font-bold text-muted-foreground uppercase tracking-wider pl-1">Credential Name</label>
485 <input
486 type="text"
487 placeholder="e.g., My Personal Bot, Company Page"
488 className="w-full mt-1.5 bg-input border border-border rounded-lg py-2.5 px-3 text-sm focus:ring-2 focus:ring-primary/20 outline-none transition-all"
489 value={newCredential.name}
490 onChange={(e) => setNewCredential(prev => ({ ...prev, name: e.target.value }))}
491 />
492 </div>
493 {node.appData?.credentialConfig?.fields?.map(cField => (
494 <div key={cField.id}>
495 <label className="text-xs font-bold text-muted-foreground uppercase tracking-wider pl-1">{cField.name}</label>
496 {cField.type === 'textarea' ? (
497 <textarea
498 placeholder={cField.placeholder}
499 className="w-full mt-1.5 bg-input border border-border rounded-lg py-2.5 px-3 text-sm min-h-[80px] focus:ring-2 focus:ring-primary/20 outline-none transition-all"
500 value={newCredential.data[cField.id] || ""}
501 onChange={(e) => setNewCredential(prev => ({ ...prev, data: { ...prev.data, [cField.id]: e.target.value } }))}
502 />
503 ) : (
504 <input
505 type="text"
506 placeholder={cField.placeholder}
507 className="w-full mt-1.5 bg-input border border-border rounded-lg py-2.5 px-3 text-sm focus:ring-2 focus:ring-primary/20 outline-none transition-all"
508 value={newCredential.data[cField.id] || ""}
509 onChange={(e) => setNewCredential(prev => ({ ...prev, data: { ...prev.data, [cField.id]: e.target.value } }))}
510 />
511 )}
512 </div>
513 ))}
514 <button
515 type="button"
516 onClick={handleSaveCredential}
517 disabled={isSavingCredential}
518 className="w-full py-2.5 bg-primary text-primary-foreground font-bold rounded-lg text-sm hover:bg-primary/90 transition-colors shadow-sm disabled:opacity-50 flex items-center justify-center gap-2"
519 >
520 {isSavingCredential ? "Saving..." : "Save Credential"}
521 </button>
522 </div>
523 </div>
524 )}
525 </div>
526 ) : field.type === 'select' ? (
527 <select
528 className="w-full bg-input border border-border rounded-lg py-2.5 px-3 text-sm outline-none focus:ring-2 focus:ring-primary/20 transition-all appearance-none cursor-pointer disabled:opacity-60"
529 value={fieldValues[field.id] || ""}
530 onChange={(e) => handleFieldChange(field.id, e.target.value)}
531 disabled={loadingOptions[field.id] || (field.optionsUrl && !fieldOptions[field.id])}
532 >
533 <option value="">
534 {loadingOptions[field.id] || (field.optionsUrl && !fieldOptions[field.id])
535 ? "Loading…"
536 : (field.placeholder || "Select option")}
537 </option>
538 {(Array.isArray(field.options) ? field.options : Array.isArray(fieldOptions[field.id]) ? fieldOptions[field.id] : [])?.map(opt => (
539 <option key={opt.id} value={opt.id}>{opt.name}</option>
540 ))}
541 </select>
542 ) : field.type === 'textarea' ? (
543 <textarea
544 placeholder={field.placeholder}
545 className="w-full bg-input border border-border rounded-lg py-2.5 px-3 text-sm outline-none focus:ring-2 focus:ring-primary/20 transition-all min-h-[100px]"
546 value={fieldValues[field.id] || ""}
547 onChange={(e) => handleFieldChange(field.id, e.target.value)}
548 />
549 ) : field.type === 'checkbox' ? (
550 <label className="flex items-center gap-3 p-3 bg-input border border-border rounded-lg cursor-pointer hover:bg-sidebar-accent/50 transition-colors">
551 <input
552 type="checkbox"
553 className="w-4 h-4 rounded border-border text-primary focus:ring-primary/20"
554 checked={fieldValues[field.id] !== undefined ? fieldValues[field.id] : (field.defaultValue || false)}
555 onChange={(e) => handleFieldChange(field.id, e.target.checked)}
556 />
557 <span className="text-sm font-medium">{field.placeholder || "Enable this option"}</span>
558 </label>
559 ) : field.type === 'table_columns_mapper' ? (
560 <TableColumnsMapper field={field} />
561 ) : field.type === 'keyvalue' ? (
562 <KeyValueField
563 values={fieldValues[field.id]}
564 onChange={(val) => handleFieldChange(field.id, val)}
565 placeholder={field.placeholder}
566 />
567 ) : (
568 <input
569 type={field.type || "text"}
570 placeholder={field.placeholder}
571 className="w-full bg-input border border-border rounded-lg py-2.5 px-3 text-sm outline-none focus:ring-2 focus:ring-primary/20 transition-all"
572 value={fieldValues[field.id] || ""}
573 onChange={(e) => handleFieldChange(field.id, e.target.value)}
574 />
575 )}
576 {field.variables && field.variables.length > 0 && (
577 <div className="flex flex-wrap gap-1.5 mt-1.5">
578 {field.variables.map(v => (
579 <button
580 key={v.token}
581 type="button"
582 onClick={() => {
583 const current = fieldValues[field.id] || "";
584 handleFieldChange(field.id, current + v.token);
585 }}
586 className="inline-flex items-center gap-1 px-2 py-0.5 text-[10px] font-semibold bg-primary/10 text-primary border border-primary/20 rounded-md hover:bg-primary/20 transition-colors cursor-pointer"
587 title={`Click to insert ${v.token}`}
588 >
589 <span className="opacity-60">{'{ }'}</span> {v.label}
590 </button>
591 ))}
592 </div>
593 )}
594 </div>
595 );
596 })}
597 </div>
598 ) : (
599 <div className="p-8 border-2 border-dashed border-border rounded-2xl bg-card/30 flex flex-col items-center justify-center text-center space-y-3">
600 <div className="w-12 h-12 rounded-full bg-sidebar-accent flex items-center justify-center">
601 <Database className="w-6 h-6 text-muted-foreground opacity-30" />
602 </div>
603 <div>
604 <p className="text-sm font-medium text-foreground">Next phase details</p>
605 <p className="text-xs text-muted-foreground max-w-[200px] mt-1">Specific options for this action will appear here in the final implementation.</p>
606 </div>
607 </div>
608 )}
609 </div>
610
611
612 {testResponse && (
613 <div className={`p-4 rounded-xl border ${testResponse.success ? 'bg-emerald-500/5 border-emerald-500/10' : 'bg-rose-500/5 border-rose-500/10'} space-y-2 animate-in fade-in slide-in-from-top-2 duration-300`}>
614 <div className="flex items-center justify-between">
615 <span className={`text-xs font-bold uppercase tracking-wider ${testResponse.success ? 'text-emerald-600' : 'text-rose-600'}`}>
616 {testResponse.success ? 'Success' : 'Failed'}
617 {testResponse.code && ` (Status: ${testResponse.code})`}
618 </span>
619 <button
620 onClick={() => setTestResponse(null)}
621 className="text-[10px] text-muted-foreground hover:text-foreground underline"
622 >
623 Clear
624 </button>
625 </div>
626 {testResponse.body && (
627 <div className="max-h-[150px] overflow-y-auto bg-card/50 rounded-lg p-2 border border-border/50">
628 <pre className="text-[10px] font-mono text-muted-foreground whitespace-pre-wrap break-all">
629 {typeof testResponse.body === 'string' ? testResponse.body : JSON.stringify(testResponse.body, null, 2)}
630 </pre>
631 </div>
632 )}
633 {testResponse.message && !testResponse.body && (
634 <p className="text-xs text-muted-foreground">{testResponse.message}</p>
635 )}
636 </div>
637 )}
638
639 <div className="flex items-center gap-3 pt-4 border-t border-sidebar-border">
640 <button
641 onClick={() => setStep(1)}
642 className="flex-1 flex items-center justify-center gap-2 py-2.5 px-4 bg-sidebar-accent hover:bg-sidebar-accent/80 text-foreground rounded-xl text-sm font-semibold transition-all border border-transparent hover:border-border"
643 >
644 <ArrowLeft className="w-4 h-4" />
645 Change Action
646 </button>
647 {selectedAction?.id && (selectedAction.id.includes('webhook') || selectedAction.id.includes('api') || node.appData.id === 'google_sheets' || node.appData.id === 'openai' || node.appData.id === 'claude' || node.appData.id === 'gemini' || node.appData.id === 'mistral' || node.appData.id === 'groq' || node.appData.id === 'openrouter') && node.type !== 'trigger' && (
648 <button
649 onClick={handleTestRun}
650 disabled={isTesting}
651 className="flex-1 py-2.5 px-4 bg-secondary text-secondary-foreground hover:bg-secondary/80 rounded-xl text-sm font-semibold transition-all border border-border flex items-center justify-center gap-2 disabled:opacity-50"
652 >
653 {isTesting ? (
654 <div className="w-4 h-4 border-2 border-primary border-t-transparent rounded-full animate-spin" />
655 ) : (
656 <Activity className="w-4 h-4" />
657 )}
658 Test Run
659 </button>
660 )}
661 <button
662 onClick={handleSave}
663 className="flex-1 py-2.5 px-4 bg-primary text-white hover:bg-primary/90 rounded-xl text-sm font-bold transition-all shadow-lg shadow-primary/20 hover:scale-[1.02] active:scale-[0.98]"
664 >
665 Save Configuration
666 </button>
667 </div>
668 </div>
669 );
670
671 return (
672 <Modal
673 isOpen={isOpen}
674 onClose={onClose}
675 title={step === 1 ? `Configure ${node.title}` : selectedAction?.name}
676 maxWidth="max-w-xl"
677 >
678 {step === 1 ? renderStep1() : renderStep2()}
679 </Modal>
680 );
681 }
682