File size: 9,801 Bytes
cb39be8
 
 
c29203c
 
 
cb39be8
 
aa2f55a
cb39be8
 
6aa0f7b
cb39be8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9d19ea5
 
ee20036
9d19ea5
0d70a35
 
cb39be8
 
9d19ea5
 
 
cb39be8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4f07a60
cb39be8
 
0d70a35
 
cb39be8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11d0d87
 
1d61a6b
cb39be8
 
 
 
 
 
 
03365bb
cb39be8
 
 
 
 
 
 
4f07a60
 
 
 
 
cb39be8
 
03365bb
cb39be8
4f07a60
8982932
4f07a60
aa2f55a
8982932
848ebd2
 
 
 
aa2f55a
 
 
 
43f28c4
aa2f55a
061a497
 
cb39be8
 
8982932
03365bb
 
 
da7861c
03365bb
 
8982932
03365bb
 
 
 
 
 
aa2f55a
 
8982932
aa2f55a
8982932
572326d
 
 
 
aa2f55a
 
 
 
 
 
 
03365bb
aa2f55a
8982932
03365bb
 
 
cb39be8
 
 
 
 
 
 
03365bb
 
cb39be8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
/**
 * Hugging Face OAuth Authentication and Space Management
 * Uses @huggingface/hub library for OAuth flow
 *
 * Note: The public space now supports secure multi-user connections with per-user isolation.
 * Cloning a private space is optional - most users can use the public space safely.
 */

import { oauthLoginUrl, oauthHandleRedirectIfPresent, uploadFile, createRepo } from "https://cdn.jsdelivr.net/npm/@huggingface/hub@0.15.2/+esm";

const HF_API = 'https://huggingface.co/api';
const NEW_SPACE_NAME = 'reachy_mini_remote_control';

// State management
let currentUser = null;
let userToken = null;
let oauthResult = null;

/**
 * Initialize OAuth and check authentication status
 */
async function initAuth() {
    try {
        // Check if we're returning from OAuth redirect
        oauthResult = await oauthHandleRedirectIfPresent();

        if (oauthResult) {
            // User just authenticated via OAuth
            console.log('OAuth result:', oauthResult);
            console.log('User info:', oauthResult.userInfo);
            console.log('All userInfo keys:', Object.keys(oauthResult.userInfo));

            // Extract username from fullname field (HuggingFace uses fullname for the username)
            currentUser = oauthResult.userInfo.fullname;
            userToken = oauthResult.accessToken;

            console.log('Extracted username:', currentUser);
            console.log('Extracted token:', userToken ? 'present' : 'missing');

            // Store for session (not persistent across page reloads by design)
            sessionStorage.setItem('hf_token', userToken);
            sessionStorage.setItem('hf_username', currentUser);
            sessionStorage.setItem('hf_token_expires', oauthResult.accessTokenExpiresAt);

            console.log('OAuth authentication successful:', currentUser);
            showAuthenticatedView();
            await checkForExistingSpace();
        } else {
            // Check if we have a stored session
            const storedToken = sessionStorage.getItem('hf_token');
            const storedUser = sessionStorage.getItem('hf_username');
            const tokenExpires = sessionStorage.getItem('hf_token_expires');

            if (storedToken && storedUser && tokenExpires && new Date(tokenExpires) > new Date()) {
                // Valid session exists
                userToken = storedToken;
                currentUser = storedUser;
                showAuthenticatedView();
                await checkForExistingSpace();
            }
        }
    } catch (error) {
        console.error('OAuth initialization error:', error);
        // Show login view on error
        showLoginView();
    }
}

/**
 * Redirect to Hugging Face OAuth login
 */
async function loginToHuggingFace() {
    try {
        // Generate OAuth login URL and redirect
        const loginUrl = await oauthLoginUrl();
        window.location.href = loginUrl;
    } catch (error) {
        console.error('OAuth login error:', error);
        alert('Failed to initiate login. Please try again.');
    }
}

/**
 * Show the login view
 */
function showLoginView() {
    document.getElementById('login-view').style.display = 'block';
    document.getElementById('authenticated-view').style.display = 'none';
}

/**
 * Show authenticated user view
 */
function showAuthenticatedView() {
    console.log('Showing authenticated view for user:', currentUser);
    document.getElementById('login-view').style.display = 'none';
    document.getElementById('authenticated-view').style.display = 'block';
    // Display username (from fullname field)
    document.getElementById('username').textContent = `@${currentUser}`;
}

/**
 * Check if user already has the space
 */
async function checkForExistingSpace() {
    try {
        const response = await fetch(`${HF_API}/spaces/${currentUser}/${NEW_SPACE_NAME}`, {
            headers: {
                'Authorization': `Bearer ${userToken}`
            }
        });

        if (response.ok) {
            showExistingSpaceView();
        } else {
            showNoSpaceView();
        }
    } catch (error) {
        console.error('Error checking for space:', error);
        showNoSpaceView();
    }
}

