solvox / src /renderer /pages /ContactsPage.tsx
muthuk1's picture
🚀 Final: +ContactsPage +ScanPage +Sparklines, types.ts synced, TS 0 errors, Coinbase design, complete README
9ff7e0c verified
import React, { useState, useEffect } from 'react';
import { useToast } from '../components/ui/index';
interface Contact { name: string; address: string; notes?: string; txCount: number; }
export default function ContactsPage() {
const [contacts, setContacts] = useState<Contact[]>([]);
const [name, setName] = useState(''); const [addr, setAddr] = useState(''); const [notes, setNotes] = useState('');
const [searchQ, setSearchQ] = useState(''); const [resolved, setResolved] = useState<any>(null);
const [err, setErr] = useState('');
const { addToast } = useToast();
useEffect(() => { load(); }, []);
const load = async () => { if (window.solvox) { const c = await window.solvox.ai.getContacts(); setContacts(c || []); } };
const add = async () => {
if (!name.trim() || !addr.trim()) return setErr('Name and address required');
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(addr.trim())) return setErr('Invalid Solana address');
if (window.solvox) {
const r = await window.solvox.ai.addContact({ name: name.trim(), address: addr.trim(), notes: notes.trim(), txCount: 0 });
if (r.success) { setName(''); setAddr(''); setNotes(''); setErr(''); load(); addToast({ type: 'success', title: `${name} added to contacts` }); }
else setErr(r.error || 'Failed');
}
};
const resolve = async () => {
if (!searchQ.trim()) return;
if (window.solvox) {
const r = await window.solvox.ai.resolveContact(searchQ.trim());
setResolved(r.success ? r.contact : null);
}
};
return (
<div className="max-w-3xl mx-auto px-8 py-section">
<div className="flex items-center justify-between mb-8">
<div>
<h2 className="display-text text-title-lg text-ink">Contacts</h2>
<p className="text-body-sm text-body mt-1">Semantic contact book — find anyone by name, description, or nickname</p>
</div>
<span className="badge-pill-blue badge-pill text-[10px]">AI-POWERED</span>
</div>
{/* AI Contact Search */}
<div className="card mb-6">
<div className="text-caption-strong text-muted uppercase tracking-wider mb-3">Semantic search</div>
<p className="text-caption text-muted mb-3">Type a name, nickname, or description. Embeddings find the closest match — no exact spelling needed.</p>
<div className="flex gap-2">
<input value={searchQ} onChange={e => setSearchQ(e.target.value)} onKeyDown={e => e.key === 'Enter' && resolve()}
placeholder='Try "alice", "my friend who works at…", "the devnet test wallet"' className="search-pill flex-1 text-body-sm" />
<button onClick={resolve} className="btn-primary text-body-sm py-2 px-5">Resolve</button>
</div>
<div className="text-caption text-muted mt-2 flex items-center gap-1.5">
<div className="w-1 h-1 rounded-full bg-primary" />
Powered by @qvac/embed-llamacpp — cosine similarity over embedded contacts
</div>
{resolved && (
<div className="mt-4 bg-surface-soft rounded-xl p-4 page-enter">
<div className="text-caption-strong text-primary uppercase tracking-wider mb-2">Match found</div>
<div className="flex items-center justify-between">
<div>
<div className="text-title-sm text-ink">{resolved.name}</div>
<div className="text-caption font-mono text-muted mt-0.5">{resolved.address}</div>
</div>
<div className="text-right">
<span className="badge-pill-green badge-pill text-[10px]">{(resolved.confidence * 100).toFixed(0)}% MATCH</span>
<button onClick={() => { navigator.clipboard.writeText(resolved.address); addToast({ type: 'info', title: 'Address copied' }); }}
className="btn-text text-body-sm ml-2">Copy</button>
</div>
</div>
</div>
)}
{resolved === null && searchQ && (
<div className="mt-3 text-body-sm text-muted text-center">No matching contact found</div>
)}
</div>
{/* Add Contact */}
<div className="card mb-6">
<div className="text-caption-strong text-muted uppercase tracking-wider mb-3">Add contact</div>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<input value={name} onChange={e => setName(e.target.value)} placeholder="Name (e.g. Alice)" className="input-field text-body-sm" />
<input value={addr} onChange={e => setAddr(e.target.value)} placeholder="Solana address" className="input-field text-body-sm font-mono" />
</div>
<input value={notes} onChange={e => setNotes(e.target.value)} placeholder="Notes (optional — helps AI match)" className="input-field text-body-sm" />
{err && <div className="text-body-sm text-semantic-down">{err}</div>}
<button onClick={add} className="btn-primary text-body-sm">Add contact</button>
</div>
<div className="text-caption text-muted mt-3">
Contact names, addresses, and notes are embedded locally for semantic resolution. Say "send to Alice" and the AI resolves the address.
</div>
</div>
{/* Contact List */}
<div className="card" style={{ padding: 0 }}>
<div className="px-xl pt-xl pb-3">
<div className="text-caption-strong text-muted uppercase tracking-wider">All contacts · {contacts.length}</div>
</div>
{contacts.length === 0 ? (
<div className="px-xl pb-xl text-center py-8">
<div className="text-title-md text-ink mb-1">No contacts yet</div>
<div className="text-body-sm text-muted">Add your first contact above. The AI will embed it for semantic search.</div>
</div>
) : (
<div>
{contacts.map((c, i) => (
<div key={i} className="asset-row px-5">
<div className="asset-icon mr-3 text-sm font-bold text-primary">{c.name.charAt(0).toUpperCase()}</div>
<div className="flex-1 min-w-0">
<div className="text-title-sm text-ink">{c.name}</div>
<div className="text-caption font-mono text-muted">{c.address.slice(0, 12)}…{c.address.slice(-6)}</div>
{c.notes && <div className="text-caption text-muted-soft">{c.notes}</div>}
</div>
<span className="badge-pill text-[10px]">{c.txCount} TX</span>
</div>
))}
</div>
)}
</div>
</div>
);
}