muthuk1 commited on
Commit
5f5514e
·
verified ·
1 Parent(s): d3cad6d

🎨 Complete Coinbase design system rebuild: white canvas, single blue accent, pill CTAs, editorial spacing, hairline borders, dark hero bands, asset rows, mono numbers, 96px section rhythm

Browse files
src/renderer/App.tsx CHANGED
@@ -18,148 +18,77 @@ type AppState = 'loading' | 'onboarding' | 'locked' | 'unlocked';
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
-
56
- const handleUnlock = async (pk: string) => {
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
- }
69
  };
70
 
71
- const handleOnboardingComplete = (pk: string) => {
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
  }
 
18
  function AppContent() {
19
  const [appState, setAppState] = useState<AppState>('loading');
20
  const [currentPage, setCurrentPage] = useState<Page>('dashboard');
 
21
  const [publicKey, setPublicKey] = useState<string | null>(null);
22
  const [balance, setBalance] = useState({ sol: 0, usdt: 0 });
23
  const [aiStatus, setAiStatus] = useState<any>(null);
24
  const { addToast } = useToast();
25
 
26
+ useEffect(() => { init(); }, []);
27
+ useEffect(() => { if (window.solvox) return window.solvox.on.locked(() => setAppState('locked')); }, []);
28
 
29
+ const init = async () => {
 
 
 
 
 
30
  try {
31
  if (!window.solvox) { setAppState('unlocked'); return; }
32
  const exists = await window.solvox.wallet.exists();
33
  if (!exists) { setAppState('onboarding'); return; }
34
  const unlocked = await window.solvox.wallet.isUnlocked();
35
  setAppState(unlocked ? 'unlocked' : 'locked');
36
+ if (unlocked) { setPublicKey(await window.solvox.wallet.getPublicKey()); refreshBalance(); }
 
 
 
37
  } catch { setAppState('onboarding'); }
38
  };
39
 
40
  const refreshBalance = async () => {
41
+ try { if (!window.solvox) return; const r = await window.solvox.wallet.getBalance(); if (r.success) setBalance({ sol: r.sol || 0, usdt: r.usdt || 0 }); } catch {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  };
43
 
44
+ const handleUnlock = (pk: string) => {
45
+ setPublicKey(pk); setAppState('unlocked'); refreshBalance();
46
+ addToast({ type: 'success', title: 'Wallet unlocked' });
47
+ if (window.solvox) window.solvox.ai.initialize().then(r => { if (r.success) window.solvox.ai.getStatus().then(setAiStatus); });
 
48
  };
49
 
50
+ const handleLock = async () => { if (window.solvox) await window.solvox.wallet.lock(); setAppState('locked'); };
 
 
 
 
 
 
 
 
51
 
52
+ if (appState === 'loading') return <Loading />;
53
+ if (appState === 'onboarding') return <OnboardingScreen onComplete={pk => { setPublicKey(pk); setAppState('unlocked'); refreshBalance(); }} />;
54
  if (appState === 'locked') return <LockScreen onUnlock={handleUnlock} />;
55
 
56
+ const pages: Record<Page, JSX.Element> = {
57
+ dashboard: <Dashboard balance={balance} publicKey={publicKey} onRefresh={refreshBalance} onNavigate={setCurrentPage} />,
58
+ send: <SendPage balance={balance} onSent={refreshBalance} />,
59
+ history: <HistoryPage />,
60
+ voice: <VoicePage aiStatus={aiStatus} />,
61
+ security: <SecurityPage />,
62
+ settings: <SettingsPage onLock={handleLock} />,
 
 
 
 
63
  };
64
 
65
  return (
66
+ <div className="flex h-screen bg-canvas">
67
+ <Sidebar currentPage={currentPage} onNavigate={setCurrentPage} />
68
  <div className="flex-1 flex flex-col overflow-hidden">
69
  <TopBar publicKey={publicKey} balance={balance} aiStatus={aiStatus} onLock={handleLock} />
70
+ <main className="flex-1 overflow-y-auto">
71
+ <div className="page-enter" key={currentPage}>{pages[currentPage]}</div>
72
  </main>
73
  </div>
74
  </div>
75
  );
76
  }
77
 
78
+ export default function App() { return <ToastProvider><AppContent /></ToastProvider>; }
 
 
 
 
 
 
79
 
80
+ function Loading() {
81
  return (
82
+ <div className="h-screen flex flex-col items-center justify-center bg-canvas">
83
+ <div className="w-12 h-12 rounded-full bg-primary flex items-center justify-center mb-6">
84
+ <span className="text-on-primary font-bold text-title-md">SV</span>
 
 
 
 
 
 
 
 
 
 
 
85
  </div>
86
+ <div className="display-text text-display-sm text-ink mb-2">SolVox</div>
87
+ <div className="text-body-md text-body">Initializing local AI engine…</div>
88
+ <div className="mt-8 w-48 h-1 bg-surface-strong rounded-pill overflow-hidden">
89
+ <div className="h-full bg-primary rounded-pill" style={{ width: '60%', animation: 'shimmer 1.5s infinite', background: 'linear-gradient(90deg, #0052ff 0%, #003ecc 50%, #0052ff 100%)', backgroundSize: '200% 100%' }} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  </div>
91
+ <div className="text-caption text-muted mt-4">Powered by QVAC SDK · 100% Local AI</div>
92
  </div>
93
  );
94
  }
src/renderer/components/Sidebar.tsx CHANGED
@@ -1,118 +1,57 @@
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>
 
1
+ import React from 'react';
2
 
3
+ interface SidebarProps { currentPage: string; onNavigate: (page: any) => void; }
 
 
 
 
4
 
5
+ const nav = [
6
+ { id: 'dashboard', label: 'Home' },
7
+ { id: 'voice', label: 'Voice AI' },
8
+ { id: 'send', label: 'Send' },
9
+ { id: 'history', label: 'Transactions' },
10
+ { id: 'security', label: 'Security' },
11
+ { id: 'settings', label: 'Settings' },
12
  ];
13
 
14
  export default function Sidebar({ currentPage, onNavigate }: SidebarProps) {
 
 
15
  return (
16
+ <aside className="w-[200px] flex flex-col bg-canvas border-r border-hairline">
17
+ {/* Wordmark */}
18
+ <div className="px-5 pt-6 pb-5">
19
+ <div className="flex items-center gap-2">
20
+ <div className="w-7 h-7 rounded-full bg-primary flex items-center justify-center">
21
+ <span className="text-on-primary text-caption-strong font-bold">SV</span>
 
 
 
 
22
  </div>
23
+ <span className="text-title-md text-ink">SolVox</span>
24
  </div>
25
  </div>
26
 
27
+ <div className="hairline-soft mx-5" />
28
 
29
+ {/* Nav Links */}
30
  <nav className="flex-1 px-3 py-4 space-y-0.5">
31
+ {nav.map(item => (
32
+ <button key={item.id} onClick={() => onNavigate(item.id)}
33
+ className={`w-full text-left px-3 py-2.5 rounded-lg text-nav-link transition-colors ${
34
+ currentPage === item.id
35
+ ? 'text-primary bg-primary/[0.06] font-semibold'
36
+ : 'text-body hover:text-ink hover:bg-surface-soft'
37
+ }`}>
38
+ {item.label}
39
+ </button>
40
+ ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  </nav>
42
 
43
+ {/* QVAC Status */}
44
+ <div className="px-4 pb-4">
45
+ <div className="bg-surface-soft rounded-xl p-3">
46
+ <div className="text-caption-strong text-muted uppercase tracking-wider mb-1.5">QVAC Engine</div>
47
+ <div className="flex items-center gap-1.5 flex-wrap">
48
+ {['LLM', 'STT', 'TTS', 'EMB', 'NMT', 'OCR'].map(m => (
49
+ <span key={m} className="badge-pill text-[10px] py-0">{m}</span>
50
+ ))}
51
+ </div>
52
+ <div className="flex items-center gap-1.5 mt-2">
53
+ <div className="w-1.5 h-1.5 rounded-full bg-semantic-up" />
54
+ <span className="text-caption text-muted">100% local</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  </div>
56
  </div>
57
  </div>
src/renderer/components/TopBar.tsx CHANGED
@@ -1,98 +1,45 @@
1
  import React, { useState } from 'react';
2
- import { AnimatedNumber } from './ui/index';
3
 
4
- interface TopBarProps {
5
- publicKey: string | null;
6
- balance: { sol: number; usdt: number };
7
- aiStatus: any;
8
- onLock: () => void;
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
  );
 
1
  import React, { useState } from 'react';
2
+ import { Num } from './ui/index';
3
 
4
+ interface TopBarProps { publicKey: string | null; balance: { sol: number; usdt: number }; aiStatus: any; onLock: () => void; }
 
 
 
 
 
5
 
6
  export default function TopBar({ publicKey, balance, aiStatus, onLock }: TopBarProps) {
7
  const [copied, setCopied] = useState(false);
8
+ const short = publicKey ? `${publicKey.slice(0, 4)}${publicKey.slice(-4)}` : '—';
9
+ const copy = () => { if (publicKey) { navigator.clipboard.writeText(publicKey); setCopied(true); setTimeout(() => setCopied(false), 1500); } };
10
+ const mods = aiStatus ? [aiStatus.llm, aiStatus.transcription, aiStatus.tts, aiStatus.embed, aiStatus.translation, aiStatus.ocr].filter(Boolean).length : 0;
 
 
 
 
 
 
 
 
 
 
11
 
12
  return (
13
+ <header className="h-[56px] border-b border-hairline flex items-center justify-between px-5 bg-canvas">
14
+ {/* Left */}
15
  <div className="flex items-center gap-3">
16
+ <button onClick={copy} className="flex items-center gap-2 px-3 py-1.5 rounded-pill bg-surface-strong text-body-sm text-muted hover:text-ink transition-colors">
17
+ <span className="w-1.5 h-1.5 rounded-full bg-semantic-up" />
18
+ <span className="font-mono text-caption">{short}</span>
19
+ <span className="text-caption text-muted-soft">{copied ? '✓' : '⊕'}</span>
 
 
 
 
20
  </button>
21
+ <span className="badge-pill text-[10px]">DEVNET</span>
 
 
 
 
22
  </div>
23
 
24
+ {/* Center Balances */}
25
+ <div className="flex items-center gap-6">
26
+ <div className="text-center">
27
+ <div className="text-caption text-muted">SOL</div>
28
+ <div className="text-title-sm text-ink"><Num value={balance.sol} decimals={4} /></div>
 
 
 
 
 
 
 
29
  </div>
30
+ <div className="w-px h-6 bg-hairline" />
31
+ <div className="text-center">
32
+ <div className="text-caption text-muted">USDT</div>
33
+ <div className="text-title-sm text-ink"><Num value={balance.usdt} decimals={2} /></div>
 
 
 
 
 
 
 
 
 
34
  </div>
35
  </div>
36
 
37
+ {/* Right */}
38
  <div className="flex items-center gap-3">
39
+ <span className={`badge-pill text-[10px] ${mods > 0 ? 'badge-pill-blue' : ''}`}>
40
+ {mods > 0 ? `${mods}/6 AI` : 'Loading…'}
41
+ </span>
42
+ <button onClick={onLock} className="btn-secondary text-body-sm py-2 px-3">Lock</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  </div>
44
  </header>
45
  );
src/renderer/components/ui/index.tsx CHANGED
@@ -1,43 +1,29 @@
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>
@@ -47,187 +33,69 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
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
  ))}
@@ -235,28 +103,31 @@ export function StepIndicator({ steps, current }: { steps: string[]; current: nu
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
  }
 
 
 
 
 
 
 
1
  import React, { useState, useEffect, useCallback, createContext, useContext } from 'react';
2
 
3
+ /* ═══ TOAST SYSTEM ═══════════════════════════════════════════════════ */
4
+ type ToastType = 'success' | 'error' | 'info';
5
+ interface Toast { id: string; type: ToastType; title: string; message?: string; }
6
+ const ToastContext = createContext<{ addToast: (t: Omit<Toast, 'id'>) => void }>({ addToast: () => {} });
 
 
 
 
 
7
  export const useToast = () => useContext(ToastContext);
8
 
9
  export function ToastProvider({ children }: { children: React.ReactNode }) {
10
  const [toasts, setToasts] = useState<Toast[]>([]);
 
11
  const addToast = useCallback((t: Omit<Toast, 'id'>) => {
12
+ const id = Date.now().toString(36);
13
+ setToasts(p => [...p, { ...t, id }]);
14
+ setTimeout(() => setToasts(p => p.filter(x => x.id !== id)), 4000);
15
  }, []);
 
16
  return (
17
  <ToastContext.Provider value={{ addToast }}>
18
  {children}
19
+ <div className="fixed top-4 right-4 z-50 flex flex-col gap-2">
20
  {toasts.map(t => (
21
+ <div key={t.id} className="bg-canvas rounded-xl p-4 shadow-soft border border-hairline min-w-[300px] page-enter cursor-pointer" onClick={() => setToasts(p => p.filter(x => x.id !== t.id))}>
22
+ <div className="flex items-center gap-3">
23
+ <div className={`w-2 h-2 rounded-full ${t.type === 'success' ? 'bg-semantic-up' : t.type === 'error' ? 'bg-semantic-down' : 'bg-primary'}`} />
24
+ <div>
25
+ <div className="text-title-sm text-ink">{t.title}</div>
26
+ {t.message && <div className="text-body-sm text-body mt-0.5">{t.message}</div>}
 
 
 
 
 
 
 
27
  </div>
28
  </div>
29
  </div>
 
33
  );
34
  }
35
 
36
+ /* ═══ ANIMATED NUMBER (CoinbaseMono) ═════════════════════════════════ */
37
+ export function Num({ value, decimals = 2, prefix = '', suffix = '' }: { value: number; decimals?: number; prefix?: string; suffix?: string }) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  const [display, setDisplay] = useState(value);
 
39
  useEffect(() => {
40
  const diff = value - display;
41
+ if (Math.abs(diff) < 0.0001) { setDisplay(value); return; }
 
42
  let step = 0;
43
  const interval = setInterval(() => {
44
  step++;
45
+ setDisplay(display + diff * (1 - Math.pow(1 - step / 16, 3)));
46
+ if (step >= 16) { setDisplay(value); clearInterval(interval); }
47
+ }, 30);
 
 
48
  return () => clearInterval(interval);
49
  }, [value]);
50
+ return <span className="number-mono">{prefix}{display.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals })}{suffix}</span>;
 
 
 
 
 
51
  }
52
 
53
+ /* ═══ ASSET ROW ══════════════════════════════════════════════════════ */
54
+ export function AssetRow({ icon, name, ticker, price, change, onClick }: {
55
+ icon: string; name: string; ticker: string; price: number; change?: number; onClick?: () => void;
 
 
 
56
  }) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  return (
58
+ <div className="asset-row px-4 cursor-pointer" onClick={onClick}>
59
+ <div className="asset-icon mr-3 text-sm font-bold text-ink">{icon}</div>
60
+ <div className="flex-1">
61
+ <div className="text-title-sm text-ink">{name}</div>
62
+ <div className="text-caption text-muted">{ticker}</div>
63
+ </div>
64
+ <div className="text-right">
65
+ <div className="number-mono text-number-display text-ink">${price.toLocaleString(undefined, { minimumFractionDigits: 2 })}</div>
66
+ {change !== undefined && (
67
+ <div className={`number-mono text-caption ${change >= 0 ? 'text-semantic-up' : 'text-semantic-down'}`}>
68
+ {change >= 0 ? '+' : ''}{change.toFixed(2)}%
69
+ </div>
70
+ )}
71
  </div>
72
  </div>
73
  );
74
  }
75
 
76
+ /* ═══ PRODUCT UI CARD (Dark) ═════════════════════════════════════════ */
77
+ export function ProductCard({ children, className = '' }: { children: React.ReactNode; className?: string }) {
78
+ return <div className={`card-dark ${className}`}>{children}</div>;
79
+ }
80
 
81
+ /* ═══ STEP INDICATOR ═════════════════════════════════════════════════ */
82
  export function StepIndicator({ steps, current }: { steps: string[]; current: number }) {
83
  return (
84
+ <div className="flex items-center justify-center gap-3 mb-12">
85
  {steps.map((label, i) => (
86
  <React.Fragment key={i}>
87
+ <div className="flex flex-col items-center gap-1.5">
88
+ <div className={`w-8 h-8 rounded-full flex items-center justify-center text-caption-strong transition-colors ${
89
+ i < current ? 'bg-primary text-on-primary' :
90
+ i === current ? 'bg-primary text-on-primary' :
91
+ 'bg-surface-strong text-muted'
92
  }`}>
93
  {i < current ? '✓' : i + 1}
94
  </div>
95
+ <span className={`text-caption ${i <= current ? 'text-ink' : 'text-muted'}`}>{label}</span>
96
  </div>
97
  {i < steps.length - 1 && (
98
+ <div className={`w-10 h-0.5 -mt-5 rounded-full ${i < current ? 'bg-primary' : 'bg-surface-strong'}`} />
 
 
99
  )}
100
  </React.Fragment>
101
  ))}
 
103
  );
104
  }
105
 
106
+ /* ═══ PIPELINE TRACE (shows which QVAC module did what) ══════════════ */
107
+ export function PipelineTrace({ steps }: { steps: Array<{ module: string; operation: string; input: string; output: string; durationMs: number }> }) {
108
+ if (!steps || steps.length === 0) return null;
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  return (
110
+ <div className="bg-surface-soft rounded-xl p-4 mt-3">
111
+ <div className="text-caption-strong text-muted uppercase tracking-wider mb-2">QVAC Pipeline Trace</div>
112
+ <div className="space-y-1.5">
113
+ {steps.map((s, i) => (
114
+ <div key={i} className="flex items-start gap-2 text-caption">
115
+ <div className="w-1.5 h-1.5 rounded-full bg-primary mt-1.5 shrink-0" />
116
+ <div className="flex-1 min-w-0">
117
+ <span className="font-mono text-primary text-caption-strong">{s.module}</span>
118
+ <span className="text-muted mx-1">→</span>
119
+ <span className="text-body">{s.operation}</span>
120
+ <span className="text-muted-soft ml-1 number-mono">{s.durationMs}ms</span>
121
+ </div>
122
+ </div>
123
+ ))}
124
  </div>
125
+ </div>
 
126
  );
127
  }
128
+
129
+ /* ═══ RISK BADGE ═════════════════════════════════════════════════════ */
130
+ export function RiskBadge({ level, score }: { level: string; score: number }) {
131
+ const colors: Record<string, string> = { safe: 'badge-pill-green', caution: 'badge-pill', warning: 'badge-pill-red', danger: 'badge-pill-red' };
132
+ return <span className={`badge-pill ${colors[level] || 'badge-pill'}`}>{level.toUpperCase()} · {score}</span>;
133
+ }
src/renderer/index.css CHANGED
@@ -2,256 +2,193 @@
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); }
 
2
  @tailwind components;
3
  @tailwind utilities;
4
 
5
+ /* ─── Reset ──────────────────────────────────────────────────────────── */
6
  * { margin: 0; padding: 0; box-sizing: border-box; }
7
 
8
  body {
9
+ font-family: 'Inter', -apple-system, system-ui, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
10
+ background: #ffffff;
11
+ color: #0a0b0d;
12
  overflow: hidden;
13
  height: 100vh;
14
  -webkit-app-region: drag;
15
+ -webkit-font-smoothing: antialiased;
16
+ -moz-osx-font-smoothing: grayscale;
17
  }
18
  button, input, select, textarea, a, label { -webkit-app-region: no-drag; }
19
 
20
  /* ─── Scrollbar ──────────────────────────────────────────────────────── */
21
+ ::-webkit-scrollbar { width: 6px; }
22
  ::-webkit-scrollbar-track { background: transparent; }
23
+ ::-webkit-scrollbar-thumb { background: #dee1e6; border-radius: 100px; }
24
+ ::-webkit-scrollbar-thumb:hover { background: #7c828a; }
 
 
 
 
 
 
 
 
 
25
 
26
+ /* ─── Display Type (Inter as CoinbaseDisplay substitute) ─────────────── */
27
+ .display-text {
28
+ font-family: 'Inter', sans-serif;
29
+ font-weight: 400;
30
+ letter-spacing: -0.02em;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  }
32
 
33
+ /* ─── Mono Numbers ──────────────────────────────────────────────────── */
34
+ .number-mono {
35
+ font-family: 'JetBrains Mono', 'Geist Mono', monospace;
36
+ font-weight: 500;
37
+ font-variant-numeric: tabular-nums;
 
 
 
 
 
 
 
 
 
 
 
38
  }
39
 
40
+ /* ═══ COINBASE BUTTON SYSTEM ═════════════════════════════════════════ */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
+ .btn-primary {
43
+ @apply inline-flex items-center justify-center px-5 py-3 text-button font-semibold text-on-primary rounded-pill transition-colors;
44
+ background-color: #0052ff;
45
+ }
46
+ .btn-primary:hover { background-color: #003ecc; }
47
+ .btn-primary:active { background-color: #003ecc; }
48
+ .btn-primary:disabled { background-color: #a8b8cc; cursor: not-allowed; }
49
 
50
+ .btn-primary-lg {
51
+ @apply inline-flex items-center justify-center px-8 py-4 text-button font-semibold text-on-primary rounded-pill transition-colors;
52
+ background-color: #0052ff;
53
+ height: 56px;
 
54
  }
55
+ .btn-primary-lg:hover { background-color: #003ecc; }
56
 
57
+ .btn-secondary {
58
+ @apply inline-flex items-center justify-center px-5 py-3 text-button font-semibold text-ink bg-surface-strong rounded-pill transition-colors;
 
 
59
  }
60
+ .btn-secondary:hover { background-color: #dee1e6; }
61
+
62
+ .btn-secondary-dark {
63
+ @apply inline-flex items-center justify-center px-5 py-3 text-button font-semibold text-on-dark rounded-pill transition-colors;
64
+ background-color: #16181c;
 
 
 
65
  }
66
+ .btn-secondary-dark:hover { background-color: #1e2025; }
67
+
68
+ .btn-outline-dark {
69
+ @apply inline-flex items-center justify-center px-5 py-3 text-button font-semibold text-on-dark rounded-pill transition-colors border border-white/30 bg-transparent;
 
 
 
 
 
 
70
  }
71
+ .btn-outline-dark:hover { border-color: #ffffff; }
72
+
73
+ .btn-text {
74
+ @apply inline-flex items-center justify-center px-2 py-1 font-semibold rounded-pill transition-colors bg-transparent;
75
+ color: #0052ff;
76
+ font-size: 16px;
77
  }
78
+ .btn-text:hover { background-color: rgba(0, 82, 255, 0.06); }
79
 
80
+ /* ═══ COINBASE CARDS ═════════════════════════════════════════════════ */
 
81
 
82
+ .card {
83
+ @apply bg-canvas rounded-xl p-xl;
84
+ border: 1px solid #dee1e6;
 
 
 
 
 
 
85
  }
86
+ .card-flat {
87
+ @apply bg-canvas rounded-xl p-xl;
 
 
 
 
 
88
  }
89
+ .card-dark {
90
+ @apply rounded-xl p-xl;
91
+ background: #16181c;
92
+ color: #ffffff;
93
  }
94
 
95
+ /* ═══ COINBASE FORMS ═════════════════════════════════════════════════ */
96
+
97
  .input-field {
98
+ @apply w-full px-4 py-3.5 text-body-md bg-canvas text-ink rounded-md transition-all;
99
+ border: 1px solid #dee1e6;
100
+ height: 48px;
101
  }
102
  .input-field:focus {
103
+ outline: none;
104
+ border-color: #0052ff;
105
+ border-width: 2px;
106
+ padding-left: 15px; /* compensate for 2px border */
107
  }
108
+ .input-field::placeholder { color: #7c828a; }
109
 
110
+ .search-pill {
111
+ @apply w-full px-5 py-3 text-body-sm bg-surface-strong text-ink rounded-pill transition-all;
112
+ height: 44px;
113
+ border: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  }
115
+ .search-pill:focus { outline: none; box-shadow: 0 0 0 2px #0052ff; }
116
+
117
+ /* ═══ BADGE PILLS ════════════════════════════════════════════════════ */
118
 
119
+ .badge-pill {
120
+ @apply inline-flex items-center px-2.5 py-0.5 rounded-pill text-caption-strong uppercase tracking-wider;
121
+ background: #eef0f3;
122
+ color: #0a0b0d;
123
  }
124
+ .badge-pill-blue {
125
+ background: rgba(0, 82, 255, 0.08);
126
+ color: #0052ff;
127
+ }
128
+ .badge-pill-green {
129
+ background: rgba(5, 177, 105, 0.08);
130
+ color: #05b169;
131
+ }
132
+ .badge-pill-red {
133
+ background: rgba(207, 32, 47, 0.08);
134
+ color: #cf202f;
135
  }
136
 
137
+ /* ═══ DARK HERO BAND ═════════════════════════════════════════════════ */
138
+
139
+ .hero-band-dark {
140
+ background: #0a0b0d;
141
+ color: #ffffff;
142
+ padding: 96px 0;
143
  }
144
 
145
+ /* ═══ ASSET ROW ══════════════════════════════════════════════════════ */
146
+
147
+ .asset-row {
148
+ @apply flex items-center py-4 transition-colors;
149
+ border-bottom: 1px solid #eef0f3;
150
  }
151
+ .asset-row:hover { background: #f7f7f7; }
152
+ .asset-row:last-child { border-bottom: none; }
153
 
154
+ .asset-icon {
155
+ @apply flex items-center justify-center rounded-full;
156
+ width: 32px; height: 32px;
157
+ background: #eef0f3;
 
 
 
 
 
 
 
158
  }
159
+
160
+ /* ═══ HAIRLINE DIVIDER ═══════════════════════════════════════════════ */
161
+
162
+ .hairline { @apply w-full; height: 1px; background: #dee1e6; }
163
+ .hairline-soft { @apply w-full; height: 1px; background: #eef0f3; }
164
+
165
+ /* ═══ PAGE TRANSITIONS ═══════════════════════════════════════════════ */
166
+
167
+ .page-enter { animation: page-fade 0.25s ease-out; }
168
+ @keyframes page-fade {
169
+ from { opacity: 0; transform: translateY(8px); }
170
+ to { opacity: 1; transform: translateY(0); }
171
  }
172
 
173
+ /* ═══ TOGGLE SWITCH ══════════════════════════════════════════════════ */
174
+
175
+ .toggle {
176
+ @apply relative inline-flex items-center cursor-pointer;
177
+ width: 44px; height: 24px;
178
+ }
179
+ .toggle-track {
180
+ @apply w-full h-full rounded-full transition-colors;
181
+ background: #dee1e6;
182
  }
183
+ .toggle-track.active { background: #0052ff; }
184
+ .toggle-thumb {
185
+ @apply absolute w-5 h-5 rounded-full bg-white shadow-soft transition-all;
186
+ top: 2px; left: 2px;
187
  }
188
+ .toggle-thumb.active { left: 22px; }
189
+
190
+ /* ═══ SECTION RHYTHM ═════════════════════════════════════════════════ */
191
 
192
+ .section-light { @apply bg-canvas; padding: 96px 0; }
193
+ .section-soft { @apply bg-surface-soft; padding: 96px 0; }
194
+ .section-dark { background: #0a0b0d; color: #ffffff; padding: 96px 0; }
 
 
 
 
 
 
 
 
 
 
src/renderer/index.html CHANGED
@@ -1,15 +1,16 @@
1
  <!DOCTYPE html>
2
- <html lang="en" class="dark">
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <meta http-equiv="Content-Security-Policy"
7
- content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.mainnet-beta.solana.com https://api.devnet.solana.com; object-src 'none'; base-uri 'self';">
8
- <title>SolVox — Voice-First AI Wallet</title>
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
 
11
  </head>
12
- <body class="bg-sol-dark text-sol-text">
13
  <div id="root"></div>
14
  <script type="module" src="./main.tsx"></script>
15
  </body>
 
1
  <!DOCTYPE html>
2
+ <html lang="en">
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta http-equiv="Content-Security-Policy"
7
+ content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.mainnet-beta.solana.com https://api.devnet.solana.com; font-src 'self' https://fonts.gstatic.com; object-src 'none'; base-uri 'self';">
8
+ <title>SolVox</title>
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
12
  </head>
13
+ <body>
14
  <div id="root"></div>
15
  <script type="module" src="./main.tsx"></script>
16
  </body>
src/renderer/pages/Dashboard.tsx CHANGED
@@ -1,209 +1,118 @@
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>
183
- ))}
184
- </div>
185
- )}
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>
207
  </div>
208
  );
209
  }
 
1
  import React, { useEffect, useState } from 'react';
2
+ import { Num, AssetRow, PipelineTrace } from '../components/ui/index';
3
 
4
+ interface Props { balance: { sol: number; usdt: number }; publicKey: string | null; onRefresh: () => void; onNavigate: (p: any) => void; }
 
 
 
 
 
5
 
6
+ export default function Dashboard({ balance, publicKey, onRefresh, onNavigate }: Props) {
7
+ const [txs, setTxs] = useState<any[]>([]);
8
  const [aiStatus, setAiStatus] = useState<any>(null);
 
 
 
9
 
10
+ useEffect(() => {
11
+ if (!window.solvox) return;
12
+ Promise.all([window.solvox.wallet.getHistory(5), window.solvox.ai.getStatus()])
13
+ .then(([h, s]) => { if (h.success) setTxs(h.history || []); setAiStatus(s); }).catch(() => {});
14
+ }, []);
 
 
 
 
15
 
16
+ const total = balance.sol * 170 + balance.usdt;
17
+ const mods = aiStatus ? [
18
+ { k: 'llm', n: 'LLM', p: '@qvac/llm-llamacpp' }, { k: 'transcription', n: 'Speech-to-Text', p: '@qvac/transcription-whispercpp' },
19
+ { k: 'tts', n: 'Text-to-Speech', p: '@qvac/tts-onnx' }, { k: 'embed', n: 'Embeddings', p: '@qvac/embed-llamacpp' },
20
+ { k: 'translation', n: 'Translation', p: '@qvac/translation-nmtcpp' }, { k: 'ocr', n: 'OCR', p: '@qvac/ocr-onnx' },
21
+ ] : [];
22
 
23
  return (
24
+ <div className="max-w-content mx-auto px-8 py-section">
25
+ {/* ── Hero Band ──────────────────────────────────────────────── */}
26
+ <section className="bg-surface-dark rounded-xl p-xl mb-12 relative overflow-hidden">
 
 
27
  <div className="relative z-10">
28
+ <div className="text-on-dark-soft text-body-sm mb-1">Total portfolio value</div>
29
+ <div className="text-on-dark display-text text-display-lg mb-2"><Num value={total} decimals={2} prefix="$" /></div>
30
+ <div className="flex items-center gap-3 mt-4">
31
+ <button onClick={() => onNavigate('send')} className="btn-primary">Send</button>
32
+ <button onClick={() => onNavigate('voice')} className="btn-secondary-dark">Voice AI</button>
33
+ <button onClick={onRefresh} className="btn-outline-dark">Refresh</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  </div>
35
  </div>
36
+ {/* Floating product-UI card — Coinbase signature */}
37
+ <div className="absolute right-8 top-6 w-[260px]">
38
+ <div className="card-dark p-4 mb-2 shadow-soft">
39
+ <div className="text-on-dark-soft text-caption mb-1">SOL Balance</div>
40
+ <div className="text-on-dark number-mono text-number-display"><Num value={balance.sol} decimals={4} /></div>
41
+ <div className="text-on-dark-soft text-caption mt-1">≈ $<Num value={balance.sol * 170} decimals={2} /></div>
 
 
 
 
 
 
 
42
  </div>
43
+ <div className="card-dark p-4 shadow-soft ml-4">
44
+ <div className="text-on-dark-soft text-caption mb-1">USDT Balance</div>
45
+ <div className="text-on-dark number-mono text-number-display"><Num value={balance.usdt} decimals={2} /></div>
46
  </div>
47
  </div>
48
+ </section>
49
 
50
+ {/* ── Assets ─────────────────────────────────────────────────── */}
51
+ <section className="mb-12">
52
+ <div className="text-title-lg display-text text-ink mb-6">Assets</div>
53
+ <div className="card">
54
+ <AssetRow icon="◎" name="Solana" ticker="SOL" price={balance.sol * 170} change={2.34} />
55
+ <AssetRow icon="₮" name="Tether" ticker="USDT" price={balance.usdt} change={0.01} />
 
 
 
 
 
 
 
 
56
  </div>
57
+ </section>
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
+ {/* ── AI Engine + Transactions ──────────────────────────────── */}
60
+ <div className="grid grid-cols-5 gap-6">
61
+ <section className="col-span-3">
62
+ <div className="text-title-lg display-text text-ink mb-6">QVAC AI Engine</div>
63
+ <div className="card">
64
+ {mods.map(m => (
65
+ <div key={m.k} className="flex items-center justify-between py-3 border-b border-hairline-soft last:border-0">
66
+ <div>
67
+ <div className="text-title-sm text-ink">{m.n}</div>
68
+ <div className="text-caption text-muted font-mono">{m.p}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  </div>
70
+ <span className={`badge-pill text-[10px] ${(aiStatus as any)?.[m.k] ? 'badge-pill-green' : ''}`}>
71
+ {(aiStatus as any)?.[m.k] ? 'ACTIVE' : 'LOADING'}
72
+ </span>
73
  </div>
74
  ))}
75
+ <div className="pt-3 mt-1 text-caption text-muted flex items-center gap-1.5">
76
+ <div className="w-1.5 h-1.5 rounded-full bg-semantic-up" />
77
+ All inference runs 100% locally via QVAC Fabric (Vulkan GPU)
78
+ </div>
79
  </div>
80
+ </section>
 
 
 
 
 
81
 
82
+ <section className="col-span-2">
83
+ <div className="text-title-lg display-text text-ink mb-6">Recent activity</div>
84
+ <div className="card">
85
+ {txs.length === 0 ? (
86
+ <div className="text-center py-8">
87
+ <div className="text-body text-muted">No transactions yet</div>
88
+ <div className="text-caption text-muted-soft mt-1">Try "Send 1 SOL to…"</div>
 
 
 
 
89
  </div>
90
+ ) : txs.map((tx, i) => (
91
+ <div key={i} className="flex items-center gap-3 py-3 border-b border-hairline-soft last:border-0">
92
+ <div className={`w-7 h-7 rounded-full flex items-center justify-center text-caption-strong ${tx.status === 'success' ? 'bg-semantic-up/10 text-semantic-up' : 'bg-semantic-down/10 text-semantic-down'}`}>
93
+ {tx.status === 'success' ? '✓' : '✗'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  </div>
95
+ <div className="flex-1 min-w-0">
96
+ <div className="text-caption font-mono text-muted truncate">{tx.signature?.slice(0, 16)}…</div>
97
+ <div className="text-caption text-muted-soft">{tx.timestamp ? new Date(tx.timestamp).toLocaleDateString() : '—'}</div>
98
+ </div>
99
+ <span className={`badge-pill text-[10px] ${tx.status === 'success' ? 'badge-pill-green' : 'badge-pill-red'}`}>{tx.status}</span>
100
+ </div>
101
+ ))}
102
+ </div>
103
+ </section>
104
  </div>
105
 
106
+ {/* ── Wallet Address ────────────────────────────────────────── */}
107
+ <section className="mt-12">
108
+ <div className="bg-surface-soft rounded-xl p-5 flex items-center justify-between">
109
+ <div>
110
+ <div className="text-caption-strong text-muted uppercase tracking-wider">Wallet address</div>
111
+ <div className="text-body-sm font-mono text-muted mt-1">{publicKey || '—'}</div>
 
 
 
 
 
112
  </div>
113
+ <button onClick={() => publicKey && navigator.clipboard.writeText(publicKey)} className="btn-secondary text-body-sm py-2 px-4">Copy</button>
 
 
 
114
  </div>
115
+ </section>
116
  </div>
117
  );
118
  }
src/renderer/pages/HistoryPage.tsx CHANGED
@@ -4,97 +4,66 @@ 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
  ))}
64
  </div>
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>
 
4
  const [txs, setTxs] = useState<any[]>([]);
5
  const [loading, setLoading] = useState(true);
6
  const [query, setQuery] = useState('');
7
+ const [rag, setRag] = useState<any[]>([]);
8
  const [searching, setSearching] = useState(false);
9
 
10
  useEffect(() => { load(); }, []);
11
+ const load = async () => { setLoading(true); try { if (window.solvox) { const r = await window.solvox.wallet.getHistory(20); if (r.success) setTxs(r.history || []); } } catch {} setLoading(false); };
12
+ const search = async () => { if (!query.trim()) return; setSearching(true); try { if (window.solvox) { const r = await window.solvox.rag.search(query, 'transaction'); if (r.success) setRag(r.results || []); } } catch {} setSearching(false); };
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  return (
15
+ <div className="max-w-content mx-auto px-8 py-section">
16
+ <div className="flex items-center justify-between mb-6">
17
+ <h2 className="display-text text-title-lg text-ink">Transactions</h2>
18
+ <button onClick={load} className="btn-secondary text-body-sm py-2 px-3">Refresh</button>
 
 
 
19
  </div>
20
 
21
  {/* AI Search */}
22
+ <div className="bg-surface-soft rounded-xl p-4 mb-8">
23
  <div className="flex gap-2">
24
+ <input value={query} onChange={e => setQuery(e.target.value)} onKeyDown={e => e.key === 'Enter' && search()}
25
+ placeholder='Search with AI "last payment to Alice"' className="search-pill flex-1 text-body-sm" />
26
+ <button onClick={search} disabled={searching} className="btn-primary text-body-sm py-2 px-4 disabled:opacity-50">{searching ? '…' : 'Search'}</button>
 
 
 
 
 
27
  </div>
28
+ <div className="text-caption text-muted mt-2 flex items-center gap-1.5">
29
+ <div className="w-1 h-1 rounded-full bg-primary" />
30
+ Semantic search via @qvac/embed-llamacpp — 100% local
31
  </div>
32
+ {rag.length > 0 && (
33
+ <div className="mt-3 space-y-2">
34
+ <div className="text-caption-strong text-primary uppercase tracking-wider">AI Results</div>
35
+ {rag.map((r, i) => (
36
+ <div key={i} className="bg-canvas rounded-lg p-3 border border-hairline">
37
+ <div className="text-body-sm text-ink">{r.text}</div>
38
+ <div className="text-caption text-muted mt-1 number-mono">{(r.score * 100).toFixed(0)}% match</div>
 
 
 
 
39
  </div>
40
  ))}
41
  </div>
42
  )}
43
  </div>
44
 
45
+ {/* Table */}
46
+ <div className="card" style={{ padding: 0 }}>
47
  {loading ? (
48
+ <div className="p-12 text-center text-body text-muted">Loading…</div>
 
 
49
  ) : txs.length === 0 ? (
50
+ <div className="p-12 text-center">
51
+ <div className="text-title-md text-ink mb-1">No transactions yet</div>
52
+ <div className="text-body-sm text-muted">Send or receive tokens to see history.</div>
 
 
 
53
  </div>
54
  ) : (
55
+ <div>
56
  {txs.map((tx, i) => (
57
+ <div key={i} className="asset-row px-5">
58
+ <div className={`w-8 h-8 rounded-full flex items-center justify-center text-caption-strong mr-3 ${tx.status === 'success' ? 'bg-semantic-up/10 text-semantic-up' : 'bg-semantic-down/10 text-semantic-down'}`}>
 
 
59
  {tx.status === 'success' ? '✓' : '✗'}
60
  </div>
61
  <div className="flex-1 min-w-0">
62
+ <div className="text-body-sm font-mono text-ink truncate">{tx.signature?.slice(0, 20)}</div>
63
+ <div className="text-caption text-muted">{tx.timestamp ? new Date(tx.timestamp).toLocaleString() : 'Pending'}</div>
64
  </div>
65
+ <span className={`badge-pill text-[10px] mr-3 ${tx.status === 'success' ? 'badge-pill-green' : 'badge-pill-red'}`}>{tx.status}</span>
66
+ <a href={`https://solscan.io/tx/${tx.signature}`} target="_blank" rel="noopener noreferrer" className="btn-text text-body-sm py-0 px-1">View →</a>
 
67
  </div>
68
  ))}
69
  </div>
src/renderer/pages/LockScreen.tsx CHANGED
@@ -1,132 +1,78 @@
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
  }
 
1
  import React, { useState, useRef, useEffect } from 'react';
 
2
 
3
+ interface Props { onUnlock: (pk: string) => void; }
4
 
5
+ export default function LockScreen({ onUnlock }: Props) {
6
  const [pin, setPin] = useState('');
7
  const [error, setError] = useState('');
8
  const [loading, setLoading] = useState(false);
9
+ const [bio, setBio] = useState(false);
10
+ const ref = useRef<HTMLInputElement>(null);
 
11
 
12
+ useEffect(() => { ref.current?.focus(); if (window.solvox) window.solvox.auth.biometricAvailable().then(b => { setBio(b); if (b) doBio(); }); }, []);
13
 
14
+ const doBio = async () => {
15
+ if (!window.solvox) return; setLoading(true);
 
 
 
 
 
 
 
 
16
  const r = await window.solvox.auth.biometric('Unlock SolVox');
17
  if (r.success) { const pk = await window.solvox.wallet.getPublicKey(); if (pk) onUnlock(pk); }
18
+ else setError(r.error || 'Failed');
19
  setLoading(false);
20
  };
21
 
22
+ const submit = async (e: React.FormEvent) => {
23
  e.preventDefault();
24
+ if (pin.length < 6) return setError('PIN must be at least 6 digits');
25
  setLoading(true); setError('');
26
  try {
27
  if (window.solvox) {
28
  const r = await window.solvox.auth.unlock(pin);
29
  if (r.success) { const pk = await window.solvox.wallet.getPublicKey(); if (pk) onUnlock(pk); }
30
+ else setError(r.remainingAttempts !== undefined ? `${r.error} (${r.remainingAttempts} left)` : r.error || 'Failed');
31
+ } else onUnlock('Dev' + Date.now());
32
+ } catch (e: any) { setError(e.message); }
33
  setLoading(false); setPin('');
34
  };
35
 
 
 
 
36
  const dots = Array.from({ length: 8 }, (_, i) => i < pin.length);
37
 
38
  return (
39
+ <div className="h-screen flex flex-col items-center justify-center bg-canvas">
40
+ <div className="w-full max-w-xs">
 
 
41
  {/* Logo */}
42
+ <div className="text-center mb-12">
43
+ <div className="w-12 h-12 mx-auto rounded-full bg-primary flex items-center justify-center mb-4">
44
+ <span className="text-on-primary font-bold text-title-md">SV</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  </div>
46
+ <h1 className="display-text text-display-sm text-ink mb-1">SolVox</h1>
47
+ <p className="text-body-sm text-muted">Voice-First AI Wallet</p>
48
  </div>
49
 
50
  {/* PIN Dots */}
51
+ <div className="flex justify-center gap-3 mb-6">
52
+ {dots.map((f, i) => (
53
+ <div key={i} className={`w-3 h-3 rounded-full transition-all duration-200 ${f ? 'bg-primary' : 'bg-surface-strong'}`} />
 
 
 
54
  ))}
55
  </div>
56
 
57
+ <form onSubmit={submit}>
58
+ <input ref={ref} type="password" inputMode="numeric" value={pin} onChange={e => setPin(e.target.value.replace(/\D/g, ''))}
59
+ maxLength={8} className="sr-only" disabled={loading} />
60
+ <div onClick={() => ref.current?.focus()} className="bg-surface-soft rounded-xl p-4 text-center cursor-text mb-4 border border-hairline hover:border-primary/30 transition-colors">
61
+ <div className="text-title-lg font-mono text-ink h-8 flex items-center justify-center tracking-[0.5em]">
62
+ {pin ? '●'.repeat(pin.length) : <span className="text-body-sm text-muted tracking-normal">Tap to enter PIN</span>}
 
 
 
 
 
63
  </div>
64
  </div>
65
+ {error && <div className="text-body-sm text-semantic-down text-center mb-4 bg-semantic-down/5 rounded-md p-2">{error}</div>}
66
+ <button type="submit" disabled={loading || pin.length < 6} className="btn-primary w-full disabled:opacity-40">{loading ? 'Unlocking…' : 'Unlock'}</button>
 
 
 
 
 
 
 
 
 
67
  </form>
68
 
69
+ {bio && <button onClick={doBio} className="btn-secondary w-full mt-3">Use Touch ID</button>}
 
 
 
 
 
70
 
71
+ <div className="mt-8 text-center text-caption text-muted-soft flex items-center justify-center gap-1.5">
72
+ <div className="w-1.5 h-1.5 rounded-full bg-semantic-up" />
73
+ Encrypted locally · No cloud · QVAC on-device
 
 
74
  </div>
75
  </div>
 
 
 
76
  </div>
77
  );
78
  }
src/renderer/pages/OnboardingScreen.tsx CHANGED
@@ -1,168 +1,122 @@
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');
12
  const [mnemonic, setMnemonic] = useState('');
13
- const [pin, setPin] = useState('');
14
- const [pinConfirm, setPinConfirm] = useState('');
15
- const [publicKey, setPublicKey] = useState('');
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
  )}
168
  </div>
 
1
  import React, { useState } from 'react';
2
+ import { StepIndicator } from '../components/ui/index';
3
 
4
+ interface Props { onComplete: (pk: string) => void; }
5
+ type Step = 'welcome' | 'choose' | 'import' | 'pin' | 'done';
6
+ const LABELS = ['Welcome', 'Wallet', 'Secure', 'Done'];
7
+ const idx = (s: Step) => s === 'welcome' ? 0 : s === 'choose' || s === 'import' ? 1 : s === 'pin' ? 2 : 3;
8
 
9
+ export default function OnboardingScreen({ onComplete }: Props) {
 
 
 
10
  const [step, setStep] = useState<Step>('welcome');
11
  const [mnemonic, setMnemonic] = useState('');
12
+ const [pin, setPin] = useState(''); const [pinC, setPinC] = useState('');
13
+ const [pk, setPk] = useState('');
14
+ const [err, setErr] = useState('');
15
+ const [busy, setBusy] = useState(false);
 
16
 
17
+ const create = async () => {
18
+ setBusy(true); setErr('');
19
  try {
20
+ if (window.solvox) { const r = await window.solvox.wallet.create(); if (r.success && r.publicKey) { setPk(r.publicKey); setStep('pin'); } else setErr(r.error || 'Failed'); }
21
+ else { setPk('Dev' + Date.now()); setStep('pin'); }
22
+ } catch (e: any) { setErr(e.message); }
23
+ setBusy(false);
 
 
 
24
  };
25
 
26
+ const imp = async () => {
27
+ if (!mnemonic.trim()) return setErr('Enter recovery phrase');
28
+ const w = mnemonic.trim().split(/\s+/); if (w.length !== 12 && w.length !== 24) return setErr('Must be 12 or 24 words');
29
+ setBusy(true); setErr('');
 
30
  try {
31
+ if (window.solvox) { const r = await window.solvox.wallet.import(mnemonic.trim()); if (r.success && r.publicKey) { setPk(r.publicKey); setStep('pin'); } else setErr(r.error || 'Invalid'); }
32
+ else { setPk('DevImp' + Date.now()); setStep('pin'); }
33
+ } catch (e: any) { setErr(e.message); }
34
+ setBusy(false);
 
 
 
35
  };
36
 
37
+ const setP = async () => {
38
+ if (pin.length < 6) return setErr('Min 6 digits');
39
+ if (pin !== pinC) return setErr('PINs don\'t match');
40
+ setBusy(true); setErr('');
41
+ try { if (window.solvox) { const r = await window.solvox.auth.setPin(pin); if (!r.success) { setErr(r.error || 'Failed'); setBusy(false); return; } } setStep('done'); }
42
+ catch (e: any) { setErr(e.message); }
43
+ setBusy(false);
 
 
44
  };
45
 
46
  return (
47
+ <div className="h-screen flex items-center justify-center bg-canvas">
48
+ <div className="w-full max-w-md px-6">
49
+ {step !== 'welcome' && <StepIndicator steps={LABELS} current={idx(step)} />}
 
50
 
51
  {step === 'welcome' && (
52
+ <div className="text-center page-enter">
53
+ <div className="w-14 h-14 mx-auto rounded-full bg-primary flex items-center justify-center mb-6">
54
+ <span className="text-on-primary font-bold text-title-lg">SV</span>
55
  </div>
56
+ <h1 className="display-text text-display-lg text-ink mb-3">SolVox</h1>
57
+ <p className="text-body-md text-body mb-2">Voice-First Private AI Wallet</p>
58
+ <p className="text-body-sm text-muted mb-10">Powered by QVAC SDK · 100% Local AI · Zero Cloud</p>
59
+ <div className="grid grid-cols-3 gap-3 mb-10">
60
+ {[{ t: 'Voice Control', d: 'Talk to your wallet' }, { t: 'Local AI', d: '6 QVAC modules' }, { t: 'Self-Custody', d: 'Your keys only' }].map(f => (
61
+ <div key={f.t} className="card text-center" style={{ padding: '20px' }}>
62
+ <div className="text-title-sm text-ink">{f.t}</div>
63
+ <div className="text-caption text-muted mt-1">{f.d}</div>
 
 
 
 
 
 
64
  </div>
65
  ))}
66
  </div>
67
+ <button onClick={() => setStep('choose')} className="btn-primary-lg">Get started</button>
 
 
 
68
  </div>
69
  )}
70
 
71
+ {step === 'choose' && (
72
+ <div className="space-y-4 page-enter">
73
+ <h2 className="display-text text-display-sm text-ink text-center mb-6">Set up your wallet</h2>
74
+ <button onClick={create} disabled={busy} className="w-full card text-left hover:border-primary/30 hover:shadow-soft transition-all">
75
+ <div className="text-title-md text-ink">Create new wallet</div>
76
+ <div className="text-body-sm text-body mt-1">Generate a fresh 24-word recovery phrase</div>
 
 
 
 
 
 
 
 
77
  </button>
78
+ <button onClick={() => setStep('import')} className="w-full card text-left hover:border-primary/30 hover:shadow-soft transition-all">
79
+ <div className="text-title-md text-ink">Import existing wallet</div>
80
+ <div className="text-body-sm text-body mt-1">Use a 12 or 24 word recovery phrase</div>
 
 
 
 
 
 
 
 
81
  </button>
82
+ {err && <div className="text-body-sm text-semantic-down text-center">{err}</div>}
83
  </div>
84
  )}
85
 
86
  {step === 'import' && (
87
+ <div className="space-y-5 page-enter">
88
+ <h2 className="display-text text-display-sm text-ink text-center">Import recovery phrase</h2>
89
+ <p className="text-body-sm text-muted text-center">Stays on your device — never sent anywhere.</p>
90
+ <textarea value={mnemonic} onChange={e => setMnemonic(e.target.value)} placeholder="word1 word2 word3 " rows={4} className="input-field font-mono text-body-sm resize-none" style={{ height: 'auto' }} />
91
+ {err && <div className="text-body-sm text-semantic-down">{err}</div>}
 
92
  <div className="flex gap-3">
93
+ <button onClick={() => { setStep('choose'); setErr(''); }} className="btn-secondary flex-1">Back</button>
94
+ <button onClick={imp} disabled={busy} className="btn-primary flex-1 disabled:opacity-50">{busy ? 'Importing…' : 'Import'}</button>
 
 
95
  </div>
96
  </div>
97
  )}
98
 
99
  {step === 'pin' && (
100
+ <div className="space-y-5 page-enter">
101
+ <h2 className="display-text text-display-sm text-ink text-center">Set your PIN</h2>
102
+ <p className="text-body-sm text-muted text-center">Encrypts your wallet with AES-256-GCM.</p>
103
+ <input type="password" inputMode="numeric" value={pin} onChange={e => setPin(e.target.value.replace(/\D/g, ''))} placeholder="Enter PIN (min 6)" maxLength={12} className="input-field text-center text-title-lg tracking-[0.5em] font-mono" />
104
+ <input type="password" inputMode="numeric" value={pinC} onChange={e => setPinC(e.target.value.replace(/\D/g, ''))} placeholder="Confirm PIN" maxLength={12} className="input-field text-center text-title-lg tracking-[0.5em] font-mono" />
105
+ {pin.length >= 6 && pin === pinC && <div className="text-center"><span className="badge-pill-green badge-pill">PINs match</span></div>}
106
+ {err && <div className="text-body-sm text-semantic-down text-center">{err}</div>}
107
+ <button onClick={setP} disabled={busy || pin.length < 6} className="btn-primary w-full disabled:opacity-40">{busy ? 'Encrypting…' : 'Continue'}</button>
 
 
 
 
108
  </div>
109
  )}
110
 
111
  {step === 'done' && (
112
+ <div className="text-center page-enter">
113
+ <div className="w-14 h-14 mx-auto rounded-full bg-semantic-up/10 flex items-center justify-center mb-6">
114
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#05b169" strokeWidth="2.5" strokeLinecap="round"><polyline points="20 6 9 17 4 12"/></svg>
115
  </div>
116
+ <h2 className="display-text text-display-sm text-ink mb-2">You're all set</h2>
117
+ <p className="text-body-md text-body mb-4">Wallet encrypted & secured. AI runs 100% locally.</p>
118
+ <div className="bg-surface-soft rounded-xl p-3 font-mono text-caption text-muted break-all mb-6">{pk}</div>
119
+ <button onClick={() => onComplete(pk)} className="btn-primary-lg">Open SolVox</button>
 
 
120
  </div>
121
  )}
122
  </div>
src/renderer/pages/SecurityPage.tsx CHANGED
@@ -1,164 +1,105 @@
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>
132
  )}
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>
 
1
  import React, { useState, useEffect } from 'react';
2
+ import { useToast } from '../components/ui/index';
3
 
4
  export default function SecurityPage() {
5
+ const [s, setS] = useState<any>({});
6
+ const [wl, setWl] = useState<any[]>([]);
7
  const [anomalies, setAnomalies] = useState<any[]>([]);
8
+ const [addr, setAddr] = useState(''); const [label, setLabel] = useState('');
9
+ const [err, setErr] = useState('');
 
10
  const { addToast } = useToast();
11
 
12
  useEffect(() => { load(); }, []);
13
+ const load = async () => { if (!window.solvox) return; try { const [a, b, c] = await Promise.all([window.solvox.security.getSettings(), window.solvox.security.getWhitelist(), window.solvox.security.getAnomalies()]); setS(a); setWl(b); setAnomalies(c); } catch {} };
14
+ const upd = async (k: string, v: any) => { const n = { ...s, [k]: v }; setS(n); if (window.solvox) { await window.solvox.security.updateSettings(n); addToast({ type: 'success', title: 'Saved' }); } };
15
+ const addWl = async () => { if (!addr.trim() || !label.trim()) return setErr('Both required'); if (window.solvox) { const r = await window.solvox.security.addWhitelist(addr.trim(), label.trim()); if (r.success) { setAddr(''); setLabel(''); setErr(''); load(); } else setErr(r.error || 'Failed'); } };
16
+ const rmWl = async (a: string) => { if (window.solvox) { await window.solvox.security.removeWhitelist(a); load(); } };
17
 
18
+ const Toggle = ({ k, label, desc }: { k: string; label: string; desc: string }) => (
19
+ <div className="flex items-center justify-between py-3 border-b border-hairline-soft last:border-0">
20
+ <div><div className="text-title-sm text-ink">{label}</div><div className="text-caption text-muted">{desc}</div></div>
21
+ <button onClick={() => upd(k, !s[k])} className="toggle">
22
+ <div className={`toggle-track ${s[k] ? 'active' : ''}`} />
23
+ <div className={`toggle-thumb ${s[k] ? 'active' : ''}`} />
24
+ </button>
25
+ </div>
26
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  return (
29
+ <div className="max-w-3xl mx-auto px-8 py-section">
30
+ <h2 className="display-text text-title-lg text-ink mb-8">Security</h2>
31
 
32
+ {/* Limits */}
33
+ <div className="card mb-6">
34
+ <div className="text-caption-strong text-muted uppercase tracking-wider mb-4">Transaction limits</div>
35
+ <div className="grid grid-cols-2 gap-4">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  {[
37
+ { k: 'maxSingleTx', l: 'Max per transaction', u: 'tokens', v: s.maxSingleTx || 1000 },
38
+ { k: 'maxDailyVolume', l: 'Daily limit', u: '/day', v: s.maxDailyVolume || 5000 },
39
+ { k: 'velocityLimit', l: 'Max TX/hour', u: '', v: s.velocityLimit || 10 },
40
+ { k: 'cooldownMinutes', l: 'Cooldown', u: 'min', v: s.cooldownMinutes || 1 },
41
+ ].map(f => (
42
+ <div key={f.k}>
43
+ <label className="text-caption text-muted block mb-1">{f.l}</label>
44
+ <div className="flex items-center gap-2">
45
+ <input type="number" value={f.v} onChange={e => upd(f.k, Number(e.target.value))} className="input-field text-body-sm" />
46
+ {f.u && <span className="text-caption text-muted whitespace-nowrap">{f.u}</span>}
 
47
  </div>
 
 
 
 
48
  </div>
49
  ))}
50
  </div>
51
  </div>
52
 
53
+ {/* Toggles */}
54
+ <div className="card mb-6">
55
+ <div className="text-caption-strong text-muted uppercase tracking-wider mb-4">Features</div>
56
+ <Toggle k="whitelistEnabled" label="Address whitelisting" desc="Only allow sends to approved addresses" />
57
+ <Toggle k="anomalyDetection" label="AI anomaly detection" desc="LLM analyzes spending patterns locally" />
58
+ <Toggle k="requireConfirmation" label="Transaction confirmation" desc="Always require explicit approval" />
59
+ </div>
60
+
61
  {/* Whitelist */}
62
+ <div className="card mb-6">
63
+ <div className="text-caption-strong text-muted uppercase tracking-wider mb-3">Address whitelist</div>
64
  <div className="flex gap-2 mb-3">
65
+ <input value={label} onChange={e => setLabel(e.target.value)} placeholder="Label" className="input-field text-body-sm w-28" />
66
+ <input value={addr} onChange={e => setAddr(e.target.value)} placeholder="Solana address" className="input-field text-body-sm flex-1 font-mono" />
67
+ <button onClick={addWl} className="btn-primary text-body-sm py-2 px-4">Add</button>
68
  </div>
69
+ {err && <div className="text-caption text-semantic-down mb-2">{err}</div>}
70
+ {wl.length === 0 ? <div className="text-body-sm text-muted text-center py-4">No addresses whitelisted</div> : (
 
 
71
  <div className="space-y-2">
72
+ {wl.map((e, i) => (
73
+ <div key={i} className="flex items-center justify-between bg-surface-soft rounded-lg p-3">
74
+ <div><div className="text-title-sm text-ink">{e.label}</div><div className="text-caption font-mono text-muted">{e.address.slice(0, 10)}…{e.address.slice(-6)}</div></div>
75
+ <button onClick={() => rmWl(e.address)} className="btn-text text-semantic-down text-body-sm">Remove</button>
 
 
 
76
  </div>
77
  ))}
78
  </div>
79
  )}
80
  </div>
81
 
82
+ {/* Anomalies */}
83
+ <div className="card">
84
  <div className="flex items-center justify-between mb-3">
85
+ <div className="text-caption-strong text-muted uppercase tracking-wider">Anomaly log</div>
86
+ <span className="badge-pill-blue badge-pill text-[10px]">AI-POWERED</span>
87
  </div>
88
  {anomalies.length === 0 ? (
89
+ <div className="text-center py-6">
90
+ <div className="text-body-sm text-semantic-up">All clear no anomalies detected ✓</div>
91
+ <div className="text-caption text-muted mt-1">Analyzed by @qvac/llm-llamacpp + @qvac/embed-llamacpp</div>
 
 
92
  </div>
93
  ) : (
94
+ <div className="space-y-2">
95
  {anomalies.map((a, i) => (
96
+ <div key={i} className={`rounded-lg p-3 border ${a.severity === 'high' ? 'bg-semantic-down/5 border-semantic-down/15' : a.severity === 'medium' ? 'bg-accent-yellow/5 border-accent-yellow/15' : 'bg-surface-soft border-hairline'}`}>
 
 
97
  <div className="flex items-center gap-2 mb-1">
98
+ <span className={`badge-pill text-[10px] ${a.severity === 'high' ? 'badge-pill-red' : ''}`}>{a.severity.toUpperCase()}</span>
99
+ <span className="text-caption text-muted">{a.type}</span>
 
 
100
  </div>
101
+ <div className="text-body-sm text-ink">{a.description}</div>
102
+ <div className="text-caption text-muted-soft mt-1">{new Date(a.timestamp).toLocaleString()}</div>
103
  </div>
104
  ))}
105
  </div>
src/renderer/pages/SendPage.tsx CHANGED
@@ -1,28 +1,35 @@
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
 
@@ -30,139 +37,101 @@ export default function SendPage({ balance, onSent }: SendPageProps) {
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
  ))}
73
  </div>
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>
 
1
  import React, { useState } from 'react';
2
+ import { Num, useToast } from '../components/ui/index';
3
 
4
+ interface Props { balance: { sol: number; usdt: number }; onSent: () => void; }
5
 
6
+ export default function SendPage({ balance, onSent }: Props) {
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<any>(null);
13
+ const [step, setStep] = useState<'form' | 'confirm' | 'done'>('form');
14
+ const [risk, setRisk] = useState<any>(null);
15
  const { addToast } = useToast();
16
 
17
  const max = token === 'SOL' ? balance.sol : balance.usdt;
18
+ const amt = parseFloat(amount) || 0;
 
19
 
20
+ const review = async () => {
21
  setError('');
22
+ if (!to.trim()) return setError('Recipient address required');
23
  if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(to.trim())) return setError('Invalid Solana address');
24
+ if (!amt || amt <= 0) return setError('Enter a valid amount');
25
+ if (amt > max) return setError(`Insufficient balance. Max: ${max.toFixed(token === 'SOL' ? 4 : 2)} ${token}`);
26
+ // AI risk assessment before showing confirmation
27
+ if (window.solvox) {
28
+ try {
29
+ const r = await window.solvox.ai.assessRisk({ amount: amt, token, to: to.trim() });
30
+ if (r.success) setRisk(r.risk);
31
+ } catch {}
32
+ }
33
  setStep('confirm');
34
  };
35
 
 
37
  setLoading(true); setError('');
38
  try {
39
  const r = window.solvox
40
+ ? await window.solvox.ai.executeConfirmed({ token, amount: amt, to: to.trim() })
41
+ : { success: true, signature: 'dev_' + Date.now(), explorer: '#' };
42
  if (r.success) {
43
+ setSuccess(r); setStep('done'); onSent();
44
+ addToast({ type: 'success', title: `${amount} ${token} sent` });
 
 
45
  } else { setError(r.error || 'Transaction failed'); setStep('form'); }
46
  } catch (e: any) { setError(e.message); setStep('form'); }
47
  setLoading(false);
48
  };
49
 
50
+ const reset = () => { setTo(''); setAmount(''); setError(''); setSuccess(null); setRisk(null); setStep('form'); };
51
 
52
  return (
53
+ <div className="max-w-md mx-auto px-8 py-section">
54
+ <h2 className="text-title-lg display-text text-ink mb-2">Send {token}</h2>
55
+ <p className="text-body-sm text-body mb-8">Balance: <span className="number-mono"><Num value={max} decimals={token === 'SOL' ? 4 : 2} /></span> {token}</p>
 
 
 
 
 
 
 
 
56
 
57
  {step === 'form' && (
58
+ <div className="card space-y-5">
59
+ {/* Token */}
60
+ <div className="flex gap-2 p-1 bg-surface-soft rounded-pill">
61
  {(['SOL', 'USDT'] as const).map(t => (
62
+ <button key={t} onClick={() => setToken(t)} className={`flex-1 py-2 rounded-pill text-button transition-colors ${token === t ? 'bg-primary text-on-primary' : 'text-body hover:text-ink'}`}>
 
 
 
 
63
  {t === 'SOL' ? '◎' : '₮'} {t}
64
  </button>
65
  ))}
66
  </div>
 
67
  {/* Recipient */}
68
  <div>
69
+ <label className="text-caption-strong text-muted uppercase tracking-wider block mb-1.5">Recipient</label>
70
+ <input value={to} onChange={e => setTo(e.target.value)} placeholder="Solana address" className="input-field font-mono text-body-sm" />
 
71
  </div>
 
72
  {/* Amount */}
73
  <div>
74
+ <div className="flex justify-between items-center mb-1.5">
75
+ <label className="text-caption-strong text-muted uppercase tracking-wider">Amount</label>
76
+ <button onClick={() => setAmount(max.toString())} className="btn-text text-body-sm py-0 px-1">Max</button>
 
 
 
 
 
 
 
77
  </div>
78
+ <input type="number" value={amount} onChange={e => setAmount(e.target.value)} placeholder="0.00" step="any" className="input-field number-mono text-title-lg text-center" />
 
 
 
 
 
79
  <div className="flex gap-2 mt-2">
80
  {[25, 50, 75, 100].map(p => (
81
+ <button key={p} onClick={() => setAmount((max * p / 100).toFixed(token === 'SOL' ? 4 : 2))} className="flex-1 py-1.5 rounded-pill text-caption-strong text-muted bg-surface-soft hover:bg-surface-strong transition-colors">{p}%</button>
 
 
 
82
  ))}
83
  </div>
84
  </div>
85
+ {error && <div className="text-body-sm text-semantic-down bg-semantic-down/5 rounded-md p-3">{error}</div>}
86
+ <button onClick={review} className="btn-primary w-full">Review transaction</button>
 
 
 
 
87
  </div>
88
  )}
89
 
90
  {step === 'confirm' && (
91
+ <div className="card space-y-5 page-enter">
92
  <div className="text-center mb-2">
93
+ <div className="text-body text-muted">Confirm transaction</div>
94
+ <div className="display-text text-display-sm text-ink mt-1">{amount} {token}</div>
95
  </div>
96
+ <div className="bg-surface-soft rounded-xl p-4 space-y-3 text-body-sm">
97
+ <div className="flex justify-between"><span className="text-muted">To</span><span className="font-mono text-caption">{to.slice(0, 10)}…{to.slice(-6)}</span></div>
98
+ <div className="hairline-soft" />
99
+ <div className="flex justify-between"><span className="text-muted">Fee</span><span className="number-mono text-caption">~0.000005 SOL</span></div>
100
+ <div className="flex justify-between"><span className="text-muted">Network</span><span className="badge-pill text-[10px]">DEVNET</span></div>
 
101
  </div>
102
+ {/* AI Risk Assessment */}
103
+ {risk && (
104
+ <div className={`rounded-xl p-4 text-body-sm ${risk.level === 'safe' ? 'bg-semantic-up/5' : risk.level === 'danger' ? 'bg-semantic-down/5' : 'bg-accent-yellow/5'}`}>
105
+ <div className="flex items-center gap-2 mb-1">
106
+ <span className={`badge-pill text-[10px] ${risk.level === 'safe' ? 'badge-pill-green' : 'badge-pill-red'}`}>AI RISK: {risk.level.toUpperCase()}</span>
107
+ <span className="number-mono text-caption text-muted">{risk.score}/100</span>
108
+ </div>
109
+ {risk.factors?.length > 0 && <ul className="text-caption text-body mt-1 space-y-0.5">{risk.factors.map((f: string, i: number) => <li key={i}>· {f}</li>)}</ul>}
110
+ {risk.recommendation && <div className="text-caption text-muted mt-1">{risk.recommendation}</div>}
111
+ <div className="text-caption text-muted-soft mt-1 flex items-center gap-1">
112
+ <div className="w-1 h-1 rounded-full bg-primary" />
113
+ Analyzed by @qvac/llm-llamacpp + @qvac/embed-llamacpp
114
+ </div>
115
  </div>
116
+ )}
117
+ {error && <div className="text-body-sm text-semantic-down">{error}</div>}
 
 
118
  <div className="flex gap-3">
119
+ <button onClick={() => setStep('form')} className="btn-secondary flex-1">Cancel</button>
120
+ <button onClick={send} disabled={loading} className="btn-primary flex-1 disabled:opacity-50">{loading ? 'Sending…' : `Send ${token}`}</button>
 
 
121
  </div>
122
  </div>
123
  )}
124
 
125
+ {step === 'done' && success && (
126
+ <div className="card text-center space-y-5 page-enter">
127
+ <div className="w-14 h-14 mx-auto rounded-full bg-semantic-up/10 flex items-center justify-center">
128
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#05b169" strokeWidth="2.5" strokeLinecap="round"><polyline points="20 6 9 17 4 12"/></svg>
 
 
 
 
129
  </div>
130
+ <div className="display-text text-display-sm text-ink">Sent</div>
131
+ <p className="text-body text-muted">{amount} {token} sent successfully</p>
132
+ <div className="bg-surface-soft rounded-xl p-3 font-mono text-caption text-muted break-all">{success.signature}</div>
133
+ <a href={success.explorer} target="_blank" rel="noopener noreferrer" className="btn-text text-body-sm">View on Solscan →</a>
134
+ <button onClick={reset} className="btn-primary w-full">Send another</button>
135
  </div>
136
  )}
137
  </div>
src/renderer/pages/SettingsPage.tsx CHANGED
@@ -1,104 +1,87 @@
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
  );
 
1
  import React, { useState } from 'react';
2
 
3
+ interface Props { onLock: () => void; }
4
 
5
+ const models = [
6
+ { name: 'LLM', model: 'Llama 3.2 3B Instruct', q: 'Q4_K_M', size: '2.0 GB', pkg: 'llm-llamacpp' },
7
+ { name: 'Embeddings', model: 'Nomic Embed Text v1.5', q: 'Q4_K_M', size: '260 MB', pkg: 'embed-llamacpp' },
8
+ { name: 'Speech-to-Text', model: 'Whisper Base.en', q: 'GGML', size: '150 MB', pkg: 'transcription-whispercpp' },
9
+ { name: 'Text-to-Speech', model: 'Piper Amy', q: 'ONNX', size: '75 MB', pkg: 'tts-onnx' },
10
+ { name: 'Translation', model: 'OPUS MT (EN↔ES)', q: 'NMT', size: '50 MB', pkg: 'translation-nmtcpp' },
11
+ { name: 'OCR', model: 'PaddleOCR v4', q: 'ONNX', size: '30 MB', pkg: 'ocr-onnx' },
12
+ ];
13
 
14
+ export default function SettingsPage({ onLock }: Props) {
15
+ const [network, setNetwork] = useState('devnet');
 
 
 
 
 
 
16
 
17
  return (
18
+ <div className="max-w-2xl mx-auto px-8 py-section">
19
+ <h2 className="display-text text-title-lg text-ink mb-8">Settings</h2>
20
 
21
  {/* Network */}
22
+ <div className="card mb-6">
23
+ <div className="text-caption-strong text-muted uppercase tracking-wider mb-3">Network</div>
24
+ <div className="flex gap-2 p-1 bg-surface-soft rounded-pill">
25
+ {[{ id: 'devnet', l: 'Devnet' }, { id: 'mainnet-beta', l: 'Mainnet' }, { id: 'testnet', l: 'Testnet' }].map(n => (
26
+ <button key={n.id} onClick={() => setNetwork(n.id)} className={`flex-1 py-2 rounded-pill text-button transition-colors ${network === n.id ? 'bg-primary text-on-primary' : 'text-body hover:text-ink'}`}>{n.l}</button>
 
 
 
 
 
 
 
 
 
 
27
  ))}
28
  </div>
29
+ <p className="text-caption text-muted mt-2">
30
+ {network === 'devnet' && 'Test tokens only. No real value.'}
31
+ {network === 'mainnet-beta' && 'Real tokens. Transactions irreversible.'}
32
+ {network === 'testnet' && 'Development purposes only.'}
33
  </p>
34
  </div>
35
 
36
+ {/* AI Models — Coinbase pricing-tier-card pattern */}
37
+ <div className="card mb-6">
38
  <div className="flex items-center justify-between mb-4">
39
+ <div className="text-caption-strong text-muted uppercase tracking-wider">QVAC AI Models</div>
40
+ <span className="badge-pill text-[10px]">6 MODULES · ~2.6 GB</span>
41
  </div>
42
+ <div className="space-y-0">
43
  {models.map(m => (
44
+ <div key={m.name} className="flex items-center justify-between py-3 border-b border-hairline-soft last:border-0">
45
+ <div>
46
+ <div className="text-title-sm text-ink">{m.name}</div>
47
+ <div className="text-caption text-muted">{m.model}</div>
48
+ <div className="text-caption text-muted-soft font-mono">@qvac/{m.pkg}</div>
49
+ </div>
50
+ <div className="text-right">
51
+ <span className="badge-pill text-[10px] mr-2">{m.q}</span>
52
+ <span className="number-mono text-caption text-muted">{m.size}</span>
53
  </div>
 
54
  </div>
55
  ))}
56
  </div>
57
+ <div className="hairline-soft mt-3 mb-3" />
58
+ <div className="text-caption text-muted flex items-center gap-1.5">
59
+ <div className="w-1.5 h-1.5 rounded-full bg-semantic-up" />
60
  All models run locally via QVAC Fabric (Vulkan GPU) — no CUDA required
61
  </div>
62
  </div>
63
 
64
  {/* About */}
65
+ <div className="card mb-6">
66
+ <div className="text-caption-strong text-muted uppercase tracking-wider mb-3">About</div>
67
+ <div className="text-body-sm text-body space-y-2">
68
+ <p><strong className="text-ink">SolVox</strong> — voice-first, privacy-preserving AI wallet for Solana.</p>
69
+ <p>Powered by <strong className="text-primary">Tether QVAC SDK</strong> — local AI that never phones home.</p>
 
 
 
 
 
70
  </div>
71
+ <div className="flex flex-wrap gap-1.5 mt-3">
72
+ {['Electron', 'React', 'TypeScript', 'QVAC SDK', 'Solana', 'Vulkan'].map(t => (
73
+ <span key={t} className="badge-pill text-[10px]">{t}</span>
74
+ ))}
75
  </div>
76
+ <div className="hairline-soft my-3" />
77
+ <div className="text-caption text-muted-soft">Colosseum Frontier Hackathon · Tether QVAC Track</div>
78
  </div>
79
 
80
+ {/* Lock */}
81
+ <div className="card border-semantic-down/20" style={{ borderColor: 'rgba(207, 32, 47, 0.15)' }}>
82
+ <div className="text-caption-strong text-semantic-down uppercase tracking-wider mb-3">Danger zone</div>
83
+ <button onClick={onLock} className="w-full py-3 rounded-pill border border-semantic-down/30 text-semantic-down text-button hover:bg-semantic-down/5 transition-colors">Lock wallet</button>
84
+ <p className="text-caption text-muted text-center mt-2">Zeroes private key from memory. PIN required to unlock.</p>
 
 
 
 
85
  </div>
86
  </div>
87
  );
src/renderer/pages/VoicePage.tsx CHANGED
@@ -1,237 +1,151 @@
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>
237
  </div>
 
1
  import React, { useState, useRef, useEffect } from 'react';
2
+ import { PipelineTrace } from '../components/ui/index';
3
 
4
+ interface Props { aiStatus: any; }
5
+ interface Msg { id: string; role: 'user' | 'assistant' | 'system'; text: string; pipeline?: any[]; actions?: any[]; ts: Date; }
6
 
7
+ const SUGGESTIONS = ['What is my balance?', 'Send 5 SOL to alice.sol', 'Show recent transactions', 'Help me understand gas fees'];
 
 
 
 
 
 
 
8
 
9
+ export default function VoicePage({ aiStatus }: Props) {
10
+ const [msgs, setMsgs] = useState<Msg[]>([
11
+ { id: '0', role: 'assistant', text: "I'm your SolVox AI assistant — running 100% locally via 6 QVAC modules. Ask me to send tokens, check balances, search transactions, or explain anything about Solana.", pipeline: [], actions: [], ts: new Date() },
12
  ]);
13
  const [input, setInput] = useState('');
14
+ const [recording, setRecording] = useState(false);
15
+ const [processing, setProcessing] = useState(false);
16
+ const [wave, setWave] = useState<number[]>(new Array(40).fill(1));
17
+ const recRef = useRef<MediaRecorder | null>(null);
18
+ const chunks = useRef<Blob[]>([]);
19
+ const endRef = useRef<HTMLDivElement>(null);
20
+ const analyser = useRef<AnalyserNode | null>(null);
21
+ const raf = useRef<number | null>(null);
22
+
23
+ useEffect(() => { endRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [msgs]);
24
+
25
+ const startRec = async () => {
 
 
26
  try {
27
  const stream = await navigator.mediaDevices.getUserMedia({ audio: { sampleRate: 16000, channelCount: 1, echoCancellation: true, noiseSuppression: true } });
28
+ const ctx = new AudioContext(); const src = ctx.createMediaStreamSource(stream); const a = ctx.createAnalyser(); a.fftSize = 128; src.connect(a); analyser.current = a;
29
+ const tick = () => { if (!analyser.current) return; const d = new Uint8Array(analyser.current.frequencyBinCount); analyser.current.getByteFrequencyData(d); setWave(Array.from(d).slice(0, 40).map(v => Math.max(1, v / 8))); raf.current = requestAnimationFrame(tick); }; tick();
30
+ const rec = new MediaRecorder(stream, { mimeType: 'audio/webm' }); chunks.current = [];
31
+ rec.ondataavailable = e => { if (e.data.size > 0) chunks.current.push(e.data); };
32
+ rec.onstop = async () => { stream.getTracks().forEach(t => t.stop()); if (raf.current) cancelAnimationFrame(raf.current); setWave(new Array(40).fill(1)); const b = new Blob(chunks.current, { type: 'audio/webm' }); processVoice(await b.arrayBuffer()); };
33
+ recRef.current = rec; rec.start(); setRecording(true);
34
+ } catch { add('system', 'Microphone access denied.'); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  };
36
 
37
+ const stopRec = () => { if (recRef.current && recording) { recRef.current.stop(); setRecording(false); } };
 
 
38
 
39
  const processVoice = async (data: ArrayBuffer) => {
40
+ setProcessing(true);
41
  try {
42
  if (window.solvox) {
43
  const r = await window.solvox.ai.processVoice(data);
44
  if (r.success) {
45
+ add('user', r.transcription || '[voice]');
46
+ add('assistant', r.agentResult?.response || 'Done.', r.pipelineSteps, r.agentResult?.actions);
47
+ if (r.responseAudio) { const u = URL.createObjectURL(new Blob([r.responseAudio], { type: 'audio/wav' })); new Audio(u).play().catch(() => {}); }
48
+ } else add('system', r.error || 'Voice processing failed');
49
+ } else { add('user', '[voice — dev]'); add('assistant', 'QVAC models needed. Type commands instead.'); }
50
+ } catch (e: any) { add('system', e.message); }
51
+ setProcessing(false);
52
  };
53
 
54
  const handleSend = async () => {
55
+ if (!input.trim()) return; const text = input.trim(); setInput(''); add('user', text); setProcessing(true);
 
 
 
 
56
  try {
57
  if (window.solvox) {
58
+ const r = await window.solvox.ai.chat(text);
59
+ if (r.success) add('assistant', r.response, r.pipelineSteps, r.actions);
60
+ else add('assistant', r.error || 'Could not process.');
61
+ } else add('assistant', `[Dev] "${text}" needs QVAC models.`);
62
+ } catch (e: any) { add('system', e.message); }
63
+ setProcessing(false);
 
 
 
64
  };
65
 
66
+ const add = (role: Msg['role'], text: string, pipeline?: any[], actions?: any[]) => {
67
+ setMsgs(p => [...p, { id: Date.now().toString(36), role, text, pipeline, actions, ts: new Date() }]);
68
  };
69
 
70
+ const mods = aiStatus ? [aiStatus.llm, aiStatus.transcription, aiStatus.tts, aiStatus.embed, aiStatus.translation, aiStatus.ocr].filter(Boolean).length : 0;
71
 
72
  return (
73
+ <div className="flex flex-col h-full max-w-content mx-auto px-8 py-6">
74
  {/* Header */}
75
  <div className="flex items-center justify-between mb-4">
76
  <div>
77
+ <h2 className="text-title-lg display-text text-ink">Voice AI Assistant</h2>
78
+ <p className="text-body-sm text-body">Powered by 6 QVAC packages · All inference local</p>
 
 
 
 
 
 
79
  </div>
80
+ <span className={`badge-pill ${mods >= 4 ? 'badge-pill-green' : mods > 0 ? 'badge-pill-blue' : ''}`}>
81
+ {mods >= 4 ? 'AI ONLINE' : mods > 0 ? 'PARTIAL' : 'LOADING'}
82
+ </span>
83
  </div>
84
 
85
+ {/* Chat */}
86
+ <div className="flex-1 overflow-y-auto card mb-4 space-y-4" style={{ padding: '24px' }}>
87
+ {msgs.map(m => (
88
+ <div key={m.id} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'} page-enter`}>
89
+ <div className={`max-w-[75%] ${
90
+ m.role === 'user' ? 'bg-primary text-on-primary rounded-xl rounded-br-sm px-4 py-3' :
91
+ m.role === 'system' ? 'bg-surface-soft text-muted border border-hairline rounded-xl px-4 py-3' :
92
+ 'bg-surface-soft text-ink rounded-xl rounded-bl-sm px-4 py-3'
93
  }`}>
94
+ {m.role === 'assistant' && <div className="text-caption-strong text-primary mb-1">SolVox AI</div>}
95
+ <p className="text-body-sm whitespace-pre-wrap">{m.text}</p>
96
+ {m.actions && m.actions.length > 0 && (
97
+ <div className="flex flex-wrap gap-1 mt-2">
98
+ {m.actions.map((a: any, i: number) => (
99
+ <span key={i} className="badge-pill-blue badge-pill text-[10px]">{a.tool}</span>
100
+ ))}
 
 
 
 
 
 
 
 
 
 
101
  </div>
102
  )}
103
+ {m.pipeline && m.pipeline.length > 0 && <PipelineTrace steps={m.pipeline} />}
104
+ <div className="text-caption text-muted-soft mt-1.5">{m.ts.toLocaleTimeString()}</div>
105
  </div>
106
  </div>
107
  ))}
108
+ {processing && (
109
  <div className="flex justify-start">
110
+ <div className="bg-surface-soft rounded-xl px-4 py-3 flex items-center gap-2">
111
+ <div className="w-2 h-2 rounded-full bg-primary animate-pulse" />
112
+ <span className="text-body-sm text-muted">Processing locally</span>
113
  </div>
114
  </div>
115
  )}
116
+ <div ref={endRef} />
117
  </div>
118
 
119
+ {/* Suggestions */}
120
+ {msgs.length <= 2 && !recording && (
121
+ <div className="flex flex-wrap gap-2 mb-3">
122
+ {SUGGESTIONS.map(s => (
123
+ <button key={s} onClick={() => setInput(s)} className="btn-secondary text-body-sm py-1.5 px-3">{s}</button>
 
 
 
124
  ))}
125
  </div>
126
  )}
127
 
128
+ {/* Waveform */}
129
+ {recording && (
130
+ <div className="bg-surface-soft rounded-xl p-3 mb-3 flex items-center justify-center gap-px h-12">
131
+ {wave.map((h, i) => (
132
+ <div key={i} className="w-[3px] rounded-full bg-primary transition-all duration-75" style={{ height: `${h}px`, opacity: 0.3 + h / 40 }} />
133
+ ))}
 
 
 
 
 
 
 
 
 
134
  </div>
135
  )}
136
 
137
+ {/* Input */}
138
  <div className="flex items-center gap-3">
139
+ <button onMouseDown={startRec} onMouseUp={stopRec} onMouseLeave={stopRec} onTouchStart={startRec} onTouchEnd={stopRec} disabled={processing}
140
+ className={`w-11 h-11 rounded-pill flex items-center justify-center transition-colors ${recording ? 'bg-semantic-down' : 'bg-primary'} disabled:opacity-40`}>
141
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="white">
142
+ {recording ? <rect x="6" y="6" width="12" height="12" rx="2" /> : <><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"/></>}
 
 
 
 
 
 
 
 
 
 
143
  </svg>
144
  </button>
 
145
  <div className="flex-1 relative">
146
+ <input value={input} onChange={e => setInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleSend()}
147
+ placeholder={recording ? 'Listening…' : 'Type a command or hold mic…'} className="input-field pr-20" disabled={processing || recording} />
148
+ <button onClick={handleSend} disabled={!input.trim() || processing} className="absolute right-1.5 top-1/2 -translate-y-1/2 btn-text text-body-sm disabled:opacity-20">Send</button>
 
 
 
 
 
149
  </div>
150
  </div>
151
  </div>
tailwind.config.js CHANGED
@@ -1,87 +1,86 @@
1
  /** @type {import('tailwindcss').Config} */
2
  module.exports = {
3
  content: ['./src/renderer/**/*.{ts,tsx,html}'],
4
- darkMode: 'class',
5
  theme: {
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
  },
 
1
  /** @type {import('tailwindcss').Config} */
2
  module.exports = {
3
  content: ['./src/renderer/**/*.{ts,tsx,html}'],
 
4
  theme: {
5
  extend: {
6
  colors: {
7
+ // Brand & Accent
8
+ primary: '#0052ff',
9
+ 'primary-active': '#003ecc',
10
+ 'primary-disabled': '#a8b8cc',
11
+ 'accent-yellow': '#f4b000',
12
+ // Surfaces
13
+ canvas: '#ffffff',
14
+ 'surface-soft': '#f7f7f7',
15
+ 'surface-strong': '#eef0f3',
16
+ 'surface-dark': '#0a0b0d',
17
+ 'surface-dark-elevated': '#16181c',
18
+ // Hairlines
19
+ hairline: '#dee1e6',
20
+ 'hairline-soft': '#eef0f3',
21
+ // Text
22
+ ink: '#0a0b0d',
23
+ body: '#5b616e',
24
+ 'body-strong': '#0a0b0d',
25
+ muted: '#7c828a',
26
+ 'muted-soft': '#a8acb3',
27
+ 'on-primary': '#ffffff',
28
+ 'on-dark': '#ffffff',
29
+ 'on-dark-soft': '#a8acb3',
30
+ // Trading
31
+ 'semantic-up': '#05b169',
32
+ 'semantic-down': '#cf202f',
33
  },
34
  fontFamily: {
35
+ display: ['Inter', '-apple-system', 'system-ui', 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', 'sans-serif'],
36
+ sans: ['Inter', '-apple-system', 'system-ui', 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', 'sans-serif'],
37
+ mono: ['JetBrains Mono', 'Geist Mono', 'monospace'],
38
+ },
39
+ fontSize: {
40
+ 'display-mega': ['80px', { lineHeight: '1.0', letterSpacing: '-2px', fontWeight: '400' }],
41
+ 'display-xl': ['64px', { lineHeight: '1.0', letterSpacing: '-1.6px', fontWeight: '400' }],
42
+ 'display-lg': ['52px', { lineHeight: '1.0', letterSpacing: '-1.3px', fontWeight: '400' }],
43
+ 'display-md': ['44px', { lineHeight: '1.09', letterSpacing: '-1px', fontWeight: '400' }],
44
+ 'display-sm': ['36px', { lineHeight: '1.11', letterSpacing: '-0.5px', fontWeight: '400' }],
45
+ 'title-lg': ['32px', { lineHeight: '1.13', letterSpacing: '-0.4px', fontWeight: '400' }],
46
+ 'title-md': ['18px', { lineHeight: '1.33', letterSpacing: '0', fontWeight: '600' }],
47
+ 'title-sm': ['16px', { lineHeight: '1.25', letterSpacing: '0', fontWeight: '600' }],
48
+ 'body-md': ['16px', { lineHeight: '1.5', letterSpacing: '0', fontWeight: '400' }],
49
+ 'body-strong': ['16px', { lineHeight: '1.5', letterSpacing: '0', fontWeight: '700' }],
50
+ 'body-sm': ['14px', { lineHeight: '1.5', letterSpacing: '0', fontWeight: '400' }],
51
+ 'caption': ['13px', { lineHeight: '1.5', letterSpacing: '0', fontWeight: '400' }],
52
+ 'caption-strong': ['12px', { lineHeight: '1.5', letterSpacing: '0', fontWeight: '600' }],
53
+ 'number-display': ['18px', { lineHeight: '1.4', letterSpacing: '0', fontWeight: '500' }],
54
+ 'button': ['16px', { lineHeight: '1.15', letterSpacing: '0', fontWeight: '600' }],
55
+ 'nav-link': ['14px', { lineHeight: '1.4', letterSpacing: '0', fontWeight: '500' }],
56
+ },
57
+ spacing: {
58
+ 'xxs': '4px',
59
+ 'xs': '8px',
60
+ 'sm': '12px',
61
+ 'base': '16px',
62
+ 'md': '20px',
63
+ 'lg': '24px',
64
+ 'xl': '32px',
65
+ 'xxl': '48px',
66
+ 'section': '96px',
67
  },
68
  borderRadius: {
69
+ 'none': '0px',
70
+ 'xs': '4px',
71
+ 'sm': '8px',
72
+ 'md': '12px',
73
+ 'lg': '16px',
74
+ 'xl': '24px',
75
+ 'pill': '100px',
76
+ 'full': '9999px',
77
  },
78
  boxShadow: {
79
+ 'soft': '0 4px 12px rgba(0, 0, 0, 0.04)',
80
+ 'none': 'none',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  },
82
+ maxWidth: {
83
+ 'content': '1200px',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  },
85
  },
86
  },