@cybermem/dashboard 0.8.5 → 0.8.9

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.
@@ -1,8 +1,10 @@
1
1
  "use client";
2
2
 
3
3
  import { Button } from "@/components/ui/button";
4
+ import { ConfirmationModal } from "@/components/ui/confirmation-modal";
4
5
  import { Input } from "@/components/ui/input";
5
6
  import { Label } from "@/components/ui/label";
7
+ import { TintButton } from "@/components/ui/tint-button";
6
8
  import { useDashboard } from "@/lib/data/dashboard-context";
7
9
  import {
8
10
  Check,
@@ -11,7 +13,6 @@ import {
11
13
  Download,
12
14
  Eye,
13
15
  EyeOff,
14
- Key,
15
16
  Loader2,
16
17
  RotateCcw,
17
18
  Server,
@@ -22,20 +23,17 @@ import {
22
23
  X,
23
24
  } from "lucide-react";
24
25
  import { useEffect, useState } from "react";
26
+ import { toast } from "sonner";
25
27
 
26
28
  export default function SettingsModal({ onClose }: { onClose: () => void }) {
27
29
  const [apiKey, setApiKey] = useState("");
28
30
  const [endpoint, setEndpoint] = useState("");
29
31
  const [isManaged, setIsManaged] = useState(false);
30
- const [adminPassword, setAdminPassword] = useState(
31
- localStorage.getItem("adminPassword") || "admin",
32
- );
33
32
  const [settings, setSettings] = useState<any>(null);
34
33
  const [isLoading, setIsLoading] = useState(true);
35
34
 
36
35
  const { isDemo, toggleDemo } = useDashboard();
37
36
  const [showApiKey, setShowApiKey] = useState(false);
38
- const [showAdminPassword, setShowAdminPassword] = useState(false);
39
37
 
40
38
  const [showRegenConfirm, setShowRegenConfirm] = useState(false);
41
39
  const [regenInputValue, setRegenInputValue] = useState("");
@@ -55,6 +53,7 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
55
53
  navigator.clipboard.writeText(text);
56
54
  setCopiedId(id);
57
55
  setTimeout(() => setCopiedId(null), 2000);
56
+ toast.success("Copied to clipboard!");
58
57
  };
59
58
 
60
59
  // Fetch settings from server
@@ -105,16 +104,19 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
105
104
  setShowApiKey(true);
106
105
  setShowRegenConfirm(false);
107
106
  setRegenInputValue("");
107
+ toast.success("Token Regenerated!", {
108
+ description: "All connected clients will need to be updated.",
109
+ });
108
110
  } catch (e) {
109
111
  console.error(e);
110
- alert("Failed to regenerate key on server.");
112
+ toast.error("Failed to regenerate token");
111
113
  }
112
114
  };
113
115
 
114
116
  const [saved, setSaved] = useState(false);
115
117
 
116
118
  const handleSave = () => {
117
- localStorage.setItem("adminPassword", adminPassword);
119
+ // Settings saved
118
120
  setSaved(true);
119
121
  setTimeout(() => setSaved(false), 2000);
120
122
  };
@@ -243,92 +245,90 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
243
245
 
244
246
  {/* Content */}
245
247
  <div className="p-6 space-y-6">
246
- {/* Security */}
248
+ {/* Access Token */}
247
249
  <section>
248
250
  <h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
249
251
  <Shield className="w-5 h-5" />
250
- Security
252
+ Access Token
251
253
  </h3>
252
254
  <div className="bg-white/5 border border-white/10 rounded-lg p-5 space-y-4 shadow-[inset_0_0_20px_rgba(255,255,255,0.02)] backdrop-blur-sm">
255
+ {/* Token Display with inline regenerate */}
253
256
  <div className="space-y-2">
