File size: 7,963 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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
import React, { useState, useRef } from 'react';
import { PipelineTrace, useToast } from '../components/ui/index';

export default function ScanPage() {
  const [imageData, setImageData] = useState<ArrayBuffer | null>(null);
  const [preview, setPreview] = useState<string>('');
  const [result, setResult] = useState<any>(null);
  const [loading, setLoading] = useState(false);
  const fileRef = useRef<HTMLInputElement>(null);
  const { addToast } = useToast();

  const handleFile = async (file: File) => {
    const buffer = await file.arrayBuffer();
    setImageData(buffer);
    setPreview(URL.createObjectURL(file));
    setResult(null);
  };

  const handleDrop = (e: React.DragEvent) => {
    e.preventDefault();
    const file = e.dataTransfer.files[0];
    if (file && file.type.startsWith('image/')) handleFile(file);
  };

  const process = async () => {
    if (!imageData) return;
    setLoading(true);
    try {
      if (window.solvox) {
        const r = await window.solvox.ai.ocrPayment(imageData);
        if (r.success) {
          setResult(r);
          addToast({ type: 'success', title: 'Document scanned', message: 'Payment data extracted via OCR + LLM' });
        } else {
          addToast({ type: 'error', title: 'Scan failed', message: r.error });
        }
      } else {
        setResult({
          rawText: '[Dev mode] OCR requires @qvac/ocr-onnx model',
          extractedData: { amount: 25.50, token: 'USDT', recipient: null, memo: 'Invoice #1234', confidence: 0.85 },
          pipelineSteps: [
            { module: '@qvac/ocr-onnx', operation: 'Image → Text', input: 'uploaded image', output: 'Invoice text…', durationMs: 340 },
            { module: '@qvac/llm-llamacpp', operation: 'Extract payment data', input: 'OCR text', output: '{"amount": 25.50}', durationMs: 210 },
          ],
        });
      }
    } catch (e: any) { addToast({ type: 'error', title: 'Error', message: e.message }); }
    setLoading(false);
  };

  const reset = () => { setImageData(null); setPreview(''); setResult(null); };

  return (
    <div className="max-w-2xl mx-auto px-8 py-section">
      <div className="mb-8">
        <h2 className="display-text text-title-lg text-ink">Scan & Pay</h2>
        <p className="text-body-sm text-body mt-1">Upload an invoice, QR code, or screenshot. QVAC extracts payment data locally.</p>
      </div>

      {/* Upload Area */}
      {!preview && (
        <div className="card page-enter" style={{ padding: 0 }}>
          <div
            onDrop={handleDrop}
            onDragOver={e => e.preventDefault()}
            onClick={() => fileRef.current?.click()}
            className="flex flex-col items-center justify-center py-16 px-8 cursor-pointer hover:bg-surface-soft transition-colors rounded-xl"
          >
            <div className="w-16 h-16 rounded-full bg-surface-strong flex items-center justify-center mb-4">
              <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#0052ff" strokeWidth="1.5" strokeLinecap="round">
                <rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" />
              </svg>
            </div>
            <div className="text-title-md text-ink mb-1">Drop an image or click to upload</div>
            <div className="text-body-sm text-muted">Invoice, QR code, screenshot, receipt</div>
            <div className="flex items-center gap-1.5 mt-4">
              <div className="w-1 h-1 rounded-full bg-primary" />
              <span className="text-caption text-muted">@qvac/ocr-onnx → @qvac/llm-llamacpp pipeline</span>
            </div>
          </div>
          <input ref={fileRef} type="file" accept="image/*" className="hidden" onChange={e => { const f = e.target.files?.[0]; if (f) handleFile(f); }} />
        </div>
      )}

      {/* Preview + Process */}
      {preview && !result && (
        <div className="card page-enter space-y-5">
          <div className="rounded-xl overflow-hidden bg-surface-soft">
            <img src={preview} alt="Uploaded" className="w-full max-h-[300px] object-contain" />
          </div>
          <div className="flex gap-3">
            <button onClick={reset} className="btn-secondary flex-1">Cancel</button>
            <button onClick={process} disabled={loading} className="btn-primary flex-1 disabled:opacity-50">
              {loading ? 'Scanning…' : 'Extract payment data'}
            </button>
          </div>
          {loading && (
            <div className="text-center">
              <div className="inline-flex items-center gap-2 px-4 py-2 bg-surface-soft rounded-pill">
                <div className="w-2 h-2 rounded-full bg-primary animate-pulse" />
                <span className="text-body-sm text-muted">Running OCR → LLM pipeline locally…</span>
              </div>
            </div>
          )}
        </div>
      )}

      {/* Results */}
      {result && (
        <div className="space-y-6 page-enter">
          {/* Extracted Data */}
          <div className="card">
            <div className="text-caption-strong text-muted uppercase tracking-wider mb-4">Extracted payment data</div>
            <div className="space-y-3">
              {result.extractedData?.amount && (
                <div className="flex justify-between items-center py-2 border-b border-hairline-soft">
                  <span className="text-body-sm text-muted">Amount</span>
                  <span className="number-mono text-number-display text-ink">{result.extractedData.amount} {result.extractedData.token || '—'}</span>
                </div>
              )}
              {result.extractedData?.recipient && (
                <div className="flex justify-between items-center py-2 border-b border-hairline-soft">
                  <span className="text-body-sm text-muted">Recipient</span>
                  <span className="font-mono text-caption text-ink">{result.extractedData.recipient}</span>
                </div>
              )}
              {result.extractedData?.memo && (
                <div className="flex justify-between items-center py-2 border-b border-hairline-soft">
                  <span className="text-body-sm text-muted">Memo</span>
                  <span className="text-body-sm text-ink">{result.extractedData.memo}</span>
                </div>
              )}
              <div className="flex justify-between items-center py-2">
                <span className="text-body-sm text-muted">Confidence</span>
                <span className={`badge-pill text-[10px] ${(result.extractedData?.confidence || 0) > 0.7 ? 'badge-pill-green' : 'badge-pill-red'}`}>
                  {((result.extractedData?.confidence || 0) * 100).toFixed(0)}%
                </span>
              </div>
            </div>

            {result.extractedData?.amount && (
              <button className="btn-primary w-full mt-4">
                Send {result.extractedData.amount} {result.extractedData.token || 'tokens'} →
              </button>
            )}
          </div>

          {/* Raw OCR Text */}
          <div className="card">
            <div className="text-caption-strong text-muted uppercase tracking-wider mb-2">Raw OCR output</div>
            <div className="bg-surface-soft rounded-lg p-3 text-caption font-mono text-muted whitespace-pre-wrap max-h-[200px] overflow-y-auto">
              {result.rawText}
            </div>
          </div>

          {/* Pipeline Trace */}
          {result.pipelineSteps && <PipelineTrace steps={result.pipelineSteps} />}

          {/* Preview */}
          <div className="rounded-xl overflow-hidden bg-surface-soft">
            <img src={preview} alt="Scanned" className="w-full max-h-[200px] object-contain opacity-60" />
          </div>

          <button onClick={reset} className="btn-secondary w-full">Scan another document</button>
        </div>
      )}
    </div>
  );
}