muthuk1 commited on
Commit
383d246
·
verified ·
1 Parent(s): 945e815

✨ Major frontend enhancement: glassmorphism, animations, toast system, premium component library, orbit loaders, particles, animated counters, security score ring, PIN dot visualization, waveform visualizer, staggered animations, 40+ CSS animations

Browse files
src/renderer/App.tsx CHANGED
@@ -1,5 +1,6 @@
1
- import React, { useState, useEffect, useCallback } from 'react';
2
  import './types';
 
3
  import LockScreen from './pages/LockScreen';
4
  import OnboardingScreen from './pages/OnboardingScreen';
5
  import Dashboard from './pages/Dashboard';
@@ -14,58 +15,41 @@ import TopBar from './components/TopBar';
14
  type Page = 'dashboard' | 'send' | 'history' | 'voice' | 'security' | 'settings';
15
  type AppState = 'loading' | 'onboarding' | 'locked' | 'unlocked';
16
 
17
- export default function App() {
18
  const [appState, setAppState] = useState<AppState>('loading');
19
  const [currentPage, setCurrentPage] = useState<Page>('dashboard');
 
20
  const [publicKey, setPublicKey] = useState<string | null>(null);
21
  const [balance, setBalance] = useState({ sol: 0, usdt: 0 });
22
  const [aiStatus, setAiStatus] = useState<any>(null);
 
23
 
24
- // Check wallet state on mount
25
- useEffect(() => {
26
- checkWalletState();
27
- }, []);
28
 
29
- // Listen for lock events
30
  useEffect(() => {
31
  if (!window.solvox) return;
32
- const cleanup = window.solvox.on.locked(() => {
33
- setAppState('locked');
34
- });
35
- return cleanup;
36
  }, []);
37
 
38
  const checkWalletState = async () => {
39
  try {
40
- if (!window.solvox) {
41
- // Development mode — show dashboard
42
- setAppState('unlocked');
43
- return;
44
- }
45
  const exists = await window.solvox.wallet.exists();
46
- if (!exists) {
47
- setAppState('onboarding');
48
- return;
49
- }
50
  const unlocked = await window.solvox.wallet.isUnlocked();
51
  setAppState(unlocked ? 'unlocked' : 'locked');
52
  if (unlocked) {
53
- const pk = await window.solvox.wallet.getPublicKey();
54
- setPublicKey(pk);
55
  refreshBalance();
56
  }
57
- } catch {
58
- setAppState('onboarding');
59
- }
60
  };
61
 
62
  const refreshBalance = async () => {
63
  try {
64
  if (!window.solvox) return;
65
- const result = await window.solvox.wallet.getBalance();
66
- if (result.success) {
67
- setBalance({ sol: result.sol || 0, usdt: result.usdt || 0 });
68
- }
69
  } catch {}
70
  };
71
 
@@ -73,11 +57,12 @@ export default function App() {
73
  setPublicKey(pk);
74
  setAppState('unlocked');
75
  refreshBalance();
76
- // Initialize AI in background
77
  if (window.solvox) {
78
- window.solvox.ai.initialize().then(result => {
79
- if (result.success) {
80
  window.solvox.ai.getStatus().then(setAiStatus);
 
81
  }
82
  });
83
  }
@@ -87,79 +72,94 @@ export default function App() {
87
  setPublicKey(pk);
88
  setAppState('unlocked');
89
  refreshBalance();
 
90
  };
91
 
92
  const handleLock = async () => {
93
- if (window.solvox) {
94
- await window.solvox.wallet.lock();
95
- }
96
  setAppState('locked');
97
  };
98
 
99
- // Render based on app state
100
- if (appState === 'loading') {
101
- return <LoadingScreen />;
102
- }
103
-
104
- if (appState === 'onboarding') {
105
- return <OnboardingScreen onComplete={handleOnboardingComplete} />;
106
- }
107
 
108
- if (appState === 'locked') {
109
- return <LockScreen onUnlock={handleUnlock} />;
110
- }
111
 
112
  const renderPage = () => {
 
113
  switch (currentPage) {
114
- case 'dashboard':
115
- return <Dashboard balance={balance} publicKey={publicKey} onRefresh={refreshBalance} />;
116
- case 'send':
117
- return <SendPage balance={balance} onSent={refreshBalance} />;
118
- case 'history':
119
- return <HistoryPage />;
120
- case 'voice':
121
- return <VoicePage aiStatus={aiStatus} />;
122
- case 'security':
123
- return <SecurityPage />;
124
- case 'settings':
125
- return <SettingsPage onLock={handleLock} />;
126
- default:
127
- return <Dashboard balance={balance} publicKey={publicKey} onRefresh={refreshBalance} />;
128
  }
129
  };
130
 
131
  return (
132
- <div className="flex h-screen bg-sol-dark">
133
- <Sidebar currentPage={currentPage} onNavigate={setCurrentPage} />
134
  <div className="flex-1 flex flex-col overflow-hidden">
135
- <TopBar
136
- publicKey={publicKey}
137
- balance={balance}
138
- aiStatus={aiStatus}
139
- onLock={handleLock}
140
- />
141
- <main className="flex-1 overflow-y-auto p-6">
142
- {renderPage()}
143
  </main>
144
  </div>
145
  </div>
146
  );
147
  }
148
 
 
 
 
 
 
 
 
 
149
  function LoadingScreen() {
150
  return (
151
- <div className="h-screen flex flex-col items-center justify-center bg-sol-dark">
152
- <div className="text-6xl font-bold gradient-text mb-4">SolVox</div>
153
- <div className="text-sol-muted text-lg">Initializing local AI engine...</div>
154
- <div className="mt-8 flex space-x-2">
155
- {[0, 1, 2, 3, 4].map(i => (
156
- <div
157
- key={i}
158
- className="w-2 h-8 bg-sol-purple rounded-full waveform-bar"
159
- style={{ '--delay': `${0.3 + i * 0.1}s`, '--max-height': '32px' } as any}
160
- />
 
 
 
161
  ))}
162
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  </div>
164
  );
165
  }
 
1
+ import React, { useState, useEffect } from 'react';
2
  import './types';
3
+ import { ToastProvider, useToast } from './components/ui/index';
4
  import LockScreen from './pages/LockScreen';
5
  import OnboardingScreen from './pages/OnboardingScreen';
6
  import Dashboard from './pages/Dashboard';
 
15
  type Page = 'dashboard' | 'send' | 'history' | 'voice' | 'security' | 'settings';
16
  type AppState = 'loading' | 'onboarding' | 'locked' | 'unlocked';
17
 
18
+ function AppContent() {
19
  const [appState, setAppState] = useState<AppState>('loading');
20
  const [currentPage, setCurrentPage] = useState<Page>('dashboard');
21
+ const [prevPage, setPrevPage] = useState<Page>('dashboard');
22
  const [publicKey, setPublicKey] = useState<string | null>(null);
23
  const [balance, setBalance] = useState({ sol: 0, usdt: 0 });
24
  const [aiStatus, setAiStatus] = useState<any>(null);
25
+ const { addToast } = useToast();
26
 
27
+ useEffect(() => { checkWalletState(); }, []);
 
 
 
28
 
 
29
  useEffect(() => {
30
  if (!window.solvox) return;
31
+ return window.solvox.on.locked(() => { setAppState('locked'); });
 
 
 
32
  }, []);
33
 
34
  const checkWalletState = async () => {
35
  try {
36
+ if (!window.solvox) { setAppState('unlocked'); return; }
 
 
 
 
37
  const exists = await window.solvox.wallet.exists();
38
+ if (!exists) { setAppState('onboarding'); return; }
 
 
 
39
  const unlocked = await window.solvox.wallet.isUnlocked();
40
  setAppState(unlocked ? 'unlocked' : 'locked');
41
  if (unlocked) {
42
+ setPublicKey(await window.solvox.wallet.getPublicKey());
 
43
  refreshBalance();
44
  }
45
+ } catch { setAppState('onboarding'); }
 
 
46
  };
47
 
48
  const refreshBalance = async () => {
49
  try {
50
  if (!window.solvox) return;
51
+ const r = await window.solvox.wallet.getBalance();
52
+ if (r.success) setBalance({ sol: r.sol || 0, usdt: r.usdt || 0 });
 
 
53
  } catch {}
54
  };
55
 
 
57
  setPublicKey(pk);
58
  setAppState('unlocked');
59
  refreshBalance();
60
+ addToast({ type: 'success', title: 'Wallet Unlocked', message: 'Welcome back to SolVox' });
61
  if (window.solvox) {
62
+ window.solvox.ai.initialize().then(r => {
63
+ if (r.success) {
64
  window.solvox.ai.getStatus().then(setAiStatus);
65
+ addToast({ type: 'info', title: 'AI Engine Ready', message: '6 QVAC modules loaded locally' });
66
  }
67
  });
68
  }
 
72
  setPublicKey(pk);
73
  setAppState('unlocked');
74
  refreshBalance();
75
+ addToast({ type: 'success', title: 'Wallet Created!', message: 'Your keys are encrypted and stored locally' });
76
  };
77
 
78
  const handleLock = async () => {
79
+ if (window.solvox) await window.solvox.wallet.lock();
 
 
80
  setAppState('locked');
81
  };
82
 
83
+ const navigate = (page: Page) => {
84
+ setPrevPage(currentPage);
85
+ setCurrentPage(page);
86
+ };
 
 
 
 
87
 
88
+ if (appState === 'loading') return <LoadingScreen />;
89
+ if (appState === 'onboarding') return <OnboardingScreen onComplete={handleOnboardingComplete} />;
90
+ if (appState === 'locked') return <LockScreen onUnlock={handleUnlock} />;
91
 
92
  const renderPage = () => {
93
+ const props = { key: currentPage };
94
  switch (currentPage) {
95
+ case 'dashboard': return <Dashboard {...props} balance={balance} publicKey={publicKey} onRefresh={refreshBalance} onNavigate={navigate} />;
96
+ case 'send': return <SendPage {...props} balance={balance} onSent={refreshBalance} />;
97
+ case 'history': return <HistoryPage {...props} />;
98
+ case 'voice': return <VoicePage {...props} aiStatus={aiStatus} />;
99
+ case 'security': return <SecurityPage {...props} />;
100
+ case 'settings': return <SettingsPage {...props} onLock={handleLock} />;
101
+ default: return <Dashboard balance={balance} publicKey={publicKey} onRefresh={refreshBalance} onNavigate={navigate} />;
 
 
 
 
 
 
 
102
  }
103
  };
104
 
105
  return (
106
+ <div className="flex h-screen mesh-bg">
107
+ <Sidebar currentPage={currentPage} onNavigate={navigate} />
108
  <div className="flex-1 flex flex-col overflow-hidden">
109
+ <TopBar publicKey={publicKey} balance={balance} aiStatus={aiStatus} onLock={handleLock} />
110
+ <main className="flex-1 overflow-y-auto p-5">
111
+ <div className="page-enter">{renderPage()}</div>
 
 
 
 
 
112
  </main>
113
  </div>
114
  </div>
115
  );
116
  }
117
 
118
+ export default function App() {
119
+ return (
120
+ <ToastProvider>
121
+ <AppContent />
122
+ </ToastProvider>
123
+ );
124
+ }
125
+
126
  function LoadingScreen() {
127
  return (
128
+ <div className="h-screen flex flex-col items-center justify-center mesh-bg relative">
129
+ {/* Background particles */}
130
+ <div className="absolute inset-0 overflow-hidden pointer-events-none">
131
+ {Array.from({ length: 12 }, (_, i) => (
132
+ <div key={i} className="particle" style={{
133
+ width: 2 + Math.random() * 3, height: 2 + Math.random() * 3,
134
+ left: `${10 + Math.random() * 80}%`, top: `${10 + Math.random() * 80}%`,
135
+ background: i % 2 === 0 ? '#9945FF' : '#14F195',
136
+ '--dur': `${5 + Math.random() * 5}s`, '--opacity': 0.1 + Math.random() * 0.15,
137
+ '--x1': `${-20 + Math.random() * 40}px`, '--y1': `${-30 + Math.random() * 60}px`,
138
+ '--x2': `${-20 + Math.random() * 40}px`, '--y2': `${-40 + Math.random() * 80}px`,
139
+ '--x3': `${-20 + Math.random() * 40}px`, '--y3': `${-20 + Math.random() * 40}px`,
140
+ } as any} />
141
  ))}
142
  </div>
143
+
144
+ {/* Logo */}
145
+ <div className="relative z-10 flex flex-col items-center">
146
+ <div className="w-20 h-20 rounded-3xl bg-gradient-to-br from-sol-purple to-sol-green flex items-center justify-center mb-6 shadow-glow-lg animate-float">
147
+ <span className="text-white text-2xl font-black">SV</span>
148
+ </div>
149
+ <div className="text-4xl font-extrabold gradient-text mb-2">SolVox</div>
150
+ <div className="text-sol-muted text-sm mb-8">Initializing local AI engine...</div>
151
+
152
+ {/* Orbit rings */}
153
+ <div className="relative w-16 h-16">
154
+ <div className="orbit-ring absolute inset-0" />
155
+ <div className="orbit-ring orbit-ring-2 absolute inset-2" />
156
+ <div className="absolute inset-0 flex items-center justify-center">
157
+ <div className="w-2 h-2 rounded-full bg-sol-purple animate-pulse" />
158
+ </div>
159
+ </div>
160
+
161
+ <div className="mt-6 text-[11px] text-sol-muted/60">Powered by QVAC SDK • 100% Local AI</div>
162
+ </div>
163
  </div>
164
  );
165
  }
src/renderer/components/Sidebar.tsx CHANGED
@@ -1,55 +1,118 @@
1
- import React from 'react';
2
 
3
  interface SidebarProps {
4
  currentPage: string;
5
  onNavigate: (page: any) => void;
 
6
  }
7
 
8
  const navItems = [
9
- { id: 'dashboard', label: 'Dashboard', icon: '🏠' },
10
- { id: 'voice', label: 'Voice AI', icon: '🎤' },
11
- { id: 'send', label: 'Send', icon: '📤' },
12
- { id: 'history', label: 'History', icon: '📋' },
13
- { id: 'security', label: 'Security', icon: '🛡️' },
14
- { id: 'settings', label: 'Settings', icon: '⚙' },
15
  ];
16
 