254
- <Label htmlFor="admin-password">Admin Password</Label>
255
- <div className="relative">
256
- <Input
257
- id="admin-password"
258
- value={adminPassword}
259
- onChange={(e) => setAdminPassword(e.target.value)}
260
- className="bg-black/40 border-white/10 text-white"
261
- type={showAdminPassword ? "text" : "password"}
262
- />
263
- <button
264
- onClick={() => setShowAdminPassword(!showAdminPassword)}
265
- className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-white"
257
+ <Label htmlFor="access-token">Your Access Token</Label>
258
+ <div className="flex gap-2">
259
+ <div className="relative flex-1">
260
+ <Input
261
+ id="access-token"
262
+ value={apiKey || "Token not generated yet"}
263
+ readOnly
264
+ className="bg-black/40 border-white/10 text-white font-mono text-sm pr-10"
265
+ type={showApiKey ? "text" : "password"}
266
+ />
267
+ <button
268
+ onClick={() => setShowApiKey(!showApiKey)}
269
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-white"
270
+ >
271
+ {showApiKey ? (
272
+ <EyeOff className="w-4 h-4" />
273
+ ) : (
274
+ <Eye className="w-4 h-4" />
275
+ )}
276
+ </button>
277
+ </div>
278
+ <TintButton
279
+ tint="neutral"
280
+ variant="ghost"
281
+ size="icon"
282
+ onClick={() => copyToClipboard(apiKey, "accesstoken")}
283
+ title="Copy token"
266
284
  >
