| <script lang="ts"> |
| import { onMount } from 'svelte'; |
| import { authStore } from './lib/stores/auth'; |
| import { uiStore } from './lib/stores/ui'; |
| import AppHeader from './lib/components/Layout/AppHeader.svelte'; |
| import ProgressBar from './lib/components/Layout/ProgressBar.svelte'; |
| import TabBar, { type TabId } from './lib/components/Layout/TabBar.svelte'; |
| import Scanner from './lib/components/Pages/Scanner.svelte'; |
| import Encounters from './lib/components/Pages/Encounters.svelte'; |
| import Pictuary from './lib/components/Pages/Pictuary.svelte'; |
| import type { HuggingFaceLibs, GradioLibs, GradioClient } from './lib/types'; |
| import { setQwenClientResetter } from './lib/utils/qwenTimeout'; |
| |
| |
| let hfAuth: HuggingFaceLibs | null = $state(null); |
| let gradioClient: GradioLibs | null = $state(null); |
| |
| |
| let fluxClient: GradioClient | null = $state(null); |
| let joyCaptionClient: GradioClient | null = $state(null); |
| let zephyrClient: GradioClient | null = $state(null); |
| let qwenClient: GradioClient | null = $state(null); |
| |
| |
| let activeTab: TabId = $state('scanner'); |
| |
| |
| const tabNames: Record<TabId, string> = { |
| scanner: 'Scanner', |
| encounters: 'Encounters', |
| pictuary: 'Pictuary' |
| }; |
| |
| |
| const auth = $derived(authStore); |
| |
| |
| let isDetailPageOpen = $state(false); |
| let isInBattle = $state(false); |
| |
| $effect(() => { |
| const unsubscribe = uiStore.subscribe(state => { |
| isDetailPageOpen = state.isDetailPageOpen; |
| isInBattle = state.isInBattle; |
| }); |
| return unsubscribe; |
| }); |
| |
| onMount(async () => { |
| // Load HF libraries |
| const script1 = document.createElement('script'); |
| script1.type = 'module'; |
| script1.textContent = ` |
| import { |
| oauthLoginUrl, |
| oauthHandleRedirectIfPresent |
| } from "https://cdn.jsdelivr.net/npm/@huggingface/hub@0.21/+esm"; |
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; |
| |
| window.hfAuth = { oauthLoginUrl, oauthHandleRedirectIfPresent }; |
| window.gradioClient = { Client }; |
| `; |
| document.head.appendChild(script1); |
| |
| // Wait for libraries to load |
| await new Promise(resolve => { |
| const checkLibs = setInterval(() => { |
| if (window.hfAuth && window.gradioClient) { |
| clearInterval(checkLibs); |
| resolve(undefined); |
| } |
| }, 100); |
| }); |
| |
| hfAuth = window.hfAuth as HuggingFaceLibs; |
| gradioClient = window.gradioClient as GradioLibs; |
| |
| |
| try { |
| const session = await hfAuth.oauthHandleRedirectIfPresent(); |
| authStore.setSession(session); |
| |
| // Start the app |
| await initializeClients(session?.accessToken || null); |
| } catch (err) { |
| console.error("OAuth handling error:", err); |
| authStore.setSession(null); |
| await initializeClients(null); |
| } |
| }); |
| |
| async function initializeClients(hfToken: string | null) { |
| if (!gradioClient) return; |
| |
| authStore.setBannerMessage("Connecting to AI services..."); |
| |
| try { |
| const opts = hfToken ? { hf_token: hfToken } : {}; |
| |
| |
| fluxClient = await gradioClient.Client.connect( |
| "black-forest-labs/FLUX.1-schnell", |
| opts |
| ); |
| |
| joyCaptionClient = await gradioClient.Client.connect( |
| "fancyfeast/joy-caption-alpha-two", |
| opts |
| ); |
| |
| zephyrClient = await gradioClient.Client.connect( |
| "Fraser/zephyr-7b", |
| opts |
| ); |
| |
| qwenClient = await gradioClient.Client.connect( |
| "Qwen/Qwen3-Demo", |
| opts |
| ); |
| |
| authStore.setBannerMessage(""); |
| |
| |
| setQwenClientResetter(async () => { |
| console.log('🔄 Resetting qwen client connection...'); |
| const opts = hfToken ? { hf_token: hfToken } : {}; |
| qwenClient = await gradioClient.Client.connect( |
| "Qwen/Qwen3-Demo", |
| opts |
| ); |
| }); |
| |
| } catch (err) { |
| console.error(err); |
| authStore.setBannerMessage(`❌ Failed to connect: ${err}`); |
| } |
| } |
| |
| function handleTabChange(tab: TabId) { |
| activeTab = tab; |
| } |
| </script> |
|
|
| <div class="app"> |
| {#if !isDetailPageOpen && !isInBattle} |
| <ProgressBar /> |
| <AppHeader {hfAuth} currentTab={tabNames[activeTab]} /> |
| {/if} |
| |
| <main class="app-content" class:detail-open={isDetailPageOpen}> |
| {#if activeTab === 'scanner'} |
| <Scanner |
| {fluxClient} |
| {joyCaptionClient} |
| {zephyrClient} |
| {qwenClient} |
| /> |
| {:else if activeTab === 'encounters'} |
| <Encounters /> |
| {:else if activeTab === 'pictuary'} |
| <Pictuary /> |
| {/if} |
| </main> |
| |
| {#if !isDetailPageOpen && !isInBattle} |
| <TabBar {activeTab} onTabChange={handleTabChange} /> |
| {/if} |
| </div> |
|
|
| <style> |
| .app { |
| display: flex; |
| flex-direction: column; |
| height: 100vh; |
| height: 100dvh; /* Dynamic viewport height for mobile */ |
| background: white; |
| overflow: hidden; |
| } |
| |
| .app-content { |
| flex: 1; |
| overflow: hidden; |
| position: relative; |
| padding-bottom: calc(70px + env(safe-area-inset-bottom, 0)); |
| } |
| |
| .app-content.detail-open { |
| padding-bottom: 0; |
| } |
| |
| |
| :global(.app-content > *) { |
| height: 100%; |
| } |
| </style> |