File size: 17,271 Bytes
86a4e7b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a13ea2e
 
acf9557
86a4e7b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f74c3fb
 
 
 
86a4e7b
a13ea2e
 
 
efd4ae9
 
86a4e7b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80c510e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86a4e7b
 
 
efd4ae9
 
a13ea2e
f74c3fb
 
 
 
658a4b0
f74c3fb
a13ea2e
efd4ae9
a13ea2e
 
86a4e7b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
efd4ae9
80c510e
 
 
 
acf9557
80c510e
 
 
 
 
 
 
 
 
 
 
 
 
 
efd4ae9
86a4e7b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
efd4ae9
 
 
 
86a4e7b
efd4ae9
 
acf9557
efd4ae9
86a4e7b
efd4ae9
862fb14
 
 
 
 
 
 
 
 
 
 
86a4e7b
862fb14
 
 
86a4e7b
862fb14
 
86a4e7b
 
 
efd4ae9
86a4e7b
 
efd4ae9
86a4e7b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a13ea2e
 
 
f74c3fb
 
 
 
a13ea2e
 
 
 
 
 
 
 
 
 
acf9557
a13ea2e
 
 
 
 
 
 
 
 
86a4e7b
 
 
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
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HuggingChat | Minimal AI Interface</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <style>
        .message-stream::after {
            content: "";
            width: 8px;
            height: 20px;
            background: #3b82f6;
            display: inline-block;
            animation: cursor-blink 1s infinite;
            vertical-align: middle;
            margin-left: 2px;
        }
        
        @keyframes cursor-blink {
            0% { opacity: 0; }
            50% { opacity: 1; }
            100% { opacity: 0; }
        }
        
        .gradient-bg {
            background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
        }
        
        .chat-container {
            height: calc(100vh - 160px);
        }
        
        @media (max-width: 640px) {
            .chat-container {
                height: calc(100vh - 140px);
            }
        }
    </style>