17
  export default function Sidebar({ currentPage, onNavigate }: SidebarProps) {
 
 
18
  return (
19
- <aside className="w-64 bg-sol-card border-r border-sol-border flex flex-col">
20
  {/* Logo */}
21
- <div className="p-6 border-b border-sol-border">
22
- <h1 className="text-2xl font-bold gradient-text">SolVox</h1>
23
- <p className="text-xs text-sol-muted mt-1">Voice-First AI Wallet</p>
 
 
 
 
 
 
 
24
  </div>
25
 
 
 
26
  {/* Navigation */}
27
- <nav className="flex-1 p-4 space-y-1">
28
- {navItems.map(item => (
29
- <button
30
- key={item.id}
31
- onClick={() => onNavigate(item.id)}
32
- className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl text-left transition-all duration-200 ${
33
- currentPage === item.id
34
- ? 'bg-sol-purple/20 text-sol-purple border border-sol-purple/30'
35
- : 'text-sol-muted hover:bg-sol-border/30 hover:text-sol-text'
36
- }`}
37
- >
38
- <span className="text-lg">{item.icon}</span>
39
- <span className="font-medium text-sm">{item.label}</span>
40
- </button>
41
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  </nav>
43
 
44
- {/* QVAC Badge */}
45
- <div className="p-4 border-t border-sol-border">
46
- <div className="glass rounded-xl p-4 text-center">
47
- <div className="text-xs text-sol-muted mb-1">Powered by</div>
48
- <div className="text-sm font-bold text-tether-green">QVAC SDK</div>
49
- <div className="text-xs text-sol-muted mt-1">100% Local AI</div>
50
- <div className="flex justify-center gap-1 mt-2">
51
- <span className="w-2 h-2 rounded-full bg-sol-green animate-pulse" />
52
- <span className="text-xs text-sol-green">All AI runs on-device</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  </div>
54
  </div>
55
  </div>
 
1
+ import React, { useState } from 'react';
2
 
3
  interface SidebarProps {
4
  currentPage: string;
5
  onNavigate: (page: any) => void;
6
+ collapsed?: boolean;
7
  }
8
 
9
  const navItems = [
10
+ { id: 'dashboard', label: 'Dashboard', icon: '', activeIcon: '◉' },
11
+ { id: 'voice', label: 'Voice AI', icon: '', activeIcon: '◎' },
12
+ { id: 'send', label: 'Send', icon: '', activeIcon: '↗' },
13
+ { id: 'history', label: 'History', icon: '', activeIcon: '☰' },
14
+ { id: 'security', label: 'Security', icon: '', activeIcon: '◈' },
15
+ { id: 'settings', label: 'Settings', icon: '⚙', activeIcon: '⚙' },
16
  ];
17
 
18
  export default function Sidebar({ currentPage, onNavigate }: SidebarProps) {
19
+ const [hovered, setHovered] = useState<string | null>(null);
20
+
21
  return (
22
+ <aside className="w-[220px] flex flex-col bg-sol-darker/50 border-r border-sol-border/40">
23
  {/* Logo */}
24
+ <div className="px-5 pt-7 pb-5">
25
+ <div className="flex items-center gap-2.5">
26
+ <div className="w-9 h-9 rounded-xl bg-gradient-to-br from-sol-purple to-sol-green flex items-center justify-center shadow-glow-sm">
27
+ <span className="text-white text-sm font-black">SV</span>
28
+ </div>
29
+ <div>
30
+ <div className="text-base font-extrabold tracking-tight gradient-text-static">SolVox</div>
31
+ <div className="text-[10px] text-sol-muted font-medium -mt-0.5">Voice AI Wallet</div>
32
+ </div>
33
+ </div>
34
  </div>
35
 
36
+ <div className="divider mx-5" />
37
+
38
  {/* Navigation */}
39
+ <nav className="flex-1 px-3 py-4 space-y-0.5">
40
+ {navItems.map(item => {
41
+ const isActive = currentPage === item.id;
42
+ const isHovered = hovered === item.id;
43
+ return (
44
+ <button
45
+ key={item.id}
46
+ onClick={() => onNavigate(item.id)}
47
+ onMouseEnter={() => setHovered(item.id)}
48
+ onMouseLeave={() => setHovered(null)}
49
+ className={`relative w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-left transition-all duration-300 group ${
50
+ isActive
51
+ ? 'bg-sol-purple/12 text-sol-text'
52
+ : 'text-sol-muted hover:text-sol-text hover:bg-white/[0.03]'
53
+ }`}
54
+ >
55
+ {/* Active indicator bar */}
56
+ {isActive && <div className="nav-active-bar" />}
57
+
58
+ {/* Icon */}
59
+ <div className={`w-8 h-8 rounded-lg flex items-center justify-center text-sm font-bold transition-all duration-300 ${
60
+ isActive
61
+ ? 'bg-sol-purple/20 text-sol-purple'
62
+ : 'bg-transparent text-sol-muted group-hover:bg-sol-purple/8 group-hover:text-sol-purple'
63
+ }`}>
64
+ {item.icon}
65
+ </div>
66
+
67
+ {/* Label */}
68
+ <span className={`text-[13px] font-medium transition-colors ${
69
+ isActive ? 'text-sol-text' : ''
70
+ }`}>
71
+ {item.label}
72
+ </span>
73
+
74
+ {/* Hover glow */}
75
+ {(isActive || isHovered) && (
76
+ <div className="absolute inset-0 rounded-xl bg-sol-purple/[0.04] pointer-events-none" />
77
+ )}
78
+ </button>
79
+ );
80
+ })}
81
  </nav>
82
 
83
+ {/* QVAC Engine Badge */}
84
+ <div className="px-3 pb-4">
85
+ <div className="relative overflow-hidden rounded-2xl border border-sol-border/40 p-4">
86
+ {/* Subtle background gradient */}
87
+ <div className="absolute inset-0 bg-gradient-to-br from-tether-green/5 to-sol-purple/5" />
88
+
89
+ <div className="relative">
90
+ <div className="flex items-center gap-2 mb-2.5">
91
+ <div className="w-6 h-6 rounded-md bg-tether-green/15 flex items-center justify-center">
92
+ <span className="text-tether-green text-xs font-bold">Q</span>
93
+ </div>
94
+ <div>
95
+ <div className="text-[11px] font-bold text-tether-green">QVAC Engine</div>
96
+ <div className="text-[9px] text-sol-muted">6 AI modules active</div>
97
+ </div>
98
+ </div>
99
+
100
+ {/* Module dots */}
101
+ <div className="flex items-center gap-1">
102
+ {['LLM', 'STT', 'TTS', 'EMB', 'NMT', 'OCR'].map((m, i) => (
103
+ <div key={m} className="flex flex-col items-center">
104
+ <div className="w-1.5 h-1.5 rounded-full bg-sol-green animate-pulse" style={{ animationDelay: `${i * 200}ms` }} />
105
+ <span className="text-[7px] text-sol-muted mt-0.5">{m}</span>
106
+ </div>
107
+ ))}
108
+ </div>
109
+
110
+ <div className="mt-2.5 pt-2 border-t border-sol-border/30">
111
+ <div className="flex items-center gap-1.5">
112
+ <div className="w-1.5 h-1.5 rounded-full bg-sol-green" />
113
+ <span className="text-[9px] text-sol-muted">100% local • Vulkan GPU</span>
114
+ </div>
115
+ </div>
116
  </div>
117
  </div>
118
  </div>
src/renderer/components/TopBar.tsx CHANGED
@@ -1,4 +1,5 @@
1
- import React from 'react';
 
2
 
3
  interface TopBarProps {
4
  publicKey: string | null;
@@ -8,77 +9,91 @@ interface TopBarProps {
8
  }
9
 
10
  export default function TopBar({ publicKey, balance, aiStatus, onLock }: TopBarProps) {
11
- const shortAddress = publicKey
12
- ? `${publicKey.slice(0, 6)}...${publicKey.slice(-4)}`
13
- : '—';
14
 
15
  const copyAddress = () => {
16
  if (publicKey) {
17
  navigator.clipboard.writeText(publicKey);
 
 
18
  }
19
  };
20
 
 
 
 
 
21
  return (
22
- <header className="h-16 border-b border-sol-border flex items-center justify-between px-6 bg-sol-card/50">
23
- {/* Left: Wallet Address */}
24
- <div className="flex items-center gap-4">
25
  <button
26
  onClick={copyAddress}
27
- className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-sol-dark/50 border border-sol-border hover:border-sol-purple transition-colors"
28
- title="Click to copy address"
29
  >
30
- <span className="w-2 h-2 rounded-full bg-sol-green" />
31
- <span className="text-sm font-mono text-sol-muted">{shortAddress}</span>
32
- <span className="text-xs">📋</span>
33
  </button>
34
- <div className="text-xs text-sol-muted">Devnet</div>
 
 
 
 
35
  </div>
36
 
37
- {/* Center: Balance */}
38
- <div className="flex items-center gap-6">
39
- <div className="text-center">
40
- <div className="text-xs text-sol-muted">SOL</div>
41
- <div className="text-sm font-bold text-sol-purple">
42
- {balance.sol.toFixed(4)}
 
 
 
 
 
43
  </div>
44
  </div>
45
- <div className="w-px h-8 bg-sol-border" />
46
- <div className="text-center">
47
- <div className="text-xs text-sol-muted">USDT</div>
48
- <div className="text-sm font-bold text-tether-green">
49
- {balance.usdt.toFixed(2)}
 
 
 
 
 
 
 
50
  </div>
51
  </div>
52
  </div>
53
 
54
  {/* Right: AI Status + Lock */}
55
  <div className="flex items-center gap-3">
56
- {/* AI Status Indicators */}
57
- <div className="flex items-center gap-1.5">
58
- <StatusDot active={aiStatus?.llm} label="LLM" />
59
- <StatusDot active={aiStatus?.transcription} label="STT" />
60
- <StatusDot active={aiStatus?.tts} label="TTS" />
61
- <StatusDot active={aiStatus?.embed} label="EMB" />
62
- <StatusDot active={aiStatus?.translation} label="NMT" />
63
- <StatusDot active={aiStatus?.ocr} label="OCR" />
64
  </div>
65
 
 
66
  <button
67
  onClick={onLock}
68
- className="px-3 py-1.5 rounded-lg bg-danger/10 text-danger text-sm hover:bg-danger/20 transition-colors"
69
  >
70
- 🔒 Lock
 
 
 
71
  </button>
72
  </div>
73
  </header>
74
  );
75
  }
76
-
77
- function StatusDot({ active, label }: { active: boolean; label: string }) {
78
- return (
79
- <div className="flex flex-col items-center" title={`${label}: ${active ? 'Active' : 'Offline'}`}>
80
- <div className={`w-1.5 h-1.5 rounded-full ${active ? 'bg-sol-green' : 'bg-sol-muted/30'}`} />
81
- <span className="text-[8px] text-sol-muted mt-0.5">{label}</span>
82
- </div>
83
- );
84
- }
 
1
+ import React, { useState } from 'react';
2
+ import { AnimatedNumber } from './ui/index';
3
 
4
  interface TopBarProps {
5
  publicKey: string | null;
 
9
  }
10
 
11
  export default function TopBar({ publicKey, balance, aiStatus, onLock }: TopBarProps) {
12
+ const [copied, setCopied] = useState(false);
13
+ const shortAddress = publicKey ? `${publicKey.slice(0, 4)}...${publicKey.slice(-4)}` : '—';
 
14
 
15
  const copyAddress = () => {
16
  if (publicKey) {
17
  navigator.clipboard.writeText(publicKey);
18
+ setCopied(true);
19
+ setTimeout(() => setCopied(false), 2000);
20
  }
21
  };
22
 
23
+ const activeModules = aiStatus
24
+ ? [aiStatus.llm, aiStatus.transcription, aiStatus.tts, aiStatus.embed, aiStatus.translation, aiStatus.ocr].filter(Boolean).length
25
+ : 0;
26
+
27
  return (
28
+ <header className="h-14 border-b border-sol-border/30 flex items-center justify-between px-5 bg-sol-darker/30 backdrop-blur-sm">
29
+ {/* Left: Address + Network */}
30
+ <div className="flex items-center gap-3">
31
  <button
32
  onClick={copyAddress}
33
+ className="flex items-center gap-2 px-2.5 py-1.5 rounded-lg bg-sol-card/50 border border-sol-border/40 hover:border-sol-purple/30 transition-all group"
34
+ title="Copy full address"
35
  >
36
+ <div className="w-1.5 h-1.5 rounded-full bg-sol-green" />
37
+ <span className="text-xs font-mono text-sol-muted group-hover:text-sol-text transition-colors">{shortAddress}</span>
38
+ <span className="text-[10px] text-sol-muted">{copied ? '✓' : '⊕'}</span>
39
  </button>
40
+
41
+ <div className="badge badge-warn">
42
+ <div className="w-1 h-1 rounded-full bg-warning" />
43
+ Devnet
44
+ </div>
45
  </div>
46
 
47
+ {/* Center: Balances */}
48
+ <div className="flex items-center gap-5">
49
+ <div className="flex items-center gap-2">
50
+ <div className="w-6 h-6 rounded-full bg-gradient-to-br from-sol-purple to-sol-purple-light flex items-center justify-center">
51
+ <span className="text-white text-[10px] font-bold">◎</span>
52
+ </div>
53
+ <div>
54
+ <div className="text-[10px] text-sol-muted leading-none">SOL</div>
55
+ <div className="text-sm font-bold text-sol-text leading-tight">
56
+ <AnimatedNumber value={balance.sol} decimals={4} />
57
+ </div>
58
  </div>
59
  </div>
60
+
61
+ <div className="w-px h-7 bg-sol-border/40" />
62
+
63
+ <div className="flex items-center gap-2">
64
+ <div className="w-6 h-6 rounded-full bg-gradient-to-br from-tether-green to-tether-dark flex items-center justify-center">
65
+ <span className="text-white text-[10px] font-bold">₮</span>
66
+ </div>
67
+ <div>
68
+ <div className="text-[10px] text-sol-muted leading-none">USDT</div>
69
+ <div className="text-sm font-bold text-sol-text leading-tight">
70
+ <AnimatedNumber value={balance.usdt} decimals={2} />
71
+ </div>
72
  </div>
73
  </div>
74
  </div>
75
 
76
  {/* Right: AI Status + Lock */}
77
  <div className="flex items-center gap-3">
78
+ {/* AI Status */}
79
+ <div className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-sol-card/30 border border-sol-border/30">
80
+ <div className={`w-1.5 h-1.5 rounded-full ${activeModules > 0 ? 'bg-sol-green animate-pulse' : 'bg-sol-muted/30'}`} />
81
+ <span className="text-[10px] font-medium text-sol-muted">
82
+ {activeModules > 0 ? `${activeModules}/6 AI` : 'AI Loading'}
83
+ </span>
 
 
84
  </div>
85
 
86
+ {/* Lock Button */}
87
  <button
88
  onClick={onLock}
89
+ className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border border-sol-border/40 text-sol-muted hover:text-danger hover:border-danger/30 hover:bg-danger/5 transition-all"
90
  >
91
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
92
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>
93
+ </svg>
94
+ <span className="text-[11px] font-medium">Lock</span>
95
  </button>
96
  </div>
97
  </header>
98
  );
99
  }
 
 
 
 
 
 
 
 
 
src/renderer/components/ui/index.tsx ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useCallback, createContext, useContext } from 'react';
2
+
3
+ /* ═══════════════════════════════════════════════════════════════════════
4
+ TOAST SYSTEM
5
+ ═══════════════════════════════════════════════════════════════════════ */
6
+
7
+ type ToastType = 'success' | 'error' | 'warning' | 'info';
8
+ interface Toast { id: string; type: ToastType; title: string; message?: string; duration?: number; }
9
+ interface ToastCtx { addToast: (t: Omit<Toast, 'id'>) => void; }
10
+
11
+ const ToastContext = createContext<ToastCtx>({ addToast: () => {} });
12
+ export const useToast = () => useContext(ToastContext);
13
+
14
+ export function ToastProvider({ children }: { children: React.ReactNode }) {
15
+ const [toasts, setToasts] = useState<Toast[]>([]);
16
+
17
+ const addToast = useCallback((t: Omit<Toast, 'id'>) => {
18
+ const id = Date.now().toString(36) + Math.random().toString(36).slice(2);
19
+ setToasts(prev => [...prev, { ...t, id }]);
20
+ setTimeout(() => setToasts(prev => prev.filter(x => x.id !== id)), t.duration ?? 4000);
21
+ }, []);
22
+
23
+ return (
24
+ <ToastContext.Provider value={{ addToast }}>
25
+ {children}
26
+ <div className="fixed top-4 right-4 z-50 flex flex-col gap-2 pointer-events-none">
27
+ {toasts.map(t => (
28
+ <div key={t.id} className="toast-enter pointer-events-auto" onClick={() => setToasts(prev => prev.filter(x => x.id !== t.id))}>
29
+ <div className={`glass-strong rounded-xl px-4 py-3 min-w-[280px] max-w-[380px] shadow-card flex items-start gap-3 cursor-pointer hover:opacity-90 transition-opacity ${
30
+ t.type === 'success' ? 'border-l-2 border-l-sol-green' :
31
+ t.type === 'error' ? 'border-l-2 border-l-danger' :
32
+ t.type === 'warning' ? 'border-l-2 border-l-warning' :
33
+ 'border-l-2 border-l-info'
34
+ }`}>
35
+ <span className="text-lg mt-0.5">
36
+ {t.type === 'success' ? '✅' : t.type === 'error' ? '❌' : t.type === 'warning' ? '⚠️' : 'ℹ️'}
37
+ </span>
38
+ <div className="flex-1 min-w-0">
39
+ <div className="text-sm font-semibold text-sol-text">{t.title}</div>
40
+ {t.message && <div className="text-xs text-sol-muted mt-0.5 truncate">{t.message}</div>}
41
+ </div>
42
+ </div>
43
+ </div>
44
+ ))}
45
+ </div>
46
+ </ToastContext.Provider>
47
+ );
48
+ }
49
+
50
+ /* ═══════════════════════════════════════════════════════════════════════
51
+ MODAL
52
+ ═══════════════════════════════════════════════════════════════════════ */
53
+
54
+ interface ModalProps {
55
+ open: boolean;
56
+ onClose: () => void;
57
+ title?: string;
58
+ children: React.ReactNode;
59
+ size?: 'sm' | 'md' | 'lg';
60
+ }
61
+
62
+ export function Modal({ open, onClose, title, children, size = 'md' }: ModalProps) {
63
+ if (!open) return null;
64
+ const widths = { sm: 'max-w-sm', md: 'max-w-lg', lg: 'max-w-2xl' };
65
+ return (
66
+ <div className="fixed inset-0 z-40 flex items-center justify-center fade-in" onClick={onClose}>
67
+ <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
68
+ <div
69
+ className={`relative ${widths[size]} w-full mx-4 glass-strong rounded-3xl p-6 shadow-card scale-in`}
70
+ onClick={e => e.stopPropagation()}
71
+ >
72
+ {title && (
73
+ <div className="flex items-center justify-between mb-5">
74
+ <h3 className="text-lg font-bold text-sol-text">{title}</h3>
75
+ <button onClick={onClose} className="w-8 h-8 rounded-lg flex items-center justify-center text-sol-muted hover:text-sol-text hover:bg-sol-border/30 transition-all">✕</button>
76
+ </div>
77
+ )}
78
+ {children}
79
+ </div>
80
+ </div>
81
+ );
82
+ }
83
+
84
+ /* ═══════════════════════════════════════════════════════════════════════
85
+ SHIMMER SKELETON
86
+ ═══════════════════════════════════════════════════════════════════════ */
87
+
88
+ export function Shimmer({ className = '', height = 'h-4' }: { className?: string; height?: string }) {
89
+ return <div className={`shimmer rounded-lg ${height} ${className}`} />;
90
+ }
91
+
92
+ export function ShimmerCard() {
93
+ return (
94
+ <div className="glass rounded-2xl p-6 space-y-3">
95
+ <Shimmer height="h-3" className="w-24" />
96
+ <Shimmer height="h-8" className="w-40" />
97
+ <Shimmer height="h-3" className="w-32" />
98
+ </div>
99
+ );
100
+ }
101
+
102
+ /* ═══════════════════════════════════════════════════════════════════════
103
+ ANIMATED COUNTER (for balance display)
104
+ ═══════════════════════════════════════════════════════════════════════ */
105
+
106
+ export function AnimatedNumber({ value, decimals = 2, prefix = '', suffix = '', className = '' }: {
107
+ value: number; decimals?: number; prefix?: string; suffix?: string; className?: string;
108
+ }) {
109
+ const [display, setDisplay] = useState(value);
110
+
111
+ useEffect(() => {
112
+ const diff = value - display;
113
+ if (Math.abs(diff) < 0.001) { setDisplay(value); return; }
114
+ const steps = 20;
115
+ let step = 0;
116
+ const interval = setInterval(() => {
117
+ step++;
118
+ const progress = step / steps;
119
+ const eased = 1 - Math.pow(1 - progress, 3); // easeOutCubic
120
+ setDisplay(display + diff * eased);
121
+ if (step >= steps) { setDisplay(value); clearInterval(interval); }
122
+ }, 25);
123
+ return () => clearInterval(interval);
124
+ }, [value]);
125
+
126
+ return (
127
+ <span className={`ticker ${className}`}>
128
+ {prefix}{display.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals })}{suffix}
129
+ </span>
130
+ );
131
+ }
132
+
133
+ /* ═══════════════════════════════════════════════════════════════════════
134
+ CIRCULAR PROGRESS (for security score)
135
+ ═══════════════════════════════════════════════════════════════════════ */
136
+
137
+ export function CircularProgress({ value, size = 80, strokeWidth = 6, color = '#9945FF', children }: {
138
+ value: number; size?: number; strokeWidth?: number; color?: string; children?: React.ReactNode;
139
+ }) {
140
+ const r = (size - strokeWidth) / 2;
141
+ const circ = 2 * Math.PI * r;
142
+ const offset = circ - (value / 100) * circ;
143
+ return (
144
+ <div className="relative inline-flex items-center justify-center" style={{ width: size, height: size }}>
145
+ <svg width={size} height={size} className="-rotate-90">
146
+ <circle cx={size/2} cy={size/2} r={r} fill="none" stroke="rgba(45,45,94,0.4)" strokeWidth={strokeWidth} />
147
+ <circle cx={size/2} cy={size/2} r={r} fill="none" stroke={color} strokeWidth={strokeWidth}
148
+ strokeDasharray={circ} strokeDashoffset={offset} strokeLinecap="round"
149
+ className="transition-all duration-1000 ease-out" />
150
+ </svg>
151
+ <div className="absolute inset-0 flex items-center justify-center">{children}</div>
152
+ </div>
153
+ );
154
+ }
155
+
156
+ /* ═══════════════════════════════════════════════════════════════════════
157
+ PARTICLES BACKGROUND (for lock/onboarding screens)
158
+ ═══════════════════════════════════════════════════════════════════════ */
159
+
160
+ export function ParticlesBackground() {
161
+ const particles = Array.from({ length: 20 }, (_, i) => ({
162
+ id: i,
163
+ size: 2 + Math.random() * 4,
164
+ x: Math.random() * 100,
165
+ y: Math.random() * 100,
166
+ dur: 4 + Math.random() * 8,
167
+ opacity: 0.05 + Math.random() * 0.15,
168
+ color: i % 3 === 0 ? '#9945FF' : i % 3 === 1 ? '#14F195' : '#26A17B',
169
+ x1: -30 + Math.random() * 60, y1: -40 + Math.random() * 80,
170
+ x2: -30 + Math.random() * 60, y2: -60 + Math.random() * 120,
171
+ x3: -30 + Math.random() * 60, y3: -30 + Math.random() * 60,
172
+ }));
173
+
174
+ return (
175
+ <div className="absolute inset-0 overflow-hidden pointer-events-none">
176
+ {particles.map(p => (
177
+ <div key={p.id} className="particle" style={{
178
+ width: p.size, height: p.size,
179
+ left: `${p.x}%`, top: `${p.y}%`,
180
+ background: p.color,
181
+ '--dur': `${p.dur}s`, '--opacity': p.opacity,
182
+ '--x1': `${p.x1}px`, '--y1': `${p.y1}px`,
183
+ '--x2': `${p.x2}px`, '--y2': `${p.y2}px`,
184
+ '--x3': `${p.x3}px`, '--y3': `${p.y3}px`,
185
+ } as any} />
186
+ ))}
187
+ </div>
188
+ );
189
+ }
190
+
191
+ /* ═══════════════════════════════════════════════════════════════════════
192
+ ORBIT LOADER (for AI loading states)
193
+ ══════════════════════════════════════════════════════════════════���════ */
194
+
195
+ export function OrbitLoader({ size = 64 }: { size?: number }) {
196
+ return (
197
+ <div className="relative" style={{ width: size, height: size }}>
198
+ <div className="orbit-ring absolute inset-0" />
199
+ <div className="orbit-ring orbit-ring-2 absolute" style={{ inset: size * 0.15 }} />
200
+ <div className="orbit-ring orbit-ring-3 absolute" style={{ inset: size * 0.3 }} />
201
+ <div className="absolute inset-0 flex items-center justify-center">
202
+ <div className="w-2 h-2 rounded-full bg-sol-purple animate-pulse" />
203
+ </div>
204
+ </div>
205
+ );
206
+ }
207
+
208
+ /* ═══════════════════════════════════════════════════════════════════════
209
+ STEP INDICATOR (for onboarding)
210
+ ═══════════════════════════════════════════════════════════════════════ */
211
+
212
+ export function StepIndicator({ steps, current }: { steps: string[]; current: number }) {
213
+ return (
214
+ <div className="flex items-center justify-center gap-2 mb-8">
215
+ {steps.map((label, i) => (
216
+ <React.Fragment key={i}>
217
+ <div className="flex flex-col items-center gap-1">
218
+ <div className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold transition-all duration-500 ${
219
+ i < current ? 'bg-sol-green text-sol-dark' :
220
+ i === current ? 'bg-sol-purple text-white glow-purple' :
221
+ 'bg-sol-border text-sol-muted'
222
+ }`}>
223
+ {i < current ? '✓' : i + 1}
224
+ </div>
225
+ <span className={`text-[10px] ${i <= current ? 'text-sol-text' : 'text-sol-muted'}`}>{label}</span>
226
+ </div>
227
+ {i < steps.length - 1 && (
228
+ <div className={`w-12 h-0.5 rounded-full transition-all duration-500 -mt-4 ${
229
+ i < current ? 'bg-sol-green' : 'bg-sol-border'
230
+ }`} />
231
+ )}
232
+ </React.Fragment>
233
+ ))}
234
+ </div>
235
+ );
236
+ }
237
+
238
+ /* ═══════════════════════════════════════════════════════════════════════
239
+ ICON BUTTON
240
+ ═══════════════════════════════════════════════════════════════════════ */
241
+
242
+ export function IconButton({ icon, label, onClick, active, color = 'purple', size = 'md' }: {
243
+ icon: string; label: string; onClick?: () => void; active?: boolean; color?: 'purple' | 'green' | 'tether' | 'danger' | 'warning';
244
+ size?: 'sm' | 'md' | 'lg';
245
+ }) {
246
+ const sizes = { sm: 'w-10 h-10 text-lg', md: 'w-14 h-14 text-xl', lg: 'w-20 h-20 text-3xl' };
247
+ const colors = {
248
+ purple: active ? 'bg-sol-purple/20 text-sol-purple border-sol-purple/30' : 'bg-sol-card text-sol-muted border-sol-border hover:border-sol-purple/30 hover:text-sol-purple',
249
+ green: active ? 'bg-sol-green/20 text-sol-green border-sol-green/30' : 'bg-sol-card text-sol-muted border-sol-border hover:border-sol-green/30 hover:text-sol-green',
250
+ tether: active ? 'bg-tether-green/20 text-tether-green border-tether-green/30' : 'bg-sol-card text-sol-muted border-sol-border hover:border-tether-green/30 hover:text-tether-green',
251
+ danger: 'bg-sol-card text-sol-muted border-sol-border hover:border-danger/30 hover:text-danger',
252
+ warning: 'bg-sol-card text-sol-muted border-sol-border hover:border-warning/30 hover:text-warning',
253
+ };
254
+ return (
255
+ <button onClick={onClick} className="flex flex-col items-center gap-1.5 group">
256
+ <div className={`${sizes[size]} rounded-2xl border flex items-center justify-center transition-all duration-300 ${colors[color]} group-hover:scale-105`}>
257
+ {icon}
258
+ </div>
259
+ <span className="text-[11px] font-medium text-sol-muted group-hover:text-sol-text transition-colors">{label}</span>
260
+ </button>
261
+ );
262
+ }
src/renderer/index.css CHANGED
@@ -2,91 +2,256 @@
2
  @tailwind components;
3
  @tailwind utilities;
4
 
5
- * {
6
- margin: 0;
7
- padding: 0;
8
- box-sizing: border-box;
9
- }
10
 
11
  body {
12
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
13
- background: #0E0E2C;
14
  color: #E0E0FF;
15
  overflow: hidden;
16
  height: 100vh;
17
  -webkit-app-region: drag;
18
  }
 
 
 
 
 
 
 
19
 
20
- button, input, select, textarea, a {
21
- -webkit-app-region: no-drag;
 
 
 
 
 
22
  }
23
 
24
- /* Custom scrollbar */
25
- ::-webkit-scrollbar {
26
- width: 6px;
 
 
 
27
  }
28
- ::-webkit-scrollbar-track {
29
- background: transparent;
 
 
 
30
  }
31
- ::-webkit-scrollbar-thumb {
32
- background: #2D2D5E;
33
- border-radius: 3px;
 
 
 
34
  }
