hfstudio / frontend /src /routes /+layout.svelte
GitHub Action
Sync from GitHub: b40abfdfc77516e9dfaa185426799de16f8bf1fd
2fad28f
<script>
import '../app.css';
import { Home, Settings, Github, Menu, Mic, Layout, Code } from 'lucide-svelte';
import { onMount } from 'svelte';
import { page } from '$app/stores';
import Navbar from '$lib/components/Navbar.svelte';
let sidebarOpen = true;
// Initialize from server-injected data
let initialUser = { authenticated: false };
let isLoggedIn = false;
let username = '';
function setUserFromInitialData(userData) {
initialUser = userData;
isLoggedIn = userData?.authenticated || false;
if (userData?.authenticated && userData?.user_info) {
const userInfo = userData.user_info;
const fullName =
userInfo.name || userInfo.fullname || userInfo.login || userInfo.username || 'User';
username = fullName.split(' ')[0];
}
}
// Listen for the initial data immediately (before onMount)
if (typeof window !== 'undefined') {
// Check if data already exists
if (window.__INITIAL_USER__) {
console.log('Found initial user data:', window.__INITIAL_USER__);
setUserFromInitialData(window.__INITIAL_USER__);
} else {
console.log('No initial user data found, listening for event');
// Listen for the event
window.addEventListener('initial-user-loaded', (e) => {
console.log('Received initial user event:', e.detail);
setUserFromInitialData(e.detail);
});
}
}
let showTokenInput = false;
let tokenInput = '';
let tokenError = '';
let isLocalEnvironment = false;
let showLoginPrompt = false;
let flashButton = false;
let isSpaces = false;
onMount(() => {
// Listen for event to show login prompt
window.addEventListener('show-login-prompt', () => {
if (!isLoggedIn) {
showLoginPrompt = true;
flashButton = true;
setTimeout(() => {
flashButton = false;
}, 1600);
}
});
// Check if running on Spaces first
checkSpacesStatus().then(() => {
// If we don't have initial user data (development mode), check auth status
if (!window.__INITIAL_USER__ && !initialUser?.authenticated) {
console.log('No server data available, checking auth status via API');
checkLoginStatus();
}
});
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
checkLoginStatus();
}
});
return () => {
// Cleanup function
};
});
async function checkSpacesStatus() {
try {
const response = await fetch('/api/status');
const data = await response.json();
isSpaces = data.is_spaces || false;
} catch (error) {
console.error('Error checking Spaces status:', error);
isSpaces = false;
}
}
async function checkLocalTokenAvailability() {
// Only check if not already authenticated
if (isLoggedIn) {
return;
}
try {
const response = await fetch('/api/auth/local-token');
const data = await response.json();
if (data.available) {
isLocalEnvironment = true;
// For local tokens, create session via manual token endpoint if not already authenticated
if (data.token && !isLoggedIn) {
try {
const tokenResponse = await fetch('/api/auth/manual-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ token: data.token }),
});
if (tokenResponse.ok) {
const tokenData = await tokenResponse.json();
isLoggedIn = true;
const userInfo = tokenData.user_info;
const fullName =
userInfo.name || userInfo.fullname || userInfo.login || userInfo.username || 'User';
username = fullName.split(' ')[0];
}
} catch (error) {
console.error('Local token session creation failed:', error);
}
} else if (data.user_info && data.user_info.name !== 'Local User' && !isLoggedIn) {
isLoggedIn = true;
username = data.user_info.name.split(' ')[0];
} else if (!isLoggedIn) {
isLoggedIn = true;
username = 'Local User';
}
} else {
isLocalEnvironment = false;
}
} catch (error) {
isLocalEnvironment = false;
}
}
async function checkLoginStatus() {
// Only check if we don't have initial data or need to refresh
if (initialUser?.authenticated && isLoggedIn) return;
try {
// Check session-based auth via backend
const response = await fetch('/api/auth/user', {
credentials: 'include', // Include cookies
});
if (response.ok) {
const data = await response.json();
if (data.authenticated) {
isLoggedIn = true;
const userInfo = data.user_info;
const fullName =
userInfo.name || userInfo.fullname || userInfo.login || userInfo.username || 'User';
username = fullName.split(' ')[0];
} else {
isLoggedIn = false;
username = '';
}
} else {
isLoggedIn = false;
username = '';
}
} catch (error) {
isLoggedIn = false;
username = '';
}
}
// Removed old localStorage-based functions
function getPageTitle(pathname) {
switch (pathname) {
case '/':
return 'Text to Speech Playground';
case '/voice-cloning':
return 'Voice Cloning Playground';
default:
return 'HFStudio';
}
}
async function handleAuthAction() {
if (isLoggedIn) {
// Call backend logout endpoint to clear session
try {
await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include',
});
} catch (error) {
console.error('Logout error:', error);
}
// Clear any legacy storage
sessionStorage.removeItem('oauth_state');
isLoggedIn = false;
username = '';
// Refresh the page to ensure clean state
window.location.reload();
} else {
// Always use OAuth flow for authentication
try {
const response = await fetch('/api/auth/oauth-config');
const config = await response.json();
const scopes = config.scopes || 'inference-api';
// For local development, use backend port for callback
let redirectUri = window.location.origin + '/auth/callback';
if (window.location.hostname === 'localhost' && window.location.port === '11111') {
redirectUri = 'http://localhost:7860/auth/callback';
}
// Store current path to return to after auth
const returnPath = window.location.pathname;
const oauthUrl = `https://huggingface.co/oauth/authorize?client_id=${config.client_id}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scopes)}&response_type=code&state=${encodeURIComponent(returnPath)}`;
window.location.href = oauthUrl;
} catch (error) {
// Fallback to manual token input if OAuth config fails
showTokenInput = true;
tokenInput = '';
tokenError = '';
}
// Old approach - commented out
// if (isSpaces) {
// 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();
// Send token to backend for validation and session creation
try {
const tokenResponse = await fetch('/api/auth/manual-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ token: token }),
});
if (tokenResponse.ok) {
const tokenData = await tokenResponse.json();
isLoggedIn = true;
const userInfo = tokenData.user_info;
const fullName =
userInfo.name || userInfo.fullname || userInfo.login || userInfo.username || 'User';
username = fullName.split(' ')[0];
closeTokenInput();
} else {
const errorData = await tokenResponse.json();
tokenError = errorData.detail || 'Token validation failed';
}
} catch (error) {
tokenError = 'Failed to validate token. Please try again.';
}
} 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 relative {sidebarOpen
? ''
: 'hidden'}"
>
<div class="px-4 py-4 border-b border-gray-200 min-h-[73px] flex items-center">
<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">
<!-- Audio Section -->
<div class="mt-2 mb-1 px-2 text-xs font-medium text-gray-500 uppercase">Audio</div>
<a
href="/"
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-gray-100 transition-colors text-left
{$page.url.pathname === '/' ? 'bg-gray-100' : ''}"
>
<span>🎙️</span>
<span>Text to Speech</span>
</a>
<a
href="/voice-cloning"
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-gray-100 transition-colors text-left
{$page.url.pathname === '/voice-cloning' ? 'bg-gray-100' : ''}"
>
<Mic size={16} />
<span>Voice Cloning</span>
</a>
<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>
<!-- Image Section -->
<div class="mt-4 mb-1 px-2 text-xs font-medium text-gray-500 uppercase">Image</div>
<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>Text to Image</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>Image to Image</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>Remove Background</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>Upscale Image</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>Face Swap</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>Image to Text</span>
</button>
<!-- Video Section -->
<div class="mt-4 mb-1 px-2 text-xs font-medium text-gray-500 uppercase">Video</div>
<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>Text to Video</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>Image to Video</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>Video Enhancement</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>Lip Sync</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>Video Dubbing</span>
</button>
</nav>
</aside>
<!-- Main content -->
<main class="flex-1 overflow-auto">
<Navbar
{isLoggedIn}
{username}
{handleAuthAction}
{flashButton}
pageTitle={getPageTitle($page.url.pathname)}
/>
<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>