sitcod3 commited on
Commit
0c4dec5
·
verified ·
1 Parent(s): 34dbea5

Upload src/pages/Settings.tsx with huggingface_hub

Browse files
Files changed (1) hide show
  1. src/pages/Settings.tsx +434 -0
src/pages/Settings.tsx ADDED
@@ -0,0 +1,434 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ KeyRound,
3
+ Plus,
4
+ Trash2,
5
+ Copy,
6
+ CheckCircle2,
7
+ AlertCircle,
8
+ ShieldCheck,
9
+ Sparkles,
10
+ Globe,
11
+ Clock,
12
+ Sliders,
13
+ Terminal,
14
+ Activity,
15
+ Database
16
+ } from "lucide-react";
17
+ import { useState, useEffect } from "react";
18
+ import { useApiKey } from "../context/ApiKeyContext";
19
+ import { apiClient } from "../lib/apiClient";
20
+ import FormDrawer from "../components/FormDrawer";
21
+ import ConfirmModal from "../components/ConfirmModal";
22
+ import toast from "react-hot-toast";
23
+ import { supabase, isSupabaseConfigured, supabaseProjectRef, supabaseDashboardUrl } from "../lib/supabaseClient";
24
+
25
+ export default function Settings() {
26
+ const { apiKeys, activeKeyId, setActiveKeyId, addApiKey, removeApiKey, renameApiKey } = useApiKey();
27
+ const [activeTab, setActiveTab] = useState<"keys" | "preferences" | "sandbox" | "database">("keys");
28
+ const [isAddKeyOpen, setIsAddKeyOpen] = useState(false);
29
+ const [keyToDelete, setKeyToDelete] = useState<string | null>(null);
30
+ const [testResult, setTestResult] = useState<{ status: "idle" | "testing" | "success" | "error"; message?: string; latency?: number }>({ status: "idle" });
31
+ const [supabaseStatus, setSupabaseStatus] = useState<"testing" | "connected" | "error" | "unconfigured">("testing");
32
+
33
+ useEffect(() => {
34
+ async function checkSupabase() {
35
+ if (!isSupabaseConfigured() || !supabase) {
36
+ setSupabaseStatus("unconfigured");
37
+ return;
38
+ }
39
+ try {
40
+ // Simple ping to check if credentials are valid
41
+ // Using auth.getSession() as a lightweight check
42
+ const { error } = await supabase.auth.getSession();
43
+ if (error) throw error;
44
+ setSupabaseStatus("connected");
45
+ } catch (err) {
46
+ console.error("Supabase connection error:", err);
47
+ setSupabaseStatus("error");
48
+ }
49
+ }
50
+
51
+ if (activeTab === "database") {
52
+ checkSupabase();
53
+ }
54
+ }, [activeTab]);
55
+
56
+ const testActiveConnection = async () => {
57
+ setTestResult({ status: "testing" });
58
+ const startTime = Date.now();
59
+
60
+ try {
61
+ const res = await apiClient.get("/campaigns");
62
+ const latency = Date.now() - startTime;
63
+ setTestResult({
64
+ status: "success",
65
+ message: `Connected successfully. Response latency: ${latency}ms. Found ${res.data?.length || 0} campaigns.`,
66
+ latency,
67
+ });
68
+ toast.success("API Key verified successfully");
69
+ } catch (err: any) {
70
+ setTestResult({
71
+ status: "error",
72
+ message: err.response?.data?.error || "Failed to reach BRIDGEModulE API gateway",
73
+ });
74
+ toast.error("API connection test failed");
75
+ }
76
+ };
77
+
78
+ return (
79
+ <div className="p-6 max-w-7xl mx-auto space-y-6">
80
+ {/* Header */}
81
+ <div>
82
+ <h1 className="text-2xl font-bold tracking-tight text-zinc-100 flex items-center gap-2.5">
83
+ <KeyRound className="w-6 h-6 text-indigo-400" />
84
+ Settings & API Credentials
85
+ </h1>
86
+ <p className="text-zinc-400 text-sm mt-1">
87
+ Manage BRIDGEModulE API keys, system preferences, and test live gateway endpoints.
88
+ </p>
89
+ </div>
90
+
91
+ {/* Tabs */}
92
+ <div className="border-b border-zinc-800 flex items-center gap-2 text-sm font-medium">
93
+ <button
94
+ onClick={() => setActiveTab("keys")}
95
+ className={`px-4 py-2.5 border-b-2 font-medium text-xs flex items-center gap-2 transition-colors ${
96
+ activeTab === "keys"
97
+ ? "border-indigo-500 text-indigo-400 bg-indigo-500/5"
98
+ : "border-transparent text-zinc-400 hover:text-zinc-200"
99
+ }`}
100
+ >
101
+ <KeyRound className="w-4 h-4" /> API Key Manager ({apiKeys.length})
102
+ </button>
103
+ <button
104
+ onClick={() => setActiveTab("preferences")}
105
+ className={`px-4 py-2.5 border-b-2 font-medium text-xs flex items-center gap-2 transition-colors ${
106
+ activeTab === "preferences"
107
+ ? "border-indigo-500 text-indigo-400 bg-indigo-500/5"
108
+ : "border-transparent text-zinc-400 hover:text-zinc-200"
109
+ }`}
110
+ >
111
+ <Sliders className="w-4 h-4" /> System Preferences
112
+ </button>
113
+ <button
114
+ onClick={() => setActiveTab("sandbox")}
115
+ className={`px-4 py-2.5 border-b-2 font-medium text-xs flex items-center gap-2 transition-colors ${
116
+ activeTab === "sandbox"
117
+ ? "border-indigo-500 text-indigo-400 bg-indigo-500/5"
118
+ : "border-transparent text-zinc-400 hover:text-zinc-200"
119
+ }`}
120
+ >
121
+ <Terminal className="w-4 h-4" /> API Sandbox & Architecture
122
+ </button>
123
+ <button
124
+ onClick={() => setActiveTab("database")}
125
+ className={`px-4 py-2.5 border-b-2 font-medium text-xs flex items-center gap-2 transition-colors ${
126
+ activeTab === "database"
127
+ ? "border-emerald-500 text-emerald-400 bg-emerald-500/5"
128
+ : "border-transparent text-zinc-400 hover:text-zinc-200"
129
+ }`}
130
+ >
131
+ <Database className="w-4 h-4" /> Supabase Connection
132
+ </button>
133
+ </div>
134
+
135
+ {/* TAB 1: API KEYS */}
136
+ {activeTab === "keys" && (
137
+ <div className="space-y-6 max-w-4xl">
138
+ <div className="bg-zinc-900 border border-zinc-800 rounded-xl p-5 shadow-sm space-y-4">
139
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
140
+ <div>
141
+ <h3 className="text-base font-semibold text-zinc-100">Configured BRIDGEModulE API Keys</h3>
142
+ <p className="text-xs text-zinc-400 mt-0.5">
143
+ Store multiple keys for distinct environments or client workspaces.
144
+ </p>
145
+ </div>
146
+
147
+ <div className="flex items-center gap-2">
148
+ <button
149
+ onClick={testActiveConnection}
150
+ disabled={testResult.status === "testing"}
151
+ 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"
152
+ >
153
+ <Activity className="w-3.5 h-3.5 text-emerald-400" />
154
+ {testResult.status === "testing" ? "Testing..." : "Test Connection"}
155
+ </button>
156
+ <button
157
+ onClick={() => setIsAddKeyOpen(true)}
158
+ 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"
159
+ >
160
+ <Plus className="w-3.5 h-3.5" /> Add API Key
161
+ </button>
162
+ </div>
163
+ </div>
164
+
165
+ {/* Test Connection Banner */}
166
+ {testResult.status === "success" && (
167
+ <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">
168
+ <CheckCircle2 className="w-4 h-4 shrink-0" />
169
+ <span>{testResult.message}</span>
170
+ </div>
171
+ )}
172
+ {testResult.status === "error" && (
173
+ <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">
174
+ <AlertCircle className="w-4 h-4 shrink-0" />
175
+ <span>{testResult.message}</span>
176
+ </div>
177
+ )}
178
+
179
+ {/* Keys List */}
180
+ <div className="space-y-3 pt-2">
181
+ {apiKeys.map((k) => {
182
+ const isActive = k.id === activeKeyId;
183
+ const maskedKey = k.key.length > 8
184
+ ? `${k.key.substring(0, 4)}••••••••••••••••${k.key.substring(k.key.length - 4)}`
185
+ : "••••••••••••";
186
+
187
+ return (
188
+ <div
189
+ key={k.id}
190
+ className={`p-4 rounded-xl border transition-all flex flex-col sm:flex-row sm:items-center justify-between gap-4 ${
191
+ isActive
192
+ ? "bg-indigo-950/20 border-indigo-500/40 shadow-sm"
193
+ : "bg-zinc-950/60 border-zinc-800/80 hover:border-zinc-700"
194
+ }`}
195
+ >
196
+ <div>
197
+ <div className="flex items-center gap-2">
198
+ <span className="font-semibold text-xs text-zinc-100">{k.name}</span>
199
+ {isActive && (
200
+ <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">
201
+ Active Key
202
+ </span>
203
+ )}
204
+ </div>
205
+ <div className="font-mono text-xs text-zinc-400 mt-1 flex items-center gap-2">
206
+ <span>{maskedKey}</span>
207
+ <button
208
+ onClick={() => {
209
+ navigator.clipboard.writeText(k.key);
210
+ toast.success("API key copied to clipboard");
211
+ }}
212
+ className="text-zinc-500 hover:text-zinc-300 p-1"
213
+ >
214
+ <Copy className="w-3.5 h-3.5" />
215
+ </button>
216
+ </div>
217
+ </div>
218
+
219
+ <div className="flex items-center gap-2">
220
+ {!isActive && (
221
+ <button
222
+ onClick={() => {
223
+ setActiveKeyId(k.id);
224
+ toast.success(`Switched active key to "${k.name}"`);
225
+ }}
226
+ className="px-3 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 text-xs font-medium rounded-md transition-colors"
227
+ >
228
+ Set as Active
229
+ </button>
230
+ )}
231
+ <button
232
+ onClick={() => setKeyToDelete(k.id)}
233
+ className="p-1.5 text-zinc-500 hover:text-rose-400 hover:bg-rose-500/10 rounded-md transition-colors"
234
+ >
235
+ <Trash2 className="w-4 h-4" />
236
+ </button>
237
+ </div>
238
+ </div>
239
+ );
240
+ })}
241
+
242
+ {apiKeys.length === 0 && (
243
+ <div className="p-8 text-center text-xs text-zinc-500 bg-zinc-950/40 rounded-xl border border-dashed border-zinc-800">
244
+ No custom API keys saved. The app is currently operating in high-fidelity mock database mode.
245
+ </div>
246
+ )}
247
+ </div>
248
+ </div>
249
+ </div>
250
+ )}
251
+
252
+ {/* TAB 2: PREFERENCES */}
253
+ {activeTab === "preferences" && (
254
+ <div className="space-y-6 max-w-2xl bg-zinc-900 border border-zinc-800 rounded-xl p-6">
255
+ <h3 className="text-base font-semibold text-zinc-100 mb-4">Workspace Outreach Defaults</h3>
256
+ <div className="space-y-4 text-xs">
257
+ <div>
258
+ <label className="block font-medium text-zinc-300 mb-1">Default Sending Timezone</label>
259
+ <select
260
+ defaultValue="America/New_York"
261
+ className="w-full bg-zinc-950 border border-zinc-800 rounded-lg p-2.5 text-zinc-100"
262
+ >
263
+ <option value="America/New_York">America/New_York (EST)</option>
264
+ <option value="America/Chicago">America/Chicago (CST)</option>
265
+ <option value="America/Los_Angeles">America/Los_Angeles (PST)</option>
266
+ <option value="Europe/London">Europe/London (GMT)</option>
267
+ </select>
268
+ </div>
269
+
270
+ <div>
271
+ <label className="block font-medium text-zinc-300 mb-1">Global Mailbox Rate Limiting</label>
272
+ <input
273
+ type="number"
274
+ defaultValue="50"
275
+ className="w-full bg-zinc-950 border border-zinc-800 rounded-lg p-2.5 text-zinc-100"
276
+ />
277
+ <span className="text-[11px] text-zinc-500 mt-1 block">Maximum outbound emails per mailbox / 24 hours</span>
278
+ </div>
279
+
280
+ <div className="pt-4 flex justify-end">
281
+ <button
282
+ type="button"
283
+ onClick={() => toast.success("Preferences updated")}
284
+ className="px-4 py-2 bg-indigo-600 text-white rounded-lg font-medium"
285
+ >
286
+ Save Preferences
287
+ </button>
288
+ </div>
289
+ </div>
290
+ </div>
291
+ )}
292
+
293
+ {/* TAB 3: SANDBOX */}
294
+ {activeTab === "sandbox" && (
295
+ <div className="space-y-4 max-w-4xl bg-zinc-900 border border-zinc-800 rounded-xl p-6">
296
+ <h3 className="text-base font-semibold text-zinc-100 flex items-center gap-2">
297
+ <Terminal className="w-5 h-5 text-indigo-400" /> Backend Gateway Architecture
298
+ </h3>
299
+ <p className="text-xs text-zinc-400 leading-relaxed">
300
+ All requests from the frontend client route securely through the Node.js/Express proxy (`/api/*`).
301
+ 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`).
302
+ </p>
303
+
304
+ <div className="p-4 bg-zinc-950 border border-zinc-800 rounded-lg font-mono text-xs text-zinc-300 space-y-2">
305
+ <span className="text-emerald-400"># Example Proxy Request</span><br />
306
+ GET /api/campaigns → https://server.smartlead.ai/api/v1/campaigns?api_key=&#123;KEY&#125;<br />
307
+ POST /api/leads/search → https://server.smartlead.ai/api/v1/leads/?api_key=&#123;KEY&#125;
308
+ </div>
309
+ </div>
310
+ )}
311
+
312
+ {/* TAB 4: DATABASE / SUPABASE */}
313
+ {activeTab === "database" && (
314
+ <div className="space-y-6 max-w-4xl">
315
+ <div className="bg-zinc-900 border border-zinc-800 rounded-xl p-6 shadow-sm space-y-6">
316
+ <div>
317
+ <h3 className="text-base font-semibold text-zinc-100 flex items-center gap-2">
318
+ <Database className="w-5 h-5 text-emerald-400" /> Supabase Integration Status
319
+ </h3>
320
+ <p className="text-xs text-zinc-400 mt-1">
321
+ Check the connection to your Supabase project instance ({supabaseProjectRef()}).
322
+ To configure, provide your URL and Anon Key in your environment variables.
323
+ </p>
324
+ </div>
325
+
326
+ <div className={`p-4 rounded-xl border flex items-start gap-4 transition-colors ${
327
+ supabaseStatus === "connected" ? "bg-emerald-500/10 border-emerald-500/30" :
328
+ supabaseStatus === "unconfigured" ? "bg-amber-500/10 border-amber-500/30" :
329
+ supabaseStatus === "error" ? "bg-rose-500/10 border-rose-500/30" :
330
+ "bg-zinc-950/60 border-zinc-800"
331
+ }`}>
332
+ {supabaseStatus === "connected" && <CheckCircle2 className="w-6 h-6 text-emerald-400 shrink-0" />}
333
+ {supabaseStatus === "unconfigured" && <AlertCircle className="w-6 h-6 text-amber-400 shrink-0" />}
334
+ {supabaseStatus === "error" && <AlertCircle className="w-6 h-6 text-rose-400 shrink-0" />}
335
+ {supabaseStatus === "testing" && <Activity className="w-6 h-6 text-zinc-400 shrink-0 animate-pulse" />}
336
+
337
+ <div className="space-y-1">
338
+ <h4 className={`text-sm font-semibold ${
339
+ supabaseStatus === "connected" ? "text-emerald-400" :
340
+ supabaseStatus === "unconfigured" ? "text-amber-400" :
341
+ supabaseStatus === "error" ? "text-rose-400" :
342
+ "text-zinc-300"
343
+ }`}>
344
+ {supabaseStatus === "connected" ? "Connection Established" :
345
+ supabaseStatus === "unconfigured" ? "Missing Credentials" :
346
+ supabaseStatus === "error" ? "Connection Failed" :
347
+ "Testing Connection..."}
348
+ </h4>
349
+ <p className="text-xs text-zinc-400 leading-relaxed">
350
+ {supabaseStatus === "connected" ? `Successfully communicating with Supabase project ${supabaseProjectRef()}.` :
351
+ supabaseStatus === "unconfigured" ? "Cannot connect to Supabase. Please ensure VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY are set in your environment variables (.env file)." :
352
+ supabaseStatus === "error" ? "Failed to establish a connection with the provided credentials. Please verify your Project URL and Anon Key." :
353
+ "Pinging Supabase endpoints..."}
354
+ </p>
355
+ </div>
356
+ </div>
357
+
358
+ <div className="space-y-3 pt-2">
359
+ <h4 className="text-sm font-semibold text-zinc-200">Configuration Guide</h4>
360
+ <ol className="list-decimal list-inside space-y-2 text-xs text-zinc-400">
361
+ <li>Go to your Supabase Dashboard: <a href={supabaseDashboardUrl()} target="_blank" rel="noreferrer" className="text-emerald-400 hover:underline">Project {supabaseProjectRef()}</a></li>
362
+ <li>Navigate to Project Settings {"->"} API.</li>
363
+ <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>
364
+ <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>
365
+ </ol>
366
+ </div>
367
+ </div>
368
+ </div>
369
+ )}
370
+
371
+ {/* Add Key Drawer */}
372
+ <FormDrawer
373
+ isOpen={isAddKeyOpen}
374
+ onClose={() => setIsAddKeyOpen(false)}
375
+ title="Add BRIDGEModulE API Key"
376
+ subtitle="Retrieve your API key from Settings -> Profile -> API Keys"
377
+ >
378
+ <form
379
+ onSubmit={(e) => {
380
+ e.preventDefault();
381
+ const form = e.target as HTMLFormElement;
382
+ const name = (form.elements.namedItem("name") as HTMLInputElement).value;
383
+ const key = (form.elements.namedItem("key") as HTMLInputElement).value;
384
+
385
+ addApiKey(name, key);
386
+ toast.success("API Key saved & activated");
387
+ setIsAddKeyOpen(false);
388
+ }}
389
+ className="space-y-4 text-xs"
390
+ >
391
+ <div>
392
+ <label className="block font-medium text-zinc-300 mb-1">Key Friendly Name *</label>
393
+ <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" />
394
+ </div>
395
+ <div>
396
+ <label className="block font-medium text-zinc-300 mb-1">API Key Token *</label>
397
+ <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" />
398
+ </div>
399
+
400
+ <div className="pt-4 flex justify-end gap-2 border-t border-zinc-800">
401
+ <button
402
+ type="button"
403
+ onClick={() => setIsAddKeyOpen(false)}
404
+ className="px-3 py-2 bg-zinc-800 text-zinc-300 rounded-lg"
405
+ >
406
+ Cancel
407
+ </button>
408
+ <button
409
+ type="submit"
410
+ className="px-4 py-2 bg-indigo-600 text-white rounded-lg font-medium shadow"
411
+ >
412
+ Save Key
413
+ </button>
414
+ </div>
415
+ </form>
416
+ </FormDrawer>
417
+
418
+ {/* Delete Key Modal */}
419
+ <ConfirmModal
420
+ isOpen={Boolean(keyToDelete)}
421
+ onClose={() => setKeyToDelete(null)}
422
+ onConfirm={() => {
423
+ if (keyToDelete) {
424
+ removeApiKey(keyToDelete);
425
+ toast.success("API key deleted");
426
+ setKeyToDelete(null);
427
+ }
428
+ }}
429
+ title="Delete API Key"
430
+ description="Are you sure you want to remove this stored API key?"
431
+ />
432
+ </div>
433
+ );
434
+ }