|
|
<script> |
|
|
import '../app.css'; |
|
|
import { Home, Settings, History, Github, Menu } from 'lucide-svelte'; |
|
|
import { onMount } from 'svelte'; |
|
|
|
|
|
let currentPage = 'tts'; |
|
|
let sidebarOpen = true; |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
let hasCachedData = false; |
|
|
if (initToken && initToken === initCachedToken && initCachedUserInfo) { |
|
|
try { |
|
|
const userInfo = JSON.parse(initCachedUserInfo); |
|
|
isLoggedIn = true; |
|
|
username = userInfo.username; |
|
|
hasCachedData = true; |
|
|
} catch (e) { |
|
|
|
|
|
isCheckingAuth = true; |
|
|
} |
|
|
} else if (initToken) { |
|
|
|
|
|
isCheckingAuth = true; |
|
|
} |
|
|
|
|
|
let showTokenInput = false; |
|
|
let tokenInput = ''; |
|
|
let tokenError = ''; |
|
|
let isLocalEnvironment = false; |
|
|
let showSignInPopover = false; |
|
|
|
|
|
onMount(() => { |
|
|
window.addEventListener('show-signin-popover', () => { |
|
|
showSignInPopover = true; |
|
|
setTimeout(() => { |
|
|
showSignInPopover = false; |
|
|
}, 4000); |
|
|
}); |
|
|
|
|
|
|
|
|
if (!hasCachedData && initToken) { |
|
|
checkLocalTokenAvailability(); |
|
|
checkLoginStatus(); |
|
|
} else if (!initToken) { |
|
|
|
|
|
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() { |
|
|
|
|
|
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'); |
|
|
|
|
|
if (token) { |
|
|
|
|
|
if (token === cachedToken && cachedUserInfo) { |
|
|
try { |
|
|
const userInfo = JSON.parse(cachedUserInfo); |
|
|
isLoggedIn = true; |
|
|
username = userInfo.username; |
|
|
return; |
|
|
} catch (e) { |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
if (!isLoggedIn || token !== cachedToken) { |
|
|
fetchUserInfo(token); |
|
|
} |
|
|
} else { |
|
|
|
|
|
isLoggedIn = false; |
|
|
username = ''; |
|
|
localStorage.removeItem('hf_user_info'); |
|
|
localStorage.removeItem('hf_cached_token'); |
|
|
} |
|
|
} |
|
|
|
|
|
async function fetchUserInfo(token) { |
|
|
isCheckingAuth = true; |
|
|
|
|
|
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]; |
|
|
|
|
|
|
|
|
const userInfo = { username, fullName }; |
|
|
localStorage.setItem('hf_user_info', JSON.stringify(userInfo)); |
|
|
localStorage.setItem('hf_cached_token', token); |
|
|
} else { |
|
|
|
|
|
localStorage.removeItem('hf_access_token'); |
|
|
localStorage.removeItem('hf_user_info'); |
|
|
localStorage.removeItem('hf_cached_token'); |
|
|
isLoggedIn = false; |
|
|
username = ''; |
|
|
} |
|
|
} catch (error) { |
|
|
|
|
|
const cachedUserInfo = localStorage.getItem('hf_user_info'); |
|
|
if (cachedUserInfo) { |
|
|
try { |
|
|
const userInfo = JSON.parse(cachedUserInfo); |
|
|
isLoggedIn = true; |
|
|
username = userInfo.username; |
|
|
return; |
|
|
} catch (e) { |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
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]; |
|
|
|
|
|
|
|
|
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"> |
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<div class="p-2 relative"> |
|
|
<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" |
|
|
> |
|
|
<img src="/assets/hf-logo.png" alt="HF Logo" class="w-5 h-5" /> |
|
|
{#if isCheckingAuth} |
|
|
<span>Checking... ({isLoggedIn ? 'logged in' : 'not logged in'})</span> |
|
|
{:else if isLoggedIn} |
|
|
<span>Sign out ({username})</span> |
|
|
{:else} |
|
|
<span>Sign In</span> |
|
|
{/if} |
|
|
</button> |
|
|
|
|
|
|
|
|
{#if showSignInPopover && !isLoggedIn} |
|
|
<div class="absolute bottom-full left-0 right-0 mb-2 z-50"> |
|
|
<div class="bg-blue-600 text-white text-sm rounded-lg p-3 shadow-lg relative"> |
|
|
<div class="flex items-start gap-2"> |
|
|
<svg class="w-4 h-4 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"> |
|
|
<path |
|
|
fill-rule="evenodd" |
|
|
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" |
|
|
clip-rule="evenodd" |
|
|
/> |
|
|
</svg> |
|
|
<div> |
|
|
<p class="font-medium">Sign in required</p> |
|
|
<p class="text-blue-100 text-xs mt-1"> |
|
|
You need to sign in to use HuggingFace Inference Providers for text-to-speech |
|
|
generation. |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="absolute top-full left-1/2 transform -translate-x-1/2"> |
|
|
<div |
|
|
class="w-0 h-0 border-l-4 border-r-4 border-t-4 border-l-transparent border-r-transparent border-t-blue-600" |
|
|
></div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
{/if} |
|
|
</div> |
|
|
</aside> |
|
|
|
|
|
|
|
|
<main class="flex-1 overflow-auto"> |
|
|
<slot /> |
|
|
</main> |
|
|
|
|
|
|
|
|
{#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> |
|
|
|