/**
 * Show view for users who don't have the space yet
 */
function showNoSpaceView() {
    document.getElementById('no-space-view').style.display = 'block';
    document.getElementById('has-space-view').style.display = 'none';
}

/**
 * Show view for users who already have the space
 */
function showExistingSpaceView() {
    document.getElementById('no-space-view').style.display = 'none';
    document.getElementById('has-space-view').style.display = 'block';

    // Update space URL displays
    const spaceUrl = `${currentUser}/${NEW_SPACE_NAME}`;
    // HuggingFace converts underscores to hyphens in subdomain URLs
    // Example: andito/reachy_mini_remote_control -> andito-reachy-mini-remote-control.hf.space
    const wsUri = `wss://${currentUser}-${NEW_SPACE_NAME.replace(/_/g, '-')}.hf.space`;

    document.getElementById('space-url-display').textContent = spaceUrl;
    document.getElementById('private-uri').textContent = wsUri;
    document.getElementById('space-link').href = `https://huggingface.co/spaces/${spaceUrl}`;
}

/**
 * Create a new space and upload files
 */
async function cloneSpace() {
    const btn = document.querySelector('#no-space-view .btn');
    const btnText = document.getElementById('clone-btn-text');
    const originalText = btnText.textContent;

    try {
        // Verify we have user credentials
        if (!currentUser || !userToken) {
            throw new Error('Not authenticated. Please sign in again.');
        }

        // Update button state
        btn.disabled = true;
        btnText.textContent = '⏳ Creating space...';

        console.log('Creating space for user:', currentUser);
        console.log('Creating repo:', `${currentUser}/${NEW_SPACE_NAME}`);

        // Step 1: Create the space repository using @huggingface/hub
        const createResult = await createRepo({
            repo: {
                type: 'space',
                name: `${currentUser}/${NEW_SPACE_NAME}`
            },
            accessToken: userToken,
            credentials: {
                accessToken: userToken
            },
            hubUrl: 'https://huggingface.co',
            spaceHardware: 'cpu-basic',
            spaceSdk: 'gradio',
            private: true
        });

        console.log('Repo created successfully:', createResult);
        btnText.textContent = '⏳ Uploading files...';

        // Step 2: Upload files to the space from local directory
        const files = ['README.md', 'app.py', 'requirements.txt', 'packages.txt', 'dist.zip'];

        for (const fileName of files) {
            console.log(`Fetching ${fileName}...`);
            // Fetch file content from local remote_control_space directory
            const fileResponse = await fetch(`remote_control_space/${fileName}`);
            if (!fileResponse.ok) {
                console.error(`Failed to fetch ${fileName}`);
                continue;
            }
            const content = await fileResponse.blob();

            console.log(`Uploading ${fileName}...`);
            // Upload to space using @huggingface/hub
            const uploadResult = await uploadFile({
                repo: {
                    type: 'space',
                    name: `${currentUser}/${NEW_SPACE_NAME}`
                },
                accessToken: userToken,
                credentials: {
                    accessToken: userToken
                },
                file: {
                    path: fileName,
                    content: content
                }
            });
            console.log(`${fileName} uploaded:`, uploadResult);
        }

        btnText.textContent = '✓ Space created successfully!';

        // Wait a moment then show the space view
        setTimeout(() => {
            showExistingSpaceView();
        }, 1500);

    } catch (error) {
        console.error('Error creating space:', error);
        alert(`Failed to create space: ${error.message}\n\nPlease try again or create the space manually.`);
        btn.disabled = false;
        btnText.textContent = originalText;
    }
}

/**
 * Logout and clear stored credentials
 */
function logout() {
    if (confirm('Are you sure you want to sign out?')) {
        localStorage.removeItem('hf_token');
        localStorage.removeItem('hf_username');
        currentUser = null;
        userToken = null;

        // Reset UI
        document.getElementById('login-view').style.display = 'block';
        document.getElementById('authenticated-view').style.display = 'none';
    }
}

/**
 * Copy text to clipboard
 */
function copyToClipboard(text, buttonElement) {
    navigator.clipboard.writeText(text).then(() => {
        // Visual feedback
        if (buttonElement) {
            const originalText = buttonElement.textContent;
            buttonElement.textContent = '✓ Copied!';
            setTimeout(() => {
                buttonElement.textContent = originalText;
            }, 2000);
        }
    }).catch(err => {
        console.error('Failed to copy:', err);
        alert('Failed to copy to clipboard');
    });
}

/**
 * Copy private URI to clipboard
 */
function copyPrivateUri(event) {
    const uri = document.getElementById('private-uri').textContent;
    copyToClipboard(uri, event.currentTarget);
}

// Expose functions globally for onclick handlers
window.loginToHuggingFace = loginToHuggingFace;
window.cloneSpace = cloneSpace;
window.logout = logout;
window.copyToClipboard = copyToClipboard;
window.copyPrivateUri = copyPrivateUri;

// Initialize on page load
document.addEventListener('DOMContentLoaded', initAuth);