35
- ::-webkit-scrollbar-thumb:hover {
36
- background: #9945FF;
 
 
 
 
 
 
37
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
- /* Waveform animation bars */
40
- .waveform-bar {
41
- animation: waveform var(--delay, 0.5s) ease-in-out infinite alternate;
 
 
42
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
- @keyframes waveform {
45
- 0% { height: 4px; }
46
- 100% { height: var(--max-height, 24px); }
 
47
  }
 
48
 
49
- /* Glow effects */
50
- .glow-purple {
51
- box-shadow: 0 0 20px rgba(153, 69, 255, 0.3);
 
 
 
52
  }
53
- .glow-green {
54
- box-shadow: 0 0 20px rgba(20, 241, 149, 0.3);
 
 
 
 
 
 
 
 
55
  }
56
- .glow-tether {
57
- box-shadow: 0 0 20px rgba(38, 161, 123, 0.3);
 
 
 
58
  }
59
 
60
- /* Gradient text */
61
- .gradient-text {
62
- background: linear-gradient(135deg, #9945FF, #14F195);
63
- -webkit-background-clip: text;
64
- -webkit-text-fill-color: transparent;
65
- background-clip: text;
 
 
 
 
 
 
66
  }
 
 
 
 
67
 
68
- /* Glass effect */
69
- .glass {
70
- background: rgba(26, 26, 62, 0.6);
71
- backdrop-filter: blur(20px);
72
- border: 1px solid rgba(45, 45, 94, 0.5);
 
73
  }
74
 
75
- /* Pulse recording indicator */
76
- @keyframes recording-pulse {
77
- 0%, 100% { opacity: 1; transform: scale(1); }
78
- 50% { opacity: 0.5; transform: scale(1.1); }
79
  }
80
- .recording-pulse {
81
- animation: recording-pulse 1.5s ease-in-out infinite;
 
82
  }
83
 
84
- /* Slide transitions */
85
- .slide-enter {
86
- animation: slide-up 0.3s ease-out;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  }
88
 
89
- @keyframes slide-up {
90
- from { transform: translateY(20px); opacity: 0; }
91
- to { transform: translateY(0); opacity: 1; }
 
 
 
 
92
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  @tailwind components;
3
  @tailwind utilities;
4
 
5
+ /* ─── Reset & Base ───────────────────────────────────────────────────── */
6
+ * { margin: 0; padding: 0; box-sizing: border-box; }
 
 
 
7
 
8
  body {
9
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
10
+ background: #0A0A1E;
11
  color: #E0E0FF;
12
  overflow: hidden;
13
  height: 100vh;
14
  -webkit-app-region: drag;
15
  }
16
+ button, input, select, textarea, a, label { -webkit-app-region: no-drag; }
17
+
18
+ /* ─── Scrollbar ──────────────────────────────────────────────────────── */
19
+ ::-webkit-scrollbar { width: 5px; }
20
+ ::-webkit-scrollbar-track { background: transparent; }
21
+ ::-webkit-scrollbar-thumb { background: #2D2D5E; border-radius: 10px; }
22
+ ::-webkit-scrollbar-thumb:hover { background: #9945FF; }
23
 
24
+ /* ─── Gradient Mesh Background ───────────────────────────────────────── */
25
+ .mesh-bg {
26
+ background:
27
+ radial-gradient(ellipse at 20% 50%, rgba(153, 69, 255, 0.08) 0%, transparent 50%),
28
+ radial-gradient(ellipse at 80% 20%, rgba(20, 241, 149, 0.05) 0%, transparent 50%),
29
+ radial-gradient(ellipse at 50% 80%, rgba(38, 161, 123, 0.06) 0%, transparent 50%),
30
+ #0A0A1E;
31
  }
32
 
33
+ /* ─── Glassmorphism ──────────────────────────────────────────────────── */
34
+ .glass {
35
+ background: rgba(22, 22, 52, 0.65);
36
+ backdrop-filter: blur(24px) saturate(1.4);
37
+ -webkit-backdrop-filter: blur(24px) saturate(1.4);
38
+ border: 1px solid rgba(153, 69, 255, 0.08);
39
  }
40
+ .glass-strong {
41
+ background: rgba(26, 26, 62, 0.85);
42
+ backdrop-filter: blur(32px) saturate(1.6);
43
+ -webkit-backdrop-filter: blur(32px) saturate(1.6);
44
+ border: 1px solid rgba(153, 69, 255, 0.12);
45
  }
46
+ .glass-hover { transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
47
+ .glass-hover:hover {
48
+ background: rgba(30, 30, 68, 0.8);
49
+ border-color: rgba(153, 69, 255, 0.2);
50
+ transform: translateY(-2px);
51
+ box-shadow: 0 8px 32px rgba(153, 69, 255, 0.1);
52
  }
53
+
54
+ /* ─── Gradient Text ──────────────────────────────────────────────────── */
55
+ .gradient-text {
56
+ background: linear-gradient(135deg, #9945FF 0%, #14F195 50%, #26A17B 100%);
57
+ background-size: 200% 200%;
58
+ animation: gradient-shift 6s ease infinite;
59
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
60
+ background-clip: text;
61
  }
62
+ .gradient-text-static {
63
+ background: linear-gradient(135deg, #9945FF, #14F195);
64
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
65
+ background-clip: text;
66
+ }
67
+ @keyframes gradient-shift {
68
+ 0%,100% { background-position: 0% 50%; }
69
+ 50% { background-position: 100% 50%; }
70
+ }
71
+
72
+ /* ─── Glow FX ────────────────────────────────────────────────────────── */
73
+ .glow-purple { box-shadow: 0 0 24px rgba(153, 69, 255, 0.25), 0 0 48px rgba(153, 69, 255, 0.08); }
74
+ .glow-green { box-shadow: 0 0 24px rgba(20, 241, 149, 0.25), 0 0 48px rgba(20, 241, 149, 0.08); }
75
+ .glow-tether { box-shadow: 0 0 24px rgba(38, 161, 123, 0.25), 0 0 48px rgba(38, 161, 123, 0.08); }
76
+ .glow-danger { box-shadow: 0 0 24px rgba(255, 68, 102, 0.3); }
77
 
78
+ /* ─── Page Transitions ───────────────────────────────────────────────── */
79
+ .page-enter { animation: page-in 0.4s cubic-bezier(0.4, 0, 0.2, 1); }
80
+ @keyframes page-in {
81
+ from { opacity: 0; transform: translateY(16px) scale(0.98); }
82
+ to { opacity: 1; transform: translateY(0) scale(1); }
83
  }
84
+ .slide-enter { animation: slide-up 0.35s cubic-bezier(0.4, 0, 0.2, 1); }
85
+ @keyframes slide-up {
86
+ from { transform: translateY(24px); opacity: 0; }
87
+ to { transform: translateY(0); opacity: 1; }
88
+ }
89
+ .fade-in { animation: fadein 0.3s ease-out; }
90
+ @keyframes fadein { from { opacity: 0; } to { opacity: 1; } }
91
+ .scale-in { animation: scalein 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); }
92
+ @keyframes scalein {
93
+ from { opacity: 0; transform: scale(0.9); }
94
+ to { opacity: 1; transform: scale(1); }
95
+ }
96
+
97
+ /* ─── Waveform Animation ─────────────────────────────────────────────── */
98
+ .waveform-bar { animation: waveform var(--delay, 0.5s) ease-in-out infinite alternate; }
99
+ @keyframes waveform { 0% { height: 3px; } 100% { height: var(--max-height, 28px); } }
100
+
101
+ /* ─── Shimmer Loader ─────────────────────────────────────────────────── */
102
+ .shimmer {
103
+ background: linear-gradient(90deg, rgba(26,26,62,0.4) 25%, rgba(45,45,94,0.6) 50%, rgba(26,26,62,0.4) 75%);
104
+ background-size: 200% 100%;
105
+ animation: shimmer 1.5s infinite;
106
+ }
107
+ @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
108
 
109
+ /* ─── Recording Pulse ────────────────────────────────────────────────── */
110
+ @keyframes rec-pulse {
111
+ 0%, 100% { opacity: 1; transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 68, 102, 0.4); }
112
+ 50% { opacity: 0.85; transform: scale(1.08); box-shadow: 0 0 0 16px rgba(255, 68, 102, 0); }
113
  }
114
+ .recording-pulse { animation: rec-pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
115
 
116
+ /* ─── Orbit Rings (loading / voice) ──────────────────────────────────── */
117
+ .orbit-ring {
118
+ border: 2px solid transparent;
119
+ border-top-color: #9945FF;
120
+ border-radius: 50%;
121
+ animation: orbit 2s linear infinite;
122
  }
123
+ .orbit-ring-2 { border-top-color: #14F195; animation-duration: 3s; animation-direction: reverse; }
124
+ .orbit-ring-3 { border-top-color: #26A17B; animation-duration: 4s; }
125
+ @keyframes orbit { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
126
+
127
+ /* ─── Floating Particles ─────────────────────────────────────────────── */
128
+ .particle {
129
+ position: absolute;
130
+ border-radius: 50%;
131
+ animation: float var(--dur, 6s) ease-in-out infinite;
132
+ opacity: var(--opacity, 0.15);
133
  }
134
+ @keyframes float {
135
+ 0%, 100% { transform: translate(0, 0) scale(1); }
136
+ 25% { transform: translate(var(--x1, 20px), var(--y1, -30px)) scale(1.1); }
137
+ 50% { transform: translate(var(--x2, -15px), var(--y2, -50px)) scale(0.9); }
138
+ 75% { transform: translate(var(--x3, 25px), var(--y3, -20px)) scale(1.05); }
139
  }
140
 
141
+ /* ─── Animated Counter ───────────────────────────────────────────────── */
142
+ .num-scroll { display: inline-block; transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1); }
143
+
144
+ /* ─── Button Hover FX ────────────────────────────────────────────────── */
145
+ .btn-primary {
146
+ @apply relative overflow-hidden rounded-xl font-semibold transition-all duration-300;
147
+ background: linear-gradient(135deg, #9945FF, #7B3FE4);
148
+ }
149
+ .btn-primary::before {
150
+ content: '';
151
+ @apply absolute inset-0 rounded-xl opacity-0 transition-opacity duration-300;
152
+ background: linear-gradient(135deg, #B06CFF, #9945FF);
153
  }
154
+ .btn-primary:hover::before { @apply opacity-100; }
155
+ .btn-primary:hover { transform: translateY(-1px); box-shadow: 0 8px 24px rgba(153, 69, 255, 0.3); }
156
+ .btn-primary:active { transform: translateY(0); }
157
+ .btn-primary > * { @apply relative z-10; }
158
 
159
+ .btn-ghost {
160
+ @apply rounded-xl font-medium transition-all duration-300 border border-sol-border text-sol-muted;
161
+ }
162
+ .btn-ghost:hover {
163
+ @apply text-sol-text border-sol-purple/40;
164
+ background: rgba(153, 69, 255, 0.06);
165
  }
166
 
167
+ /* ─── Input Focus Ring ───────────────────────────────────────────────── */
168
+ .input-field {
169
+ @apply w-full px-4 py-3 bg-sol-dark/80 border border-sol-border rounded-xl text-sm transition-all duration-200;
 
170
  }
171
+ .input-field:focus {
172
+ @apply outline-none border-sol-purple/60;
173
+ box-shadow: 0 0 0 3px rgba(153, 69, 255, 0.1);
174
  }
175
 
176
+ /* ─── Badge ──────────────────────────────────────────────────────────── */
177
+ .badge { @apply inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium; }
178
+ .badge-green { @apply bg-sol-green/15 text-sol-green; }
179
+ .badge-purple { @apply bg-sol-purple/15 text-sol-purple; }
180
+ .badge-tether { @apply bg-tether-green/15 text-tether-green; }
181
+ .badge-danger { @apply bg-danger/15 text-danger; }
182
+ .badge-warn { @apply bg-warning/15 text-warning; }
183
+ .badge-muted { @apply bg-sol-muted/10 text-sol-muted; }
184
+
185
+ /* ─── Toast / Notification ───────────────────────────────────────────── */
186
+ .toast-enter { animation: toast-slide 0.35s cubic-bezier(0.34, 1.56, 0.64, 1); }
187
+ @keyframes toast-slide {
188
+ from { opacity: 0; transform: translateX(100%) scale(0.9); }
189
+ to { opacity: 1; transform: translateX(0) scale(1); }
190
+ }
191
+ .toast-exit { animation: toast-out 0.25s ease-in forwards; }
192
+ @keyframes toast-out {
193
+ to { opacity: 0; transform: translateX(100%) scale(0.9); }
194
  }
195
 
196
+ /* ─── Progress Bar ───────────────────────────────────────────────────── */
197
+ .progress-bar {
198
+ @apply h-1.5 rounded-full overflow-hidden bg-sol-border/30;
199
+ }
200
+ .progress-fill {
201
+ @apply h-full rounded-full transition-all duration-700 ease-out;
202
+ background: linear-gradient(90deg, #9945FF, #14F195);
203
  }
204
+
205
+ /* ─── Divider ────────────────────────────────────────────────────────── */
206
+ .divider {
207
+ @apply w-full h-px;
208
+ background: linear-gradient(90deg, transparent, rgba(153, 69, 255, 0.15), transparent);
209
+ }
210
+
211
+ /* ─── Sidebar Active Indicator ───────────────────────────────────────── */
212
+ .nav-active-bar {
213
+ @apply absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-8 rounded-r-full;
214
+ background: linear-gradient(180deg, #9945FF, #14F195);
215
+ }
216
+
217
+ /* ─── Scrollbar for chat ─────────────────────────────────────────────── */
218
+ .chat-scroll::-webkit-scrollbar { width: 4px; }
219
+ .chat-scroll::-webkit-scrollbar-thumb { background: rgba(153, 69, 255, 0.2); border-radius: 10px; }
220
+
221
+ /* ─── Circular Voice Button Glow ─────────────────────────────────────── */
222
+ .voice-btn-idle {
223
+ box-shadow:
224
+ 0 0 0 0 rgba(153, 69, 255, 0.4),
225
+ 0 0 20px rgba(153, 69, 255, 0.2),
226
+ inset 0 0 20px rgba(153, 69, 255, 0.1);
227
+ animation: voice-idle 3s ease-in-out infinite;
228
+ }
229
+ @keyframes voice-idle {
230
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(153, 69, 255, 0.4), 0 0 20px rgba(153, 69, 255, 0.2); }
231
+ 50% { box-shadow: 0 0 0 12px rgba(153, 69, 255, 0), 0 0 40px rgba(153, 69, 255, 0.3); }
232
+ }
233
+
234
+ .voice-btn-active {
235
+ box-shadow:
236
+ 0 0 0 0 rgba(255, 68, 102, 0.5),
237
+ 0 0 30px rgba(255, 68, 102, 0.3);
238
+ animation: voice-active 1s ease-in-out infinite;
239
+ }
240
+ @keyframes voice-active {
241
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(255, 68, 102, 0.5), 0 0 30px rgba(255, 68, 102, 0.3); }
242
+ 50% { box-shadow: 0 0 0 20px rgba(255, 68, 102, 0), 0 0 50px rgba(255, 68, 102, 0.4); }
243
+ }
244
+
245
+ /* ─── Staggered Children ─────────────────────────────────────────────── */
246
+ .stagger > * { animation: slide-up 0.35s cubic-bezier(0.4, 0, 0.2, 1) backwards; }
247
+ .stagger > *:nth-child(1) { animation-delay: 0ms; }
248
+ .stagger > *:nth-child(2) { animation-delay: 60ms; }
249
+ .stagger > *:nth-child(3) { animation-delay: 120ms; }
250
+ .stagger > *:nth-child(4) { animation-delay: 180ms; }
251
+ .stagger > *:nth-child(5) { animation-delay: 240ms; }
252
+ .stagger > *:nth-child(6) { animation-delay: 300ms; }
253
+ .stagger > *:nth-child(7) { animation-delay: 360ms; }
254
+ .stagger > *:nth-child(8) { animation-delay: 420ms; }
255
+
256
+ /* ─── Number Ticker ──────────────────────────────────────────────────── */
257
+ .ticker { font-variant-numeric: tabular-nums; transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1); }
src/renderer/pages/Dashboard.tsx CHANGED
@@ -1,162 +1,182 @@
1
  import React, { useEffect, useState } from 'react';
 
2
 
3
  interface DashboardProps {
4
  balance: { sol: number; usdt: number };
5
  publicKey: string | null;
6
  onRefresh: () => void;
 
7
  }
8
 
9
- export default function Dashboard({ balance, publicKey, onRefresh }: DashboardProps) {
10
  const [recentTxs, setRecentTxs] = useState<any[]>([]);
11
  const [aiStatus, setAiStatus] = useState<any>(null);
 
12
 
13
- useEffect(() => {
14
- loadData();
15
- }, []);
16
 
17
  const loadData = async () => {
18
- if (!window.solvox) return;
19
  try {
20
- const [historyResult, statusResult] = await Promise.all([
21
- window.solvox.wallet.getHistory(5),
22
- window.solvox.ai.getStatus(),
23
- ]);
24
- if (historyResult.success) setRecentTxs(historyResult.history || []);
25
- setAiStatus(statusResult);
26
  } catch {}
 
27
  };
28
 
29
- const totalUSD = balance.sol * 170 + balance.usdt; // Rough SOL price estimate
 
30
 
31
  return (
32
- <div className="space-y-6 slide-enter">
33
- {/* Balance Cards */}
34
- <div className="grid grid-cols-3 gap-4">
35
- <div className="glass rounded-2xl p-6 glow-purple">
36
- <div className="text-sm text-sol-muted mb-1">Total Portfolio</div>
37
- <div className="text-3xl font-bold gradient-text">
38
- ${totalUSD.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  </div>
40
- <button
41
- onClick={onRefresh}
42
- className="mt-3 text-xs text-sol-muted hover:text-sol-purple transition-colors"
43
- >
44
- ↻ Refresh
45
- </button>
46
  </div>
 
47
 
48
- <div className="glass rounded-2xl p-6">
49
- <div className="flex items-center gap-2 mb-2">
50
- <div className="w-8 h-8 rounded-full bg-sol-purple/20 flex items-center justify-center">
51
- <span className="text-sol-purple font-bold text-sm">◎</span>
 
 
 
 
 
 
52
  </div>
53
- <div className="text-sm text-sol-muted">SOL</div>
54
  </div>
55
- <div className="text-2xl font-bold">{balance.sol.toFixed(4)}</div>
56
- <div className="text-xs text-sol-muted mt-1">
57
- ${(balance.sol * 170).toFixed(2)}
58
  </div>
59
  </div>
60
 
61
- <div className="glass rounded-2xl p-6">
62
- <div className="flex items-center gap-2 mb-2">
63
- <div className="w-8 h-8 rounded-full bg-tether-green/20 flex items-center justify-center">
64
- <span className="text-tether-green font-bold text-sm">₮</span>
 
 
 
 
65
  </div>
66
- <div className="text-sm text-sol-muted">USDT</div>
67
  </div>
68
- <div className="text-2xl font-bold">{balance.usdt.toFixed(2)}</div>
69
- <div className="text-xs text-sol-muted mt-1">
70
- ${balance.usdt.toFixed(2)}
71
  </div>
72
  </div>
73
  </div>
74
 
75
- {/* Quick Actions */}
76
- <div className="grid grid-cols-4 gap-3">
77
- {[
78
- { icon: '📤', label: 'Send', color: 'sol-purple' },
79
- { icon: '📥', label: 'Receive', color: 'sol-green' },
80
- { icon: '🎤', label: 'Voice AI', color: 'tether-green' },
81
- { icon: '🔍', label: 'Scan QR', color: 'warning' },
82
- ].map(action => (
83
- <button
84
- key={action.label}
85
- className={`glass rounded-xl p-4 text-center hover:border-${action.color} transition-all group`}
86
- >
87
- <div className="text-2xl mb-1">{action.icon}</div>
88
- <div className="text-xs font-medium text-sol-muted group-hover:text-sol-text">
89
- {action.label}
90
- </div>
91
- </button>
92
- ))}
93
  </div>
94
 
95
- {/* AI Status & Recent Activity */}
96
- <div className="grid grid-cols-2 gap-4">
97
- {/* AI Engine Status */}
98
- <div className="glass rounded-2xl p-6">
99
- <h3 className="text-lg font-semibold mb-4">🧠 AI Engine (QVAC)</h3>
100
- <div className="space-y-3">
 
 
 
 
 
 
101
  {[
102
- { name: 'LLM (Llama 3.2)', active: aiStatus?.llm, pkg: '@qvac/llm-llamacpp' },
103
- { name: 'Speech-to-Text', active: aiStatus?.transcription, pkg: '@qvac/transcription-whispercpp' },
104
- { name: 'Text-to-Speech', active: aiStatus?.tts, pkg: '@qvac/tts-onnx' },
105
- { name: 'Embeddings', active: aiStatus?.embed, pkg: '@qvac/embed-llamacpp' },
106
- { name: 'Translation', active: aiStatus?.translation, pkg: '@qvac/translation-nmtcpp' },
107
- { name: 'OCR', active: aiStatus?.ocr, pkg: '@qvac/ocr-onnx' },
108
- ].map(model => (
109
- <div key={model.name} className="flex items-center justify-between">
110
- <div>
111
- <div className="text-sm">{model.name}</div>
112
- <div className="text-xs text-sol-muted font-mono">{model.pkg}</div>
 
113
  </div>
114
- <div className={`flex items-center gap-1.5 text-xs ${model.active ? 'text-sol-green' : 'text-sol-muted'}`}>
115
- <span className={`w-2 h-2 rounded-full ${model.active ? 'bg-sol-green' : 'bg-sol-muted/30'}`} />
116
- {model.active ? 'Active' : 'Loading...'}
117
  </div>
118
  </div>
119
  ))}
120
  </div>
121
- <div className="mt-4 pt-3 border-t border-sol-border">
122
- <div className="flex items-center gap-2 text-xs text-sol-muted">
123
- <span className="w-2 h-2 rounded-full bg-tether-green" />
124
- All inference runs 100% locally via Vulkan GPU
125
- </div>
126
  </div>
127
  </div>
128
 
129
- {/* Recent Transactions */}
130
- <div className="glass rounded-2xl p-6">
131
- <h3 className="text-lg font-semibold mb-4">📋 Recent Activity</h3>
132
- {recentTxs.length === 0 ? (
133
- <div className="text-center py-8">
134
- <div className="text-4xl mb-2">📭</div>
135
- <div className="text-sol-muted text-sm">No transactions yet</div>
136
- <div className="text-xs text-sol-muted mt-1">
137
- Try saying "Send 1 SOL to..."
 
 
138
  </div>
 
 
139
  </div>
140
  ) : (
141
- <div className="space-y-3">
142
  {recentTxs.map((tx, i) => (
143
- <div key={i} className="flex items-center justify-between py-2 border-b border-sol-border/30 last:border-0">
144
- <div className="flex items-center gap-3">
145
- <div className={`w-8 h-8 rounded-full flex items-center justify-center ${
146
- tx.status === 'success' ? 'bg-sol-green/20' : 'bg-danger/20'
147
- }`}>
148
- {tx.status === 'success' ? '✓' : '✗'}
149
- </div>
150
- <div>
151
- <div className="text-sm font-mono">
152
- {tx.signature?.slice(0, 12)}...
153
- </div>
154
- <div className="text-xs text-sol-muted">
155
- {tx.timestamp ? new Date(tx.timestamp).toLocaleDateString() : 'Pending'}
156
- </div>
157
- </div>
158
  </div>
159
- <div className={`text-sm ${tx.status === 'success' ? 'text-sol-green' : 'text-danger'}`}>
160
  {tx.status}
161
  </div>
162
  </div>
@@ -166,18 +186,21 @@ export default function Dashboard({ balance, publicKey, onRefresh }: DashboardPr
166
  </div>
167
  </div>
168
 
169
- {/* Wallet Address */}
170
  <div className="glass rounded-2xl p-4">
171
  <div className="flex items-center justify-between">
172
- <div>
173
- <div className="text-xs text-sol-muted mb-1">Your Wallet Address</div>
174
- <div className="text-sm font-mono text-sol-muted">{publicKey || '—'}</div>
 
 
 
 
 
175
  </div>
176
- <button
177
- onClick={() => publicKey && navigator.clipboard.writeText(publicKey)}
178
- className="px-4 py-2 rounded-lg bg-sol-purple/20 text-sol-purple text-sm hover:bg-sol-purple/30 transition-colors"
179
- >
180
- 📋 Copy
181
  </button>
182
  </div>
183
  </div>
 
1
  import React, { useEffect, useState } from 'react';
2
+ import { AnimatedNumber, IconButton, ShimmerCard, CircularProgress } from '../components/ui/index';
3
 
4
  interface DashboardProps {
5
  balance: { sol: number; usdt: number };
6
  publicKey: string | null;
7
  onRefresh: () => void;
8
+ onNavigate: (page: any) => void;
9
  }
10
 
11
+ export default function Dashboard({ balance, publicKey, onRefresh, onNavigate }: DashboardProps) {
12
  const [recentTxs, setRecentTxs] = useState<any[]>([]);
13
  const [aiStatus, setAiStatus] = useState<any>(null);
14
+ const [loading, setLoading] = useState(true);
15
 
16
+ useEffect(() => { loadData(); }, []);
 
 
17
 
18
  const loadData = async () => {
19
+ if (!window.solvox) { setLoading(false); return; }
20
  try {
21
+ const [h, s] = await Promise.all([window.solvox.wallet.getHistory(5), window.solvox.ai.getStatus()]);
22
+ if (h.success) setRecentTxs(h.history || []);
23
+ setAiStatus(s);
 
 
 
24
  } catch {}
25
+ setLoading(false);
26
  };
27
 
28
+ const totalUSD = balance.sol * 170 + balance.usdt;
29
+ const activeModules = aiStatus ? [aiStatus.llm, aiStatus.transcription, aiStatus.tts, aiStatus.embed, aiStatus.translation, aiStatus.ocr].filter(Boolean).length : 0;
30
 
31
  return (
32
+ <div className="space-y-5 stagger">
33
+ {/* ── Hero Balance Card ─────────────────────────────────────────── */}
34
+ <div className="relative overflow-hidden rounded-3xl p-6 bg-gradient-to-br from-sol-purple/10 via-sol-card to-tether-green/5 border border-sol-purple/10">
35
+ <div className="absolute -right-16 -top-16 w-48 h-48 rounded-full bg-sol-purple/8 blur-3xl" />
36
+ <div className="absolute -left-8 -bottom-8 w-32 h-32 rounded-full bg-sol-green/6 blur-2xl" />
37
+ <div className="relative z-10">
38
+ <div className="flex items-start justify-between">
39
+ <div>
40
+ <div className="text-sm text-sol-muted mb-1 font-medium">Total Portfolio Value</div>
41
+ <div className="text-4xl font-extrabold gradient-text-static mb-1">
42
+ <AnimatedNumber value={totalUSD} decimals={2} prefix="$" />
43
+ </div>
44
+ <div className="flex items-center gap-3 mt-2">
45
+ <div className="badge badge-green">
46
+ <div className="w-1 h-1 rounded-full bg-sol-green" />
47
+ Devnet
48
+ </div>
49
+ <button onClick={onRefresh} className="text-xs text-sol-muted hover:text-sol-purple transition-colors flex items-center gap-1">
50
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg>
51
+ Refresh
52
+ </button>
53
+ </div>
54
+ </div>
55
+ <CircularProgress value={activeModules / 6 * 100} size={72} strokeWidth={5} color="#14F195">
56
+ <div className="text-center">
57
+ <div className="text-sm font-bold text-sol-green">{activeModules}</div>
58
+ <div className="text-[8px] text-sol-muted">/6 AI</div>
59
+ </div>
60
+ </CircularProgress>
61
  </div>
 
 
 
 
 
 
62
  </div>
63
+ </div>
64
 
65
+ {/* ── Token Cards ───────────────────────────────────────────────── */}
66
+ <div className="grid grid-cols-2 gap-4">
67
+ <div className="glass glass-hover rounded-2xl p-5">
68
+ <div className="flex items-center gap-3 mb-3">
69
+ <div className="w-10 h-10 rounded-xl bg-gradient-to-br from-sol-purple to-sol-purple-light flex items-center justify-center shadow-glow-sm">
70
+ <span className="text-white font-bold text-sm">◎</span>
71
+ </div>
72
+ <div>
73
+ <div className="text-xs text-sol-muted font-medium">Solana</div>
74
+ <div className="text-lg font-bold"><AnimatedNumber value={balance.sol} decimals={4} /></div>
75
  </div>
 
76
  </div>
77
+ <div className="flex items-center justify-between">
78
+ <span className="text-xs text-sol-muted">≈ <AnimatedNumber value={balance.sol * 170} decimals={2} prefix="$" /></span>
79
+ <div className="progress-bar w-20"><div className="progress-fill" style={{ width: `${Math.min(100, balance.sol * 10)}%` }} /></div>
80
  </div>
81
  </div>
82
 
83
+ <div className="glass glass-hover rounded-2xl p-5">
84
+ <div className="flex items-center gap-3 mb-3">
85
+ <div className="w-10 h-10 rounded-xl bg-gradient-to-br from-tether-green to-tether-dark flex items-center justify-center shadow-glow-sm">
86
+ <span className="text-white font-bold text-sm">₮</span>
87
+ </div>
88
+ <div>
89
+ <div className="text-xs text-sol-muted font-medium">Tether USDT</div>
90
+ <div className="text-lg font-bold"><AnimatedNumber value={balance.usdt} decimals={2} /></div>
91
  </div>
 
92
  </div>
93
+ <div className="flex items-center justify-between">
94
+ <span className="text-xs text-sol-muted">≈ <AnimatedNumber value={balance.usdt} decimals={2} prefix="$" /></span>
95
+ <div className="progress-bar w-20"><div className="progress-fill" style={{ width: `${Math.min(100, balance.usdt / 10)}%`, background: 'linear-gradient(90deg, #26A17B, #14F195)' }} /></div>
96
  </div>
97
  </div>
98
  </div>
99
 
100
+ {/* ── Quick Actions ─────────────────────────────────────────────── */}
101
+ <div className="glass rounded-2xl p-5">
102
+ <div className="text-xs text-sol-muted font-semibold uppercase tracking-wider mb-4">Quick Actions</div>
103
+ <div className="flex items-center justify-around">
104
+ <IconButton icon="↗" label="Send" color="purple" onClick={() => onNavigate('send')} />
105
+ <IconButton icon="↙" label="Receive" color="green" />
106
+ <IconButton icon="◎" label="Voice AI" color="tether" onClick={() => onNavigate('voice')} />
107
+ <IconButton icon="⊞" label="Scan QR" color="warning" />
108
+ <IconButton icon="☰" label="History" color="purple" onClick={() => onNavigate('history')} />
109
+ </div>
 
 
 
 
 
 
 
 
110
  </div>
111
 
112
+ {/* ── AI Status + Recent Activity ───────────────────────────────── */}
113
+ <div className="grid grid-cols-5 gap-4">
114
+ {/* AI Engine 3 cols */}
115
+ <div className="col-span-3 glass rounded-2xl p-5">
116
+ <div className="flex items-center justify-between mb-4">
117
+ <div className="text-sm font-bold text-sol-text">QVAC AI Engine</div>
118
+ <div className="badge badge-tether">
119
+ <div className="w-1 h-1 rounded-full bg-tether-green animate-pulse" />
120
+ Local
121
+ </div>
122
+ </div>
123
+ <div className="space-y-2.5 stagger">
124
  {[
125
+ { name: 'LLM Llama 3.2 3B', active: aiStatus?.llm, pkg: 'llm-llamacpp', color: '#9945FF' },
126
+ { name: 'Speech-to-Text — Whisper', active: aiStatus?.transcription, pkg: 'transcription-whispercpp', color: '#14F195' },
127
+ { name: 'Text-to-Speech — Piper', active: aiStatus?.tts, pkg: 'tts-onnx', color: '#26A17B' },
128
+ { name: 'Embeddings — Nomic', active: aiStatus?.embed, pkg: 'embed-llamacpp', color: '#B06CFF' },
129
+ { name: 'Translation — MarianMT', active: aiStatus?.translation, pkg: 'translation-nmtcpp', color: '#FFB830' },
130
+ { name: 'OCR — PaddleOCR', active: aiStatus?.ocr, pkg: 'ocr-onnx', color: '#3B82F6' },
131
+ ].map(m => (
132
+ <div key={m.name} className="flex items-center gap-3 py-1.5">
133
+ <div className="w-1 h-8 rounded-full" style={{ background: m.active ? m.color : 'rgba(45,45,94,0.5)' }} />
134
+ <div className="flex-1 min-w-0">
135
+ <div className="text-xs font-semibold text-sol-text truncate">{m.name}</div>
136
+ <div className="text-[10px] text-sol-muted font-mono">@qvac/{m.pkg}</div>
137
  </div>
138
+ <div className={`badge ${m.active ? 'badge-green' : 'badge-muted'}`}>
139
+ {m.active ? '● Active' : '○ Loading'}
 
140
  </div>
141
  </div>
142
  ))}
143
  </div>
144
+ <div className="divider mt-4 mb-3" />
145
+ <div className="flex items-center gap-2 text-[10px] text-sol-muted">
146
+ <span className="w-1.5 h-1.5 rounded-full bg-tether-green" />
147
+ All inference runs 100% locally via QVAC Fabric (Vulkan GPU)
 
148
  </div>
149
  </div>
150
 
151
+ {/* Recent Activity — 2 cols */}
152
+ <div className="col-span-2 glass rounded-2xl p-5">
153
+ <div className="text-sm font-bold text-sol-text mb-4">Recent Activity</div>
154
+ {loading ? (
155
+ <div className="space-y-3">
156
+ {[1,2,3].map(i => <div key={i} className="shimmer rounded-lg h-12" />)}
157
+ </div>
158
+ ) : recentTxs.length === 0 ? (
159
+ <div className="flex flex-col items-center justify-center py-8 text-center">
160
+ <div className="w-14 h-14 rounded-2xl bg-sol-card flex items-center justify-center mb-3">
161
+ <span className="text-2xl opacity-40">📭</span>
162
  </div>
163
+ <div className="text-xs text-sol-muted">No transactions yet</div>
164
+ <div className="text-[10px] text-sol-muted/60 mt-1">Try "Send 1 SOL to..."</div>
165
  </div>
166
  ) : (
167
+ <div className="space-y-2">
168
  {recentTxs.map((tx, i) => (
169
+ <div key={i} className="flex items-center gap-3 p-2.5 rounded-xl bg-sol-dark/40 hover:bg-sol-dark/60 transition-colors">
170
+ <div className={`w-8 h-8 rounded-lg flex items-center justify-center text-xs ${
171
+ tx.status === 'success' ? 'bg-sol-green/10 text-sol-green' : 'bg-danger/10 text-danger'
172
+ }`}>
173
+ {tx.status === 'success' ? '✓' : '✗'}
174
+ </div>
175
+ <div className="flex-1 min-w-0">
176
+ <div className="text-xs font-mono text-sol-muted truncate">{tx.signature?.slice(0, 16)}...</div>
177
+ <div className="text-[10px] text-sol-muted/60">{tx.timestamp ? new Date(tx.timestamp).toLocaleDateString() : 'Pending'}</div>
 
 
 
 
 
 
178
  </div>
179
+ <div className={`badge text-[10px] ${tx.status === 'success' ? 'badge-green' : 'badge-danger'}`}>
180
  {tx.status}
181
  </div>
182
  </div>
 
186
  </div>
187
  </div>
188
 
189
+ {/* ── Wallet Address ────────────────────────────────────────────── */}
190
  <div className="glass rounded-2xl p-4">
191
  <div className="flex items-center justify-between">
192
+ <div className="flex items-center gap-3">
193
+ <div className="w-8 h-8 rounded-lg bg-sol-purple/10 flex items-center justify-center">
194
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#9945FF" strokeWidth="2"><rect x="1" y="4" width="22" height="16" rx="2"/><line x1="1" y1="10" x2="23" y2="10"/></svg>
195
+ </div>
196
+ <div>
197
+ <div className="text-[10px] text-sol-muted uppercase tracking-wider font-semibold">Wallet Address</div>
198
+ <div className="text-xs font-mono text-sol-muted/80">{publicKey || '—'}</div>
199
+ </div>
200
  </div>
201
+ <button onClick={() => publicKey && navigator.clipboard.writeText(publicKey)}
202
+ className="btn-ghost px-3 py-1.5 text-xs">
203
+ Copy
 
 
204
  </button>
205
  </div>
206
  </div>
src/renderer/pages/HistoryPage.tsx CHANGED
@@ -1,81 +1,63 @@
1
  import React, { useState, useEffect } from 'react';
2
 
3
  export default function HistoryPage() {
4
- const [transactions, setTransactions] = useState<any[]>([]);
5
  const [loading, setLoading] = useState(true);
6
- const [searchQuery, setSearchQuery] = useState('');
7
  const [ragResults, setRagResults] = useState<any[]>([]);
 
8
 
9
- useEffect(() => {
10
- loadHistory();
11
- }, []);
12
 
13
- const loadHistory = async () => {
14
  setLoading(true);
15
- try {
16
- if (window.solvox) {
17
- const result = await window.solvox.wallet.getHistory(20);
18
- if (result.success) {
19
- setTransactions(result.history || []);
20
- }
21
- }
22
- } catch {}
23
  setLoading(false);
24
  };
25
 
26
- const handleSearch = async () => {
27
- if (!searchQuery.trim()) return;
28
- try {
29
- if (window.solvox) {
30
- const result = await window.solvox.rag.search(searchQuery);
31
- if (result.success) {
32
- setRagResults(result.results || []);
33
- }
34
- }
35
- } catch {}
36
  };
37
 
38
  return (
39
- <div className="space-y-6 slide-enter">
40
  <div className="flex items-center justify-between">
41
- <h2 className="text-2xl font-bold">📋 Transaction History</h2>
42
- <button
43
- onClick={loadHistory}
44
- className="px-4 py-2 rounded-lg bg-sol-purple/20 text-sol-purple text-sm hover:bg-sol-purple/30 transition-colors"
45
- >
46
- ↻ Refresh
47
  </button>
48
  </div>
49
 
50
- {/* Semantic Search (RAG) */}
51
- <div className="glass rounded-xl p-4">
52
  <div className="flex gap-2">
53
- <input
54
- value={searchQuery}
55
- onChange={(e) => setSearchQuery(e.target.value)}
56
- onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
57
- placeholder='Search transactions with AI... (e.g. "last payment to Alice")'
58
- className="flex-1 px-4 py-2 bg-sol-dark border border-sol-border rounded-lg text-sm focus:border-sol-purple focus:outline-none"
59
- />
60
- <button
61
- onClick={handleSearch}
62
- className="px-4 py-2 rounded-lg bg-tether-green text-white text-sm hover:bg-tether-green/90 transition-colors"
63
- >
64
- 🔍 AI Search
65
  </button>
66
  </div>
67
- <div className="text-xs text-sol-muted mt-2">
68
- Powered by @qvac/embed-llamacpp semantic search runs 100% locally
 
69
  </div>
70
 
71
  {ragResults.length > 0 && (
72
- <div className="mt-3 space-y-2">
73
- <div className="text-sm font-semibold text-sol-purple">AI Search Results:</div>
74
  {ragResults.map((r, i) => (
75
- <div key={i} className="bg-sol-dark rounded-lg p-3 text-sm">
76
- <div>{r.text}</div>
77
- <div className="text-xs text-sol-muted mt-1">
78
- Relevance: {(r.score * 100).toFixed(1)}%
 
79
  </div>
80
  </div>
81
  ))}
@@ -83,62 +65,39 @@ export default function HistoryPage() {
83
  )}
84
  </div>
85
 
86
- {/* Transaction List */}
87
  <div className="glass rounded-2xl overflow-hidden">
88
  {loading ? (
89
- <div className="p-12 text-center text-sol-muted">Loading transactions...</div>
90
- ) : transactions.length === 0 ? (
91
- <div className="p-12 text-center">
92
- <div className="text-5xl mb-4">📭</div>
93
- <div className="text-lg font-semibold mb-1">No Transactions Yet</div>
94
- <div className="text-sol-muted text-sm">
95
- Your transaction history will appear here once you send or receive tokens.
96
  </div>
 
 
97
  </div>
98
  ) : (
99
- <table className="w-full">
100
- <thead>
101
- <tr className="border-b border-sol-border text-sm text-sol-muted">
102
- <th className="text-left px-6 py-3">Status</th>
103
- <th className="text-left px-6 py-3">Signature</th>
104
- <th className="text-left px-6 py-3">Date</th>
105
- <th className="text-right px-6 py-3">Details</th>
106
- </tr>
107
- </thead>
108
- <tbody>
109
- {transactions.map((tx, i) => (
110
- <tr key={i} className="border-b border-sol-border/30 hover:bg-sol-dark/50 transition-colors">
111
- <td className="px-6 py-4">
112
- <span className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs ${
113
- tx.status === 'success'
114
- ? 'bg-sol-green/20 text-sol-green'
115
- : 'bg-danger/20 text-danger'
116
- }`}>
117
- {tx.status === 'success' ? '✓' : '✗'} {tx.status}
118
- </span>
119
- </td>
120
- <td className="px-6 py-4 font-mono text-xs text-sol-muted">
121
- {tx.signature?.slice(0, 20)}...
122
- </td>
123
- <td className="px-6 py-4 text-sm text-sol-muted">
124
- {tx.timestamp
125
- ? new Date(tx.timestamp).toLocaleString()
126
- : 'Pending'}
127
- </td>
128
- <td className="px-6 py-4 text-right">
129
- <a
130
- href={`https://solscan.io/tx/${tx.signature}`}
131
- target="_blank"
132
- rel="noopener noreferrer"
133
- className="text-sol-purple text-xs hover:underline"
134
- >
135
- View →
136
- </a>
137
- </td>
138
- </tr>
139
- ))}
140
- </tbody>
141
- </table>
142
  )}
143
  </div>
144
  </div>
 
1
  import React, { useState, useEffect } from 'react';
2
 
3
  export default function HistoryPage() {
4
+ const [txs, setTxs] = useState<any[]>([]);
5
  const [loading, setLoading] = useState(true);
6
+ const [query, setQuery] = useState('');
7
  const [ragResults, setRagResults] = useState<any[]>([]);
8
+ const [searching, setSearching] = useState(false);
9
 
10
+ useEffect(() => { load(); }, []);
 
 
11
 
12
+ const load = async () => {
13
  setLoading(true);
14
+ try { if (window.solvox) { const r = await window.solvox.wallet.getHistory(20); if (r.success) setTxs(r.history || []); } } catch {}
 
 
 
 
 
 
 
15
  setLoading(false);
16
  };
17
 
18
+ const search = async () => {
19
+ if (!query.trim()) return;
20
+ setSearching(true);
21
+ try { if (window.solvox) { const r = await window.solvox.rag.search(query); if (r.success) setRagResults(r.results || []); } } catch {}
22
+ setSearching(false);
 
 
 
 
 
23
  };
24
 
25
  return (
26
+ <div className="space-y-5 page-enter">
27
  <div className="flex items-center justify-between">
28
+ <h2 className="text-xl font-bold">Transaction History</h2>
29
+ <button onClick={load} className="btn-ghost px-3 py-1.5 text-xs flex items-center gap-1.5">
30
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg>
31
+ Refresh
 
 
32
  </button>
33
  </div>
34
 
35
+ {/* AI Search */}
36
+ <div className="glass rounded-2xl p-4">
37
  <div className="flex gap-2">
38
+ <div className="flex-1 relative">
39
+ <input value={query} onChange={e => setQuery(e.target.value)} onKeyDown={e => e.key === 'Enter' && search()}
40
+ placeholder='Search with AI — "last payment to Alice"' className="input-field text-xs pl-9" />
41
+ <svg className="absolute left-3 top-1/2 -translate-y-1/2 text-sol-muted" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
42
+ </div>
43
+ <button onClick={search} disabled={searching} className="btn-primary px-4 py-2 text-white text-xs disabled:opacity-50">
44
+ <span>{searching ? '...' : 'Search'}</span>
 
 
 
 
 
45
  </button>
46
  </div>
47
+ <div className="flex items-center gap-1.5 mt-2">
48
+ <div className="w-1 h-1 rounded-full bg-tether-green" />
49
+ <span className="text-[10px] text-sol-muted">Powered by @qvac/embed-llamacpp — 100% local semantic search</span>
50
  </div>
51
 
52
  {ragResults.length > 0 && (
53
+ <div className="mt-3 space-y-2 stagger">
54
+ <div className="text-xs font-semibold text-sol-purple">AI Results</div>
55
  {ragResults.map((r, i) => (
56
+ <div key={i} className="bg-sol-dark/50 rounded-xl p-3 text-xs border border-sol-border/30">
57
+ <div className="text-sol-text">{r.text}</div>
58
+ <div className="mt-1 flex items-center gap-2">
59
+ <div className="progress-bar w-16"><div className="progress-fill" style={{ width: `${r.score * 100}%` }} /></div>
60
+ <span className="text-[10px] text-sol-muted">{(r.score * 100).toFixed(0)}% match</span>
61
  </div>
62
  </div>
63
  ))}
 
65
  )}
66
  </div>
67
 
68
+ {/* Transaction Table */}
69
  <div className="glass rounded-2xl overflow-hidden">
70
  {loading ? (
71
+ <div className="p-8 space-y-3">
72
+ {[1,2,3,4].map(i => <div key={i} className="shimmer rounded-lg h-14" />)}
73
+ </div>
74
+ ) : txs.length === 0 ? (
75
+ <div className="flex flex-col items-center justify-center py-16 text-center">
76
+ <div className="w-16 h-16 rounded-2xl bg-sol-card flex items-center justify-center mb-4">
77
+ <span className="text-3xl opacity-30">📭</span>
78
  </div>
79
+ <div className="text-sm font-semibold mb-1">No Transactions Yet</div>
80
+ <div className="text-xs text-sol-muted max-w-xs">Send or receive tokens and your history will appear here.</div>
81
  </div>
82
  ) : (
83
+ <div className="divide-y divide-sol-border/20">
84
+ {txs.map((tx, i) => (
85
+ <div key={i} className="flex items-center gap-4 px-5 py-3.5 hover:bg-sol-dark/30 transition-colors">
86
+ <div className={`w-9 h-9 rounded-xl flex items-center justify-center text-xs font-bold ${
87
+ tx.status === 'success' ? 'bg-sol-green/10 text-sol-green' : 'bg-danger/10 text-danger'
88
+ }`}>
89
+ {tx.status === 'success' ? '✓' : '✗'}
90
+ </div>
91
+ <div className="flex-1 min-w-0">
92
+ <div className="text-xs font-mono text-sol-muted truncate">{tx.signature?.slice(0, 24)}...</div>
93
+ <div className="text-[10px] text-sol-muted/60">{tx.timestamp ? new Date(tx.timestamp).toLocaleString() : 'Pending'}</div>
94
+ </div>
95
+ <div className={`badge ${tx.status === 'success' ? 'badge-green' : 'badge-danger'}`}>{tx.status}</div>
96
+ <a href={`https://solscan.io/tx/${tx.signature}`} target="_blank" rel="noopener noreferrer"
97
+ className="text-sol-purple text-[10px] hover:underline">View ↗</a>
98
+ </div>
99
+ ))}
100
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  )}
102
  </div>
103
  </div>
src/renderer/pages/LockScreen.tsx CHANGED
@@ -1,142 +1,132 @@
1
  import React, { useState, useRef, useEffect } from 'react';
 
2
 
3
- interface LockScreenProps {
4
- onUnlock: (publicKey: string) => void;
5
- }
6
 
7
  export default function LockScreen({ onUnlock }: LockScreenProps) {
8
  const [pin, setPin] = useState('');
9
  const [error, setError] = useState('');
10
  const [loading, setLoading] = useState(false);
11
  const [biometricAvailable, setBiometricAvailable] = useState(false);
 
12
  const inputRef = useRef<HTMLInputElement>(null);
13
 
14
- useEffect(() => {
15
- inputRef.current?.focus();
16
- checkBiometric();
17
- }, []);
18
 
19
  const checkBiometric = async () => {
20
- if (window.solvox) {
21
- const available = await window.solvox.auth.biometricAvailable();
22
- setBiometricAvailable(available);
23
- if (available) {
24
- handleBiometric();
25
- }
26
- }
27
  };
28
 
29
  const handleBiometric = async () => {
30
  if (!window.solvox) return;
31
  setLoading(true);
32
- const result = await window.solvox.auth.biometric('Unlock SolVox');
33
- if (result.success) {
34
- const pk = await window.solvox.wallet.getPublicKey();
35
- if (pk) onUnlock(pk);
36
- } else {
37
- setError(result.error || 'Biometric failed');
38
- }
39
  setLoading(false);
40
  };
41
 
42
- const handlePinSubmit = async (e: React.FormEvent) => {
43
  e.preventDefault();
44
- if (pin.length < 6) {
45
- setError('PIN must be at least 6 digits');
46
- return;
47
- }
48
- setLoading(true);
49
- setError('');
50
-
51
  try {
52
  if (window.solvox) {
53
- const result = await window.solvox.auth.unlock(pin);
54
- if (result.success) {
55
- const pk = await window.solvox.wallet.getPublicKey();
56
- if (pk) onUnlock(pk);
57
- } else {
58
- setError(result.error || 'Unlock failed');
59
- if (result.remainingAttempts !== undefined) {
60
- setError(`${result.error} (${result.remainingAttempts} attempts remaining)`);
61
- }
62
- }
63
- } else {
64
- // Dev mode
65
- onUnlock('DevModePublicKey123456789');
66
- }
67
- } catch (err: any) {
68
- setError(err.message);
69
- }
70
- setLoading(false);
71
- setPin('');
72
  };
73
 
 
 
 
 
 
74
  return (
75
- <div className="h-screen flex flex-col items-center justify-center bg-sol-dark">
76
- <div className="w-full max-w-sm">
 
 
77
  {/* Logo */}
78
- <div className="text-center mb-12">
79
- <h1 className="text-5xl font-bold gradient-text mb-2">SolVox</h1>
80
- <p className="text-sol-muted">Voice-First AI Wallet</p>
 
 
 
81
  </div>
82
 
83
- {/* Lock Icon */}
84
- <div className="text-center mb-8">
85
- <div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-sol-card border-2 border-sol-border">
86
- <span className="text-4xl">🔒</span>
 
 
 
 
 
87
  </div>
88
  </div>
89
 
90
- {/* PIN Form */}
91
- <form onSubmit={handlePinSubmit} className="space-y-4">
92
- <div>
93
- <input
94
- ref={inputRef}
95
- type="password"
96
- inputMode="numeric"
97
- pattern="[0-9]*"
98
- value={pin}
99
- onChange={(e) => setPin(e.target.value.replace(/\D/g, ''))}
100
- placeholder="Enter PIN"
101
- maxLength={12}
102
- className="w-full px-4 py-3 bg-sol-card border border-sol-border rounded-xl text-center text-2xl tracking-[0.5em] font-mono text-sol-text focus:border-sol-purple focus:outline-none focus:ring-1 focus:ring-sol-purple/50 transition-colors"
103
- disabled={loading}
104
- />
 
 
 
 
 
 
 
105
  </div>
106
 
107
  {error && (
108
- <div className="text-danger text-sm text-center bg-danger/10 rounded-lg p-2">
109
  {error}
110
  </div>
111
  )}
112
 
113
- <button
114
- type="submit"
115
- disabled={loading || pin.length < 6}
116
- className="w-full py-3 rounded-xl bg-sol-purple text-white font-semibold hover:bg-sol-purple/90 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
117
- >
118
- {loading ? 'Unlocking...' : 'Unlock'}
119
  </button>
120
  </form>
121
 
122
- {/* Biometric */}
123
  {biometricAvailable && (
124
- <button
125
- onClick={handleBiometric}
126
- className="w-full mt-4 py-3 rounded-xl border border-sol-border text-sol-muted hover:text-sol-text hover:border-sol-purple transition-colors"
127
- >
128
- 🫰 Use Touch ID
129
  </button>
130
  )}
131
 
132
- {/* Security Badge */}
133
  <div className="mt-8 text-center">
134
- <div className="inline-flex items-center gap-2 text-xs text-sol-muted">
135
- <span className="w-2 h-2 rounded-full bg-sol-green" />
136
- All data encrypted locally • No cloud
137
  </div>
138
  </div>
139
  </div>
 
 
 
140
  </div>
141
  );
142
  }
 
1
  import React, { useState, useRef, useEffect } from 'react';
2
+ import { ParticlesBackground } from '../components/ui/index';
3
 
4
+ interface LockScreenProps { onUnlock: (publicKey: string) => void; }
 
 
5
 
6
  export default function LockScreen({ onUnlock }: LockScreenProps) {
7
  const [pin, setPin] = useState('');
8
  const [error, setError] = useState('');
9
  const [loading, setLoading] = useState(false);
10
  const [biometricAvailable, setBiometricAvailable] = useState(false);
11
+ const [shake, setShake] = useState(false);
12
  const inputRef = useRef<HTMLInputElement>(null);
13
 
14
+ useEffect(() => { inputRef.current?.focus(); checkBiometric(); }, []);
 
 
 
15
 
16
  const checkBiometric = async () => {
17
+ if (!window.solvox) return;
18
+ const available = await window.solvox.auth.biometricAvailable();
19
+ setBiometricAvailable(available);
20
+ if (available) handleBiometric();
 
 
 
21
  };
22
 
23
  const handleBiometric = async () => {
24
  if (!window.solvox) return;
25
  setLoading(true);
26
+ const r = await window.solvox.auth.biometric('Unlock SolVox');
27
+ if (r.success) { const pk = await window.solvox.wallet.getPublicKey(); if (pk) onUnlock(pk); }
28
+ else { setError(r.error || 'Biometric failed'); }
 
 
 
 
29
  setLoading(false);
30
  };
31
 
32
+ const handleSubmit = async (e: React.FormEvent) => {
33
  e.preventDefault();
34
+ if (pin.length < 6) { triggerShake('PIN must be at least 6 digits'); return; }
35
+ setLoading(true); setError('');
 
 
 
 
 
36
  try {
37
  if (window.solvox) {
38
+ const r = await window.solvox.auth.unlock(pin);
39
+ if (r.success) { const pk = await window.solvox.wallet.getPublicKey(); if (pk) onUnlock(pk); }
40
+ else { triggerShake(r.remainingAttempts !== undefined ? `${r.error} (${r.remainingAttempts} left)` : r.error || 'Unlock failed'); }
41
+ } else { onUnlock('DevMode' + Date.now()); }
42
+ } catch (err: any) { triggerShake(err.message); }
43
+ setLoading(false); setPin('');
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  };
45
 
46
+ const triggerShake = (msg: string) => { setError(msg); setShake(true); setTimeout(() => setShake(false), 500); };
47
+
48
+ // PIN dots visualization
49
+ const dots = Array.from({ length: 8 }, (_, i) => i < pin.length);
50
+
51
  return (
52
+ <div className="h-screen flex flex-col items-center justify-center mesh-bg relative">
53
+ <ParticlesBackground />
54
+
55
+ <div className="w-full max-w-xs relative z-10">
56
  {/* Logo */}
57
+ <div className="text-center mb-10 fade-in">
58
+ <div className="w-16 h-16 mx-auto rounded-2xl bg-gradient-to-br from-sol-purple to-sol-green flex items-center justify-center mb-4 shadow-glow-lg animate-float">
59
+ <span className="text-white text-xl font-black">SV</span>
60
+ </div>
61
+ <h1 className="text-3xl font-extrabold gradient-text mb-1">SolVox</h1>
62
+ <p className="text-xs text-sol-muted">Voice-First AI Wallet</p>
63
  </div>
64
 
65
+ {/* Lock Visual */}
66
+ <div className="flex justify-center mb-8">
67
+ <div className="relative">
68
+ <div className="w-14 h-14 rounded-full bg-sol-card border-2 border-sol-border/50 flex items-center justify-center">
69
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#9945FF" strokeWidth="2" strokeLinecap="round">
70
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>
71
+ </svg>
72
+ </div>
73
+ <div className="absolute -inset-3 rounded-full border border-sol-purple/10 animate-pulse" />
74
  </div>
75
  </div>
76
 
77
+ {/* PIN Dots */}
78
+ <div className={`flex justify-center gap-3 mb-6 transition-transform ${shake ? 'animate-[shake_0.5s_ease-in-out]' : ''}`}
79
+ style={shake ? { animation: 'shake 0.4s ease-in-out' } : {}}>
80
+ {dots.map((filled, i) => (
81
+ <div key={i} className={`w-3 h-3 rounded-full transition-all duration-200 ${
82
+ filled ? 'bg-sol-purple scale-110 shadow-glow-sm' : 'bg-sol-border/40'
83
+ }`} />
84
+ ))}
85
+ </div>
86
+
87
+ {/* Hidden PIN Input */}
88
+ <form onSubmit={handleSubmit}>
89
+ <input ref={inputRef} type="password" inputMode="numeric" pattern="[0-9]*" value={pin}
90
+ onChange={e => setPin(e.target.value.replace(/\D/g, ''))} maxLength={8}
91
+ className="sr-only" autoFocus disabled={loading} />
92
+
93
+ {/* Tap to focus area */}
94
+ <div onClick={() => inputRef.current?.focus()}
95
+ className="glass-strong rounded-2xl p-4 text-center cursor-text mb-4 hover:border-sol-purple/20 transition-all">
96
+ <div className="text-2xl tracking-[0.6em] font-mono text-sol-text h-8 flex items-center justify-center">
97
+ {pin ? '●'.repeat(pin.length) : <span className="text-sm text-sol-muted tracking-normal">Tap to enter PIN</span>}
98
+ </div>
99
  </div>
100
 
101
  {error && (
102
+ <div className="text-danger text-xs text-center bg-danger/8 border border-danger/15 rounded-xl p-2.5 mb-4 fade-in">
103
  {error}
104
  </div>
105
  )}
106
 
107
+ <button type="submit" disabled={loading || pin.length < 6}
108
+ className="btn-primary w-full py-3.5 text-white disabled:opacity-40 disabled:shadow-none">
109
+ <span>{loading ? 'Unlocking...' : 'Unlock'}</span>
 
 
 
110
  </button>
111
  </form>
112
 
 
113
  {biometricAvailable && (
114
+ <button onClick={handleBiometric}
115
+ className="btn-ghost w-full mt-3 py-3 text-sm flex items-center justify-center gap-2">
116
+ <span>🫰</span> Use Touch ID
 
 
117
  </button>
118
  )}
119
 
 
120
  <div className="mt-8 text-center">
121
+ <div className="inline-flex items-center gap-2 text-[10px] text-sol-muted/60">
122
+ <div className="w-1.5 h-1.5 rounded-full bg-sol-green/60" />
123
+ Encrypted locally • No cloud • QVAC AI on-device
124
  </div>
125
  </div>
126
  </div>
127
+
128
+ {/* Shake keyframes */}
129
+ <style>{`@keyframes shake { 0%,100% { transform: translateX(0); } 20%,60% { transform: translateX(-8px); } 40%,80% { transform: translateX(8px); } }`}</style>
130
  </div>
131
  );
132
  }
src/renderer/pages/OnboardingScreen.tsx CHANGED
@@ -1,10 +1,11 @@
1
  import React, { useState } from 'react';
 
2
 
3
- interface OnboardingScreenProps {
4
- onComplete: (publicKey: string) => void;
5
- }
6
 
7
- type Step = 'welcome' | 'create_or_import' | 'create' | 'import' | 'pin' | 'done';
 
8
 
9
  export default function OnboardingScreen({ onComplete }: OnboardingScreenProps) {
10
  const [step, setStep] = useState<Step>('welcome');
@@ -15,240 +16,152 @@ export default function OnboardingScreen({ onComplete }: OnboardingScreenProps)
15
  const [error, setError] = useState('');
16
  const [loading, setLoading] = useState(false);
17
 
18
- const handleCreateWallet = async () => {
19
- setLoading(true);
20
- setError('');
21
  try {
22
  if (window.solvox) {
23
- const result = await window.solvox.wallet.create();
24
- if (result.success && result.publicKey) {
25
- setPublicKey(result.publicKey);
26
- setStep('pin');
27
- } else {
28
- setError(result.error || 'Failed to create wallet');
29
- }
30
- } else {
31
- setPublicKey('DevMode123');
32
- setStep('pin');
33
- }
34
- } catch (err: any) {
35
- setError(err.message);
36
- }
37
  setLoading(false);
38
  };
39
 
40
- const handleImportWallet = async () => {
41
- if (!mnemonic.trim()) {
42
- setError('Please enter your recovery phrase');
43
- return;
44
- }
45
- const words = mnemonic.trim().split(/\s+/);
46
- if (words.length !== 12 && words.length !== 24) {
47
- setError('Recovery phrase must be 12 or 24 words');
48
- return;
49
- }
50
- setLoading(true);
51
- setError('');
52
  try {
53
  if (window.solvox) {
54
- const result = await window.solvox.wallet.import(mnemonic.trim());
55
- if (result.success && result.publicKey) {
56
- setPublicKey(result.publicKey);
57
- setStep('pin');
58
- } else {
59
- setError(result.error || 'Invalid recovery phrase');
60
- }
61
- } else {
62
- setPublicKey('DevImported123');
63
- setStep('pin');
64
- }
65
- } catch (err: any) {
66
- setError(err.message);
67
- }
68
  setLoading(false);
69
  };
70
 
71
- const handleSetPin = async () => {
72
- if (pin.length < 6) {
73
- setError('PIN must be at least 6 digits');
74
- return;
75
- }
76
- if (pin !== pinConfirm) {
77
- setError('PINs do not match');
78
- return;
79
- }
80
- setLoading(true);
81
- setError('');
82
  try {
83
- if (window.solvox) {
84
- const result = await window.solvox.auth.setPin(pin);
85
- if (!result.success) {
86
- setError(result.error || 'Failed to set PIN');
87
- setLoading(false);
88
- return;
89
- }
90
- }
91
  setStep('done');
92
- } catch (err: any) {
93
- setError(err.message);
94
- }
95
  setLoading(false);
96
  };
97
 
98
  return (
99
- <div className="h-screen flex items-center justify-center bg-sol-dark">
100
- <div className="w-full max-w-lg p-8">
 
 
 
101
  {step === 'welcome' && (
102
  <div className="text-center slide-enter">
103
- <h1 className="text-6xl font-bold gradient-text mb-4">SolVox</h1>
104
- <p className="text-xl text-sol-muted mb-2">Voice-First Private AI Wallet</p>
105
- <p className="text-sm text-sol-muted mb-12">
106
- Powered by QVAC 100% Local AI • Zero Cloud Dependencies
107
- </p>
108
- <div className="grid grid-cols-3 gap-4 mb-12">
 
 
109
  {[
110
  { icon: '🎤', label: 'Voice Control', desc: 'Talk to your wallet' },
111
- { icon: '🧠', label: 'Local AI', desc: 'All AI runs on-device' },
112
- { icon: '🔒', label: 'Self-Custody', desc: 'Your keys, your coins' },
113
  ].map(f => (
114
- <div key={f.label} className="glass rounded-xl p-4 text-center">
115
- <div className="text-3xl mb-2">{f.icon}</div>
116
- <div className="text-sm font-semibold">{f.label}</div>
117
- <div className="text-xs text-sol-muted mt-1">{f.desc}</div>
118
  </div>
119
  ))}
120
  </div>
121
- <button
122
- onClick={() => setStep('create_or_import')}
123
- className="px-8 py-4 rounded-xl bg-sol-purple text-white font-bold text-lg hover:bg-sol-purple/90 transition-all glow-purple"
124
- >
125
- Get Started
126
  </button>
127
  </div>
128
  )}
129
 
130
  {step === 'create_or_import' && (
131
- <div className="space-y-6 slide-enter">
132
- <h2 className="text-3xl font-bold text-center mb-8">Set Up Your Wallet</h2>
133
- <button
134
- onClick={handleCreateWallet}
135
- disabled={loading}
136
- className="w-full p-6 rounded-xl glass border-2 border-transparent hover:border-sol-purple transition-all text-left group"
137
- >
138
  <div className="flex items-center gap-4">
139
- <span className="text-4xl">✨</span>
 
 
140
  <div>
141
- <div className="text-lg font-semibold group-hover:text-sol-purple transition-colors">
142
- Create New Wallet
143
- </div>
144
- <div className="text-sm text-sol-muted">
145
- Generate a fresh wallet with a new recovery phrase
146
- </div>
147
  </div>
148
  </div>
149
  </button>
150
- <button
151
- onClick={() => setStep('import')}
152
- className="w-full p-6 rounded-xl glass border-2 border-transparent hover:border-tether-green transition-all text-left group"
153
- >
154
  <div className="flex items-center gap-4">
155
- <span className="text-4xl">📥</span>
 
 
156
  <div>
157
- <div className="text-lg font-semibold group-hover:text-tether-green transition-colors">
158
- Import Existing Wallet
159
- </div>
160
- <div className="text-sm text-sol-muted">
161
- Use a 12 or 24 word recovery phrase
162
- </div>
163
  </div>
164
  </div>
165
  </button>
166
- {error && <div className="text-danger text-sm text-center">{error}</div>}
167
  </div>
168
  )}
169
 
170
  {step === 'import' && (
171
- <div className="space-y-6 slide-enter">
172
- <h2 className="text-2xl font-bold text-center">Import Recovery Phrase</h2>
173
- <p className="text-sm text-sol-muted text-center">
174
- Enter your 12 or 24 word recovery phrase. This stays on your device — never sent anywhere.
175
- </p>
176
- <textarea
177
- value={mnemonic}
178
- onChange={(e) => setMnemonic(e.target.value)}
179
- placeholder="word1 word2 word3 ..."
180
- rows={4}
181
- className="w-full px-4 py-3 bg-sol-card border border-sol-border rounded-xl text-sol-text font-mono text-sm focus:border-sol-purple focus:outline-none resize-none"
182
- />
183
- {error && <div className="text-danger text-sm">{error}</div>}
184
  <div className="flex gap-3">
185
- <button
186
- onClick={() => { setStep('create_or_import'); setError(''); }}
187
- className="flex-1 py-3 rounded-xl border border-sol-border text-sol-muted hover:text-sol-text transition-colors"
188
- >
189
- Back
190
- </button>
191
- <button
192
- onClick={handleImportWallet}
193
- disabled={loading}
194
- className="flex-1 py-3 rounded-xl bg-tether-green text-white font-semibold hover:bg-tether-green/90 disabled:opacity-50 transition-all"
195
- >
196
- {loading ? 'Importing...' : 'Import'}
197
  </button>
198
  </div>
199
  </div>
200
  )}
201
 
202
  {step === 'pin' && (
203
- <div className="space-y-6 slide-enter">
204
- <h2 className="text-2xl font-bold text-center">Set Your PIN</h2>
205
- <p className="text-sm text-sol-muted text-center">
206
- This PIN encrypts your wallet. You'll need it every time you open SolVox.
207
- </p>
208
- <input
209
- type="password"
210
- inputMode="numeric"
211
- value={pin}
212
- onChange={(e) => setPin(e.target.value.replace(/\D/g, ''))}
213
- placeholder="Enter PIN (min 6 digits)"
214
- maxLength={12}
215
- className="w-full px-4 py-3 bg-sol-card border border-sol-border rounded-xl text-center text-2xl tracking-[0.5em] font-mono focus:border-sol-purple focus:outline-none"
216
- />
217
- <input
218
- type="password"
219
- inputMode="numeric"
220
- value={pinConfirm}
221
- onChange={(e) => setPinConfirm(e.target.value.replace(/\D/g, ''))}
222
- placeholder="Confirm PIN"
223
- maxLength={12}
224
- className="w-full px-4 py-3 bg-sol-card border border-sol-border rounded-xl text-center text-2xl tracking-[0.5em] font-mono focus:border-sol-purple focus:outline-none"
225
- />
226
- {error && <div className="text-danger text-sm text-center">{error}</div>}
227
- <button
228
- onClick={handleSetPin}
229
- disabled={loading || pin.length < 6}
230
- className="w-full py-3 rounded-xl bg-sol-purple text-white font-semibold hover:bg-sol-purple/90 disabled:opacity-50 transition-all"
231
- >
232
- {loading ? 'Setting up...' : 'Continue'}
233
  </button>
234
  </div>
235
  )}
236
 
237
  {step === 'done' && (
238
- <div className="text-center space-y-6 slide-enter">
239
- <div className="text-6xl mb-4">🎉</div>
240
- <h2 className="text-3xl font-bold">You're All Set!</h2>
241
- <p className="text-sol-muted">
242
- Your wallet is created and secured. Your AI assistant runs 100% locally — no data ever leaves your device.
243
- </p>
244
- <div className="glass rounded-xl p-4 font-mono text-xs text-sol-muted break-all">
245
- {publicKey}
246
  </div>
247
- <button
248
- onClick={() => onComplete(publicKey)}
249
- className="px-8 py-4 rounded-xl bg-sol-purple text-white font-bold text-lg hover:bg-sol-purple/90 transition-all glow-purple"
250
- >
251
- Open SolVox
252
  </button>
253
  </div>
254
  )}
 
1
  import React, { useState } from 'react';
2
+ import { ParticlesBackground, StepIndicator } from '../components/ui/index';
3
 
4
+ interface OnboardingScreenProps { onComplete: (publicKey: string) => void; }
5
+ type Step = 'welcome' | 'create_or_import' | 'import' | 'pin' | 'done';
 
6
 
7
+ const STEP_LABELS = ['Welcome', 'Wallet', 'Secure', 'Done'];
8
+ const stepIndex = (s: Step) => s === 'welcome' ? 0 : s === 'create_or_import' ? 1 : s === 'import' ? 1 : s === 'pin' ? 2 : 3;
9
 
10
  export default function OnboardingScreen({ onComplete }: OnboardingScreenProps) {
11
  const [step, setStep] = useState<Step>('welcome');
 
16
  const [error, setError] = useState('');
17
  const [loading, setLoading] = useState(false);
18
 
19
+ const createWallet = async () => {
20
+ setLoading(true); setError('');
 
21
  try {
22
  if (window.solvox) {
23
+ const r = await window.solvox.wallet.create();
24
+ if (r.success && r.publicKey) { setPublicKey(r.publicKey); setStep('pin'); }
25
+ else setError(r.error || 'Failed');
26
+ } else { setPublicKey('DevMode' + Date.now()); setStep('pin'); }
27
+ } catch (e: any) { setError(e.message); }
 
 
 
 
 
 
 
 
 
28
  setLoading(false);
29
  };
30
 
31
+ const importWallet = async () => {
32
+ if (!mnemonic.trim()) return setError('Enter your recovery phrase');
33
+ const w = mnemonic.trim().split(/\s+/);
34
+ if (w.length !== 12 && w.length !== 24) return setError('Must be 12 or 24 words');
35
+ setLoading(true); setError('');
 
 
 
 
 
 
 
36
  try {
37
  if (window.solvox) {
38
+ const r = await window.solvox.wallet.import(mnemonic.trim());
39
+ if (r.success && r.publicKey) { setPublicKey(r.publicKey); setStep('pin'); }
40
+ else setError(r.error || 'Invalid phrase');
41
+ } else { setPublicKey('DevImport' + Date.now()); setStep('pin'); }
42
+ } catch (e: any) { setError(e.message); }
 
 
 
 
 
 
 
 
 
43
  setLoading(false);
44
  };
45
 
46
+ const setWalletPin = async () => {
47
+ if (pin.length < 6) return setError('PIN must be at least 6 digits');
48
+ if (pin !== pinConfirm) return setError('PINs do not match');
49
+ setLoading(true); setError('');
 
 
 
 
 
 
 
50
  try {
51
+ if (window.solvox) { const r = await window.solvox.auth.setPin(pin); if (!r.success) { setError(r.error || 'Failed'); setLoading(false); return; } }
 
 
 
 
 
 
 
52
  setStep('done');
53
+ } catch (e: any) { setError(e.message); }
 
 
54
  setLoading(false);
55
  };
56
 
57
  return (
58
+ <div className="h-screen flex items-center justify-center mesh-bg relative overflow-hidden">
59
+ <ParticlesBackground />
60
+ <div className="w-full max-w-md p-6 relative z-10">
61
+ {step !== 'welcome' && <StepIndicator steps={STEP_LABELS} current={stepIndex(step)} />}
62
+
63
  {step === 'welcome' && (
64
  <div className="text-center slide-enter">
65
+ <div className="w-20 h-20 mx-auto rounded-3xl bg-gradient-to-br from-sol-purple to-sol-green flex items-center justify-center mb-6 shadow-glow-lg animate-float">
66
+ <span className="text-white text-2xl font-black">SV</span>
67
+ </div>
68
+ <h1 className="text-5xl font-extrabold gradient-text mb-3">SolVox</h1>
69
+ <p className="text-lg text-sol-muted mb-1">Voice-First Private AI Wallet</p>
70
+ <p className="text-xs text-sol-muted/60 mb-10">Powered by QVAC • 100% Local AI • Zero Cloud</p>
71
+
72
+ <div className="grid grid-cols-3 gap-3 mb-10 stagger">
73
  {[
74
  { icon: '🎤', label: 'Voice Control', desc: 'Talk to your wallet' },
75
+ { icon: '🧠', label: 'Local AI', desc: '6 QVAC modules' },
76
+ { icon: '🔒', label: 'Self-Custody', desc: 'Your keys only' },
77
  ].map(f => (
78
+ <div key={f.label} className="glass glass-hover rounded-2xl p-4 text-center">
79
+ <div className="text-2xl mb-2">{f.icon}</div>
80
+ <div className="text-xs font-semibold text-sol-text">{f.label}</div>
81
+ <div className="text-[10px] text-sol-muted mt-0.5">{f.desc}</div>
82
  </div>
83
  ))}
84
  </div>
85
+
86
+ <button onClick={() => setStep('create_or_import')} className="btn-primary px-10 py-4 text-white text-lg">
87
+ <span>Get Started</span>
 
 
88
  </button>
89
  </div>
90
  )}
91
 
92
  {step === 'create_or_import' && (
93
+ <div className="space-y-4 slide-enter">
94
+ <h2 className="text-2xl font-bold text-center mb-6">Create Your Wallet</h2>
95
+ <button onClick={createWallet} disabled={loading}
96
+ className="w-full glass glass-hover rounded-2xl p-5 text-left group border-2 border-transparent hover:border-sol-purple/20 transition-all">
 
 
 
97
  <div className="flex items-center gap-4">
98
+ <div className="w-12 h-12 rounded-xl bg-gradient-to-br from-sol-purple to-sol-purple-light flex items-center justify-center shadow-glow-sm group-hover:scale-105 transition-transform">
99
+ <span className="text-white text-xl">✦</span>
100
+ </div>
101
  <div>
102
+ <div className="text-base font-bold group-hover:text-sol-purple transition-colors">Create New Wallet</div>
103
+ <div className="text-xs text-sol-muted">Generate a fresh 24-word recovery phrase</div>
 
 
 
 
104
  </div>
105
  </div>
106
  </button>
107
+ <button onClick={() => setStep('import')}
108
+ className="w-full glass glass-hover rounded-2xl p-5 text-left group border-2 border-transparent hover:border-tether-green/20 transition-all">
 
 
109
  <div className="flex items-center gap-4">
110
+ <div className="w-12 h-12 rounded-xl bg-gradient-to-br from-tether-green to-tether-dark flex items-center justify-center shadow-glow-sm group-hover:scale-105 transition-transform">
111
+ <span className="text-white text-xl">↓</span>
112
+ </div>
113
  <div>
114
+ <div className="text-base font-bold group-hover:text-tether-green transition-colors">Import Existing</div>
115
+ <div className="text-xs text-sol-muted">Use a 12 or 24 word recovery phrase</div>
 
 
 
 
116
  </div>
117
  </div>
118
  </button>
119
+ {error && <div className="text-danger text-xs text-center">{error}</div>}
120
  </div>
121
  )}
122
 
123
  {step === 'import' && (
124
+ <div className="space-y-5 slide-enter">
125
+ <h2 className="text-xl font-bold text-center">Import Recovery Phrase</h2>
126
+ <p className="text-xs text-sol-muted text-center">Stays on your device — never sent anywhere.</p>
127
+ <textarea value={mnemonic} onChange={e => setMnemonic(e.target.value)} placeholder="word1 word2 word3 ..." rows={4}
128
+ className="input-field font-mono text-xs resize-none" />
129
+ {error && <div className="text-danger text-xs">{error}</div>}
 
 
 
 
 
 
 
130
  <div className="flex gap-3">
131
+ <button onClick={() => { setStep('create_or_import'); setError(''); }} className="btn-ghost flex-1 py-3">Back</button>
132
+ <button onClick={importWallet} disabled={loading} className="btn-primary flex-1 py-3 text-white disabled:opacity-50">
133
+ <span>{loading ? 'Importing...' : 'Import'}</span>
 
 
 
 
 
 
 
 
 
134
  </button>
135
  </div>
136
  </div>
137
  )}
138
 
139
  {step === 'pin' && (
140
+ <div className="space-y-5 slide-enter">
141
+ <h2 className="text-xl font-bold text-center">Set Your PIN</h2>
142
+ <p className="text-xs text-sol-muted text-center">This encrypts your wallet with AES-256-GCM + PBKDF2.</p>
143
+ <input type="password" inputMode="numeric" value={pin} onChange={e => setPin(e.target.value.replace(/\D/g, ''))}
144
+ placeholder="Enter PIN (min 6 digits)" maxLength={12} className="input-field text-center text-xl tracking-[0.5em] font-mono" />
145
+ <input type="password" inputMode="numeric" value={pinConfirm} onChange={e => setPinConfirm(e.target.value.replace(/\D/g, ''))}
146
+ placeholder="Confirm PIN" maxLength={12} className="input-field text-center text-xl tracking-[0.5em] font-mono" />
147
+ {pin.length >= 6 && pin === pinConfirm && <div className="badge badge-green mx-auto">✓ PINs match</div>}
148
+ {error && <div className="text-danger text-xs text-center">{error}</div>}
149
+ <button onClick={setWalletPin} disabled={loading || pin.length < 6} className="btn-primary w-full py-3.5 text-white disabled:opacity-40">
150
+ <span>{loading ? 'Encrypting...' : 'Continue'}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  </button>
152
  </div>
153
  )}
154
 
155
  {step === 'done' && (
156
+ <div className="text-center space-y-6 scale-in">
157
+ <div className="w-16 h-16 mx-auto rounded-full bg-sol-green/15 flex items-center justify-center">
158
+ <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#14F195" strokeWidth="2.5" strokeLinecap="round"><polyline points="20 6 9 17 4 12"/></svg>
 
 
 
 
 
159
  </div>
160
+ <h2 className="text-3xl font-bold gradient-text-static">You're All Set!</h2>
161
+ <p className="text-sm text-sol-muted">Wallet encrypted & secured. Your AI assistant runs 100% locally.</p>
162
+ <div className="glass rounded-xl p-3 font-mono text-[10px] text-sol-muted break-all">{publicKey}</div>
163
+ <button onClick={() => onComplete(publicKey)} className="btn-primary px-10 py-4 text-white text-lg">
164
+ <span>Open SolVox</span>
165
  </button>
166
  </div>
167
  )}
src/renderer/pages/SecurityPage.tsx CHANGED
@@ -1,226 +1,131 @@
1
  import React, { useState, useEffect } from 'react';
 
2
 
3
  export default function SecurityPage() {
4
  const [settings, setSettings] = useState<any>({});
5
  const [whitelist, setWhitelist] = useState<any[]>([]);
6
  const [anomalies, setAnomalies] = useState<any[]>([]);
7
- const [newAddress, setNewAddress] = useState('');
8
  const [newLabel, setNewLabel] = useState('');
9
  const [error, setError] = useState('');
10
- const [saved, setSaved] = useState(false);
11
 
12
- useEffect(() => {
13
- loadSecurity();
14
- }, []);
15
 
16
- const loadSecurity = async () => {
17
  if (!window.solvox) return;
18
  try {
19
- const [settingsResult, whitelistResult, anomalyResult] = await Promise.all([
20
- window.solvox.security.getSettings(),
21
- window.solvox.security.getWhitelist(),
22
- window.solvox.security.getAnomalies(),
23
- ]);
24
- setSettings(settingsResult);
25
- setWhitelist(whitelistResult);
26
- setAnomalies(anomalyResult);
27
  } catch {}
28
  };
29
 
30
- const handleUpdateSettings = async (key: string, value: any) => {
31
- const updated = { ...settings, [key]: value };
32
- setSettings(updated);
33
- if (window.solvox) {
34
- await window.solvox.security.updateSettings(updated);
35
- setSaved(true);
36
- setTimeout(() => setSaved(false), 2000);
37
- }
38
  };
39
 
40
- const handleAddWhitelist = async () => {
41
- if (!newAddress.trim() || !newLabel.trim()) {
42
- setError('Address and label are required');
43
- return;
44
- }
45
  if (window.solvox) {
46
- const result = await window.solvox.security.addWhitelist(newAddress.trim(), newLabel.trim());
47
- if (result.success) {
48
- setNewAddress('');
49
- setNewLabel('');
50
- setError('');
51
- loadSecurity();
52
- } else {
53
- setError(result.error || 'Failed to add');
54
- }
55
  }
56
  };
57
 
58
- const handleRemoveWhitelist = async (address: string) => {
59
- if (window.solvox) {
60
- await window.solvox.security.removeWhitelist(address);
61
- loadSecurity();
62
- }
63
- };
64
 
65
  return (
66
- <div className="space-y-6 max-w-3xl slide-enter">
67
- <h2 className="text-2xl font-bold">🛡️ Security Center</h2>
68
 
69
- {saved && (
70
- <div className="bg-sol-green/20 text-sol-green rounded-xl p-3 text-sm text-center">
71
- Settings saved
 
 
 
 
 
 
 
72
  </div>
73
- )}
74
 
75
- {/* Transaction Limits */}
76
- <div className="glass rounded-2xl p-6">
77
- <h3 className="text-lg font-semibold mb-4">Transaction Limits</h3>
78
- <div className="grid grid-cols-2 gap-4">
79
- <div>
80
- <label className="text-sm text-sol-muted block mb-1">Max Single Transaction</label>
81
- <div className="flex items-center gap-2">
82
- <input
83
- type="number"
84
- value={settings.maxSingleTx || 1000}
85
- onChange={(e) => handleUpdateSettings('maxSingleTx', Number(e.target.value))}
86
- className="flex-1 px-3 py-2 bg-sol-dark border border-sol-border rounded-lg text-sm focus:border-sol-purple focus:outline-none"
87
- />
88
- <span className="text-sol-muted text-sm">tokens</span>
89
- </div>
90
- </div>
91
- <div>
92
- <label className="text-sm text-sol-muted block mb-1">Max Daily Volume</label>
93
- <div className="flex items-center gap-2">
94
- <input
95
- type="number"
96
- value={settings.maxDailyVolume || 5000}
97
- onChange={(e) => handleUpdateSettings('maxDailyVolume', Number(e.target.value))}
98
- className="flex-1 px-3 py-2 bg-sol-dark border border-sol-border rounded-lg text-sm focus:border-sol-purple focus:outline-none"
99
- />
100
- <span className="text-sol-muted text-sm">tokens/day</span>
101
- </div>
102
- </div>
103
- <div>
104
- <label className="text-sm text-sol-muted block mb-1">Max Transactions/Hour</label>
105
- <input
106
- type="number"
107
- value={settings.velocityLimit || 10}
108
- onChange={(e) => handleUpdateSettings('velocityLimit', Number(e.target.value))}
109
- className="w-full px-3 py-2 bg-sol-dark border border-sol-border rounded-lg text-sm focus:border-sol-purple focus:outline-none"
110
- />
111
- </div>
112
- <div>
113
- <label className="text-sm text-sol-muted block mb-1">Cooldown (minutes)</label>
114
- <input
115
- type="number"
116
- value={settings.cooldownMinutes || 1}
117
- onChange={(e) => handleUpdateSettings('cooldownMinutes', Number(e.target.value))}
118
- className="w-full px-3 py-2 bg-sol-dark border border-sol-border rounded-lg text-sm focus:border-sol-purple focus:outline-none"
119
- />
120
  </div>
121
  </div>
122
  </div>
123
 
124
- {/* Security Toggles */}
125
- <div className="glass rounded-2xl p-6">
126
- <h3 className="text-lg font-semibold mb-4">Security Features</h3>
127
- <div className="space-y-4">
128
  {[
129
- {
130
- key: 'whitelistEnabled',
131
- label: 'Address Whitelisting',
132
- desc: 'Only allow transactions to pre-approved addresses',
133
- icon: '📋',
134
- },
135
- {
136
- key: 'anomalyDetection',
137
- label: 'AI Anomaly Detection',
138
- desc: 'Detect unusual transaction patterns using local AI',
139
- icon: '🧠',
140
- },
141
- {
142
- key: 'requireConfirmation',
143
- label: 'Transaction Confirmation',
144
- desc: 'Always require explicit confirmation before sending',
145
- icon: '✅',
146
- },
147
- ].map(toggle => (
148
- <div key={toggle.key} className="flex items-center justify-between py-2">
149
  <div className="flex items-center gap-3">
150
- <span className="text-2xl">{toggle.icon}</span>
151
  <div>
152
- <div className="text-sm font-semibold">{toggle.label}</div>
153
- <div className="text-xs text-sol-muted">{toggle.desc}</div>
154
  </div>
155
  </div>
156
- <button
157
- onClick={() => handleUpdateSettings(toggle.key, !settings[toggle.key])}
158
- className={`relative w-12 h-6 rounded-full transition-colors ${
159
- settings[toggle.key] ? 'bg-sol-green' : 'bg-sol-border'
160
- }`}
161
- >
162
- <span
163
- className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-transform ${
164
- settings[toggle.key] ? 'translate-x-6' : 'translate-x-0.5'
165
- }`}
166
- />
167
  </button>
168
  </div>
169
  ))}
170
  </div>
171
  </div>
172
 
173
- {/* Address Whitelist */}
174
- <div className="glass rounded-2xl p-6">
175
- <h3 className="text-lg font-semibold mb-4">📋 Address Whitelist</h3>
176
-
177
- {/* Add New */}
178
- <div className="flex gap-2 mb-4">
179
- <input
180
- value={newLabel}
181
- onChange={(e) => setNewLabel(e.target.value)}
182
- placeholder="Label (e.g. Alice)"
183
- className="w-32 px-3 py-2 bg-sol-dark border border-sol-border rounded-lg text-sm focus:border-sol-purple focus:outline-none"
184
- />
185
- <input
186
- value={newAddress}
187
- onChange={(e) => setNewAddress(e.target.value)}
188
- placeholder="Solana address"
189
- className="flex-1 px-3 py-2 bg-sol-dark border border-sol-border rounded-lg text-sm font-mono focus:border-sol-purple focus:outline-none"
190
- />
191
- <button
192
- onClick={handleAddWhitelist}
193
- className="px-4 py-2 rounded-lg bg-sol-purple text-white text-sm hover:bg-sol-purple/90 transition-colors"
194
- >
195
- + Add
196
- </button>
197
  </div>
198
- {error && <div className="text-danger text-xs mb-3">{error}</div>}
199
-
200
- {/* Whitelist Entries */}
201
  {whitelist.length === 0 ? (
202
- <div className="text-center py-6 text-sol-muted text-sm">
203
- No whitelisted addresses yet
204
- </div>
205
  ) : (
206
  <div className="space-y-2">
207
- {whitelist.map((entry, i) => (
208
- <div key={i} className="flex items-center justify-between bg-sol-dark rounded-lg p-3">
209
  <div>
210
- <div className="text-sm font-semibold">{entry.label}</div>
211
- <div className="text-xs font-mono text-sol-muted">
212
- {entry.address.slice(0, 12)}...{entry.address.slice(-8)}
213
- </div>
214
- <div className="text-xs text-sol-muted mt-0.5">
215
- Added: {new Date(entry.addedAt).toLocaleDateString()}
216
- </div>
217
  </div>
218
- <button
219
- onClick={() => handleRemoveWhitelist(entry.address)}
220
- className="text-danger text-xs hover:underline"
221
- >
222
- Remove
223
- </button>
224
  </div>
225
  ))}
226
  </div>
@@ -228,37 +133,32 @@ export default function SecurityPage() {
228
  </div>
229
 
230
  {/* Anomaly Log */}
231
- <div className="glass rounded-2xl p-6">
232
- <h3 className="text-lg font-semibold mb-4">🔍 Anomaly Detection Log</h3>
233
- <p className="text-xs text-sol-muted mb-3">
234
- Powered by AI-driven pattern analysis — runs locally via QVAC
235
- </p>
236
  {anomalies.length === 0 ? (
237
- <div className="text-center py-6 text-sol-muted text-sm">
238
- No anomalies detected all clear
 
 
 
239
  </div>
240
  ) : (
241
- <div className="space-y-2">
242
- {anomalies.map((anomaly, i) => (
243
- <div key={i} className={`rounded-lg p-3 ${
244
- anomaly.severity === 'high' ? 'bg-danger/10 border border-danger/30' :
245
- anomaly.severity === 'medium' ? 'bg-warning/10 border border-warning/30' :
246
- 'bg-sol-dark border border-sol-border'
247
  }`}>
248
  <div className="flex items-center gap-2 mb-1">
249
- <span className={`text-xs px-2 py-0.5 rounded-full ${
250
- anomaly.severity === 'high' ? 'bg-danger/20 text-danger' :
251
- anomaly.severity === 'medium' ? 'bg-warning/20 text-warning' :
252
- 'bg-sol-muted/20 text-sol-muted'
253
- }`}>
254
- {anomaly.severity.toUpperCase()}
255
- </span>
256
- <span className="text-xs text-sol-muted">{anomaly.type}</span>
257
- </div>
258
- <div className="text-sm">{anomaly.description}</div>
259
- <div className="text-xs text-sol-muted mt-1">
260
- {new Date(anomaly.timestamp).toLocaleString()}
261
  </div>
 
 
262
  </div>
263
  ))}
264
  </div>
 
1
  import React, { useState, useEffect } from 'react';
2
+ import { CircularProgress, useToast } from '../components/ui/index';
3
 
4
  export default function SecurityPage() {
5
  const [settings, setSettings] = useState<any>({});
6
  const [whitelist, setWhitelist] = useState<any[]>([]);
7
  const [anomalies, setAnomalies] = useState<any[]>([]);
8
+ const [newAddr, setNewAddr] = useState('');
9
  const [newLabel, setNewLabel] = useState('');
10
  const [error, setError] = useState('');
11
+ const { addToast } = useToast();
12
 
13
+ useEffect(() => { load(); }, []);
 
 
14
 
15
+ const load = async () => {
16
  if (!window.solvox) return;
17
  try {
18
+ const [s, w, a] = await Promise.all([window.solvox.security.getSettings(), window.solvox.security.getWhitelist(), window.solvox.security.getAnomalies()]);
19
+ setSettings(s); setWhitelist(w); setAnomalies(a);
 
 
 
 
 
 
20
  } catch {}
21
  };
22
 
23
+ const update = async (k: string, v: any) => {
24
+ const u = { ...settings, [k]: v };
25
+ setSettings(u);
26
+ if (window.solvox) { await window.solvox.security.updateSettings(u); addToast({ type: 'success', title: 'Saved', duration: 1500 }); }
 
 
 
 
27
  };
28
 
29
+ const addWL = async () => {
30
+ if (!newAddr.trim() || !newLabel.trim()) return setError('Both required');
 
 
 
31
  if (window.solvox) {
32
+ const r = await window.solvox.security.addWhitelist(newAddr.trim(), newLabel.trim());
33
+ if (r.success) { setNewAddr(''); setNewLabel(''); setError(''); load(); addToast({ type: 'success', title: 'Address whitelisted' }); }
34
+ else setError(r.error || 'Failed');
 
 
 
 
 
 
35
  }
36
  };
37
 
38
+ const removeWL = async (addr: string) => { if (window.solvox) { await window.solvox.security.removeWhitelist(addr); load(); } };
39
+
40
+ // Security score
41
+ const features = [settings.whitelistEnabled, settings.anomalyDetection, settings.requireConfirmation, (settings.maxSingleTx || 0) < 5000, (settings.velocityLimit || 0) <= 10];
42
+ const score = Math.round((features.filter(Boolean).length / features.length) * 100);
43
+ const scoreColor = score >= 80 ? '#14F195' : score >= 50 ? '#FFB830' : '#FF4466';
44
 
45
  return (
46
+ <div className="space-y-5 max-w-3xl page-enter">
47
+ <h2 className="text-xl font-bold">Security Center</h2>
48
 
49
+ {/* Security Score + Limits */}
50
+ <div className="grid grid-cols-3 gap-4">
51
+ <div className="glass rounded-2xl p-5 flex flex-col items-center justify-center">
52
+ <CircularProgress value={score} size={90} strokeWidth={6} color={scoreColor}>
53
+ <div className="text-center">
54
+ <div className="text-lg font-extrabold" style={{ color: scoreColor }}>{score}</div>
55
+ <div className="text-[8px] text-sol-muted">/ 100</div>
56
+ </div>
57
+ </CircularProgress>
58
+ <div className="text-xs font-semibold text-sol-text mt-3">Security Score</div>
59
  </div>
 
60
 
61
+ <div className="col-span-2 glass rounded-2xl p-5">
62
+ <div className="text-xs text-sol-muted font-semibold uppercase tracking-wider mb-3">Transaction Limits</div>
63
+ <div className="grid grid-cols-2 gap-3">
64
+ {[
65
+ { key: 'maxSingleTx', label: 'Max per TX', suffix: 'tokens', val: settings.maxSingleTx || 1000 },
66
+ { key: 'maxDailyVolume', label: 'Daily limit', suffix: '/day', val: settings.maxDailyVolume || 5000 },
67
+ { key: 'velocityLimit', label: 'Max TX/hour', suffix: '', val: settings.velocityLimit || 10 },
68
+ { key: 'cooldownMinutes', label: 'Cooldown', suffix: 'min', val: settings.cooldownMinutes || 1 },
69
+ ].map(f => (
70
+ <div key={f.key}>
71
+ <label className="text-[10px] text-sol-muted block mb-1">{f.label}</label>
72
+ <div className="flex items-center gap-1.5">
73
+ <input type="number" value={f.val} onChange={e => update(f.key, Number(e.target.value))}
74
+ className="input-field text-xs py-1.5 w-full" />
75
+ {f.suffix && <span className="text-[10px] text-sol-muted whitespace-nowrap">{f.suffix}</span>}
76
+ </div>
77
+ </div>
78
+ ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  </div>
80
  </div>
81
  </div>
82
 
83
+ {/* Toggles */}
84
+ <div className="glass rounded-2xl p-5">
85
+ <div className="text-xs text-sol-muted font-semibold uppercase tracking-wider mb-4">Security Features</div>
86
+ <div className="space-y-3 stagger">
87
  {[
88
+ { key: 'whitelistEnabled', label: 'Address Whitelisting', desc: 'Only allow sends to approved addresses', icon: '📋' },
89
+ { key: 'anomalyDetection', label: 'AI Anomaly Detection', desc: 'Local AI flags unusual patterns', icon: '🧠' },
90
+ { key: 'requireConfirmation', label: 'Transaction Confirmation', desc: 'Always require explicit approval', icon: '✅' },
91
+ ].map(t => (
92
+ <div key={t.key} className="flex items-center justify-between py-2 px-3 rounded-xl hover:bg-sol-dark/30 transition-colors">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  <div className="flex items-center gap-3">
94
+ <div className="w-9 h-9 rounded-lg bg-sol-card flex items-center justify-center text-lg">{t.icon}</div>
95
  <div>
96
+ <div className="text-xs font-semibold text-sol-text">{t.label}</div>
97
+ <div className="text-[10px] text-sol-muted">{t.desc}</div>
98
  </div>
99
  </div>
100
+ <button onClick={() => update(t.key, !settings[t.key])}
101
+ className={`relative w-11 h-6 rounded-full transition-all duration-300 ${settings[t.key] ? 'bg-sol-green' : 'bg-sol-border/60'}`}>
102
+ <span className={`absolute top-0.5 w-5 h-5 rounded-full bg-white shadow transition-all duration-300 ${settings[t.key] ? 'left-[22px]' : 'left-0.5'}`} />
 
 
 
 
 
 
 
 
103
  </button>
104
  </div>
105
  ))}
106
  </div>
107
  </div>
108
 
109
+ {/* Whitelist */}
110
+ <div className="glass rounded-2xl p-5">
111
+ <div className="text-xs text-sol-muted font-semibold uppercase tracking-wider mb-3">Address Whitelist</div>
112
+ <div className="flex gap-2 mb-3">
113
+ <input value={newLabel} onChange={e => setNewLabel(e.target.value)} placeholder="Label" className="input-field text-xs w-28 py-1.5" />
114
+ <input value={newAddr} onChange={e => setNewAddr(e.target.value)} placeholder="Solana address" className="input-field text-xs flex-1 py-1.5 font-mono" />
115
+ <button onClick={addWL} className="btn-primary px-3 py-1.5 text-white text-xs"><span>Add</span></button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  </div>
117
+ {error && <div className="text-danger text-[10px] mb-2">{error}</div>}
 
 
118
  {whitelist.length === 0 ? (
119
+ <div className="text-center py-6 text-xs text-sol-muted/60">No addresses whitelisted</div>
 
 
120
  ) : (
121
  <div className="space-y-2">
122
+ {whitelist.map((e, i) => (
123
+ <div key={i} className="flex items-center justify-between bg-sol-dark/40 rounded-xl p-3">
124
  <div>
125
+ <div className="text-xs font-semibold">{e.label}</div>
126
+ <div className="text-[10px] font-mono text-sol-muted">{e.address.slice(0, 10)}...{e.address.slice(-6)}</div>
 
 
 
 
 
127
  </div>
128
+ <button onClick={() => removeWL(e.address)} className="text-danger text-[10px] hover:underline">Remove</button>
 
 
 
 
 
129
  </div>
130
  ))}
131
  </div>
 
133
  </div>
134
 
135
  {/* Anomaly Log */}
136
+ <div className="glass rounded-2xl p-5">
137
+ <div className="flex items-center justify-between mb-3">
138
+ <div className="text-xs text-sol-muted font-semibold uppercase tracking-wider">Anomaly Detection Log</div>
139
+ <div className="badge badge-tether text-[10px]">AI-Powered Local</div>
140
+ </div>
141
  {anomalies.length === 0 ? (
142
+ <div className="text-center py-8">
143
+ <div className="w-12 h-12 mx-auto rounded-xl bg-sol-green/10 flex items-center justify-center mb-2">
144
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#14F195" strokeWidth="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
145
+ </div>
146
+ <div className="text-xs text-sol-muted">All clear — no anomalies detected</div>
147
  </div>
148
  ) : (
149
+ <div className="space-y-2 stagger">
150
+ {anomalies.map((a, i) => (
151
+ <div key={i} className={`rounded-xl p-3 border ${
152
+ a.severity === 'high' ? 'bg-danger/5 border-danger/15' : a.severity === 'medium' ? 'bg-warning/5 border-warning/15' : 'bg-sol-dark/30 border-sol-border/30'
 
 
153
  }`}>
154
  <div className="flex items-center gap-2 mb-1">
155
+ <div className={`badge text-[10px] ${a.severity === 'high' ? 'badge-danger' : a.severity === 'medium' ? 'badge-warn' : 'badge-muted'}`}>
156
+ {a.severity.toUpperCase()}
157
+ </div>
158
+ <span className="text-[10px] text-sol-muted">{a.type}</span>
 
 
 
 
 
 
 
 
159
  </div>
160
+ <div className="text-xs text-sol-text">{a.description}</div>
161
+ <div className="text-[10px] text-sol-muted/60 mt-1">{new Date(a.timestamp).toLocaleString()}</div>
162
  </div>
163
  ))}
164
  </div>
src/renderer/pages/SendPage.tsx CHANGED
@@ -1,101 +1,72 @@
1
  import React, { useState } from 'react';
 
2
 
3
- interface SendPageProps {
4
- balance: { sol: number; usdt: number };
5
- onSent: () => void;
6
- }
7
 
8
  export default function SendPage({ balance, onSent }: SendPageProps) {
9
  const [token, setToken] = useState<'SOL' | 'USDT'>('SOL');
10
  const [to, setTo] = useState('');
11
  const [amount, setAmount] = useState('');
12
- const [memo, setMemo] = useState('');
13
  const [loading, setLoading] = useState(false);
14
  const [error, setError] = useState('');
15
  const [success, setSuccess] = useState<{ signature: string; explorer: string } | null>(null);
16
  const [step, setStep] = useState<'form' | 'confirm' | 'result'>('form');
 
17
 
18
- const maxAmount = token === 'SOL' ? balance.sol : balance.usdt;
 
 
19
 
20
- const handleConfirm = () => {
21
  setError('');
22
- if (!to.trim()) {
23
- setError('Recipient address is required');
24
- return;
25
- }
26
- if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(to.trim())) {
27
- setError('Invalid Solana address');
28
- return;
29
- }
30
- const amountNum = parseFloat(amount);
31
- if (!amountNum || amountNum <= 0) {
32
- setError('Enter a valid amount');
33
- return;
34
- }
35
- if (amountNum > maxAmount) {
36
- setError(`Insufficient balance. Max: ${maxAmount.toFixed(token === 'SOL' ? 4 : 2)} ${token}`);
37
- return;
38
- }
39
  setStep('confirm');
40
  };
41
 
42
- const handleSend = async () => {
43
- setLoading(true);
44
- setError('');
45
  try {
46
- let result;
47
- if (window.solvox) {
48
- if (token === 'SOL') {
49
- result = await window.solvox.wallet.sendSOL(to.trim(), parseFloat(amount));
50
- } else {
51
- result = await window.solvox.wallet.sendUSDT(to.trim(), parseFloat(amount));
52
- }
53
- } else {
54
- result = { success: true, signature: 'dev_sig_123', explorer: '#' };
55
- }
56
-
57
- if (result.success) {
58
- setSuccess({ signature: result.signature!, explorer: result.explorer! });
59
  setStep('result');
60
  onSent();
61
- } else {
62
- setError(result.error || 'Transaction failed');
63
- setStep('form');
64
- }
65
- } catch (err: any) {
66
- setError(err.message);
67
- setStep('form');
68
- }
69
  setLoading(false);
70
  };
71
 
72
- const reset = () => {
73
- setTo('');
74
- setAmount('');
75
- setMemo('');
76
- setError('');
77
- setSuccess(null);
78
- setStep('form');
79
- };
80
 
81
  return (
82
- <div className="max-w-lg mx-auto slide-enter">
83
- <h2 className="text-2xl font-bold mb-6">Send {token}</h2>
 
 
 
 
 
 
 
 
 
84
 
85
  {step === 'form' && (
86
- <div className="glass rounded-2xl p-6 space-y-5">
87
  {/* Token Selector */}
88
- <div className="flex gap-2">
89
  {(['SOL', 'USDT'] as const).map(t => (
90
- <button
91
- key={t}
92
- onClick={() => setToken(t)}
93
- className={`flex-1 py-2 rounded-xl font-semibold transition-all ${
94
- token === t
95
- ? t === 'SOL' ? 'bg-sol-purple text-white' : 'bg-tether-green text-white'
96
- : 'bg-sol-dark text-sol-muted border border-sol-border'
97
- }`}
98
- >
99
  {t === 'SOL' ? '◎' : '₮'} {t}
100
  </button>
101
  ))}
@@ -103,129 +74,95 @@ export default function SendPage({ balance, onSent }: SendPageProps) {
103
 
104
  {/* Recipient */}
105
  <div>
106
- <label className="text-sm text-sol-muted block mb-1">Recipient Address</label>
107
- <input
108
- value={to}
109
- onChange={(e) => setTo(e.target.value)}
110
- placeholder="Enter Solana address"
111
- className="w-full px-4 py-3 bg-sol-dark border border-sol-border rounded-xl font-mono text-sm focus:border-sol-purple focus:outline-none"
112
- />
113
  </div>
114
 
115
  {/* Amount */}
116
  <div>
117
- <div className="flex justify-between items-center mb-1">
118
- <label className="text-sm text-sol-muted">Amount</label>
119
- <button
120
- onClick={() => setAmount(maxAmount.toString())}
121
- className="text-xs text-sol-purple hover:underline"
122
- >
123
- Max: {maxAmount.toFixed(token === 'SOL' ? 4 : 2)} {token}
124
  </button>
125
  </div>
126
  <div className="relative">
127
- <input
128
- type="number"
129
- value={amount}
130
- onChange={(e) => setAmount(e.target.value)}
131
- placeholder="0.00"
132
- step="any"
133
- min="0"
134
- className="w-full px-4 py-3 bg-sol-dark border border-sol-border rounded-xl text-lg font-bold focus:border-sol-purple focus:outline-none pr-16"
135
- />
136
- <span className="absolute right-4 top-1/2 -translate-y-1/2 text-sol-muted font-semibold">
137
- {token}
138
- </span>
 
 
 
 
 
139
  </div>
140
  </div>
141
 
142
- {error && (
143
- <div className="text-danger text-sm bg-danger/10 rounded-lg p-3">{error}</div>
144
- )}
145
 
146
- <button
147
- onClick={handleConfirm}
148
- className="w-full py-3 rounded-xl bg-sol-purple text-white font-semibold hover:bg-sol-purple/90 transition-all"
149
- >
150
- Review Transaction
151
  </button>
152
  </div>
153
  )}
154
 
155
  {step === 'confirm' && (
156
- <div className="glass rounded-2xl p-6 space-y-4">
157
- <h3 className="text-lg font-semibold text-center">Confirm Transaction</h3>
158
- <div className="bg-sol-dark rounded-xl p-4 space-y-3">
159
- <div className="flex justify-between">
160
- <span className="text-sol-muted">Token</span>
161
- <span className="font-semibold">{token}</span>
162
- </div>
163
- <div className="flex justify-between">
164
- <span className="text-sol-muted">Amount</span>
165
- <span className="font-bold text-lg">{amount} {token}</span>
166
- </div>
167
- <div className="flex justify-between">
168
- <span className="text-sol-muted">To</span>
169
- <span className="font-mono text-xs">{to.slice(0, 12)}...{to.slice(-8)}</span>
170
- </div>
171
- <div className="flex justify-between">
172
- <span className="text-sol-muted">Network Fee</span>
173
- <span className="text-sm">~0.000005 SOL</span>
174
- </div>
175
  </div>
176
 
177
- {/* Security Warning */}
178
- <div className="bg-warning/10 border border-warning/30 rounded-xl p-3 text-sm">
179
- <div className="font-semibold text-warning mb-1">⚠️ Verify Details</div>
180
- <div className="text-sol-muted text-xs">
181
- Please double-check the recipient address and amount. Blockchain transactions are irreversible.
182
  </div>
183
  </div>
184
 
185
- {error && <div className="text-danger text-sm">{error}</div>}
186
 
187
  <div className="flex gap-3">
188
- <button
189
- onClick={() => setStep('form')}
190
- className="flex-1 py-3 rounded-xl border border-sol-border text-sol-muted hover:text-sol-text transition-colors"
191
- >
192
- Cancel
193
- </button>
194
- <button
195
- onClick={handleSend}
196
- disabled={loading}
197
- className="flex-1 py-3 rounded-xl bg-sol-purple text-white font-semibold hover:bg-sol-purple/90 disabled:opacity-50 transition-all"
198
- >
199
- {loading ? 'Sending...' : `Send ${token}`}
200
  </button>
201
  </div>
202
  </div>
203
  )}
204
 
205
  {step === 'result' && success && (
206
- <div className="glass rounded-2xl p-6 text-center space-y-4 slide-enter">
207
- <div className="text-6xl mb-2">✅</div>
208
- <h3 className="text-2xl font-bold">Transaction Sent!</h3>
209
- <p className="text-sol-muted">
210
- {amount} {token} sent successfully
211
- </p>
212
- <div className="bg-sol-dark rounded-xl p-3 font-mono text-xs text-sol-muted break-all">
213
- {success.signature}
214
  </div>
215
- <a
216
- href={success.explorer}
217
- target="_blank"
218
- rel="noopener noreferrer"
219
- className="block text-sol-purple text-sm hover:underline"
220
- >
221
- View on Solscan →
222
  </a>
223
- <button
224
- onClick={reset}
225
- className="w-full py-3 rounded-xl bg-sol-purple text-white font-semibold hover:bg-sol-purple/90 transition-all"
226
- >
227
- Send Another
228
- </button>
229
  </div>
230
  )}
231
  </div>
 
1
  import React, { useState } from 'react';
2
+ import { AnimatedNumber, useToast } from '../components/ui/index';
3
 
4
+ interface SendPageProps { balance: { sol: number; usdt: number }; onSent: () => void; }
 
 
 
5
 
6
  export default function SendPage({ balance, onSent }: SendPageProps) {
7
  const [token, setToken] = useState<'SOL' | 'USDT'>('SOL');
8
  const [to, setTo] = useState('');
9
  const [amount, setAmount] = useState('');
 
10
  const [loading, setLoading] = useState(false);
11
  const [error, setError] = useState('');
12
  const [success, setSuccess] = useState<{ signature: string; explorer: string } | null>(null);
13
  const [step, setStep] = useState<'form' | 'confirm' | 'result'>('form');
14
+ const { addToast } = useToast();
15
 
16
+ const max = token === 'SOL' ? balance.sol : balance.usdt;
17
+ const amtNum = parseFloat(amount) || 0;
18
+ const pct = max > 0 ? Math.min(100, (amtNum / max) * 100) : 0;
19
 
20
+ const review = () => {
21
  setError('');
22
+ if (!to.trim()) return setError('Recipient address is required');
23
+ if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(to.trim())) return setError('Invalid Solana address');
24
+ if (!amtNum || amtNum <= 0) return setError('Enter a valid amount');
25
+ if (amtNum > max) return setError(`Insufficient balance. Max: ${max.toFixed(token === 'SOL' ? 4 : 2)} ${token}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  setStep('confirm');
27
  };
28
 
29
+ const send = async () => {
30
+ setLoading(true); setError('');
 
31
  try {
32
+ const r = window.solvox
33
+ ? token === 'SOL' ? await window.solvox.wallet.sendSOL(to.trim(), amtNum) : await window.solvox.wallet.sendUSDT(to.trim(), amtNum)
34
+ : { success: true, signature: 'dev_sig_' + Date.now(), explorer: '#' };
35
+ if (r.success) {
36
+ setSuccess({ signature: r.signature!, explorer: r.explorer! });
 
 
 
 
 
 
 
 
37
  setStep('result');
38
  onSent();
39
+ addToast({ type: 'success', title: 'Transaction Sent', message: `${amount} ${token} sent successfully` });
40
+ } else { setError(r.error || 'Transaction failed'); setStep('form'); }
41
+ } catch (e: any) { setError(e.message); setStep('form'); }
 
 
 
 
 
42
  setLoading(false);
43
  };
44
 
45
+ const reset = () => { setTo(''); setAmount(''); setError(''); setSuccess(null); setStep('form'); };
 
 
 
 
 
 
 
46
 
47
  return (
48
+ <div className="max-w-md mx-auto page-enter">
49
+ {/* Header */}
50
+ <div className="flex items-center gap-3 mb-6">
51
+ <div className={`w-10 h-10 rounded-xl flex items-center justify-center ${token === 'SOL' ? 'bg-gradient-to-br from-sol-purple to-sol-purple-light' : 'bg-gradient-to-br from-tether-green to-tether-dark'}`}>
52
+ <span className="text-white font-bold">{token === 'SOL' ? '◎' : '₮'}</span>
53
+ </div>
54
+ <div>
55
+ <h2 className="text-xl font-bold">Send {token}</h2>
56
+ <div className="text-xs text-sol-muted">Balance: <AnimatedNumber value={max} decimals={token === 'SOL' ? 4 : 2} /> {token}</div>
57
+ </div>
58
+ </div>
59
 
60
  {step === 'form' && (
61
+ <div className="glass rounded-3xl p-6 space-y-5 slide-enter">
62
  {/* Token Selector */}
63
+ <div className="flex gap-2 p-1 bg-sol-dark/60 rounded-xl">
64
  {(['SOL', 'USDT'] as const).map(t => (
65
+ <button key={t} onClick={() => setToken(t)}
66
+ className={`flex-1 py-2.5 rounded-lg text-sm font-semibold transition-all duration-300 ${
67
+ token === t ? (t === 'SOL' ? 'bg-gradient-to-r from-sol-purple to-sol-purple-light text-white shadow-glow-sm' : 'bg-gradient-to-r from-tether-green to-tether-dark text-white shadow-glow-sm')
68
+ : 'text-sol-muted hover:text-sol-text'
69
+ }`}>
 
 
 
 
70
  {t === 'SOL' ? '◎' : '₮'} {t}
71
  </button>
72
  ))}
 
74
 
75
  {/* Recipient */}
76
  <div>
77
+ <label className="text-xs text-sol-muted font-semibold uppercase tracking-wider block mb-2">Recipient</label>
78
+ <input value={to} onChange={e => setTo(e.target.value)} placeholder="Enter Solana address"
79
+ className="input-field font-mono text-xs" />
 
 
 
 
80
  </div>
81
 
82
  {/* Amount */}
83
  <div>
84
+ <div className="flex justify-between items-center mb-2">
85
+ <label className="text-xs text-sol-muted font-semibold uppercase tracking-wider">Amount</label>
86
+ <button onClick={() => setAmount(max.toString())} className="text-[11px] text-sol-purple hover:underline font-medium">
87
+ MAX {max.toFixed(token === 'SOL' ? 4 : 2)}
 
 
 
88
  </button>
89
  </div>
90
  <div className="relative">
91
+ <input type="number" value={amount} onChange={e => setAmount(e.target.value)}
92
+ placeholder="0.00" step="any" min="0" className="input-field text-xl font-bold pr-16 text-center" />
93
+ <span className="absolute right-4 top-1/2 -translate-y-1/2 text-sol-muted font-semibold text-sm">{token}</span>
94
+ </div>
95
+ {/* Percentage bar */}
96
+ <div className="mt-2 flex items-center gap-2">
97
+ <div className="progress-bar flex-1"><div className="progress-fill" style={{ width: `${pct}%` }} /></div>
98
+ <span className="text-[10px] text-sol-muted w-10 text-right">{pct.toFixed(0)}%</span>
99
+ </div>
100
+ {/* Quick amounts */}
101
+ <div className="flex gap-2 mt-2">
102
+ {[25, 50, 75, 100].map(p => (
103
+ <button key={p} onClick={() => setAmount((max * p / 100).toFixed(token === 'SOL' ? 4 : 2))}
104
+ className="flex-1 py-1 rounded-lg text-[10px] font-medium text-sol-muted bg-sol-dark/40 border border-sol-border/30 hover:border-sol-purple/30 hover:text-sol-purple transition-all">
105
+ {p}%
106
+ </button>
107
+ ))}
108
  </div>
109
  </div>
110
 
111
+ {error && <div className="badge-danger bg-danger/8 border border-danger/20 rounded-xl p-3 text-xs">{error}</div>}
 
 
112
 
113
+ <button onClick={review} className="btn-primary w-full py-3.5 text-white">
114
+ <span>Review Transaction</span>
 
 
 
115
  </button>
116
  </div>
117
  )}
118
 
119
  {step === 'confirm' && (
120
+ <div className="glass rounded-3xl p-6 space-y-5 scale-in">
121
+ <div className="text-center mb-2">
122
+ <div className="text-sm text-sol-muted font-medium">Confirm Transaction</div>
123
+ <div className="text-3xl font-extrabold mt-1">{amount} <span className={token === 'SOL' ? 'text-sol-purple' : 'text-tether-green'}>{token}</span></div>
124
+ </div>
125
+
126
+ <div className="bg-sol-dark/50 rounded-xl p-4 space-y-3 text-sm">
127
+ <div className="flex justify-between"><span className="text-sol-muted">To</span><span className="font-mono text-xs">{to.slice(0, 10)}...{to.slice(-6)}</span></div>
128
+ <div className="divider" />
129
+ <div className="flex justify-between"><span className="text-sol-muted">Network Fee</span><span className="text-xs">~0.000005 SOL</span></div>
130
+ <div className="flex justify-between"><span className="text-sol-muted">Network</span><span className="badge badge-warn text-[10px]">Devnet</span></div>
 
 
 
 
 
 
 
 
131
  </div>
132
 
133
+ <div className="bg-warning/6 border border-warning/15 rounded-xl p-3 flex items-start gap-2.5">
134
+ <span className="text-warning text-sm mt-0.5">⚠</span>
135
+ <div>
136
+ <div className="text-xs font-semibold text-warning">Verify carefully</div>
137
+ <div className="text-[11px] text-sol-muted mt-0.5">Blockchain transactions are irreversible. Double-check the address.</div>
138
  </div>
139
  </div>
140
 
141
+ {error && <div className="text-danger text-xs text-center">{error}</div>}
142
 
143
  <div className="flex gap-3">
144
+ <button onClick={() => setStep('form')} className="btn-ghost flex-1 py-3">Cancel</button>
145
+ <button onClick={send} disabled={loading} className="btn-primary flex-1 py-3 text-white disabled:opacity-50">
146
+ <span>{loading ? 'Sending...' : `Send ${token}`}</span>
 
 
 
 
 
 
 
 
 
147
  </button>
148
  </div>
149
  </div>
150
  )}
151
 
152
  {step === 'result' && success && (
153
+ <div className="glass rounded-3xl p-8 text-center space-y-5 scale-in">
154
+ <div className="w-16 h-16 mx-auto rounded-full bg-sol-green/15 flex items-center justify-center">
155
+ <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#14F195" strokeWidth="2.5" strokeLinecap="round"><polyline points="20 6 9 17 4 12"/></svg>
156
+ </div>
157
+ <div>
158
+ <h3 className="text-2xl font-bold">Sent!</h3>
159
+ <p className="text-sol-muted text-sm mt-1">{amount} {token} sent successfully</p>
 
160
  </div>
161
+ <div className="bg-sol-dark/50 rounded-xl p-3 font-mono text-[11px] text-sol-muted break-all">{success.signature}</div>
162
+ <a href={success.explorer} target="_blank" rel="noopener noreferrer" className="text-sol-purple text-xs hover:underline inline-flex items-center gap-1">
163
+ View on Solscan <span>↗</span>
 
 
 
 
164
  </a>
165
+ <button onClick={reset} className="btn-primary w-full py-3 text-white"><span>Send Another</span></button>
 
 
 
 
 
166
  </div>
167
  )}
168
  </div>
src/renderer/pages/SettingsPage.tsx CHANGED
@@ -1,113 +1,104 @@
1
  import React, { useState } from 'react';
2
 
3
- interface SettingsPageProps {
4
- onLock: () => void;
5
- }
6
 
7
  export default function SettingsPage({ onLock }: SettingsPageProps) {
8
  const [network, setNetwork] = useState('devnet');
9
 
 
 
 
 
 
 
 
 
 
10
  return (
11
- <div className="space-y-6 max-w-2xl slide-enter">
12
- <h2 className="text-2xl font-bold">⚙️ Settings</h2>
13
 
14
  {/* Network */}
15
- <div className="glass rounded-2xl p-6">
16
- <h3 className="text-lg font-semibold mb-4">Network</h3>
17
- <div className="flex gap-2">
18
- {['devnet', 'mainnet-beta', 'testnet'].map(net => (
19
- <button
20
- key={net}
21
- onClick={() => setNetwork(net)}
22
- className={`px-4 py-2 rounded-xl text-sm font-medium transition-all ${
23
- network === net
24
- ? 'bg-sol-purple text-white'
25
- : 'bg-sol-dark border border-sol-border text-sol-muted hover:text-sol-text'
26
- }`}
27
- >
28
- {net === 'mainnet-beta' ? 'Mainnet' : net.charAt(0).toUpperCase() + net.slice(1)}
29
  </button>
30
  ))}
31
  </div>
32
- <p className="text-xs text-sol-muted mt-2">
33
- {network === 'devnet' && '⚠ Devnet — test tokens only. No real value.'}
34
- {network === 'mainnet-beta' && '🔴 Mainnet — real tokens. Transactions are irreversible.'}
35
- {network === 'testnet' && '⚠ Testnet — for development purposes.'}
36
  </p>
37
  </div>
38
 
39
  {/* AI Models */}
40
- <div className="glass rounded-2xl p-6">
41
- <h3 className="text-lg font-semibold mb-4">🧠 AI Models (QVAC)</h3>
42
- <p className="text-sm text-sol-muted mb-4">
43
- SolVox uses 6 QVAC AI packages, all running locally on your device.
44
- </p>
45
- <div className="space-y-3">
46
- {[
47
- { name: 'LLM', model: 'Llama 3.2 3B Instruct (Q4_K_M)', size: '~2.0 GB', pkg: '@qvac/llm-llamacpp' },
48
- { name: 'Embeddings', model: 'Nomic Embed Text v1.5', size: '~260 MB', pkg: '@qvac/embed-llamacpp' },
49
- { name: 'Speech-to-Text', model: 'Whisper Base.en', size: '~150 MB', pkg: '@qvac/transcription-whispercpp' },
50
- { name: 'Text-to-Speech', model: 'Amy (en_US, medium)', size: '~75 MB', pkg: '@qvac/tts-onnx' },
51
- { name: 'Translation', model: 'OPUS MT (EN↔ES)', size: '~50 MB', pkg: '@qvac/translation-nmtcpp' },
52
- { name: 'OCR', model: 'PaddleOCR v4', size: '~30 MB', pkg: '@qvac/ocr-onnx' },
53
- ].map(m => (
54
- <div key={m.name} className="flex items-center justify-between bg-sol-dark rounded-xl p-3">
55
- <div>
56
- <div className="text-sm font-semibold">{m.name}</div>
57
- <div className="text-xs text-sol-muted">{m.model}</div>
58
- <div className="text-xs text-sol-muted font-mono">{m.pkg}</div>
59
- </div>
60
- <div className="text-right">
61
- <div className="text-xs text-sol-muted">{m.size}</div>
62
  </div>
 
63
  </div>
64
  ))}
65
  </div>
66
- <div className="mt-4 pt-3 border-t border-sol-border">
67
- <p className="text-xs text-sol-muted">
68
- Total model size: ~2.6 GB | All models are stored locally in the <code className="text-sol-purple">models/</code> directory.
69
- Models run on any GPU via Vulkan API — no CUDA required.
70
- </p>
71
  </div>
72
  </div>
73
 
74
  {/* About */}
75
- <div className="glass rounded-2xl p-6">
76
- <h3 className="text-lg font-semibold mb-4">About SolVox</h3>
77
- <div className="space-y-2 text-sm text-sol-muted">
78
- <p><strong className="text-sol-text">SolVox</strong> is a voice-first, privacy-preserving AI wallet for the Solana blockchain.</p>
79
- <p>
80
- Powered by <strong className="text-tether-green">Tether's QVAC SDK</strong> — a complete platform for running AI models
81
- directly on any device, without routing data through a centralized cloud.
82
- </p>
83
- <div className="flex flex-wrap gap-2 mt-3">
84
- <span className="px-2 py-0.5 bg-sol-dark rounded text-xs">Electron</span>
85
- <span className="px-2 py-0.5 bg-sol-dark rounded text-xs">React</span>
86
- <span className="px-2 py-0.5 bg-sol-dark rounded text-xs">TypeScript</span>
87
- <span className="px-2 py-0.5 bg-sol-dark rounded text-xs">QVAC SDK</span>
88
- <span className="px-2 py-0.5 bg-sol-dark rounded text-xs">Solana</span>
89
- <span className="px-2 py-0.5 bg-sol-dark rounded text-xs">Vulkan</span>
90
  </div>
91
  </div>
92
- <div className="mt-4 pt-3 border-t border-sol-border text-xs text-sol-muted">
93
- Built for the Colosseum Frontier Hackathon • Tether QVAC Track
 
94
  </div>
95
  </div>
96
 
97
  {/* Danger Zone */}
98
- <div className="rounded-2xl border-2 border-danger/30 p-6">
99
- <h3 className="text-lg font-semibold text-danger mb-4">⚠️ Danger Zone</h3>
100
- <div className="space-y-3">
101
- <button
102
- onClick={onLock}
103
- className="w-full py-3 rounded-xl border border-danger text-danger hover:bg-danger/10 transition-colors font-medium"
104
- >
105
- 🔒 Lock Wallet Now
106
- </button>
107
- <p className="text-xs text-sol-muted text-center">
108
- Locking zeroes your private key from memory. You'll need your PIN to unlock.
109
- </p>
110
- </div>
111
  </div>
112
  </div>
113
  );
 
1
  import React, { useState } from 'react';
2
 
3
+ interface SettingsPageProps { onLock: () => void; }
 
 
4
 
5
  export default function SettingsPage({ onLock }: SettingsPageProps) {
6
  const [network, setNetwork] = useState('devnet');
7
 
8
+ const models = [
9
+ { name: 'LLM', model: 'Llama 3.2 3B Instruct', quant: 'Q4_K_M', size: '2.0 GB', pkg: 'llm-llamacpp', color: '#9945FF' },
10
+ { name: 'Embeddings', model: 'Nomic Embed Text v1.5', quant: 'Q4_K_M', size: '260 MB', pkg: 'embed-llamacpp', color: '#B06CFF' },
11
+ { name: 'Speech-to-Text', model: 'Whisper Base.en', quant: 'GGML', size: '150 MB', pkg: 'transcription-whispercpp', color: '#14F195' },
12
+ { name: 'Text-to-Speech', model: 'Piper Amy (en_US)', quant: 'ONNX', size: '75 MB', pkg: 'tts-onnx', color: '#26A17B' },
13
+ { name: 'Translation', model: 'OPUS MT (EN↔ES)', quant: 'NMT', size: '50 MB', pkg: 'translation-nmtcpp', color: '#FFB830' },
14
+ { name: 'OCR', model: 'PaddleOCR v4', quant: 'ONNX', size: '30 MB', pkg: 'ocr-onnx', color: '#3B82F6' },
15
+ ];
16
+
17
  return (
18
+ <div className="space-y-5 max-w-2xl page-enter">
19
+ <h2 className="text-xl font-bold">Settings</h2>
20
 
21
  {/* Network */}
22
+ <div className="glass rounded-2xl p-5">
23
+ <div className="text-xs text-sol-muted font-semibold uppercase tracking-wider mb-3">Network</div>
24
+ <div className="flex gap-2 p-1 bg-sol-dark/60 rounded-xl">
25
+ {[
26
+ { id: 'devnet', label: 'Devnet', color: 'warning' },
27
+ { id: 'mainnet-beta', label: 'Mainnet', color: 'danger' },
28
+ { id: 'testnet', label: 'Testnet', color: 'muted' },
29
+ ].map(n => (
30
+ <button key={n.id} onClick={() => setNetwork(n.id)}
31
+ className={`flex-1 py-2 rounded-lg text-xs font-semibold transition-all ${
32
+ network === n.id ? 'bg-sol-card text-sol-text shadow' : 'text-sol-muted hover:text-sol-text'
33
+ }`}>
34
+ {network === n.id && <span className={`inline-block w-1.5 h-1.5 rounded-full mr-1.5 ${n.color === 'danger' ? 'bg-danger' : n.color === 'warning' ? 'bg-warning' : 'bg-sol-muted'}`} />}
35
+ {n.label}
36
  </button>
37
  ))}
38
  </div>
39
+ <p className="text-[10px] text-sol-muted mt-2">
40
+ {network === 'devnet' && '⚠ Devnet — test tokens only'}
41
+ {network === 'mainnet-beta' && '🔴 Mainnet — real tokens, irreversible'}
42
+ {network === 'testnet' && '⚠ Testnet — development only'}
43
  </p>
44
  </div>
45
 
46
  {/* AI Models */}
47
+ <div className="glass rounded-2xl p-5">
48
+ <div className="flex items-center justify-between mb-4">
49
+ <div className="text-xs text-sol-muted font-semibold uppercase tracking-wider">QVAC AI Models</div>
50
+ <div className="badge badge-tether text-[10px]">6 modules ~2.6 GB</div>
51
+ </div>
52
+ <div className="space-y-2 stagger">
53
+ {models.map(m => (
54
+ <div key={m.name} className="flex items-center gap-3 bg-sol-dark/40 rounded-xl p-3 hover:bg-sol-dark/60 transition-colors">
55
+ <div className="w-1 h-10 rounded-full" style={{ background: m.color }} />
56
+ <div className="flex-1 min-w-0">
57
+ <div className="flex items-center gap-2">
58
+ <span className="text-xs font-semibold text-sol-text">{m.name}</span>
59
+ <span className="badge badge-muted text-[9px]">{m.quant}</span>
60
+ </div>
61
+ <div className="text-[10px] text-sol-muted">{m.model}</div>
62
+ <div className="text-[9px] text-sol-muted/60 font-mono">@qvac/{m.pkg}</div>
 
 
 
 
 
 
63
  </div>
64
+ <div className="text-[10px] text-sol-muted font-medium">{m.size}</div>
65
  </div>
66
  ))}
67
  </div>
68
+ <div className="divider my-3" />
69
+ <div className="flex items-center gap-2 text-[10px] text-sol-muted">
70
+ <div className="w-1.5 h-1.5 rounded-full bg-tether-green" />
71
+ All models run locally via QVAC Fabric (Vulkan GPU) — no CUDA required
 
72
  </div>
73
  </div>
74
 
75
  {/* About */}
76
+ <div className="glass rounded-2xl p-5">
77
+ <div className="text-xs text-sol-muted font-semibold uppercase tracking-wider mb-3">About SolVox</div>
78
+ <div className="space-y-2 text-xs text-sol-muted">
79
+ <p><strong className="text-sol-text">SolVox</strong> voice-first, privacy-preserving AI wallet for Solana.</p>
80
+ <p>Powered by <strong className="text-tether-green">Tether QVAC SDK</strong> — local-first AI that never phones home.</p>
81
+ <div className="flex flex-wrap gap-1.5 mt-3">
82
+ {['Electron', 'React', 'TypeScript', 'QVAC SDK', 'Solana', 'Vulkan', 'TailwindCSS'].map(t => (
83
+ <span key={t} className="badge badge-muted text-[9px]">{t}</span>
84
+ ))}
 
 
 
 
 
 
85
  </div>
86
  </div>
87
+ <div className="divider my-3" />
88
+ <div className="text-[10px] text-sol-muted/60">
89
+ Built for Colosseum Frontier Hackathon • Tether QVAC Track
90
  </div>
91
  </div>
92
 
93
  {/* Danger Zone */}
94
+ <div className="rounded-2xl border border-danger/20 p-5 bg-danger/[0.02]">
95
+ <div className="text-xs text-danger font-semibold uppercase tracking-wider mb-3">Danger Zone</div>
96
+ <button onClick={onLock}
97
+ className="w-full py-3 rounded-xl border border-danger/30 text-danger text-sm font-medium hover:bg-danger/8 transition-all flex items-center justify-center gap-2">
98
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
99
+ Lock Wallet Now
100
+ </button>
101
+ <p className="text-[10px] text-sol-muted/60 text-center mt-2">Zeroes private key from memory. PIN required to unlock.</p>
 
 
 
 
 
102
  </div>
103
  </div>
104
  );
src/renderer/pages/VoicePage.tsx CHANGED
@@ -1,298 +1,236 @@
1
- import React, { useState, useRef, useCallback, useEffect } from 'react';
 
2
 
3
- interface VoicePageProps {
4
- aiStatus: any;
5
- }
6
 
7
- interface Message {
8
- id: string;
9
- role: 'user' | 'assistant' | 'system';
10
- text: string;
11
- intent?: any;
12
- timestamp: Date;
13
- }
 
14
 
15
  export default function VoicePage({ aiStatus }: VoicePageProps) {
16
  const [messages, setMessages] = useState<Message[]>([
17
- {
18
- id: '0',
19
- role: 'assistant',
20
- text: 'Hello! I\'m your SolVox AI assistant. I can help you send SOL and USDT, check your balance, view transactions, and more. Try saying "What is my balance?" or type a command below.',
21
- timestamp: new Date(),
22
- },
23
  ]);
24
  const [input, setInput] = useState('');
25
  const [isRecording, setIsRecording] = useState(false);
26
  const [isProcessing, setIsProcessing] = useState(false);
27
- const [waveformData, setWaveformData] = useState<number[]>(new Array(32).fill(4));
 
28
  const mediaRecorder = useRef<MediaRecorder | null>(null);
29
  const audioChunks = useRef<Blob[]>([]);
30
  const messagesEndRef = useRef<HTMLDivElement>(null);
31
  const analyserRef = useRef<AnalyserNode | null>(null);
32
  const animFrameRef = useRef<number | null>(null);
 
33
 
34
- useEffect(() => {
35
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
36
- }, [messages]);
37
 
38
- // ── Voice Recording ──
39
  const startRecording = async () => {
40
  try {
41
- const stream = await navigator.mediaDevices.getUserMedia({
42
- audio: {
43
- sampleRate: 16000,
44
- channelCount: 1,
45
- echoCancellation: true,
46
- noiseSuppression: true,
47
- },
48
- });
49
-
50
- // Waveform visualization
51
- const audioContext = new AudioContext();
52
- const source = audioContext.createMediaStreamSource(stream);
53
- const analyser = audioContext.createAnalyser();
54
- analyser.fftSize = 64;
55
- source.connect(analyser);
56
  analyserRef.current = analyser;
57
 
58
- const updateWaveform = () => {
59
  if (!analyserRef.current) return;
60
- const dataArray = new Uint8Array(analyserRef.current.frequencyBinCount);
61
- analyserRef.current.getByteFrequencyData(dataArray);
62
- const normalized = Array.from(dataArray).slice(0, 32).map(v => Math.max(4, v / 8));
63
- setWaveformData(normalized);
64
- animFrameRef.current = requestAnimationFrame(updateWaveform);
65
  };
66
- updateWaveform();
67
 
68
- const recorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
69
  audioChunks.current = [];
70
-
71
- recorder.ondataavailable = (e) => {
72
- if (e.data.size > 0) audioChunks.current.push(e.data);
73
- };
74
-
75
- recorder.onstop = async () => {
76
  stream.getTracks().forEach(t => t.stop());
77
  if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current);
78
- setWaveformData(new Array(32).fill(4));
79
-
 
80
  const blob = new Blob(audioChunks.current, { type: 'audio/webm' });
81
- const buffer = await blob.arrayBuffer();
82
- processVoice(buffer);
83
  };
84
 
85
- mediaRecorder.current = recorder;
86
- recorder.start();
87
  setIsRecording(true);
88
- } catch (err) {
89
- addMessage('system', 'Microphone access denied. Please enable microphone permissions.');
90
- }
91
  };
92
 
93
  const stopRecording = () => {
94
- if (mediaRecorder.current && isRecording) {
95
- mediaRecorder.current.stop();
96
- setIsRecording(false);
97
- }
98
  };
99
 
100
- // ── Process Voice Command ──
101
- const processVoice = async (audioData: ArrayBuffer) => {
102
  setIsProcessing(true);
103
  try {
104
  if (window.solvox) {
105
- const result = await window.solvox.ai.processVoice(audioData);
106
- if (result.success) {
107
- addMessage('user', result.transcription || '[voice input]');
108
- addMessage('assistant', result.response || 'Done.', result.intent);
109
-
110
- // Play audio response if available
111
- if (result.audio) {
112
- playAudio(result.audio);
113
- }
114
- } else {
115
- addMessage('system', `Voice processing failed: ${result.error}`);
116
- }
117
- } else {
118
- addMessage('user', '[voice input - dev mode]');
119
- addMessage('assistant', 'Voice processing requires QVAC models. In dev mode, use text commands.');
120
- }
121
- } catch (err: any) {
122
- addMessage('system', `Error: ${err.message}`);
123
- }
124
  setIsProcessing(false);
125
  };
126
 
127
- // ── Text Chat ──
128
- const handleSendMessage = async () => {
129
  if (!input.trim()) return;
130
  const text = input.trim();
131
  setInput('');
132
-
133
- addMessage('user', text);
134
  setIsProcessing(true);
135
-
136
  try {
137
  if (window.solvox) {
138
- // First parse intent
139
- const intentResult = await window.solvox.ai.parseIntent(text);
140
- const intent = intentResult.success ? intentResult.intent : null;
141
-
142
- // Then get AI response
143
- const chatResult = await window.solvox.ai.chat(text);
144
- if (chatResult.success) {
145
- addMessage('assistant', chatResult.response!, intent);
146
- } else {
147
- addMessage('assistant', chatResult.error || 'Sorry, I could not process that.');
148
- }
149
- } else {
150
- // Dev mode fallback
151
- addMessage('assistant', `[Dev mode] You said: "${text}". QVAC models needed for AI responses.`);
152
- }
153
- } catch (err: any) {
154
- addMessage('system', `Error: ${err.message}`);
155
- }
156
  setIsProcessing(false);
157
  };
158
 
159
- // ── Helpers ──
160
- const addMessage = (role: 'user' | 'assistant' | 'system', text: string, intent?: any) => {
161
- setMessages(prev => [...prev, {
162
- id: Date.now().toString(),
163
- role,
164
- text,
165
- intent,
166
- timestamp: new Date(),
167
- }]);
168
  };
169
 
170
- const playAudio = (audioData: ArrayBuffer) => {
171
- try {
172
- const blob = new Blob([audioData], { type: 'audio/wav' });
173
- const url = URL.createObjectURL(blob);
174
- const audio = new Audio(url);
175
- audio.play().catch(() => {});
176
- audio.onended = () => URL.revokeObjectURL(url);
177
- } catch {}
178
  };
179
 
 
 
180
  return (
181
- <div className="flex flex-col h-full slide-enter">
182
  {/* Header */}
183
  <div className="flex items-center justify-between mb-4">
184
  <div>
185
- <h2 className="text-2xl font-bold">🎤 Voice AI Assistant</h2>
186
- <p className="text-sm text-sol-muted">
187
- Powered by 6 QVAC packages • All local, all private
188
- </p>
189
  </div>
190
  <div className="flex items-center gap-2">
191
- {aiStatus?.llm ? (
192
- <span className="px-3 py-1 rounded-full bg-sol-green/20 text-sol-green text-xs">
193
- AI Online
194
- </span>
195
- ) : (
196
- <span className="px-3 py-1 rounded-full bg-warning/20 text-warning text-xs">
197
- Loading Models...
198
- </span>
199
- )}
200
  </div>
201
  </div>
202
 
203
- {/* Chat Messages */}
204
- <div className="flex-1 overflow-y-auto glass rounded-2xl p-4 mb-4 space-y-4">
205
  {messages.map(msg => (
206
- <div
207
- key={msg.id}
208
- className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
209
- >
210
- <div className={`max-w-[80%] rounded-2xl px-4 py-3 ${
211
- msg.role === 'user'
212
- ? 'bg-sol-purple text-white'
213
- : msg.role === 'system'
214
- ? 'bg-warning/10 text-warning border border-warning/30'
215
- : 'bg-sol-dark border border-sol-border'
216
  }`}>
217
- <p className="text-sm whitespace-pre-wrap">{msg.text}</p>
218
- {msg.intent && (
219
- <div className="mt-2 pt-2 border-t border-sol-border/30">
220
- <div className="text-xs font-mono text-sol-muted">
221
- Intent: {msg.intent.action} | Confidence: {(msg.intent.confidence * 100).toFixed(0)}%
222
- {msg.intent.amount && ` | Amount: ${msg.intent.amount} ${msg.intent.token || ''}`}
223
- {msg.intent.to && ` | To: ${msg.intent.to}`}
 
 
 
 
 
 
 
 
 
224
  </div>
225
  </div>
226
  )}
227
- <div className="text-xs text-sol-muted/50 mt-1">
228
- {msg.timestamp.toLocaleTimeString()}
229
- </div>
230
  </div>
231
  </div>
232
  ))}
233
  {isProcessing && (
234
  <div className="flex justify-start">
235
- <div className="bg-sol-dark border border-sol-border rounded-2xl px-4 py-3">
236
- <div className="flex space-x-1.5">
237
- <div className="w-2 h-2 bg-sol-purple rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
238
- <div className="w-2 h-2 bg-sol-purple rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
239
- <div className="w-2 h-2 bg-sol-purple rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
240
- </div>
241
  </div>
242
  </div>
243
  )}
244
  <div ref={messagesEndRef} />
245
  </div>
246
 
247
- {/* Waveform Visualizer (visible during recording) */}
248
- {isRecording && (
249
- <div className="flex items-center justify-center gap-0.5 h-12 mb-4">
250
- {waveformData.map((h, i) => (
251
- <div
252
- key={i}
253
- className="w-1.5 bg-sol-purple rounded-full transition-all duration-75"
254
- style={{ height: `${h}px` }}
255
- />
256
  ))}
257
  </div>
258
  )}
259
 
260
- {/* Input Area */}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  <div className="flex items-center gap-3">
262
- {/* Voice Button */}
263
  <button
264
- onMouseDown={startRecording}
265
- onMouseUp={stopRecording}
266
- onMouseLeave={stopRecording}
267
- onTouchStart={startRecording}
268
- onTouchEnd={stopRecording}
269
  disabled={isProcessing}
270
- className={`w-14 h-14 rounded-full flex items-center justify-center transition-all ${
271
- isRecording
272
- ? 'bg-danger recording-pulse scale-110'
273
- : 'bg-sol-purple hover:bg-sol-purple/80 glow-purple'
274
- } disabled:opacity-50`}
275
  title="Hold to speak"
276
  >
277
- <span className="text-2xl">{isRecording ? '' : '🎤'}</span>
 
 
 
 
 
278
  </button>
279
 
280
- {/* Text Input */}
281
  <div className="flex-1 relative">
282
- <input
283
- value={input}
284
- onChange={(e) => setInput(e.target.value)}
285
- onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()}
286
- placeholder="Type a command or hold 🎤 to speak..."
287
- className="w-full px-4 py-3 bg-sol-card border border-sol-border rounded-xl text-sm focus:border-sol-purple focus:outline-none pr-16"
288
- disabled={isProcessing}
289
- />
290
- <button
291
- onClick={handleSendMessage}
292
- disabled={!input.trim() || isProcessing}
293
- className="absolute right-2 top-1/2 -translate-y-1/2 px-3 py-1.5 rounded-lg bg-sol-purple text-white text-sm disabled:opacity-30 hover:bg-sol-purple/80 transition-all"
294
- >
295
- Send
296
  </button>
297
  </div>
298
  </div>
 
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { OrbitLoader } from '../components/ui/index';
3
 
4
+ interface VoicePageProps { aiStatus: any; }
5
+ interface Message { id: string; role: 'user' | 'assistant' | 'system'; text: string; intent?: any; timestamp: Date; }
 
6
 
7
+ const SUGGESTIONS = [
8
+ 'What is my balance?',
9
+ 'Send 5 SOL to alice.sol',
10
+ 'Show my recent transactions',
11
+ 'How much USDT do I have?',
12
+ 'Send 100 USDT to bob.sol',
13
+ 'Help me understand gas fees',
14
+ ];
15
 
16
  export default function VoicePage({ aiStatus }: VoicePageProps) {
17
  const [messages, setMessages] = useState<Message[]>([
18
+ { id: '0', role: 'assistant', text: "Hey! I'm your SolVox AI assistant — running 100% locally on your device. I can help you send SOL & USDT, check balances, search transactions, and more.\n\nTry holding the mic button or type a command below.", timestamp: new Date() },
 
 
 
 
 
19
  ]);
20
  const [input, setInput] = useState('');
21
  const [isRecording, setIsRecording] = useState(false);
22
  const [isProcessing, setIsProcessing] = useState(false);
23
+ const [waveformData, setWaveformData] = useState<number[]>(new Array(48).fill(2));
24
+ const [recordingTime, setRecordingTime] = useState(0);
25
  const mediaRecorder = useRef<MediaRecorder | null>(null);
26
  const audioChunks = useRef<Blob[]>([]);
27
  const messagesEndRef = useRef<HTMLDivElement>(null);
28
  const analyserRef = useRef<AnalyserNode | null>(null);
29
  const animFrameRef = useRef<number | null>(null);
30
+ const timerRef = useRef<NodeJS.Timeout | null>(null);
31
 
32
+ useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]);
 
 
33
 
 
34
  const startRecording = async () => {
35
  try {
36
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: { sampleRate: 16000, channelCount: 1, echoCancellation: true, noiseSuppression: true } });
37
+ const ctx = new AudioContext();
38
+ const src = ctx.createMediaStreamSource(stream);
39
+ const analyser = ctx.createAnalyser();
40
+ analyser.fftSize = 128;
41
+ src.connect(analyser);
 
 
 
 
 
 
 
 
 
42
  analyserRef.current = analyser;
43
 
44
+ const update = () => {
45
  if (!analyserRef.current) return;
46
+ const d = new Uint8Array(analyserRef.current.frequencyBinCount);
47
+ analyserRef.current.getByteFrequencyData(d);
48
+ setWaveformData(Array.from(d).slice(0, 48).map(v => Math.max(2, v / 6)));
49
+ animFrameRef.current = requestAnimationFrame(update);
 
50
  };
51
+ update();
52
 
53
+ const rec = new MediaRecorder(stream, { mimeType: 'audio/webm' });
54
  audioChunks.current = [];
55
+ rec.ondataavailable = e => { if (e.data.size > 0) audioChunks.current.push(e.data); };
56
+ rec.onstop = async () => {
 
 
 
 
57
  stream.getTracks().forEach(t => t.stop());
58
  if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current);
59
+ if (timerRef.current) clearInterval(timerRef.current);
60
+ setWaveformData(new Array(48).fill(2));
61
+ setRecordingTime(0);
62
  const blob = new Blob(audioChunks.current, { type: 'audio/webm' });
63
+ processVoice(await blob.arrayBuffer());
 
64
  };
65
 
66
+ mediaRecorder.current = rec;
67
+ rec.start();
68
  setIsRecording(true);
69
+ setRecordingTime(0);
70
+ timerRef.current = setInterval(() => setRecordingTime(p => p + 1), 1000);
71
+ } catch { addMsg('system', 'Microphone access denied. Please enable mic permissions.'); }
72
  };
73
 
74
  const stopRecording = () => {
75
+ if (mediaRecorder.current && isRecording) { mediaRecorder.current.stop(); setIsRecording(false); }
 
 
 
76
  };
77
 
78
+ const processVoice = async (data: ArrayBuffer) => {
 
79
  setIsProcessing(true);
80
  try {
81
  if (window.solvox) {
82
+ const r = await window.solvox.ai.processVoice(data);
83
+ if (r.success) {
84
+ addMsg('user', r.transcription || '[voice]');
85
+ addMsg('assistant', r.response || 'Done.', r.intent);
86
+ if (r.audio) playAudio(r.audio);
87
+ } else { addMsg('system', `Voice processing failed: ${r.error}`); }
88
+ } else { addMsg('user', '[voice — dev mode]'); addMsg('assistant', 'QVAC models needed for voice. Type commands below.'); }
89
+ } catch (e: any) { addMsg('system', e.message); }
 
 
 
 
 
 
 
 
 
 
 
90
  setIsProcessing(false);
91
  };
92
 
93
+ const handleSend = async () => {
 
94
  if (!input.trim()) return;
95
  const text = input.trim();
96
  setInput('');
97
+ addMsg('user', text);
 
98
  setIsProcessing(true);
 
99
  try {
100
  if (window.solvox) {
101
+ const [intentR, chatR] = await Promise.all([window.solvox.ai.parseIntent(text), window.solvox.ai.chat(text)]);
102
+ addMsg('assistant', chatR.success ? chatR.response! : 'Sorry, could not process that.', intentR.success ? intentR.intent : undefined);
103
+ } else { addMsg('assistant', `[Dev mode] "${text}" — QVAC models needed.`); }
104
+ } catch (e: any) { addMsg('system', e.message); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  setIsProcessing(false);
106
  };
107
 
108
+ const addMsg = (role: 'user' | 'assistant' | 'system', text: string, intent?: any) => {
109
+ setMessages(p => [...p, { id: Date.now().toString(36), role, text, intent, timestamp: new Date() }]);
 
 
 
 
 
 
 
110
  };
111
 
112
+ const playAudio = (d: ArrayBuffer) => {
113
+ try { const u = URL.createObjectURL(new Blob([d], { type: 'audio/wav' })); const a = new Audio(u); a.play().catch(() => {}); a.onended = () => URL.revokeObjectURL(u); } catch {}
 
 
 
 
 
 
114
  };
115
 
116
+ const activeModules = aiStatus ? [aiStatus.llm, aiStatus.transcription, aiStatus.tts, aiStatus.embed, aiStatus.translation, aiStatus.ocr].filter(Boolean).length : 0;
117
+
118
  return (
119
+ <div className="flex flex-col h-full page-enter">
120
  {/* Header */}
121
  <div className="flex items-center justify-between mb-4">
122
  <div>
123
+ <h2 className="text-xl font-bold text-sol-text">Voice AI Assistant</h2>
124
+ <p className="text-[11px] text-sol-muted">Powered by 6 QVAC packages • All local, all private</p>
 
 
125
  </div>
126
  <div className="flex items-center gap-2">
127
+ <div className={`badge ${activeModules >= 4 ? 'badge-green' : activeModules > 0 ? 'badge-warn' : 'badge-muted'}`}>
128
+ <div className={`w-1.5 h-1.5 rounded-full ${activeModules > 0 ? 'bg-current animate-pulse' : 'bg-sol-muted/30'}`} />
129
+ {activeModules >= 4 ? 'AI Online' : activeModules > 0 ? 'Partial' : 'Loading...'}
130
+ </div>
 
 
 
 
 
131
  </div>
132
  </div>
133
 
134
+ {/* Chat Area */}
135
+ <div className="flex-1 overflow-y-auto chat-scroll glass rounded-2xl p-4 mb-4 space-y-3">
136
  {messages.map(msg => (
137
+ <div key={msg.id} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'} slide-enter`}>
138
+ <div className={`max-w-[78%] rounded-2xl px-4 py-3 ${
139
+ msg.role === 'user' ? 'bg-gradient-to-br from-sol-purple to-sol-purple-light text-white shadow-glow-sm' :
140
+ msg.role === 'system' ? 'bg-warning/8 text-warning border border-warning/20' :
141
+ 'bg-sol-dark/60 border border-sol-border/40'
 
 
 
 
 
142
  }`}>
143
+ {msg.role === 'assistant' && (
144
+ <div className="flex items-center gap-1.5 mb-1.5">
145
+ <div className="w-4 h-4 rounded bg-tether-green/20 flex items-center justify-center">
146
+ <span className="text-[8px] text-tether-green font-bold">Q</span>
147
+ </div>
148
+ <span className="text-[10px] text-sol-muted font-medium">SolVox AI</span>
149
+ </div>
150
+ )}
151
+ <p className="text-[13px] leading-relaxed whitespace-pre-wrap">{msg.text}</p>
152
+ {msg.intent && msg.intent.action !== 'unknown' && (
153
+ <div className="mt-2 pt-2 border-t border-white/10">
154
+ <div className="flex flex-wrap gap-1.5">
155
+ <span className="badge badge-purple text-[10px]">{msg.intent.action}</span>
156
+ {msg.intent.confidence && <span className="badge badge-muted text-[10px]">{(msg.intent.confidence*100).toFixed(0)}%</span>}
157
+ {msg.intent.amount && <span className="badge badge-green text-[10px]">{msg.intent.amount} {msg.intent.token || ''}</span>}
158
+ {msg.intent.to && <span className="badge badge-tether text-[10px]">→ {msg.intent.to}</span>}
159
  </div>
160
  </div>
161
  )}
162
+ <div className="text-[10px] opacity-40 mt-1.5">{msg.timestamp.toLocaleTimeString()}</div>
 
 
163
  </div>
164
  </div>
165
  ))}
166
  {isProcessing && (
167
  <div className="flex justify-start">
168
+ <div className="bg-sol-dark/60 border border-sol-border/40 rounded-2xl px-5 py-4 flex items-center gap-3">
169
+ <OrbitLoader size={24} />
170
+ <span className="text-xs text-sol-muted">Processing locally...</span>
 
 
 
171
  </div>
172
  </div>
173
  )}
174
  <div ref={messagesEndRef} />
175
  </div>
176
 
177
+ {/* Suggestion Chips */}
178
+ {messages.length <= 2 && !isRecording && (
179
+ <div className="flex flex-wrap gap-2 mb-3 fade-in">
180
+ {SUGGESTIONS.slice(0, 4).map(s => (
181
+ <button key={s} onClick={() => { setInput(s); }}
182
+ className="px-3 py-1.5 rounded-full border border-sol-border/40 text-[11px] text-sol-muted hover:text-sol-purple hover:border-sol-purple/30 transition-all">
183
+ {s}
184
+ </button>
 
185
  ))}
186
  </div>
187
  )}
188
 
189
+ {/* Waveform (recording) */}
190
+ {isRecording && (
191
+ <div className="mb-3 fade-in">
192
+ <div className="glass rounded-xl p-3">
193
+ <div className="flex items-center justify-center gap-[2px] h-10">
194
+ {waveformData.map((h, i) => (
195
+ <div key={i} className="w-[3px] rounded-full transition-all duration-75"
196
+ style={{ height: `${h}px`, background: `linear-gradient(180deg, #9945FF ${100 - h*2}%, #14F195)`, opacity: 0.4 + (h / 40) }} />
197
+ ))}
198
+ </div>
199
+ <div className="flex items-center justify-center gap-2 mt-2">
200
+ <div className="w-2 h-2 rounded-full bg-danger animate-pulse" />
201
+ <span className="text-xs text-danger font-medium">Recording — {recordingTime}s</span>
202
+ </div>
203
+ </div>
204
+ </div>
205
+ )}
206
+
207
+ {/* Input Bar */}
208
  <div className="flex items-center gap-3">
 
209
  <button
210
+ onMouseDown={startRecording} onMouseUp={stopRecording} onMouseLeave={stopRecording}
211
+ onTouchStart={startRecording} onTouchEnd={stopRecording}
 
 
 
212
  disabled={isProcessing}
213
+ className={`w-12 h-12 rounded-2xl flex items-center justify-center transition-all duration-300 ${
214
+ isRecording ? 'bg-danger voice-btn-active' : 'bg-gradient-to-br from-sol-purple to-sol-purple-light voice-btn-idle'
215
+ } disabled:opacity-40 disabled:shadow-none`}
 
 
216
  title="Hold to speak"
217
  >
218
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="white" className={isRecording ? 'animate-pulse' : ''}>
219
+ {isRecording
220
+ ? <rect x="6" y="6" width="12" height="12" rx="2" />
221
+ : <><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round"/><line x1="12" y1="19" x2="12" y2="23" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round"/></>
222
+ }
223
+ </svg>
224
  </button>
225
 
 
226
  <div className="flex-1 relative">
227
+ <input value={input} onChange={e => setInput(e.target.value)}
228
+ onKeyDown={e => e.key === 'Enter' && handleSend()}
229
+ placeholder={isRecording ? 'Listening...' : 'Type a command or hold mic to speak...'}
230
+ className="input-field pr-20" disabled={isProcessing || isRecording} />
231
+ <button onClick={handleSend} disabled={!input.trim() || isProcessing}
232
+ className="absolute right-1.5 top-1/2 -translate-y-1/2 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all disabled:opacity-20 bg-sol-purple/20 text-sol-purple hover:bg-sol-purple/30">
233
+ Send ↵
 
 
 
 
 
 
 
234
  </button>
235
  </div>
236
  </div>
tailwind.config.js CHANGED
@@ -6,43 +6,82 @@ module.exports = {
6
  extend: {
7
  colors: {
8
  'sol-purple': '#9945FF',
 
9
  'sol-green': '#14F195',
10
- 'sol-dark': '#0E0E2C',
11
- 'sol-card': '#1A1A3E',
12
- 'sol-border': '#2D2D5E',
13
- 'sol-text': '#E0E0FF',
14
- 'sol-muted': '#8888AA',
 
 
15
  'tether-green': '#26A17B',
 
16
  'danger': '#FF4466',
 
17
  'warning': '#FFB830',
 
18
  },
19
  fontFamily: {
20
  sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
21
  mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
22
  },
 
 
 
 
 
 
 
 
 
 
 
 
23
  animation: {
24
- 'pulse-glow': 'pulse-glow 2s ease-in-out infinite',
25
  'waveform': 'waveform 0.5s ease-in-out infinite alternate',
26
- 'slide-up': 'slide-up 0.3s ease-out',
27
- 'fade-in': 'fade-in 0.2s ease-out',
 
 
 
 
 
28
  },
29
  keyframes: {
30
  'pulse-glow': {
31
- '0%, 100%': { boxShadow: '0 0 20px rgba(153, 69, 255, 0.3)' },
32
- '50%': { boxShadow: '0 0 40px rgba(153, 69, 255, 0.6)' },
33
  },
34
  'waveform': {
35
- '0%': { height: '4px' },
36
- '100%': { height: '24px' },
37
  },
38
  'slide-up': {
39
- '0%': { transform: 'translateY(10px)', opacity: '0' },
40
  '100%': { transform: 'translateY(0)', opacity: '1' },
41
  },
42
- 'fade-in': {
43
  '0%': { opacity: '0' },
44
  '100%': { opacity: '1' },
45
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  },
47
  },
48
  },
 
6
  extend: {
7
  colors: {
8
  'sol-purple': '#9945FF',
9
+ 'sol-purple-light': '#B06CFF',
10
  'sol-green': '#14F195',
11
+ 'sol-dark': '#0A0A1E',
12
+ 'sol-darker': '#060612',
13
+ 'sol-card': '#141432',
14
+ 'sol-card-hover': '#1A1A3E',
15
+ 'sol-border': '#1E1E48',
16
+ 'sol-text': '#E8E8FF',
17
+ 'sol-muted': '#7878AA',
18
  'tether-green': '#26A17B',
19
+ 'tether-dark': '#1D7D5F',
20
  'danger': '#FF4466',
21
+ 'danger-dark': '#CC3652',
22
  'warning': '#FFB830',
23
+ 'info': '#3B82F6',
24
  },
25
  fontFamily: {
26
  sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
27
  mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
28
  },
29
+ borderRadius: {
30
+ '2xl': '16px',
31
+ '3xl': '20px',
32
+ '4xl': '24px',
33
+ },
34
+ boxShadow: {
35
+ 'glow-sm': '0 0 12px rgba(153, 69, 255, 0.15)',
36
+ 'glow-md': '0 0 24px rgba(153, 69, 255, 0.2)',
37
+ 'glow-lg': '0 0 48px rgba(153, 69, 255, 0.25)',
38
+ 'card': '0 4px 24px rgba(0, 0, 0, 0.3)',
39
+ 'card-hover': '0 8px 40px rgba(0, 0, 0, 0.4)',
40
+ },
41
  animation: {
42
+ 'pulse-glow': 'pulse-glow 3s ease-in-out infinite',
43
  'waveform': 'waveform 0.5s ease-in-out infinite alternate',
44
+ 'slide-up': 'slide-up 0.35s cubic-bezier(0.4, 0, 0.2, 1)',
45
+ 'fade-in': 'fadein 0.3s ease-out',
46
+ 'scale-in': 'scalein 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)',
47
+ 'spin-slow': 'spin 4s linear infinite',
48
+ 'spin-reverse': 'spin-rev 3s linear infinite',
49
+ 'float': 'float 6s ease-in-out infinite',
50
+ 'gradient': 'gradient-shift 6s ease infinite',
51
  },
52
  keyframes: {
53
  'pulse-glow': {
54
+ '0%, 100%': { boxShadow: '0 0 20px rgba(153, 69, 255, 0.2)' },
55
+ '50%': { boxShadow: '0 0 48px rgba(153, 69, 255, 0.5)' },
56
  },
57
  'waveform': {
58
+ '0%': { height: '3px' },
59
+ '100%': { height: '28px' },
60
  },
61
  'slide-up': {
62
+ '0%': { transform: 'translateY(24px)', opacity: '0' },
63
  '100%': { transform: 'translateY(0)', opacity: '1' },
64
  },
65
+ 'fadein': {
66
  '0%': { opacity: '0' },
67
  '100%': { opacity: '1' },
68
  },
69
+ 'scalein': {
70
+ '0%': { opacity: '0', transform: 'scale(0.9)' },
71
+ '100%': { opacity: '1', transform: 'scale(1)' },
72
+ },
73
+ 'spin-rev': {
74
+ '0%': { transform: 'rotate(0deg)' },
75
+ '100%': { transform: 'rotate(-360deg)' },
76
+ },
77
+ 'float': {
78
+ '0%, 100%': { transform: 'translateY(0)' },
79
+ '50%': { transform: 'translateY(-12px)' },
80
+ },
81
+ 'gradient-shift': {
82
+ '0%, 100%': { backgroundPosition: '0% 50%' },
83
+ '50%': { backgroundPosition: '100% 50%' },
84
+ },
85
  },
86
  },
87
  },