File size: 6,603 Bytes
9ff7e0c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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>
  );
}