hfstudio / frontend /src /routes /+layout.svelte
GitHub Action
Sync from GitHub: 540be9e09823d53f4f006b4d1084d86b3d7f63be
77c0eb0
raw
history blame
16.1 kB
<script>
import '../app.css';
import { Home, Settings, History, Github, Menu } from 'lucide-svelte';
import { onMount } from 'svelte';
let currentPage = 'tts';
let sidebarOpen = true;
// Initialize from cache immediately to avoid "Checking..." flash
const initToken = typeof window !== 'undefined' ? localStorage.getItem('hf_access_token') : null;
const initCachedToken =
typeof window !== 'undefined' ? localStorage.getItem('hf_cached_token') : null;
const initCachedUserInfo =
typeof window !== 'undefined' ? localStorage.getItem('hf_user_info') : null;
let isLoggedIn = false;
let username = '';
let isCheckingAuth = false; // Start as false by default
// Check if we have valid cached data
let hasCachedData = false;
if (initToken && initToken === initCachedToken && initCachedUserInfo) {
try {
const userInfo = JSON.parse(initCachedUserInfo);
isLoggedIn = true;
username = userInfo.username;
hasCachedData = true;
} catch (e) {
// Invalid cache, will need to check
isCheckingAuth = true;
}
} else if (initToken) {
// We have a token but no valid cache, need to check
isCheckingAuth = true;
}
let showTokenInput = false;
let tokenInput = '';
let tokenError = '';
let isLocalEnvironment = false;
let showLoginPrompt = false;
let flashButton = false;
onMount(() => {
// Listen for event to show login prompt
window.addEventListener('show-login-prompt', () => {
if (!isLoggedIn) {
showLoginPrompt = true;
flashButton = true;
setTimeout(() => {
flashButton = false;
}, 1600);
}
});
// Only check if we don't already have valid cached info or need to verify
if (!hasCachedData && initToken) {
checkLocalTokenAvailability();
checkLoginStatus();
} else if (!initToken) {
// No token at all, check for local availability
checkLocalTokenAvailability();
}
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
checkLoginStatus();
}
});
window.addEventListener('storage', checkLoginStatus);
const interval = setInterval(checkLoginStatus, 1000);
return () => {
window.removeEventListener('storage', checkLoginStatus);
clearInterval(interval);
};
});
async function checkLocalTokenAvailability() {
// Skip if we already have valid cached info
if (isLoggedIn && hasCachedData) {
return;
}
isCheckingAuth = true;
try {
const response = await fetch('/api/auth/local-token');
const data = await response.json();
if (data.available) {
isLocalEnvironment = true;
localStorage.setItem('hf_access_token', data.token);
if (data.user_info && data.user_info.name !== 'Local User') {
isLoggedIn = true;
username = data.user_info.name.split(' ')[0];
} else {
isLoggedIn = true;
username = 'Local User';
}
} else {
isLocalEnvironment = false;
}
} catch (error) {
isLocalEnvironment = false;
} finally {
isCheckingAuth = false;
}
}
function checkLoginStatus() {
const token = localStorage.getItem('hf_access_token');
const cachedUserInfo = localStorage.getItem('hf_user_info');
const cachedToken = localStorage.getItem('hf_cached_token');
// Check if running on Spaces
const isOnSpaces =
typeof window !== 'undefined' &&
(window.location.hostname.includes('hf.space') ||
window.location.hostname.includes('huggingface.co'));
if (token) {
// Only use cache if NOT on Spaces
if (!isOnSpaces && token === cachedToken && cachedUserInfo) {
try {
const userInfo = JSON.parse(cachedUserInfo);
isLoggedIn = true;
username = userInfo.username;
return;
} catch (e) {
// Invalid cache, will re-fetch
}
}
// On Spaces, always fetch fresh user info; locally only fetch if token changed
if (isOnSpaces || !isLoggedIn || token !== cachedToken) {
fetchUserInfo(token);
}
} else {
// No token, clear everything
isLoggedIn = false;
username = '';
localStorage.removeItem('hf_user_info');
localStorage.removeItem('hf_cached_token');
}
}
async function fetchUserInfo(token) {
isCheckingAuth = true;
// Check if running on Spaces
const isOnSpaces =
typeof window !== 'undefined' &&
(window.location.hostname.includes('hf.space') ||
window.location.hostname.includes('huggingface.co'));
try {
const response = await fetch('https://huggingface.co/api/whoami-v2', {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
const userData = await response.json();
isLoggedIn = true;
const fullName =
userData.name || userData.fullname || userData.login || userData.username || 'User';
username = fullName.split(' ')[0];
// Only cache the user info and token when NOT on Spaces
if (!isOnSpaces) {
const userInfo = { username, fullName };
localStorage.setItem('hf_user_info', JSON.stringify(userInfo));
localStorage.setItem('hf_cached_token', token);
}
} else {
// Clear everything on auth failure
localStorage.removeItem('hf_access_token');
localStorage.removeItem('hf_user_info');
localStorage.removeItem('hf_cached_token');
isLoggedIn = false;
username = '';
}
} catch (error) {
// On network error, if we have cached info, use it
const cachedUserInfo = localStorage.getItem('hf_user_info');
if (cachedUserInfo) {
try {
const userInfo = JSON.parse(cachedUserInfo);
isLoggedIn = true;
username = userInfo.username;
return;
} catch (e) {
// Invalid cache
}
}
// Otherwise clear everything
localStorage.removeItem('hf_access_token');
localStorage.removeItem('hf_user_info');
localStorage.removeItem('hf_cached_token');
isLoggedIn = false;
username = '';
} finally {
isCheckingAuth = false;
}
}
async function handleAuthAction() {
if (isLoggedIn) {
localStorage.removeItem('hf_access_token');
localStorage.removeItem('hf_user_info');
localStorage.removeItem('hf_cached_token');
sessionStorage.removeItem('oauth_state');
isLoggedIn = false;
username = '';
} else {
if (
window.location.hostname.includes('hf.space') ||
window.location.hostname.includes('huggingface.co')
) {
try {
const response = await fetch('/api/auth/oauth-config');
const config = await response.json();
const scopes = config.scopes || 'read-repos write-repos manage-repos inference-api';
const oauthUrl = `https://huggingface.co/oauth/authorize?client_id=${config.client_id}&redirect_uri=${encodeURIComponent(window.location.origin + '/auth/callback')}&scope=${encodeURIComponent(scopes)}&response_type=code&state=${Date.now()}`;
window.location.href = oauthUrl;
} catch (error) {
showTokenInput = true;
tokenInput = '';
tokenError = '';
}
} else {
showTokenInput = true;
tokenInput = '';
tokenError = '';
}
}
}
function closeTokenInput() {
showTokenInput = false;
tokenInput = '';
tokenError = '';
}
async function submitToken() {
if (!tokenInput.trim()) {
tokenError = 'Please enter a token';
return;
}
if (!tokenInput.startsWith('hf_')) {
tokenError = 'Token should start with "hf_"';
return;
}
try {
const response = await fetch('https://huggingface.co/api/whoami-v2', {
headers: {
Authorization: `Bearer ${tokenInput.trim()}`,
},
});
if (response.ok) {
const userData = await response.json();
const token = tokenInput.trim();
localStorage.setItem('hf_access_token', token);
isLoggedIn = true;
const fullName =
userData.name || userData.fullname || userData.login || userData.username || 'User';
username = fullName.split(' ')[0];
// Cache the user info and token
const userInfo = { username, fullName };
localStorage.setItem('hf_user_info', JSON.stringify(userInfo));
localStorage.setItem('hf_cached_token', token);
closeTokenInput();
} else {
tokenError = `Invalid token (${response.status}). Please check your token and try again.`;
}
} catch (error) {
tokenError = 'Error validating token. Please try again.';
}
}
</script>
<div class="flex h-screen bg-white">
<!-- Sidebar -->
<aside
class="w-56 border-r border-gray-200 bg-white flex-shrink-0 flex flex-col h-full {sidebarOpen
? ''
: 'hidden'}"
>
<div class="p-4 border-b border-gray-200">
<div class="flex items-center gap-3">
<img src="/assets/hf-studio-logo.png" alt="HF Logo" class="w-8 h-8" />
<h1 class="text-xl font-semibold">
HFStudio<sup class="text-xs text-gray-500 ml-1">BETA</sup>
</h1>
</div>
</div>
<nav class="p-2 text-sm flex-1">
<div class="mt-2 mb-1 px-2 text-xs font-medium text-gray-500 uppercase">Tasks</div>
<button
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-gray-100 transition-colors text-left
{currentPage === 'tts' ? 'bg-gray-100' : ''}"
on:click={() => (currentPage = 'tts')}
>
<span>🎙️</span>
<span>Text to Speech</span>
</button>
<button
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed"
disabled
>
<span>🎵</span>
<span>Voice Cloning</span>
</button>
<button
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed"
disabled
>
<span>🎧</span>
<span>Speech to Text</span>
</button>
<button
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed"
disabled
>
<span>🎼</span>
<span>Sound Effects</span>
</button>
<button
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed"
disabled
>
<span>🎸</span>
<span>Music Generation</span>
</button>
<button
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left opacity-40 cursor-not-allowed"
disabled
>
<span>🔊</span>
<span>Audio Enhancement</span>
</button>
</nav>
<!-- Sign in with Hugging Face at bottom -->
<div class="p-2">
{#if !isLoggedIn && !isCheckingAuth && showLoginPrompt}
<!-- Login prompt message -->
<div
class="mb-3 px-3 py-2 bg-gradient-to-r from-amber-50 to-orange-50 rounded-lg border border-amber-200 relative"
>
<!-- Close button -->
<button
on:click={() => (showLoginPrompt = false)}
class="absolute top-2 right-2 text-gray-400 hover:text-gray-600 transition-colors"
aria-label="Dismiss"
>
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
<p class="text-sm font-medium text-gray-700 mb-1 pr-4">Hugging Face Inference</p>
<p class="text-sm text-gray-600 pr-4">
Sign in to get started with ~10 cents of free API credits per month ($2 for <a
href="https://huggingface.co/pro"
target="_blank"
class="text-amber-600 hover:text-amber-700 underline font-medium">Pro users</a
>). You can add a payment method for additional pay-as-you-go usage &#10549;
</p>
</div>
{/if}
<button
on:click={handleAuthAction}
disabled={isCheckingAuth}
class="w-full px-6 py-3 bg-black text-white rounded-lg font-medium hover:bg-gray-800 transition-colors shadow-sm flex items-center justify-center gap-2 text-sm disabled:opacity-50 disabled:cursor-not-allowed relative overflow-hidden"
>
{#if flashButton}
<div
class="absolute inset-0 -left-full animate-sweep bg-gradient-to-r from-transparent via-orange-400/40 to-transparent"
></div>
{/if}
<img src="/assets/hf-logo.png" alt="HF Logo" class="w-5 h-5 relative z-10" />
{#if isCheckingAuth}
<span class="relative z-10"
>Checking... ({isLoggedIn ? 'logged in' : 'not logged in'})</span
>
{:else if isLoggedIn}
<span class="relative z-10">Sign out ({username})</span>
{:else}
<span class="relative z-10">Sign In</span>
{/if}
</button>
</div>
</aside>
<!-- Main content -->
<main class="flex-1 overflow-auto">
<slot />
</main>
<!-- Token Input Modal -->
{#if showTokenInput}
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg p-6 max-w-md w-full mx-4 shadow-xl">
<h2 class="text-xl font-semibold mb-4">Sign In with HuggingFace Token</h2>
<div class="mb-4 p-3 bg-blue-50 rounded-md text-sm">
<p class="text-blue-800 mb-2">
<strong>Manual Token Entry:</strong> Please enter your HuggingFace token.
</p>
<p class="text-blue-700">
1. Go to <a
href="https://huggingface.co/settings/tokens"
target="_blank"
class="underline text-blue-600">HuggingFace Settings</a
><br />
2. Create a new token with "Inference API" permissions<br />
3. Copy and paste it below
</p>
{#if isLocalEnvironment}
<p class="text-blue-600 mt-2">
<strong>Tip:</strong> You can also run <code>huggingface-cli login</code> in your terminal
to automatically use your local token.
</p>
{/if}
</div>
<div class="mb-4">
<label for="token" class="block text-sm font-medium text-gray-700 mb-2">
HuggingFace Token
</label>
<input
id="token"
type="password"
bind:value={tokenInput}
placeholder="hf_..."
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent"
on:keydown={(e) => e.key === 'Enter' && submitToken()}
/>
{#if tokenError}
<p class="text-red-600 text-sm mt-1">{tokenError}</p>
{/if}
</div>
<div class="flex justify-end gap-3">
<button
on:click={closeTokenInput}
class="px-4 py-2 text-gray-600 hover:text-gray-800 transition-colors"
>
Cancel
</button>
<button
on:click={submitToken}
class="px-4 py-2 bg-orange-500 text-white rounded-md hover:bg-orange-600 transition-colors"
>
Sign In
</button>
</div>
</div>
</div>
{/if}
</div>
<style>
@keyframes sweep {
from {
transform: translateX(-100%);
}
to {
transform: translateX(300%);
}
}
.animate-sweep {
animation: sweep 1.6s linear forwards;
}
</style>