| import React, { useState, useEffect } from 'react'; |
|
|
| export default function HistoryPage() { |
| const [txs, setTxs] = useState<any[]>([]); |
| const [loading, setLoading] = useState(true); |
| const [query, setQuery] = useState(''); |
| const [rag, setRag] = useState<any[]>([]); |
| const [searching, setSearching] = useState(false); |
|
|
| useEffect(() => { load(); }, []); |
| 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); }; |
| 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); }; |
|
|
| return ( |
| <div className="max-w-content mx-auto px-8 py-section"> |
| <div className="flex items-center justify-between mb-6"> |
| <h2 className="display-text text-title-lg text-ink">Transactions</h2> |
| <button onClick={load} className="btn-secondary text-body-sm py-2 px-3">Refresh</button> |
| </div> |
| |
| {/* AI Search */} |
| <div className="bg-surface-soft rounded-xl p-4 mb-8"> |
| <div className="flex gap-2"> |
| <input value={query} onChange={e => setQuery(e.target.value)} onKeyDown={e => e.key === 'Enter' && search()} |
| placeholder='Search with AI — "last payment to Alice"' className="search-pill flex-1 text-body-sm" /> |
| <button onClick={search} disabled={searching} className="btn-primary text-body-sm py-2 px-4 disabled:opacity-50">{searching ? '…' : 'Search'}</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" /> |
| Semantic search via @qvac/embed-llamacpp — 100% local |
| </div> |
| {rag.length > 0 && ( |
| <div className="mt-3 space-y-2"> |
| <div className="text-caption-strong text-primary uppercase tracking-wider">AI Results</div> |
| {rag.map((r, i) => ( |
| <div key={i} className="bg-canvas rounded-lg p-3 border border-hairline"> |
| <div className="text-body-sm text-ink">{r.text}</div> |
| <div className="text-caption text-muted mt-1 number-mono">{(r.score * 100).toFixed(0)}% match</div> |
| </div> |
| ))} |
| </div> |
| )} |
| </div> |
| |
| {/* Table */} |
| <div className="card" style={{ padding: 0 }}> |
| {loading ? ( |
| <div className="p-12 text-center text-body text-muted">Loading…</div> |
| ) : txs.length === 0 ? ( |
| <div className="p-12 text-center"> |
| <div className="text-title-md text-ink mb-1">No transactions yet</div> |
| <div className="text-body-sm text-muted">Send or receive tokens to see history.</div> |
| </div> |
| ) : ( |
| <div> |
| {txs.map((tx, i) => ( |
| <div key={i} className="asset-row px-5"> |
| <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'}`}> |
| {tx.status === 'success' ? '✓' : '✗'} |
| </div> |
| <div className="flex-1 min-w-0"> |
| <div className="text-body-sm font-mono text-ink truncate">{tx.signature?.slice(0, 20)}…</div> |
| <div className="text-caption text-muted">{tx.timestamp ? new Date(tx.timestamp).toLocaleString() : 'Pending'}</div> |
| </div> |
| <span className={`badge-pill text-[10px] mr-3 ${tx.status === 'success' ? 'badge-pill-green' : 'badge-pill-red'}`}>{tx.status}</span> |
| <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> |
| </div> |
| ))} |
| </div> |
| )} |
| </div> |
| </div> |
| ); |
| } |
|
|