Download src/pages/Settings.tsx from sitcod3/modul-e: direct link, hf CLI and curl.
- Browser
- Download file 20.1 kB
-
https://huggingface.co/spaces/sitcod3/modul-e/resolve/main/src/pages/Settings.tsx
- Command line
-
hf download hf://spaces/sitcod3/modul-e/src/pages/Settings.tsx
-
curl -L -o Settings.tsx https://huggingface.co/spaces/sitcod3/modul-e/resolve/main/src/pages/Settings.tsx
20.1 kB
| import { | |
| KeyRound, | |
| Plus, | |
| Trash2, | |
| Copy, | |
| CheckCircle2, | |
| AlertCircle, | |
| ShieldCheck, | |
| Sparkles, | |
| Globe, | |
| Clock, | |
| Sliders, | |
| Terminal, | |
| Activity, | |
| Database | |
| } from "lucide-react"; | |
| import { useState, useEffect } from "react"; | |
| import { useApiKey } from "../context/ApiKeyContext"; | |
| import { apiClient } from "../lib/apiClient"; | |
| import FormDrawer from "../components/FormDrawer"; | |
| import ConfirmModal from "../components/ConfirmModal"; | |
| import toast from "react-hot-toast"; | |
| import { supabase, isSupabaseConfigured, supabaseProjectRef, supabaseDashboardUrl } from "../lib/supabaseClient"; | |
| export default function Settings() { | |
| const { apiKeys, activeKeyId, setActiveKeyId, addApiKey, removeApiKey, renameApiKey } = useApiKey(); | |
| const [activeTab, setActiveTab] = useState<"keys" | "preferences" | "sandbox" | "database">("keys"); | |
| const [isAddKeyOpen, setIsAddKeyOpen] = useState(false); | |
| const [keyToDelete, setKeyToDelete] = useState<string | null>(null); | |
| const [testResult, setTestResult] = useState<{ status: "idle" | "testing" | "success" | "error"; message?: string; latency?: number }>({ status: "idle" }); | |
| const [supabaseStatus, setSupabaseStatus] = useState<"testing" | "connected" | "error" | "unconfigured">("testing"); | |
| useEffect(() => { | |
| async function checkSupabase() { | |
| if (!isSupabaseConfigured() || !supabase) { | |
| setSupabaseStatus("unconfigured"); | |
| return; | |
| } | |
| try { | |
| // Simple ping to check if credentials are valid | |
| // Using auth.getSession() as a lightweight check | |
| const { error } = await supabase.auth.getSession(); | |
| if (error) throw error; | |
| setSupabaseStatus("connected"); | |
| } catch (err) { | |
| console.error("Supabase connection error:", err); | |
| setSupabaseStatus("error"); | |
| } | |
| } | |
| if (activeTab === "database") { | |
| checkSupabase(); | |
| } | |
| }, [activeTab]); | |
| const testActiveConnection = async () => { | |
| setTestResult({ status: "testing" }); | |
| const startTime = Date.now(); | |
| try { | |
| const res = await apiClient.get("/campaigns"); | |
| const latency = Date.now() - startTime; | |
| setTestResult({ | |
| status: "success", | |
| message: `Connected successfully. Response latency: ${latency}ms. Found ${res.data?.length || 0} campaigns.`, | |
| latency, | |
| }); | |
| toast.success("API Key verified successfully"); | |
| } catch (err: any) { | |
| setTestResult({ | |
| status: "error", | |
| message: err.response?.data?.error || "Failed to reach BRIDGEModulE API gateway", | |
| }); | |
| toast.error("API connection test failed"); | |
| } | |
| }; | |
| return ( | |
| <div className="p-6 max-w-7xl mx-auto space-y-6"> | |
| {/* Header */} | |
| <div> | |
| <h1 className="text-2xl font-bold tracking-tight text-zinc-100 flex items-center gap-2.5"> | |
| <KeyRound className="w-6 h-6 text-indigo-400" /> | |
| Settings & API Credentials | |
| </h1> | |
| <p className="text-zinc-400 text-sm mt-1"> | |
| Manage BRIDGEModulE API keys, system preferences, and test live gateway endpoints. | |
| </p> | |
| </div> | |
| {/* Tabs */} | |
| <div className="border-b border-zinc-800 flex items-center gap-2 text-sm font-medium"> | |
| <button | |
| onClick={() => setActiveTab("keys")} | |
| className={`px-4 py-2.5 border-b-2 font-medium text-xs flex items-center gap-2 transition-colors ${ | |
| activeTab === "keys" | |
| ? "border-indigo-500 text-indigo-400 bg-indigo-500/5" | |
| : "border-transparent text-zinc-400 hover:text-zinc-200" | |
| }`} | |
| > | |
| <KeyRound className="w-4 h-4" /> API Key Manager ({apiKeys.length}) | |
| </button> | |
| <button | |
| onClick={() => setActiveTab("preferences")} | |
| className={`px-4 py-2.5 border-b-2 font-medium text-xs flex items-center gap-2 transition-colors ${ | |
| activeTab === "preferences" | |
| ? "border-indigo-500 text-indigo-400 bg-indigo-500/5" | |
| : "border-transparent text-zinc-400 hover:text-zinc-200" | |
| }`} | |
| > | |
| <Sliders className="w-4 h-4" /> System Preferences | |
| </button> | |
| <button | |
| onClick={() => setActiveTab("sandbox")} | |
| className={`px-4 py-2.5 border-b-2 font-medium text-xs flex items-center gap-2 transition-colors ${ | |
| activeTab === "sandbox" | |
| ? "border-indigo-500 text-indigo-400 bg-indigo-500/5" | |
| : "border-transparent text-zinc-400 hover:text-zinc-200" | |
| }`} | |
| > | |
| <Terminal className="w-4 h-4" /> API Sandbox & Architecture | |
| </button> | |
| <button | |
| onClick={() => setActiveTab("database")} | |
| className={`px-4 py-2.5 border-b-2 font-medium text-xs flex items-center gap-2 transition-colors ${ | |
| activeTab === "database" | |
| ? "border-emerald-500 text-emerald-400 bg-emerald-500/5" | |
| : "border-transparent text-zinc-400 hover:text-zinc-200" | |
| }`} | |
| > | |
| <Database className="w-4 h-4" /> Supabase Connection | |
| </button> | |
| </div> | |
| {/* TAB 1: API KEYS */} | |
| {activeTab === "keys" && ( | |
| <div className="space-y-6 max-w-4xl"> | |
| <div className="bg-zinc-900 border border-zinc-800 rounded-xl p-5 shadow-sm space-y-4"> | |
| <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3"> | |
| <div> | |
| <h3 className="text-base font-semibold text-zinc-100">Configured BRIDGEModulE API Keys</h3> | |
| <p className="text-xs text-zinc-400 mt-0.5"> | |
| Store multiple keys for distinct environments or client workspaces. | |
| </p> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <button | |
| onClick={testActiveConnection} | |
| disabled={testResult.status === "testing"} | |
| className="px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-200 text-xs font-medium rounded-lg transition-colors flex items-center gap-1.5 border border-zinc-700" | |
| > | |
| <Activity className="w-3.5 h-3.5 text-emerald-400" /> | |
| {testResult.status === "testing" ? "Testing..." : "Test Connection"} | |
| </button> | |
| <button | |
| onClick={() => setIsAddKeyOpen(true)} | |
| className="px-3.5 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-medium rounded-lg transition-colors flex items-center gap-1.5" | |
| > | |
| <Plus className="w-3.5 h-3.5" /> Add API Key | |
| </button> | |
| </div> | |
| </div> | |
| {/* Test Connection Banner */} | |
| {testResult.status === "success" && ( | |
| <div className="p-3 bg-emerald-500/10 border border-emerald-500/20 rounded-lg flex items-center gap-2.5 text-xs text-emerald-400"> | |
| <CheckCircle2 className="w-4 h-4 shrink-0" /> | |
| <span>{testResult.message}</span> | |
| </div> | |
| )} | |
| {testResult.status === "error" && ( | |
| <div className="p-3 bg-rose-500/10 border border-rose-500/20 rounded-lg flex items-center gap-2.5 text-xs text-rose-400"> | |
| <AlertCircle className="w-4 h-4 shrink-0" /> | |
| <span>{testResult.message}</span> | |
| </div> | |
| )} | |
| {/* Keys List */} | |
| <div className="space-y-3 pt-2"> | |
| {apiKeys.map((k) => { | |
| const isActive = k.id === activeKeyId; | |
| const maskedKey = k.key.length > 8 | |
| ? `${k.key.substring(0, 4)}••••••••••••••••${k.key.substring(k.key.length - 4)}` | |
| : "••••••••••••"; | |
| return ( | |
| <div | |
| key={k.id} | |
| className={`p-4 rounded-xl border transition-all flex flex-col sm:flex-row sm:items-center justify-between gap-4 ${ | |
| isActive | |
| ? "bg-indigo-950/20 border-indigo-500/40 shadow-sm" | |
| : "bg-zinc-950/60 border-zinc-800/80 hover:border-zinc-700" | |
| }`} | |
| > | |
| <div> | |
| <div className="flex items-center gap-2"> | |
| <span className="font-semibold text-xs text-zinc-100">{k.name}</span> | |
| {isActive && ( | |
| <span className="text-[10px] font-bold uppercase tracking-wider bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-2 py-0.5 rounded-full"> | |
| Active Key | |
| </span> | |
| )} | |
| </div> | |
| <div className="font-mono text-xs text-zinc-400 mt-1 flex items-center gap-2"> | |
| <span>{maskedKey}</span> | |
| <button | |
| onClick={() => { | |
| navigator.clipboard.writeText(k.key); | |
| toast.success("API key copied to clipboard"); | |
| }} | |
| className="text-zinc-500 hover:text-zinc-300 p-1" | |
| > | |
| <Copy className="w-3.5 h-3.5" /> | |
| </button> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| {!isActive && ( | |
| <button | |
| onClick={() => { | |
| setActiveKeyId(k.id); | |
| toast.success(`Switched active key to "${k.name}"`); | |
| }} | |
| className="px-3 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 text-xs font-medium rounded-md transition-colors" | |
| > | |
| Set as Active | |
| </button> | |
| )} | |
| <button | |
| onClick={() => setKeyToDelete(k.id)} | |
| className="p-1.5 text-zinc-500 hover:text-rose-400 hover:bg-rose-500/10 rounded-md transition-colors" | |
| > | |
| <Trash2 className="w-4 h-4" /> | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| {apiKeys.length === 0 && ( | |
| <div className="p-8 text-center text-xs text-zinc-500 bg-zinc-950/40 rounded-xl border border-dashed border-zinc-800"> | |
| No custom API keys saved. The app is currently operating in high-fidelity mock database mode. | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* TAB 2: PREFERENCES */} | |
| {activeTab === "preferences" && ( | |
| <div className="space-y-6 max-w-2xl bg-zinc-900 border border-zinc-800 rounded-xl p-6"> | |
| <h3 className="text-base font-semibold text-zinc-100 mb-4">Workspace Outreach Defaults</h3> | |
| <div className="space-y-4 text-xs"> | |
| <div> | |
| <label className="block font-medium text-zinc-300 mb-1">Default Sending Timezone</label> | |
| <select | |
| defaultValue="America/New_York" | |
| className="w-full bg-zinc-950 border border-zinc-800 rounded-lg p-2.5 text-zinc-100" | |
| > | |
| <option value="America/New_York">America/New_York (EST)</option> | |
| <option value="America/Chicago">America/Chicago (CST)</option> | |
| <option value="America/Los_Angeles">America/Los_Angeles (PST)</option> | |
| <option value="Europe/London">Europe/London (GMT)</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label className="block font-medium text-zinc-300 mb-1">Global Mailbox Rate Limiting</label> | |
| <input | |
| type="number" | |
| defaultValue="50" | |
| className="w-full bg-zinc-950 border border-zinc-800 rounded-lg p-2.5 text-zinc-100" | |
| /> | |
| <span className="text-[11px] text-zinc-500 mt-1 block">Maximum outbound emails per mailbox / 24 hours</span> | |
| </div> | |
| <div className="pt-4 flex justify-end"> | |
| <button | |
| type="button" | |
| onClick={() => toast.success("Preferences updated")} | |
| className="px-4 py-2 bg-indigo-600 text-white rounded-lg font-medium" | |
| > | |
| Save Preferences | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* TAB 3: SANDBOX */} | |
| {activeTab === "sandbox" && ( | |
| <div className="space-y-4 max-w-4xl bg-zinc-900 border border-zinc-800 rounded-xl p-6"> | |
| <h3 className="text-base font-semibold text-zinc-100 flex items-center gap-2"> | |
| <Terminal className="w-5 h-5 text-indigo-400" /> Backend Gateway Architecture | |
| </h3> | |
| <p className="text-xs text-zinc-400 leading-relaxed"> | |
| All requests from the frontend client route securely through the Node.js/Express proxy (`/api/*`). | |
| The proxy attaches the active API key as a query parameter (`?api_key=...`) to the core REST API endpoint (`https://server.smartlead.ai/api/v1`). | |
| </p> | |
| <div className="p-4 bg-zinc-950 border border-zinc-800 rounded-lg font-mono text-xs text-zinc-300 space-y-2"> | |
| <span className="text-emerald-400"># Example Proxy Request</span><br /> | |
| GET /api/campaigns → https://server.smartlead.ai/api/v1/campaigns?api_key={KEY}<br /> | |
| POST /api/leads/search → https://server.smartlead.ai/api/v1/leads/?api_key={KEY} | |
| </div> | |
| </div> | |
| )} | |
| {/* TAB 4: DATABASE / SUPABASE */} | |
| {activeTab === "database" && ( | |
| <div className="space-y-6 max-w-4xl"> | |
| <div className="bg-zinc-900 border border-zinc-800 rounded-xl p-6 shadow-sm space-y-6"> | |
| <div> | |
| <h3 className="text-base font-semibold text-zinc-100 flex items-center gap-2"> | |
| <Database className="w-5 h-5 text-emerald-400" /> Supabase Integration Status | |
| </h3> | |
| <p className="text-xs text-zinc-400 mt-1"> | |
| Check the connection to your Supabase project instance ({supabaseProjectRef()}). | |
| To configure, provide your URL and Anon Key in your environment variables. | |
| </p> | |
| </div> | |
| <div className={`p-4 rounded-xl border flex items-start gap-4 transition-colors ${ | |
| supabaseStatus === "connected" ? "bg-emerald-500/10 border-emerald-500/30" : | |
| supabaseStatus === "unconfigured" ? "bg-amber-500/10 border-amber-500/30" : | |
| supabaseStatus === "error" ? "bg-rose-500/10 border-rose-500/30" : | |
| "bg-zinc-950/60 border-zinc-800" | |
| }`}> | |
| {supabaseStatus === "connected" && <CheckCircle2 className="w-6 h-6 text-emerald-400 shrink-0" />} | |
| {supabaseStatus === "unconfigured" && <AlertCircle className="w-6 h-6 text-amber-400 shrink-0" />} | |
| {supabaseStatus === "error" && <AlertCircle className="w-6 h-6 text-rose-400 shrink-0" />} | |
| {supabaseStatus === "testing" && <Activity className="w-6 h-6 text-zinc-400 shrink-0 animate-pulse" />} | |
| <div className="space-y-1"> | |
| <h4 className={`text-sm font-semibold ${ | |
| supabaseStatus === "connected" ? "text-emerald-400" : | |
| supabaseStatus === "unconfigured" ? "text-amber-400" : | |
| supabaseStatus === "error" ? "text-rose-400" : | |
| "text-zinc-300" | |
| }`}> | |
| {supabaseStatus === "connected" ? "Connection Established" : | |
| supabaseStatus === "unconfigured" ? "Missing Credentials" : | |
| supabaseStatus === "error" ? "Connection Failed" : | |
| "Testing Connection..."} | |
| </h4> | |
| <p className="text-xs text-zinc-400 leading-relaxed"> | |
| {supabaseStatus === "connected" ? `Successfully communicating with Supabase project ${supabaseProjectRef()}.` : | |
| supabaseStatus === "unconfigured" ? "Cannot connect to Supabase. Please ensure VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY are set in your environment variables (.env file)." : | |
| supabaseStatus === "error" ? "Failed to establish a connection with the provided credentials. Please verify your Project URL and Anon Key." : | |
| "Pinging Supabase endpoints..."} | |
| </p> | |
| </div> | |
| </div> | |
| <div className="space-y-3 pt-2"> | |
| <h4 className="text-sm font-semibold text-zinc-200">Configuration Guide</h4> | |
| <ol className="list-decimal list-inside space-y-2 text-xs text-zinc-400"> | |
| <li>Go to your Supabase Dashboard: <a href={supabaseDashboardUrl()} target="_blank" rel="noreferrer" className="text-emerald-400 hover:underline">Project {supabaseProjectRef()}</a></li> | |
| <li>Navigate to Project Settings {"->"} API.</li> | |
| <li>Copy the <strong>Project URL</strong> and add it to <code className="bg-zinc-950 px-1 py-0.5 rounded text-zinc-300">VITE_SUPABASE_URL</code>.</li> | |
| <li>Copy the <strong>anon public</strong> key and add it to <code className="bg-zinc-950 px-1 py-0.5 rounded text-zinc-300">VITE_SUPABASE_ANON_KEY</code>.</li> | |
| </ol> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Add Key Drawer */} | |
| <FormDrawer | |
| isOpen={isAddKeyOpen} | |
| onClose={() => setIsAddKeyOpen(false)} | |
| title="Add BRIDGEModulE API Key" | |
| subtitle="Retrieve your API key from Settings -> Profile -> API Keys" | |
| > | |
| <form | |
| onSubmit={(e) => { | |
| e.preventDefault(); | |
| const form = e.target as HTMLFormElement; | |
| const name = (form.elements.namedItem("name") as HTMLInputElement).value; | |
| const key = (form.elements.namedItem("key") as HTMLInputElement).value; | |
| addApiKey(name, key); | |
| toast.success("API Key saved & activated"); | |
| setIsAddKeyOpen(false); | |
| }} | |
| className="space-y-4 text-xs" | |
| > | |
| <div> | |
| <label className="block font-medium text-zinc-300 mb-1">Key Friendly Name *</label> | |
| <input name="name" required placeholder="e.g. Production Main Key" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100" /> | |
| </div> | |
| <div> | |
| <label className="block font-medium text-zinc-300 mb-1">API Key Token *</label> | |
| <input name="key" type="password" required placeholder="sl_live_..." className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100 font-mono" /> | |
| </div> | |
| <div className="pt-4 flex justify-end gap-2 border-t border-zinc-800"> | |
| <button | |
| type="button" | |
| onClick={() => setIsAddKeyOpen(false)} | |
| className="px-3 py-2 bg-zinc-800 text-zinc-300 rounded-lg" | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| type="submit" | |
| className="px-4 py-2 bg-indigo-600 text-white rounded-lg font-medium shadow" | |
| > | |
| Save Key | |
| </button> | |
| </div> | |
| </form> | |
| </FormDrawer> | |
| {/* Delete Key Modal */} | |
| <ConfirmModal | |
| isOpen={Boolean(keyToDelete)} | |
| onClose={() => setKeyToDelete(null)} | |
| onConfirm={() => { | |
| if (keyToDelete) { | |
| removeApiKey(keyToDelete); | |
| toast.success("API key deleted"); | |
| setKeyToDelete(null); | |
| } | |
| }} | |
| title="Delete API Key" | |
| description="Are you sure you want to remove this stored API key?" | |
| /> | |
| </div> | |
| ); | |
| } | |