File size: 5,246 Bytes
921d377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * FaceSwapRefInput — inline reference image picker for Face Swap.
 *
 * Additive component — rendered below the Face Swap button in IdentityTools
 * when user clicks Face Swap and no reference is yet set.
 *
 * Compact inline variant — upload zone + URL input.
 * Same pattern as AvatarStudio reference upload.
 */
import React, { useState, useCallback, useRef } from 'react';
import { Upload, X, Loader2, Check } from 'lucide-react';
import { resolveFileUrl } from '../resolveFileUrl';
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
export function FaceSwapRefInput({ backendUrl, apiKey, onReferenceReady, onCancel, }) {
    const [uploading, setUploading] = useState(false);
    const [preview, setPreview] = useState(null);
    const [urlInput, setUrlInput] = useState('');
    const [error, setError] = useState(null);
    const fileInputRef = useRef(null);
    const handleFileUpload = useCallback(async (file) => {
        setUploading(true);
        setError(null);
        // Show local preview immediately
        const localPreview = URL.createObjectURL(file);
        setPreview(localPreview);
        // Upload to backend
        const formData = new FormData();
        formData.append('file', file);
        const base = (backendUrl || '').replace(/\/+$/, '');
        const headers = {};
        if (apiKey)
            headers['x-api-key'] = apiKey;
        try {
            const res = await fetch(`${base}/upload`, {
                method: 'POST',
                headers,
                body: formData,
            });
            if (res.ok) {
                const data = await res.json();
                const uploadedUrl = data.url || data.file_url || '';
                if (uploadedUrl) {
                    onReferenceReady(uploadedUrl);
                }
                else {
                    setError('Upload succeeded but no URL returned');
                }
            }
            else {
                setError(`Upload failed: ${res.status}`);
            }
        }
        catch (e) {
            setError(e instanceof Error ? e.message : 'Upload failed');
        }
        finally {
            setUploading(false);
        }
    }, [backendUrl, apiKey, onReferenceReady]);
    const handleUrlSubmit = useCallback(() => {
        const trimmed = urlInput.trim();
        if (trimmed) {
            onReferenceReady(trimmed);
        }
    }, [urlInput, onReferenceReady]);
    return (<div className="mt-2 p-3 rounded-xl bg-cyan-500/5 border border-cyan-500/20 space-y-2.5 animate-fadeIn">
      <div className="flex items-center justify-between">
        <div className="text-[11px] text-cyan-300/60 font-medium">
          Upload the face to swap onto this image
        </div>
        <button onClick={onCancel} className="p-1 rounded-md text-white/30 hover:text-white/60 hover:bg-white/5 transition-colors">
          <X size={14}/>
        </button>
      </div>

      <div className="flex items-center gap-2">
        {/* Upload button */}
        <button onClick={() => fileInputRef.current?.click()} disabled={uploading} className={`flex items-center gap-2 px-3 py-2 rounded-lg border text-xs font-medium transition-all ${uploading
            ? 'border-cyan-500/30 bg-cyan-500/10 text-cyan-300'
            : 'border-white/10 bg-white/5 text-white/60 hover:bg-cyan-500/10 hover:border-cyan-500/30 hover:text-cyan-200'}`}>
          {uploading ? (<Loader2 size={14} className="animate-spin"/>) : preview ? (<Check size={14} className="text-green-400"/>) : (<Upload size={14}/>)}
          {uploading ? 'Uploading...' : 'Upload face'}
        </button>

        <input ref={fileInputRef} type="file" accept="image/*" className="hidden" onChange={(e) => {
            const file = e.target.files?.[0];
            if (file)
                handleFileUpload(file);
            e.target.value = '';
        }}/>

        {/* Preview thumbnail */}
        {preview && !uploading && (<div className="w-9 h-9 rounded-lg overflow-hidden border border-white/10 flex-shrink-0">
            <img src={resolveFileUrl(preview)} alt="Face preview" className="w-full h-full object-cover"/>
          </div>)}

        {/* URL input */}
        {!preview && !uploading && (<div className="flex-1 flex items-center gap-1.5">
            <span className="text-[10px] text-white/20">or</span>
            <input value={urlInput} onChange={(e) => setUrlInput(e.target.value)} placeholder="Paste image URL..." className="flex-1 px-2 py-1.5 rounded-lg bg-white/5 border border-white/10 text-white text-[11px] placeholder:text-white/20 focus:outline-none focus:border-cyan-500/50 transition-all" onKeyDown={(e) => {
                if (e.key === 'Enter')
                    handleUrlSubmit();
            }}/>
            {urlInput.trim() && (<button onClick={handleUrlSubmit} className="px-2 py-1.5 rounded-lg bg-cyan-500/20 text-cyan-300 text-[11px] font-medium hover:bg-cyan-500/30 transition-colors">
                Use
              </button>)}
          </div>)}
      </div>

      {error && (<div className="text-[10px] text-red-400">{error}</div>)}
    </div>);
}