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([]); const [name, setName] = useState(''); const [addr, setAddr] = useState(''); const [notes, setNotes] = useState(''); const [searchQ, setSearchQ] = useState(''); const [resolved, setResolved] = useState(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 (

Contacts

Semantic contact book — find anyone by name, description, or nickname

AI-POWERED
{/* AI Contact Search */}
Semantic search

Type a name, nickname, or description. Embeddings find the closest match — no exact spelling needed.

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" />
Powered by @qvac/embed-llamacpp — cosine similarity over embedded contacts
{resolved && (
Match found
{resolved.name}
{resolved.address}
{(resolved.confidence * 100).toFixed(0)}% MATCH
)} {resolved === null && searchQ && (
No matching contact found
)}
{/* Add Contact */}
Add contact
setName(e.target.value)} placeholder="Name (e.g. Alice)" className="input-field text-body-sm" /> setAddr(e.target.value)} placeholder="Solana address" className="input-field text-body-sm font-mono" />
setNotes(e.target.value)} placeholder="Notes (optional — helps AI match)" className="input-field text-body-sm" /> {err &&
{err}
}
Contact names, addresses, and notes are embedded locally for semantic resolution. Say "send to Alice" and the AI resolves the address.
{/* Contact List */}
All contacts · {contacts.length}
{contacts.length === 0 ? (
No contacts yet
Add your first contact above. The AI will embed it for semantic search.
) : (
{contacts.map((c, i) => (
{c.name.charAt(0).toUpperCase()}
{c.name}
{c.address.slice(0, 12)}…{c.address.slice(-6)}
{c.notes &&
{c.notes}
}
{c.txCount} TX
))}
)}
); }