267
- {showAdminPassword ? (
268
- <EyeOff className="w-4 h-4" />
285
+ {copiedId === "accesstoken" ? (
286
+ <Check className="h-4 w-4 text-emerald-400" />
269
287
  ) : (
270
- <Eye className="w-4 h-4" />
288
+ <Copy className="h-4 w-4" />
271
289
  )}
272
- </button>
290
+ </TintButton>
291
+ <TintButton
292
+ tint="yellow"
293
+ variant="ghost"
294
+ size="icon"
295
+ onClick={() => setShowRegenConfirm(true)}
296
+ title="Regenerate token"
297
+ >
298
+ <RotateCcw className="w-4 h-4" />
299
+ </TintButton>
273
300
  </div>
301
+ <p className="text-xs text-neutral-500">
302
+ Use this token to connect MCP clients from other devices
303
+ </p>
274
304
  </div>
275
305
 
276
- {/* Authentication Status */}
277
- <div className="space-y-2 pt-4 border-t border-white/10">
278
- <Label>Authentication Method</Label>
306
+ {/* Auth Status */}
307
+ <div className="pt-4 border-t border-white/10">
279
308
  <div className="flex items-center gap-3 p-3 bg-black/20 rounded-lg border border-white/5">
280
- {/* Show different states based on auth method */}
281
309
  {isManaged ? (
282
310
  <>
283
311
  <div className="w-2 h-2 bg-emerald-400 rounded-full animate-pulse" />
284
312
  <div className="flex-1">
285
313
  <p className="text-sm text-emerald-300 font-medium">
286
- Local Mode
314
+ Local Mode Active
287
315
  </p>
288
316
  <p className="text-xs text-neutral-500">
289
- Auto-authenticated local connection
317
+ No token needed for local connections
290
318
  </p>
291
319
  </div>
292
320
  </>
293
- ) : apiKey ? (
321
+ ) : (
294
322
  <>
295
323
  <div className="w-2 h-2 bg-yellow-400 rounded-full" />
296
324
  <div className="flex-1">
297
325
  <p className="text-sm text-yellow-300 font-medium">
298
- Security Token
326
+ Remote Mode
299
327
  </p>
300
328
  <p className="text-xs text-neutral-500">
301
- Using static token authentication
329
+ Token required for MCP client connections
302
330
  </p>
303
331
  </div>
304
- <a
305
- href="https://cybermem.dev/auth/signin"
306
- target="_blank"
307
- rel="noopener noreferrer"
308
- className="px-3 py-1.5 text-xs bg-white/10 hover:bg-white/20 rounded-lg text-white transition-colors"
309
- >
310
- Upgrade to OAuth
311
- </a>
312
- </>
313
- ) : (
314
- <>
315
- <div className="w-2 h-2 bg-neutral-400 rounded-full" />
316
- <div className="flex-1">
317
- <p className="text-sm text-neutral-300 font-medium">
318
- Not Configured
319
- </p>
320
- <p className="text-xs text-neutral-500">
321
- Connect with GitHub for secure access
322
- </p>
323
- </div>
324
- <a
325
- href="https://cybermem.dev/auth/signin"
326
- target="_blank"
327
- rel="noopener noreferrer"
328
- className="px-3 py-1.5 text-xs bg-emerald-500/20 hover:bg-emerald-500/30 border border-emerald-500/20 rounded-lg text-emerald-400 transition-colors"
329
- >
330
- Connect GitHub
331
- </a>
332
332
  </>
333
333
  )}
334
334
  </div>
@@ -336,98 +336,6 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
336
336
  </div>
337
337
  </section>
338
338
 
339
- {/* API Configuration */}
340
- {!isManaged && (
341
- <section>
342
- <h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
343
- <Key className="w-5 h-5" />
344
- Token Configuration
345
- </h3>
346
- <div className="bg-white/5 border border-white/10 rounded-lg p-5 space-y-4 shadow-[inset_0_0_20px_rgba(255,255,255,0.02)] backdrop-blur-sm">
347
- <div className="space-y-2">
348
- <Label htmlFor="api-key">Security Token</Label>
349
- <div className="flex gap-2">
350
- <div className="relative flex-1">
351
- <Input
352
- id="api-key"
353
- value={apiKey || "not-generated-yet"}
354
- readOnly
355
- className="bg-black/40 border-white/10 text-white font-mono"
356
- type={showApiKey ? "text" : "password"}
357
- />
358
- <button
359
- onClick={() => setShowApiKey(!showApiKey)}
360
- className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-white"
361
- >
362
- {showApiKey ? (
363
- <EyeOff className="w-4 h-4" />
364
- ) : (
365
- <Eye className="w-4 h-4" />
366
- )}
367
- </button>
368
- </div>
369
- <Button
370
- size="icon"
371
- variant="ghost"
372
- onClick={() => copyToClipboard(apiKey, "apikey")}
373
- >
374
- {copiedId === "apikey" ? (
375
- <Check className="h-4 w-4 text-emerald-400" />
376
- ) : (
377
- <Copy className="h-4 w-4" />
378
- )}
379
- </Button>
380
- </div>
381
-
382
- <div className="flex justify-end pt-2">
383
- {showRegenConfirm ? (
384
- <div className="flex items-center gap-2">
385
- <Input
386
- value={regenInputValue}
387
- onChange={(e) => setRegenInputValue(e.target.value)}
388
- placeholder="Type 'agree'"
389
- className="h-8 w-24 text-xs"
390
- />
391
- <Button
392
- size="sm"
393
- variant="ghost"
394
- onClick={() => setShowRegenConfirm(false)}
395
- >
396
- Cancel
397
- </Button>
398
- <Button
399
- size="sm"
400
- disabled={regenInputValue !== "agree"}
401
- onClick={confirmRegenerate}
402
- >
403
- Confirm
404
- </Button>
405
- </div>
406
- ) : (
407
- <Button
408
- size="sm"
409
- variant="ghost"
410
- onClick={() => setShowRegenConfirm(true)}
411
- >
412
- Regenerate Key
413
- </Button>
414
- )}
415
- </div>
416
- </div>
417
-
418
- <div className="space-y-2">
419
- <Label htmlFor="endpoint">Server Endpoint</Label>
420
- <Input
421
- id="endpoint"
422
- value={endpoint}
423
- onChange={(e) => setEndpoint(e.target.value)}
424
- className="bg-black/40 border-white/10 text-white"
425
- />
426
- </div>
427
- </div>
428
- </section>
429
- )}
430
-
431
339
  {/* Data Management */}
432
340
  <section>
433
341
  <h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
@@ -436,19 +344,20 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
436
344
  </h3>
437
345
  <div className="flex flex-col gap-3">
438
346
  <div className="flex items-center gap-3">
439
- <Button
440
- variant="outline"
441
- className="flex-1 justify-center bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20 text-white hover:text-white h-11 px-6 transition-all"
347
+ <TintButton
348
+ tint="neutral"
349
+ variant="solid"
350
+ className="flex-1 h-11"
442
351
  onClick={handleBackup}
443
352
  disabled={isBackingUp}
444
353
  >
445
354
  {isBackingUp ? (
446
- <Loader2 className="w-4 h-4 mr-2 animate-spin" />
355
+ <Loader2 className="w-4 h-4 animate-spin" />
447
356
  ) : (
448
- <Download className="w-4 h-4 mr-2 opacity-70" />
357
+ <Download className="w-4 h-4" />
449
358
  )}
450
- <span className="font-medium">Backup</span>
451
- </Button>
359
+ Backup
360
+ </TintButton>
452
361
 
453
362
  <div className="flex-1 relative">
454
363
  <input
@@ -459,72 +368,36 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
459
368
  onChange={handleRestore}
460
369
  disabled={isRestoring}
461
370
  />
462
- <Button
463
- variant="outline"
464
- className="w-full justify-center bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20 text-white hover:text-white h-11 px-6 transition-all"
371
+ <TintButton
372
+ tint="neutral"
373
+ variant="solid"
374
+ className="w-full h-11"
465
375
  onClick={() =>
466
376
  document.getElementById("restore-file")?.click()
467
377
  }
468
378
  disabled={isRestoring}
469
379
  >
470
380
  {isRestoring ? (
471
- <Loader2 className="w-4 h-4 mr-2 animate-spin" />
381
+ <Loader2 className="w-4 h-4 animate-spin" />
472
382
  ) : (
473
- <Upload className="w-4 h-4 mr-2 opacity-70" />
383
+ <Upload className="w-4 h-4" />
474
384
  )}
475
- <span className="font-medium">Restore</span>
476
- </Button>
385
+ Restore
386
+ </TintButton>
477
387
  </div>
478
388
 
479
- <Button
480
- variant="outline"
481
- className="flex-1 justify-center bg-red-500/5 border-red-500/10 hover:bg-red-500/10 hover:border-red-500/30 text-red-400 hover:text-red-300 h-11 px-6 transition-all"
389
+ <TintButton
390
+ tint="red"
391
+ variant="solid"
392
+ className="flex-1 h-11"
482
393
  onClick={() => setShowResetConfirm(true)}
483
394
  disabled={isResetting}
484
395
  >
485
- <Trash2 className="w-4 h-4 mr-2 opacity-70" />
486
- <span className="font-medium">Reset DB</span>
487
- </Button>
396
+ <Trash2 className="w-4 h-4" />
397
+ Reset DB
398
+ </TintButton>
488
399
  </div>
489
400
 
490
- {showResetConfirm && (
491
- <div className="p-5 bg-red-500/5 border border-red-500/20 rounded-xl space-y-4 shadow-inner">
492
- <p className="text-xs text-red-400/80 font-bold uppercase tracking-widest text-center">
493
- Danger Zone: This will permanently delete all memories!
494
- </p>
495
- <div className="flex flex-col gap-3">
496
- <Input
497
- value={resetConfirmText}
498
- onChange={(e) => setResetConfirmText(e.target.value)}
499
- placeholder="Type 'RESET' to confirm"
500
- className="h-10 bg-black/40 border-red-500/20 text-white placeholder:text-red-500/20 text-center font-mono focus:border-red-500/40"
501
- />
502
- <div className="flex gap-2">
503
- <Button
504
- className="flex-1 text-neutral-400 hover:text-white hover:bg-white/5"
505
- variant="ghost"
506
- onClick={() => {
507
- setShowResetConfirm(false);
508
- setResetConfirmText("");
509
- }}
510
- >
511
- Cancel
512
- </Button>
513
- <Button
514
- className="flex-1 bg-red-500/80 hover:bg-red-500 text-white shadow-lg active:scale-[0.98] transition-transform"
515
- disabled={resetConfirmText !== "RESET" || isResetting}
516
- onClick={handleReset}
517
- >
518
- {isResetting && (
519
- <Loader2 className="w-4 h-4 mr-2 animate-spin" />
520
- )}
521
- Confirm Reset
522
- </Button>
523
- </div>
524
- </div>
525
- </div>
526
- )}
527
-
528
401
  {operationStatus && (
529
402
  <div
530
403
  className={`p-3 rounded-xl text-sm flex items-center gap-3 animate-in fade-in slide-in-from-top-1 ${
@@ -585,33 +458,42 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
585
458
  <span className="text-[10px] uppercase text-neutral-500 font-bold tracking-[0.2em] block mb-2">
586
459
  Environment
587
460
  </span>
588
- <div className="mt-4 flex flex-col justify-center">
589
- <p className="text-emerald-400 font-bold text-2xl tracking-tight drop-shadow-[0_0_15px_rgba(52,211,153,0.3)]">
590
- Production
591
- </p>
592
- <p className="text-[10px] text-neutral-500 mt-1 uppercase tracking-[0.2em] font-medium">
593
- {settings?.isManaged
594
- ? "Managed Instance"
595
- : "Local Instance"}
596
- </p>
461
+ <div className="space-y-3">
462
+ <div className="flex justify-between items-center">
463
+ <span className="text-xs text-neutral-400">Status</span>
464
+ <code className="text-[13px] font-mono text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20">
465
+ Production
466
+ </code>
467
+ </div>
468
+ <div className="flex justify-between items-center">
469
+ <span className="text-xs text-neutral-400">Instance</span>
470
+ <code className="text-[13px] font-mono text-neutral-200 bg-white/5 px-2 py-0.5 rounded border border-white/10">
471
+ {settings?.isLocal
472
+ ? "Local"
473
+ : settings?.isManaged
474
+ ? "RPi"
475
+ : "VPS"}
476
+ </code>
477
+ </div>
597
478
  </div>
598
479
  </div>
599
480
  </div>
600
481
 
601
482
  <div className="pt-2 border-t border-white/5">
602
- <Button
603
- variant="outline"
604
- className="w-full bg-sky-500/5 border-sky-500/10 hover:bg-sky-500/10 hover:border-sky-500/30 text-sky-400 hover:text-sky-300 h-10 transition-all font-medium flex items-center justify-center gap-2 group"
483
+ <TintButton
484
+ tint="sky"
485
+ variant="solid"
486
+ className="w-full h-10"
605
487
  onClick={handleRestart}
606
488
  disabled={isRestarting}
607
489
  >
608
490
  {isRestarting ? (
609
491
  <Loader2 className="w-4 h-4 animate-spin" />
610
492
  ) : (
611
- <RotateCcw className="w-4 h-4 opacity-70 group-hover:rotate-45 transition-transform" />
493
+ <RotateCcw className="w-4 h-4" />
612
494
  )}
613
495
  {isRestarting ? "Restarting..." : "Restart Service"}
614
- </Button>
496
+ </TintButton>
615
497
  </div>
616
498
  </div>
617
499
  </section>
@@ -619,21 +501,42 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
619
501
 
620
502
  {/* Footer */}
621
503
  <div className="sticky bottom-0 bg-[#0B1116]/80 backdrop-blur-md border-t border-emerald-500/20 px-6 py-4 flex justify-end gap-3 z-10">
622
- <Button
623
- onClick={onClose}
624
- variant="ghost"
625
- className="text-neutral-400 hover:text-white hover:bg-white/5"
626
- >
627
- Cancel
628
- </Button>
629
- <Button
630
- onClick={handleSave}
631
- className="bg-emerald-500/20 hover:bg-emerald-500/30 text-emerald-400 border border-emerald-500/20"
632
- >
633
- {saved ? "Saved!" : "Save Changes"}
634
- </Button>
504
+ <TintButton tint="neutral" variant="ghost" onClick={onClose}>
505
+ Close
506
+ </TintButton>
635
507
  </div>
636
508
  </div>
509
+
510
+ {/* Regenerate Token Confirmation Modal */}
511
+ <ConfirmationModal
512
+ isOpen={showRegenConfirm}
513
+ onClose={() => {
514
+ setShowRegenConfirm(false);
515
+ setRegenInputValue("");
516
+ }}
517
+ onConfirm={confirmRegenerate}
518
+ title="Regenerate Access Token"
519
+ description="This will invalidate your current token. All connected MCP clients will need to be reconfigured with the new token."
520
+ confirmText="Regenerate"
521
+ confirmWord="confirm"
522
+ tint="yellow"
523
+ />
524
+
525
+ {/* Reset Database Confirmation Modal */}
526
+ <ConfirmationModal
527
+ isOpen={showResetConfirm}
528
+ onClose={() => {
529
+ setShowResetConfirm(false);
530
+ setResetConfirmText("");
531
+ }}
532
+ onConfirm={handleReset}
533
+ title="Reset Database"
534
+ description="This will permanently delete ALL memories and cannot be undone. Make sure you have a backup!"
535
+ confirmText="Reset Database"
536
+ confirmWord="RESET"
537
+ tint="red"
538
+ isLoading={isResetting}
539
+ />
637
540
  </div>
638
541
  );
639
542
  }
@@ -0,0 +1,116 @@
1
+ "use client";
2
+
3
+ import { Input } from "@/components/ui/input";
4
+ import { TintButton } from "@/components/ui/tint-button";
5
+ import { AlertTriangle, X } from "lucide-react";
6
+ import { useState } from "react";
7
+
8
+ interface ConfirmationModalProps {
9
+ isOpen: boolean;
10
+ onClose: () => void;
11
+ onConfirm: () => void;
12
+ title: string;
13
+ description: string;
14
+ confirmText?: string;
15
+ confirmWord?: string;
16
+ tint?: "red" | "yellow";
17
+ isLoading?: boolean;
18
+ }
19
+
20
+ export function ConfirmationModal({
21
+ isOpen,
22
+ onClose,
23
+ onConfirm,
24
+ title,
25
+ description,
26
+ confirmText = "Confirm",
27
+ confirmWord,
28
+ tint = "red",
29
+ isLoading = false,
30
+ }: ConfirmationModalProps) {
31
+ const [inputValue, setInputValue] = useState("");
32
+
33
+ if (!isOpen) return null;
34
+
35
+ const canConfirm = confirmWord ? inputValue === confirmWord : true;
36
+
37
+ const handleConfirm = () => {
38
+ if (canConfirm) {
39
+ onConfirm();
40
+ setInputValue("");
41
+ }
42
+ };
43
+
44
+ const handleClose = () => {
45
+ setInputValue("");
46
+ onClose();
47
+ };
48
+
49
+ return (
50
+ <div className="fixed inset-0 z-[60] flex items-center justify-center">
51
+ {/* Backdrop */}
52
+ <div
53
+ className="absolute inset-0 bg-black/60 backdrop-blur-sm"
54
+ onClick={handleClose}
55
+ />
56
+
57
+ {/* Modal */}
58
+ <div className="relative bg-[#0B1116]/95 backdrop-blur-xl border border-white/10 rounded-2xl shadow-2xl max-w-md w-full mx-4 overflow-hidden">
59
+ {/* Header */}
60
+ <div className="flex items-start gap-4 p-5">
61
+ <div
62
+ className={`p-2 rounded-lg ${tint === "red" ? "bg-red-500/10" : "bg-yellow-500/10"}`}
63
+ >
64
+ <AlertTriangle
65
+ className={`w-5 h-5 ${tint === "red" ? "text-red-400" : "text-yellow-400"}`}
66
+ />
67
+ </div>
68
+ <div className="flex-1">
69
+ <h3 className="text-lg font-semibold text-white">{title}</h3>
70
+ <p className="text-sm text-neutral-400 mt-1">{description}</p>
71
+ </div>
72
+ <button
73
+ onClick={handleClose}
74
+ className="text-neutral-400 hover:text-white transition-colors"
75
+ >
76
+ <X className="w-5 h-5" />
77
+ </button>
78
+ </div>
79
+
80
+ {/* Confirmation Input */}
81
+ {confirmWord && (
82
+ <div className="px-5 pb-4">
83
+ <Input
84
+ value={inputValue}
85
+ onChange={(e) => setInputValue(e.target.value)}
86
+ placeholder={`Type "${confirmWord}" to confirm`}
87
+ className="bg-black/40 border-white/10 text-white text-center font-mono placeholder:text-neutral-600"
88
+ autoFocus
89
+ />
90
+ </div>
91
+ )}
92
+
93
+ {/* Actions */}
94
+ <div className="flex gap-3 p-5 pt-0">
95
+ <TintButton
96
+ tint="neutral"
97
+ variant="outline"
98
+ className="flex-1"
99
+ onClick={handleClose}
100
+ >
101
+ Cancel
102
+ </TintButton>
103
+ <TintButton
104
+ tint={tint}
105
+ variant="solid"
106
+ className="flex-1"
107
+ onClick={handleConfirm}
108
+ disabled={!canConfirm || isLoading}
109
+ >
110
+ {isLoading ? "Processing..." : confirmText}
111
+ </TintButton>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ );
116
+ }