</head>
<body class="gradient-bg min-h-screen font-sans">
    <div class="container mx-auto px-4 py-6 max-w-4xl">
        <!-- Header -->
        <header class="flex justify-between items-center mb-6">
            <div class="flex items-center">
                <img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" alt="Hugging Face" class="h-8 mr-3">
                <h1 class="text-2xl font-bold text-gray-800">HuggingChat</h1>
            </div>
            <div id="auth-section">
                <button id="login-btn" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg flex items-center">
                    <i class="fas fa-sign-in-alt mr-2"></i> Login with Hugging Face
                </button>
                <div id="user-info" class="hidden items-center">
                    <img id="user-avatar" class="w-8 h-8 rounded-full mr-2" src="">
                    <span id="username" class="font-medium text-gray-700"></span>
                    <button id="logout-btn" class="ml-4 text-gray-500 hover:text-gray-700">
                        <i class="fas fa-sign-out-alt"></i>
                    </button>
                </div>
            </div>
        </header>

        <!-- Chat Container -->
        <div class="bg-white rounded-xl shadow-lg overflow-hidden">
            <div class="chat-container overflow-y-auto p-4" id="chat-messages">
                <div class="text-center py-10 text-gray-400" id="empty-state">
                    <i class="fas fa-comments text-4xl mb-3"></i>
                    <p class="text-lg">Start a conversation with the AI</p>
                    <p class="text-sm mt-1">Your messages will appear here</p>
                </div>
            </div>

            <!-- Input Area -->
            <div class="border-t border-gray-200 p-4 bg-gray-50">
                <div class="flex items-end">
                    <div class="flex-grow relative">
                        <textarea 
                            id="message-input" 
                            class="w-full border border-gray-300 rounded-lg px-4 py-3 pr-12 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none" 
                            placeholder="Type your message..." 
                            rows="1"
                            disabled
                        ></textarea>
                        <button id="send-btn" class="absolute right-3 bottom-3 text-blue-500 hover:text-blue-600 disabled:text-gray-400" disabled>
                            <i class="fas fa-paper-plane text-xl"></i>
                        </button>
                    </div>
                </div>
                
                <div class="mt-3 flex items-center justify-between">
                    <div class="flex items-center space-x-2">
                        <label for="model-select" class="text-sm text-gray-600">Model:</label>
                        <select id="model-select" class="text-sm border border-gray-300 rounded px-2 py-1 bg-white">
                            <option value="deepseek-ai/DeepSeek-R1-0528">DeepSeek-R1-0528</option>
                            <option value="meta-llama/Meta-Llama-3-8B-Instruct">Llama 3 8B</option>
                            <option value="mistralai/Mistral-7B-Instruct-v0.2">Mistral 7B</option>
                        </select>
                    </div>
                    <div class="text-xs text-gray-500">
                        <span id="token-counter">0 tokens</span>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script type="module">
        import { oauthLoginUrl, oauthHandleRedirectIfPresent } from 'https://cdn.jsdelivr.net/npm/@huggingface/[email protected]/+esm';
        import { InferenceClient } from 'https://cdn.jsdelivr.net/npm/@huggingface/[email protected]/+esm';
        // DOM Elements
        const loginBtn = document.getElementById('login-btn');
        const logoutBtn = document.getElementById('logout-btn');
        const userInfo = document.getElementById('user-info');
        const userAvatar = document.getElementById('user-avatar');
        const username = document.getElementById('username');
        const messageInput = document.getElementById('message-input');
        const sendBtn = document.getElementById('send-btn');
        const chatMessages = document.getElementById('chat-messages');
        const emptyState = document.getElementById('empty-state');
        const modelSelect = document.getElementById('model-select');
        const tokenCounter = document.getElementById('token-counter');
        
        // State
        let hfClient = null;
        let currentUser = null;
        let currentStream = null;
        let isGenerating = false;
        let messageCount = 0;
        
        // ---- OAuth configuration ----
        const CLIENT_ID = document.querySelector('meta[name="hf-client-id"]')?.content;
        const REDIRECT_URI = window.location.origin + window.location.pathname;
        
        // Initialize
        document.addEventListener('DOMContentLoaded', async () => {
            await processRedirect();
            await checkAuth();
            messageInput.disabled = false; // Always enable input
        });
        
        // Event Listeners
        loginBtn.addEventListener('click', handleLogin);
        logoutBtn.addEventListener('click', handleLogout);
        sendBtn.addEventListener('click', sendMessage);
        messageInput.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                if (!isGenerating) sendMessage();
            }
        });
        
        messageInput.addEventListener('input', () => {
            // Auto-resize textarea
            messageInput.style.height = 'auto';
            messageInput.style.height = (messageInput.scrollHeight) + 'px';
            
            // Enable/disable send button
            sendBtn.disabled = messageInput.value.trim() === '' || isGenerating;
        });
        
        // Functions
        async function checkAuth() {
            try {
                const token = localStorage.getItem('hf_token');
                const user = localStorage.getItem('hf_user');
                
                if (token && user) {
                    // Verify token is still valid
                    const response = await fetch('https://huggingface.co/api/whoami-v2', {
                        headers: {
                            'Authorization': `Bearer ${token}`
                        }
                    });
                    
                    if (!response.ok) {
                        throw new Error('Token expired or invalid');
                    }
                    
                    currentUser = JSON.parse(user);
                    setupAuthenticatedUI();
                }
            } catch (error) {
                console.error("Auth check failed:", error);
                handleLogout();
            }
        }
        
        async function handleLogin() {
            try {
                // Redirect user to Hugging Face OAuth page
                const url = await oauthLoginUrl({
                    clientId: CLIENT_ID,
                    redirectUri: REDIRECT_URI,
                    // default scopes: openid profile; add inference-api so we can run models
                    scopes: 'openid profile inference-api'
                });
                window.location.href = url;
            } catch (error) {
                console.error("Login redirect error:", error);
                alert(`Unable to redirect to Hugging Face login: ${error.message}`);
            }
        }
        
        function handleLogout() {
            localStorage.removeItem('hf_token');
            localStorage.removeItem('hf_user');
            currentUser = null;
            hfClient = null;
            
            // Reset UI
            loginBtn.classList.remove('hidden');
            userInfo.classList.add('hidden');
            messageInput.disabled = true;
            sendBtn.disabled = true;
            
            // Clear chat
            chatMessages.innerHTML = emptyState.outerHTML;
            messageCount = 0;
        }
        
        function setupAuthenticatedUI() {
            try {
                const token = localStorage.getItem('hf_token');
                if (!token) throw new Error('No token found');
                
                hfClient = new InferenceClient(token);
                
                // Update UI
                loginBtn.classList.add('hidden');
                userInfo.classList.remove('hidden');
                userAvatar.src = currentUser.avatar;
                username.textContent = currentUser.name;
                messageInput.disabled = false;
                sendBtn.disabled = messageInput.value.trim() === '';
                
                // Scroll to bottom
                chatMessages.scrollTop = chatMessages.scrollHeight;
            } catch (error) {
                console.error("Authentication setup failed:", error);
                handleLogout();
            }
        }
        
        async function sendMessage() {
            const message = messageInput.value.trim();
            if (!message || isGenerating) return;
            
            // Add user message to chat
            addMessage('user', message);
            messageInput.value = '';
            messageInput.style.height = 'auto';
            sendBtn.disabled = true;
            
            // Create loading message for AI
            const loadingId = addMessage('assistant', '', true);
            
            try {
                isGenerating = true;
                
                const token = localStorage.getItem('hf_token');
                if (!token || !hfClient) {
                    throw new Error("Not authenticated. Please login first.");
                }
                
                // Initialize client if not already done
                if (!hfClient) {
                    hfClient = new InferenceClient(token);
                }
                
                const model = modelSelect.value;
                // Build chat history for context
                const history = getConversationHistory().map(msg => ({
                    role: msg.role,
                    content: msg.content
                }));
                history.push({ role: 'user', content: message });

                const response = await hfClient.chatCompletion({
                    model,
                    messages: history,
                    max_tokens: 1024
                });

                const assistantMsg = response.choices?.[0]?.message?.content || '[No response]';
                updateMessage(loadingId, assistantMsg);
                chatMessages.scrollTop = chatMessages.scrollHeight;
                // optional simple token count
                tokenCounter.textContent = `${assistantMsg.split(/\s+/).length} tokens`;
                
            } catch (error) {
                console.error("Error:", error);
                updateMessage(loadingId, `Error: ${error.message}`);
            } finally {
                isGenerating = false;
                sendBtn.disabled = false;
            }
        }
        
        function addMessage(role, content, isLoading = false) {
            // Hide empty state if first message
            if (messageCount === 0 && emptyState.parentNode) {
                chatMessages.removeChild(emptyState);
            }
            
            const messageId = 'msg-' + Date.now();
            const messageDiv = document.createElement('div');
            messageDiv.className = `mb-4 flex ${role === 'user' ? 'justify-end' : 'justify-start'}`;
            
            const bubbleClass = role === 'user' 
                ? 'bg-blue-500 text-white rounded-l-xl rounded-tr-xl' 
                : 'bg-gray-100 text-gray-800 rounded-r-xl rounded-tl-xl';
            
            const loadingClass = isLoading ? 'message-stream' : '';
            
            messageDiv.innerHTML = `
                <div class="max-w-[80%]">
                    <div class="${bubbleClass} px-4 py-3 ${loadingClass}" id="${messageId}">
                        ${content}
                    </div>
                    <div class="text-xs text-gray-500 mt-1 ${role === 'user' ? 'text-right' : 'text-left'}">
                        ${role === 'user' ? 'You' : 'AI'}${new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
                    </div>
                </div>
            `;
            
            chatMessages.appendChild(messageDiv);
            chatMessages.scrollTop = chatMessages.scrollHeight;
            messageCount++;
            
            return messageId;
        }
        
        function updateMessage(id, content) {
            const element = document.getElementById(id);
            if (element) {
                element.innerHTML = content;
                
                // Remove streaming cursor if present
                if (element.classList.contains('message-stream')) {
                    element.classList.remove('message-stream');
                }
            }
        }
        
        function getConversationHistory() {
            const messages = [];
            const messageElements = chatMessages.querySelectorAll('[id^="msg-"]');
            
            messageElements.forEach(el => {
                const bubble = el.closest('.mb-4');
                const role = bubble.classList.contains('justify-end') ? 'user' : 'assistant';
                const timeElement = bubble.querySelector('.text-xs');
                const timeText = timeElement ? timeElement.textContent : '';
                
                messages.push({
                    role: role,
                    content: el.textContent
                });
            });
            
            return messages;
        }
        
        // Handle OAuth redirect result
        async function processRedirect() {
            try {
                const oauthResult = await oauthHandleRedirectIfPresent({
                    clientId: CLIENT_ID,
                    redirectUri: REDIRECT_URI
                });
                if (oauthResult) {
                    const token = oauthResult.accessToken;
                    const info = oauthResult.userInfo || {};
                    const user = {
                        name: info.name || info.preferred_username || 'Hugging Face User',
                        avatar: info.picture || 'https://huggingface.co/front/assets/huggingface_logo-noborder.svg'
                    };
                    localStorage.setItem('hf_token', token);
                    localStorage.setItem('hf_user', JSON.stringify(user));
                    currentUser = user;
                    hfClient = new InferenceClient(token);
                    setupAuthenticatedUI();
                    if (messageCount === 0) {
                        addMessage('assistant', `Hello ${user.name}! How can I help you today?`);
                    }
                }
            } catch (err) {
                console.error('OAuth processing failed:', err);
            }
        }
    </script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=reach-vb/hugging-chat" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>