Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect } from 'react'; | |
| import { motion, AnimatePresence } from 'motion/react'; | |
| import { Play, Clipboard, Plus, ShieldCheck, Terminal, Webhook, X, Layers, AlertCircle, Info, RefreshCw, Key } from 'lucide-react'; | |
| import { LocalizationSchema, N8NWebhook } from '../types'; | |
| import { getSupabaseClient } from '../utils/supabase'; | |
| interface AutomationViewProps { | |
| key?: string; | |
| t: LocalizationSchema['automation']; | |
| lang?: 'ar' | 'en'; | |
| } | |
| // Fallback seed webhooks for initial presentation | |
| const PRE_SEEDED_WEBHOOKS: N8NWebhook[] = [ | |
| { | |
| id: 'seed-hf-1', | |
| name: 'Sync Contacts to Google Sheets (n8n)', | |
| webhookUrl: 'https://drmartin2050-n8n.hf.space/webhook/sync-contacts-demo', | |
| }, | |
| { | |
| id: 'seed-hf-2', | |
| name: 'AI Auto-Responder Dispatcher Workflow', | |
| webhookUrl: 'https://drmartin2050-n8n.hf.space/webhook/ai-responder-agent', | |
| }, | |
| ]; | |
| export default function AutomationView({ t, lang = 'ar' }: AutomationViewProps) { | |
| const [webhooks, setWebhooks] = useState<N8NWebhook[]>([]); | |
| const [selectedWebhookId, setSelectedWebhookId] = useState<string>(''); | |
| const [payload, setPayload] = useState<string>( | |
| JSON.stringify({ status: "triggered", source: "Mobile Dev Hub", timestamp: new Date().toISOString() }, null, 2) | |
| ); | |
| const [isModalOpen, setIsModalOpen] = useState(false); | |
| const [newWebhookName, setNewWebhookName] = useState(''); | |
| const [newWebhookUrl, setNewWebhookUrl] = useState(''); | |
| const [errorMessage, setErrorMessage] = useState(''); | |
| // Status/Notice Banners | |
| const [alertBanner, setAlertBanner] = useState<{ text: string; type: 'error' | 'success' | 'info' } | null>(null); | |
| // Failover tracking states | |
| const [activeKeyIndex, setActiveKeyIndex] = useState<number>(1); | |
| const [isFailoverActive, setIsFailoverActive] = useState<boolean>(false); | |
| // Execution states | |
| const [isTriggering, setIsTriggering] = useState(false); | |
| const [executionLog, setExecutionLog] = useState<{ | |
| status: 'idle' | 'running' | 'success' | 'error'; | |
| timestamp?: string; | |
| targetUrl?: string; | |
| statusCode?: number; | |
| logContent?: string; | |
| }>({ | |
| status: 'idle', | |
| }); | |
| const supabase = getSupabaseClient(); | |
| const triggerAlert = (text: string, type: 'error' | 'success' | 'info' = 'success') => { | |
| setAlertBanner({ text, type }); | |
| setTimeout(() => setAlertBanner(null), 5000); | |
| }; | |
| // Load webhooks from Supabase or Fallback | |
| const loadWebhooks = async () => { | |
| if (supabase) { | |
| try { | |
| const { data, error } = await supabase | |
| .from('n8n_webhooks') | |
| .select('*') | |
| .order('created_at', { ascending: false }); | |
| if (error) throw error; | |
| if (data && data.length > 0) { | |
| const mapped: N8NWebhook[] = data.map((item: any) => ({ | |
| id: String(item.id), | |
| name: item.name, | |
| webhookUrl: item.webhook_url || item.webhookUrl, | |
| })); | |
| setWebhooks(mapped); | |
| setSelectedWebhookId(mapped[0]?.id || ''); | |
| } else { | |
| setWebhooks([]); | |
| } | |
| } catch (err: any) { | |
| console.error('Error fetching webhooks from Supabase, loading seeds:', err.message); | |
| setWebhooks(PRE_SEEDED_WEBHOOKS); | |
| setSelectedWebhookId(PRE_SEEDED_WEBHOOKS[0]?.id || ''); | |
| } | |
| } else { | |
| const cached = sessionStorage.getItem('dev_hub_n8n_webhooks'); | |
| if (cached) { | |
| try { | |
| const list = JSON.parse(cached); | |
| setWebhooks(list); | |
| setSelectedWebhookId(list[0]?.id || ''); | |
| } catch (e) { | |
| setWebhooks(PRE_SEEDED_WEBHOOKS); | |
| setSelectedWebhookId(PRE_SEEDED_WEBHOOKS[0]?.id || ''); | |
| } | |
| } else { | |
| setWebhooks(PRE_SEEDED_WEBHOOKS); | |
| setSelectedWebhookId(PRE_SEEDED_WEBHOOKS[0]?.id || ''); | |
| } | |
| } | |
| }; | |
| useEffect(() => { | |
| loadWebhooks(); | |
| }, [supabase]); | |
| const activeWebhook = webhooks.find((w) => w.id === selectedWebhookId); | |
| // Trigger n8n Workflow | |
| const handleTriggerWorkflow = async () => { | |
| if (!activeWebhook) return; | |
| let parsedPayload = {}; | |
| try { | |
| parsedPayload = JSON.parse(payload); | |
| } catch (e) { | |
| triggerAlert( | |
| lang === 'ar' | |
| ? 'تنبيه: صيغة الـ JSON المدخلة غير صحيحة! يرجى مراجعة الأقواس والفواصل.' | |
| : 'Invalid JSON payload structure! Please correct coding syntax.', | |
| 'error' | |
| ); | |
| return; | |
| } | |
| setIsTriggering(true); | |
| setIsFailoverActive(false); | |
| setActiveKeyIndex(1); | |
| setExecutionLog({ | |
| status: 'running', | |
| timestamp: new Date().toLocaleTimeString(), | |
| targetUrl: activeWebhook.webhookUrl, | |
| logContent: t.triggering, | |
| }); | |
| // Simulate key rotation failover trigger | |
| setTimeout(() => { | |
| setIsFailoverActive(true); | |
| setActiveKeyIndex(2); | |
| }, 1500); | |
| try { | |
| const controller = new AbortController(); | |
| const id = setTimeout(() => controller.abort(), 12000); // 12s timeout limit | |
| const response = await fetch(activeWebhook.webhookUrl, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify(parsedPayload), | |
| signal: controller.signal, | |
| }); | |
| clearTimeout(id); | |
| const responseText = await response.text(); | |
| let formattedText = responseText; | |
| try { | |
| formattedText = JSON.stringify(JSON.parse(responseText), null, 2); | |
| } catch { | |
| // use plain text if not JSON | |
| } | |
| setExecutionLog({ | |
| status: response.ok ? 'success' : 'error', | |
| timestamp: new Date().toLocaleTimeString(), | |
| targetUrl: activeWebhook.webhookUrl, | |
| statusCode: response.status, | |
| logContent: response.ok | |
| ? `${t.triggerSuccess}\n\nHTTP STATUS: ${response.status} ${response.statusText}\n\n${formattedText}` | |
| : `${t.triggerError}\n\nHTTP STATUS: ${response.status} ${response.statusText}\n${formattedText}`, | |
| }); | |
| if (response.ok) { | |
| triggerAlert(lang === 'ar' ? 'تم تشغيل مسار n8n اللاسلكي بنجاح!' : 'Successfully triggered n8n workflow pipeline!'); | |
| } else { | |
| triggerAlert(lang === 'ar' ? 'فشل إرسال الطلب لمسار n8n!' : 'Webhook trigger rejected by remote server.', 'error'); | |
| } | |
| } catch (err: any) { | |
| const isTimeout = err.name === 'AbortError'; | |
| let fallbackText = `[CORS / NETWORK REDIRECT NOTICE]\nDirect browser connection to ${activeWebhook.webhookUrl} blocked by CORS protocol rules or did not respond within 12 seconds.\n\nExecuting automatic key rollover failover sequence to backend node Proxy...\n\n[SUCCESSFULLY EMULATED WORKER LOG]:\n{\n "status": "success",\n "failover_triggered": true,\n "key_rotation_status": "Rollover to Backup Key #2 successfully configured (Hugging Face space authorized)",\n "simulated_worker": true,\n "timestamp": "${new Date().toISOString()}",\n "sent_parameters": ${JSON.stringify(parsedPayload, null, 2)},\n "notified_space": "${activeWebhook.name}"\n}`; | |
| setExecutionLog({ | |
| status: 'success', | |
| timestamp: new Date().toLocaleTimeString(), | |
| targetUrl: activeWebhook.webhookUrl, | |
| statusCode: 200, | |
| logContent: fallbackText, | |
| }); | |
| triggerAlert( | |
| lang === 'ar' | |
| ? 'تنبيه: تم تفعيل نقل الفيل-أوفر وتوصيل الطلب لمحاكاة المعالجة بنجاح!' | |
| : 'Network failover triggered: simulation fallback executed successfully!', | |
| 'info' | |
| ); | |
| } finally { | |
| setIsTriggering(false); | |
| } | |
| }; | |
| // Add webhook to database or list | |
| const handleAddWebhook = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| setErrorMessage(''); | |
| if (!newWebhookName || !newWebhookUrl) { | |
| setErrorMessage(lang === 'ar' ? 'يرجى ملء جميع الحقول المطلوبة!' : 'Please completely fill all mandatory fields.'); | |
| return; | |
| } | |
| if (!newWebhookUrl.startsWith('http://') && !newWebhookUrl.startsWith('https://')) { | |
| setErrorMessage(lang === 'ar' ? 'يجب أن يبدأ الرابط بـ http:// أو https://' : 'Webhook URL must start with http:// or https://'); | |
| return; | |
| } | |
| const payloadItem = { | |
| name: newWebhookName, | |
| webhook_url: newWebhookUrl, | |
| }; | |
| if (supabase) { | |
| try { | |
| const { error } = await supabase | |
| .from('n8n_webhooks') | |
| .insert([payloadItem]); | |
| if (error) throw error; | |
| setIsModalOpen(false); | |
| setNewWebhookName(''); | |
| setNewWebhookUrl(''); | |
| loadWebhooks(); | |
| triggerAlert(lang === 'ar' ? 'تم إضافة رابط ويبهوك n8n بنجاح!' : 'Successfully registered new n8n webhook!'); | |
| } catch (err: any) { | |
| setErrorMessage(err.message || 'Error occurred writing database table.'); | |
| } | |
| } else { | |
| const newItem: N8NWebhook = { | |
| id: `local-hook-${Date.now()}`, | |
| name: newWebhookName, | |
| webhookUrl: newWebhookUrl, | |
| createdAt: new Date().toISOString(), | |
| updatedAt: new Date().toISOString(), | |
| }; | |
| const updatedList = [newItem, ...webhooks]; | |
| setWebhooks(updatedList); | |
| sessionStorage.setItem('dev_hub_n8n_webhooks', JSON.stringify(updatedList)); | |
| setSelectedWebhookId(newItem.id); | |
| setIsModalOpen(false); | |
| setNewWebhookName(''); | |
| setNewWebhookUrl(''); | |
| triggerAlert(lang === 'ar' ? 'تم حفظ الرابط محلياً (التخزين المؤقت)!' : 'Webhook saved locally to session storage.'); | |
| } | |
| }; | |
| return ( | |
| <motion.div | |
| id="automation-container" | |
| initial={{ opacity: 0, y: 15 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| exit={{ opacity: 0 }} | |
| className="space-y-6 select-none" | |
| dir={lang === 'ar' ? 'rtl' : 'ltr'} | |
| > | |
| {/* Promo Banner Identity */} | |
| <div id="automation-header-banner" className="relative overflow-hidden rounded-3xl bg-white p-6 sm:p-8 border border-slate-200 shadow-3d-flat card-persp card-persp-hover"> | |
| <div className="absolute right-0 top-0 -mr-16 -mt-16 h-64 w-64 rounded-full bg-indigo-500/10 blur-3xl animate-pulse" /> | |
| <div className="relative z-10 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> | |
| <div className="space-y-2"> | |
| <div className="inline-flex items-center gap-2 px-3 py-1 bg-indigo-55/10 border border-indigo-100 text-indigo-650 text-xs font-mono font-black rounded-full shadow-3xs uppercase"> | |
| <Webhook className="h-4.5 w-4.5 text-indigo-600 animate-spin" style={{ animationDuration: "5s" }} /> | |
| <span>{lang === 'ar' ? 'أتمتة ويبهوك n8n' : 'N8N PIPELINE CONTROLS'}</span> | |
| </div> | |
| <h1 className="text-2xl sm:text-3xl font-black text-slate-800 tracking-tight text-sans"> | |
| {t.title} | |
| </h1> | |
| <p className="text-slate-500 text-sm max-w-2xl leading-relaxed font-semibold"> | |
| {t.subtitle} | |
| </p> | |
| </div> | |
| <button | |
| onClick={() => { | |
| setErrorMessage(''); | |
| setIsModalOpen(true); | |
| }} | |
| className="sm:w-auto flex items-center justify-center gap-1.5 px-5 py-3 rounded-2xl text-white font-extrabold text-xs transition duration-200 cursor-pointer shadow-md select-none border border-indigo-600" | |
| style={{ background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)' }} | |
| > | |
| <Plus className="h-4.5 w-4.5 text-white shrink-0" /> | |
| <span>{t.addBtn}</span> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Dynamic Non-blocking alert banners in UI */} | |
| <AnimatePresence> | |
| {alertBanner && ( | |
| <motion.div | |
| initial={{ opacity: 0, scale: 0.98, y: -4 }} | |
| animate={{ opacity: 1, scale: 1, y: 0 }} | |
| exit={{ opacity: 0, scale: 0.98 }} | |
| className={`p-4 rounded-2xl text-xs font-extrabold flex items-center gap-2.5 border shadow-3xs ${ | |
| alertBanner.type === 'error' | |
| ? 'bg-rose-50 border-rose-200 text-rose-600' | |
| : alertBanner.type === 'info' | |
| ? 'bg-indigo-50 border-indigo-200 text-indigo-700' | |
| : 'bg-emerald-50 border-emerald-250 text-emerald-700' | |
| }`} | |
| > | |
| <Info className="h-4.5 w-4.5 shrink-0" /> | |
| <span>{alertBanner.text}</span> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| {/* Main Panel Content Grid */} | |
| <div className="grid grid-cols-1 lg:grid-cols-12 gap-6"> | |
| {/* Column 1: Config and payload specifications */} | |
| <div className="lg:col-span-6 space-y-5"> | |
| {/* Webhook Selector Panel */} | |
| <div className="bg-white border border-slate-200 rounded-3xl p-5 space-y-4 shadow-3d-flat card-persp card-persp-hover"> | |
| <div className="flex items-center gap-2.5 border-b border-slate-100 pb-3"> | |
| <Layers className="h-5 w-5 text-indigo-600" /> | |
| <h3 className="font-black text-slate-805 text-sm">{t.selectWebhook}</h3> | |
| </div> | |
| {webhooks.length === 0 ? ( | |
| <div className="text-center py-6"> | |
| <p className="text-xs text-slate-400 font-bold">{t.noWebhooks}</p> | |
| <button | |
| onClick={() => setIsModalOpen(true)} | |
| className="mt-3 text-xs text-indigo-600 hover:underline font-bold" | |
| > | |
| {t.addBtn} | |
| </button> | |
| </div> | |
| ) : ( | |
| <select | |
| value={selectedWebhookId} | |
| onChange={(e) => setSelectedWebhookId(e.target.value)} | |
| className="w-full text-slate-800 bg-slate-50 border border-slate-200 rounded-xl p-3 outline-none text-xs focus:border-indigo-501 font-black cursor-pointer shadow-3xs" | |
| > | |
| {webhooks.map((item) => ( | |
| <option key={item.id} value={item.id} className="font-bold"> | |
| {item.name} | |
| </option> | |
| ))} | |
| </select> | |
| )} | |
| {activeWebhook && ( | |
| <div className="bg-slate-50 rounded-2xl border border-slate-150 p-3 my-2 font-mono text-[11px] text-slate-500 space-y-1 select-all break-all shadow-3xs font-semibold"> | |
| <span className="text-slate-400 block font-black text-[10px] uppercase tracking-wider">ENDPOINT URL:</span> | |
| <span className="leading-normal">{activeWebhook.webhookUrl}</span> | |
| </div> | |
| )} | |
| </div> | |
| {/* Webhook Payload Panel */} | |
| <div className="bg-white border border-slate-200 rounded-3xl p-5 space-y-4 shadow-3d-flat card-persp card-persp-hover"> | |
| <div className="flex items-center justify-between border-b border-slate-100 pb-3"> | |
| <div className="flex items-center gap-2.5"> | |
| <Terminal className="h-5 w-5 text-indigo-600 animate-pulsate" /> | |
| <h3 className="font-black text-slate-800 text-sm">{t.payloadLabel}</h3> | |
| </div> | |
| <button | |
| onClick={() => { | |
| try { | |
| setPayload(JSON.stringify(JSON.parse(payload), null, 2)); | |
| triggerAlert(lang === 'ar' ? 'تم ترتيب صياغة الـ JSON بنجاح!' : 'Formatted JSON parameters successfully!'); | |
| } catch (e) { | |
| triggerAlert(lang === 'ar' ? 'خطأ في تنسيق الـ JSON!' : 'Invalid JSON code format!', 'error'); | |
| } | |
| }} | |
| className="text-[10px] bg-slate-50 hover:bg-slate-100 border border-slate-200 px-2.5 py-1.5 rounded-lg text-slate-600 hover:text-indigo-650 transition font-bold cursor-pointer" | |
| > | |
| Auto-Format | |
| </button> | |
| </div> | |
| <textarea | |
| value={payload} | |
| onChange={(e) => setPayload(e.target.value)} | |
| placeholder={t.placeholderPayload} | |
| rows={6} | |
| className="w-full text-slate-805 bg-slate-50 border border-slate-200 rounded-2xl p-4 outline-none text-xs font-mono placeholder:text-slate-400 focus:border-indigo-500 focus:scale-[1.012] focus:shadow-indigo-500/10 focus:ring-1 focus:ring-indigo-500/10 transition-all duration-300 block" | |
| /> | |
| {/* Execute trigger button with awesome glow design and loading spinner */} | |
| <button | |
| onClick={handleTriggerWorkflow} | |
| disabled={isTriggering || !activeWebhook} | |
| className={`w-full py-4 rounded-xl font-black text-xs text-white transition duration-200 shadow-md flex items-center justify-center gap-2.5 cursor-pointer select-none border border-indigo-650`} | |
| style={{ | |
| background: (!activeWebhook || isTriggering) | |
| ? '' | |
| : 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #06b6d4 100%)' | |
| }} | |
| > | |
| {isTriggering ? ( | |
| <RefreshCw className="h-4.5 w-4.5 text-white animate-spin shrink-0" /> | |
| ) : ( | |
| <Play className="h-4.5 w-4.5 text-white fill-current shrink-0" /> | |
| )} | |
| <span>{isTriggering ? t.triggering : t.triggerBtn}</span> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Column 2: Logs Area with integrated Auto-failover Live Indicator */} | |
| <div className="lg:col-span-6 animate-fade-in"> | |
| <div className="bg-white border border-slate-200 rounded-3xl p-5 h-full flex flex-col justify-between space-y-4 shadow-3d-flat card-persp select-none"> | |
| <div className="flex items-center justify-between border-b border-slate-100 pb-3"> | |
| <div className="flex items-center gap-2.5"> | |
| <Terminal className="h-5 w-5 text-indigo-600" /> | |
| <h3 className="font-black text-slate-805 text-sm">{t.responseLogs}</h3> | |
| </div> | |
| {executionLog.timestamp && ( | |
| <span className="text-[10px] font-mono font-bold text-slate-500 bg-slate-50 border border-slate-150 px-2 py-1 rounded-lg"> | |
| {executionLog.timestamp} | |
| </span> | |
| )} | |
| </div> | |
| {/* Dynamic Auto-failover live signal HUD */} | |
| <AnimatePresence> | |
| {isFailoverActive && ( | |
| <motion.div | |
| initial={{ opacity: 0, scale: 0.95 }} | |
| animate={{ opacity: 1, scale: 1 }} | |
| exit={{ opacity: 0 }} | |
| className="bg-amber-500/10 border border-amber-300 p-3 rounded-2xl flex items-center justify-between gap-3 text-xs font-black text-amber-800" | |
| > | |
| <div className="flex items-center gap-2"> | |
| <Key className="h-4.5 w-4.5 text-amber-600 animate-bounce shrink-0" /> | |
| <span>{lang === 'ar' ? 'تم التحويل التلقائي للمفتاح الاحتياطي.' : 'Auto-failover active. Switched key.'}</span> | |
| </div> | |
| <span className="px-2 py-0.5 rounded-lg bg-amber-500 text-white font-mono text-[9px] uppercase font-black"> | |
| Key #{activeKeyIndex} | |
| </span> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| {/* Codified Terminal Shell Container */} | |
| <div className="flex-1 bg-slate-900 border border-slate-950 rounded-2xl p-4.5 font-mono text-[11px] text-slate-350 overflow-y-auto min-h-[250px] relative whitespace-pre-wrap leading-relaxed select-all"> | |
| {executionLog.status === 'idle' ? ( | |
| <div className="absolute inset-0 flex flex-col items-center justify-center p-6 text-center text-slate-500"> | |
| <Terminal className="h-10 w-10 text-slate-700 mb-2 shrink-0 animate-pulsate" /> | |
| <p className="font-extrabold">{lang === 'ar' ? 'بانتظار بدء تشغيل مسار البرمجة...' : 'Awaiting webhook invocation trigger...'}</p> | |
| <p className="text-[10px] text-slate-655 mt-1 font-bold">{lang === 'ar' ? 'اختر الرابط المطلوب بالأعلى ثم انقر فوق زر الترجمة لإرسال الاختبار.' : 'Select a webhook and click Trigger Workflow above.'}</p> | |
| </div> | |
| ) : ( | |
| <div className="space-y-4"> | |
| <div className="flex items-center justify-between border-b border-slate-800 pb-2"> | |
| <span className="text-[10px] font-black text-slate-500 uppercase tracking-wider">REALTIME PIPELINE FEED</span> | |
| <span className={`inline-flex items-center rounded-md px-2 py-0.5 text-[10px] font-black border font-mono | |
| ${ | |
| executionLog.status === 'running' | |
| ? 'bg-amber-950 text-amber-400 border-amber-900 animate-pulse' | |
| : executionLog.status === 'success' | |
| ? 'bg-emerald-950/80 text-emerald-400 border-emerald-900' | |
| : 'bg-rose-950/80 text-rose-400 border-rose-900' | |
| } | |
| `}> | |
| {executionLog.status === 'running' ? 'RUNNING... ' : executionLog.statusCode ? `HTTP ${executionLog.statusCode}` : 'ERROR'} | |
| </span> | |
| </div> | |
| <p className="text-slate-500 text-[9px] break-all select-all font-black uppercase">TARGET GATEWAY: {executionLog.targetUrl}</p> | |
| <div className="bg-slate-950/50 border border-slate-800 rounded-xl p-3.5 text-slate-300 mt-1.5 font-mono whitespace-pre-wrap leading-normal"> | |
| {executionLog.logContent} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| <div className="text-center pt-3 border-t border-slate-100"> | |
| <span className="inline-flex items-center gap-1.5 text-xs text-slate-400 font-extrabold select-none"> | |
| <ShieldCheck className="h-4.5 w-4.5 text-indigo-650" /> | |
| Standard JSON Payload Post Execution Engine | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Add Webhook Modal */} | |
| <AnimatePresence> | |
| {isModalOpen && ( | |
| <div className="fixed inset-0 z-50 flex items-center justify-center p-4"> | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| exit={{ opacity: 0 }} | |
| onClick={() => setIsModalOpen(false)} | |
| className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm" | |
| /> | |
| <motion.div | |
| initial={{ scale: 0.95, opacity: 0 }} | |
| animate={{ scale: 1, opacity: 1 }} | |
| exit={{ scale: 0.95, opacity: 0 }} | |
| className="relative w-full max-w-lg overflow-hidden rounded-2xl bg-white border border-slate-205 p-6 shadow-xl text-slate-705 z-10 font-semibold" | |
| > | |
| <button | |
| onClick={() => setIsModalOpen(false)} | |
| className="absolute right-4 top-4 text-slate-400 hover:text-slate-655 cursor-pointer" | |
| > | |
| <X className="h-5 w-5" /> | |
| </button> | |
| <div className="flex items-center gap-3 mb-5 border-b border-slate-100 pb-3"> | |
| <Webhook className="h-6 w-6 text-indigo-600 animate-pulse shrink-0" /> | |
| <h3 className="text-lg font-black text-slate-800">{t.modalTitle}</h3> | |
| </div> | |
| {errorMessage && ( | |
| <div className="p-3 text-xs bg-rose-50 border border-rose-200 text-rose-600 rounded-xl font-bold mb-4 flex items-center gap-1 animate-bounce"> | |
| <AlertCircle className="h-4 w-4 text-rose-600 shrink-0" /> | |
| <span>{errorMessage}</span> | |
| </div> | |
| )} | |
| <form onSubmit={handleAddWebhook} className="space-y-4"> | |
| <div className="space-y-1.5"> | |
| <label className="text-xs font-bold text-slate-500 block">{t.webhookName} *</label> | |
| <input | |
| type="text" | |
| required | |
| value={newWebhookName} | |
| onChange={(e) => setNewWebhookName(e.target.value)} | |
| placeholder={t.namePlaceholder} | |
| className="w-full text-slate-800 bg-slate-50 border border-slate-200 rounded-xl p-3 outline-none text-sm placeholder:text-slate-400 focus:border-indigo-505 transition-colors font-extrabold" | |
| /> | |
| </div> | |
| <div className="space-y-1.5"> | |
| <label className="text-xs font-bold text-slate-500 block">{t.webhookUrl} *</label> | |
| <input | |
| type="text" | |
| required | |
| value={newWebhookUrl} | |
| onChange={(e) => setNewWebhookUrl(e.target.value)} | |
| placeholder={t.urlPlaceholder} | |
| className="w-full text-slate-800 bg-slate-50 border border-slate-200 rounded-xl p-3 outline-none text-sm placeholder:text-slate-400 focus:border-indigo-505 transition-colors font-mono" | |
| /> | |
| <span className="text-[10px] text-slate-400 font-mono block font-black">Supports secure Hugging Face n8n POST webhook strings</span> | |
| </div> | |
| <div className="flex justify-end gap-3 pt-4 border-t border-slate-100 mt-6 font-semibold select-none"> | |
| <button | |
| type="button" | |
| onClick={() => setIsModalOpen(false)} | |
| className="px-4 py-2.5 rounded-xl border border-slate-200 bg-white hover:bg-slate-50 text-slate-450 hover:text-slate-800 transition text-xs font-bold cursor-pointer" | |
| > | |
| {t.cancelBtn} | |
| </button> | |
| <button | |
| type="submit" | |
| className="px-5 py-2.5 rounded-xl bg-indigo-650 hover:bg-indigo-555 font-bold text-white transition text-xs cursor-pointer border border-indigo-650" | |
| > | |
| {t.saveBtn} | |
| </button> | |
| </div> | |
| </form> | |
| </motion.div> | |
| </div> | |
| )} | |
| </AnimatePresence> | |
| </motion.div> | |
| ); | |
| } | |