|
|
<script> |
|
|
import '../app.css'; |
|
|
import { Home, Settings, History, Github, Menu } from 'lucide-svelte'; |
|
|
import { onMount } from 'svelte'; |
|
|
|
|
|
let currentPage = 'tts'; |
|
|
let sidebarOpen = true; |
|
|
|
|
|
|
|
|
let initialUser = |
|
|
typeof window !== 'undefined' && window.__INITIAL_USER__ |
|
|
? window.__INITIAL_USER__ |
|
|
: { authenticated: false }; |
|
|
let isLoggedIn = initialUser?.authenticated || false; |
|
|
let username = |
|
|
initialUser?.authenticated && initialUser?.user_info |
|
|
? ( |
|
|
initialUser.user_info.name || |
|
|
initialUser.user_info.fullname || |
|
|
initialUser.user_info.login || |
|
|
initialUser.user_info.username || |
|
|
'User' |
|
|
).split(' ')[0] |
|
|
: ''; |
|
|
|
|
|
|
|
|
let showTokenInput = false; |
|
|
let tokenInput = ''; |
|
|
let tokenError = ''; |
|
|
let isLocalEnvironment = false; |
|
|
let showLoginPrompt = false; |
|
|
let flashButton = false; |
|
|
let isSpaces = false; |
|
|
|
|
|
onMount(() => { |
|
|
|
|
|
window.addEventListener('show-login-prompt', () => { |
|
|
if (!isLoggedIn) { |
|
|
showLoginPrompt = true; |
|
|
flashButton = true; |
|
|
setTimeout(() => { |
|
|
flashButton = false; |
|
|
}, 1600); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
checkSpacesStatus().then(() => { |
|
|
|
|
|
if (!isLoggedIn && !isSpaces) { |
|
|
checkLocalTokenAvailability(); |
|
|
} |
|
|
|
|
|
|
|
|
if (!initialUser?.authenticated) { |
|
|
checkLoginStatus(); |
|
|
} |
|
|
}); |
|
|
|
|
|
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 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() { |
|
|
|
|
|
if (isLoggedIn) { |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const response = await fetch('/api/auth/local-token'); |
|
|
const data = await response.json(); |
|
|
|
|
|
if (data.available) { |
|
|
isLocalEnvironment = true; |
|
|
|
|
|
|
|
|
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() { |
|
|
|
|
|
if (initialUser?.authenticated && isLoggedIn) return; |
|
|
|
|
|
try { |
|
|
|
|
|
const response = await fetch('/api/auth/user', { |
|
|
credentials: 'include', |
|
|
}); |
|
|
|
|
|
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 = ''; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
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 (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(); |
|
|
|
|
|
|
|
|
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"> |
|
|
|
|
|
<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"> |
|
|
{#if !isLoggedIn && showLoginPrompt} |
|
|
|
|
|
<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" |
|
|
> |
|
|
|
|
|
<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 <span |
|
|
class="bg-gradient-to-r from-purple-500 via-pink-500 via-green-500 to-blue-500 bg-clip-text text-transparent font-bold" |
|
|
>Pro</span |
|
|
> |
|
|
</p> |
|
|
<p class="text-sm text-gray-600 pr-4"> |
|
|
Sign in to with your Hugging Face <a |
|
|
href="https://huggingface.co/pro" |
|
|
target="_blank" |
|
|
class="text-amber-600 hover:text-amber-700 underline font-medium">Pro account</a |
|
|
> to get started with $2 of free API credits per month. You can add a billing method for |
|
|
additional pay-as-you-go usage ⤵ |
|
|
</p> |
|
|
</div> |
|
|
{/if} |
|
|
|
|
|
<button |
|
|
on:click={handleAuthAction} |
|
|
disabled={false} |
|
|
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} |
|
|
{#if isLoggedIn} |
|
|
<span class="relative z-10">Logout, {username}</span> |
|
|
{:else} |
|
|
<img src="/assets/hf-logo.png" alt="HF Logo" class="w-5 h-5 relative z-10" /> |
|
|
<span class="relative z-10">Sign In</span> |
|
|
{/if} |
|
|
</button> |
|
|
</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> |
|
|
|
|
|
<style> |
|
|
@keyframes sweep { |
|
|
from { |
|
|
transform: translateX(-100%); |
|
|
} |
|
|
to { |
|
|
transform: translateX(300%); |
|
|
} |
|
|
} |
|
|
|
|
|
.animate-sweep { |
|
|
animation: sweep 1.6s linear forwards; |
|
|
} |
|
|
</style> |
|
|
|