/** * OllaBridgeModels — the "OllaBridge" provider class in Model Management. * * A special provider whose models come from the user's LINKED HomePilot / * OllaBridge nodes (e.g. a home PC with a high-end GPU running the OllaBridge * connector). Signed in with an OllaBridge account, this panel: * * 1. Lists the user's linked devices (online state, GPU name, VRAM) from * Cloud `GET /v1/devices`. * 2. Syncs the models those nodes advertise via Cloud * `GET /ollama/v1/models` (entries sourced from the relay are the * user's own shared devices — model sync, no manual entry). * 3. One tap "Use for Chat" routes inference through the Cloud relay to the * user's own GPU node: it configures provider=openai_compat with * base_url={cloud}/ollama/v1 and the picked model; the chat request then * carries the user's Cloud token as provider_api_key so the relay knows * whose node to use. * * Fully additive and self-contained: talks to the Cloud directly from the * browser (CORS is open on the Cloud), stores the token only in * localStorage, and hands the provider switch to App via a CustomEvent — * zero prop-drilling into the existing Models plumbing. */ import React, { useCallback, useEffect, useState } from 'react' import { isBffSessionEnabled } from '../account/featureFlags' import { Cloud, Cpu, Loader2, LogOut, RefreshCw, Zap } from 'lucide-react' const LS_CLOUD_TOKEN = 'homepilot_cloud_token' const LS_CLOUD_URL = 'homepilot_cloud_url' export function getCloudUrl(): string { try { const saved = (localStorage.getItem(LS_CLOUD_URL) || '').trim().replace(/\/+$/, '') // Drop known-obsolete cloud URLs so the new default/env value takes over, // while preserving an intentional self-hosted URL. The raw Hugging Face // origin is superseded by the branded app.ollabridge.com host. const legacyUrls = new Set([ 'https://ruslanmv-ollabridge.hf.space', 'https://ruslanmv-ollabridge-cloud.hf.space', ]) if (saved && legacyUrls.has(saved)) { localStorage.removeItem(LS_CLOUD_URL) } else if (saved) { return saved } } catch { /* ignore */ } const env: Record = ((import.meta as unknown as { env?: Record }).env) || {} return ((env.VITE_OLLABRIDGE_CLOUD_URL || '').trim() || 'https://app.ollabridge.com').replace(/\/+$/, '') } export function getCloudToken(): string { try { return (localStorage.getItem(LS_CLOUD_TOKEN) || '').trim() } catch { return '' } } interface DeviceInfo { id: string name: string platform?: string | null online: boolean gpu_name?: string | null vram_mb?: number | null } interface CloudModel { id: string owned_by?: string source?: string device_id?: string device_name?: string } /** Loose model-type guess from the model id — drives badges + per-tab filter. */ function guessType(id: string): string { const s = id.toLowerCase() if (/(llava|vision|-vl|minicpm-v|moondream)/.test(s)) return 'multimodal' if (/(sdxl|flux|stable-diffusion|sd15|sd3|pony|dreamshaper)/.test(s)) return 'image' if (/(svd|ltx|wan|video)/.test(s)) return 'video' if (/(upscal|esrgan|enhance|restore|gfpgan|codeformer)/.test(s)) return 'enhance' if (/lora/.test(s)) return 'LoRA' return 'chat' } /** Does a model's guessed type belong under the given Model Type tab? */ function matchesTab(filterType: string | undefined, guessed: string): boolean { if (!filterType) return true switch (filterType) { case 'chat': return guessed === 'chat' case 'multimodal': return guessed === 'multimodal' case 'image': case 'edit': return guessed === 'image' case 'video': return guessed === 'video' case 'enhance': return guessed === 'enhance' case 'lora': return guessed === 'LoRA' case 'addons': return true default: return true } } const TAB_LABEL: Record = { chat: 'chat', multimodal: 'multimodal', image: 'image', edit: 'edit', video: 'video', enhance: 'enhance', lora: 'LoRA', addons: 'add-on', } export default function OllaBridgeModels({ filterType }: { filterType?: string } = {}) { const cloudUrl = getCloudUrl() const [token, setToken] = useState(getCloudToken()) const [devices, setDevices] = useState([]) const [models, setModels] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [connecting, setConnecting] = useState(false) const [activeModel, setActiveModel] = useState( () => { try { return localStorage.getItem('homepilot_model_chat') || '' } catch { return '' } }, ) const refresh = useCallback(async (tok: string) => { if (!tok) return setLoading(true) setError('') try { const auth = { Authorization: `Bearer ${tok}` } const [devRes, modRes] = await Promise.all([ fetch(`${cloudUrl}/v1/devices`, { headers: auth }), fetch(`${cloudUrl}/ollama/v1/models`, { headers: auth }), ]) if (devRes.status === 401 || modRes.status === 401) { setError('OllaBridge session expired — reconnect to refresh your nodes.') setToken('') try { localStorage.removeItem(LS_CLOUD_TOKEN) } catch { /* ignore */ } return } const devs = devRes.ok ? await devRes.json() : [] setDevices(Array.isArray(devs) ? devs : []) const mods = modRes.ok ? await modRes.json() : { data: [] } const list: CloudModel[] = Array.isArray(mods?.data) ? mods.data : [] // Own-node models first (relay/shared_device), then the rest. list.sort((a, b) => Number(b.source === 'shared_device') - Number(a.source === 'shared_device')) setModels(list) } catch { setError('Could not reach OllaBridge Cloud.') } finally { setLoading(false) } }, [cloudUrl]) useEffect(() => { if (token) void refresh(token) }, [token, refresh]) async function handleConnect(e: React.FormEvent) { e.preventDefault() setConnecting(true) setError('') try { const r = await fetch(`${cloudUrl}/v1/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email.trim(), password }), }) const data = await r.json().catch(() => ({})) if (!r.ok || !data.token) { setError(data.detail || 'Invalid OllaBridge email or password') return } try { // Batch 7 (BFF): the token lives in this component's memory for the // session; when BFF is on we no longer persist it in browser storage. if (!isBffSessionEnabled()) localStorage.setItem(LS_CLOUD_TOKEN, data.token) localStorage.setItem(LS_CLOUD_URL, cloudUrl) } catch { /* ignore */ } setToken(data.token) setPassword('') } catch { setError('Could not reach OllaBridge Cloud.') } finally { setConnecting(false) } } function handleDisconnect() { try { localStorage.removeItem(LS_CLOUD_TOKEN) } catch { /* ignore */ } setToken('') setDevices([]) setModels([]) } function useForChat(modelId: string) { setActiveModel(modelId) // App.tsx listens for this and flips chat provider to the OllaBridge relay // (openai_compat @ {cloud}/ollama/v1) + persists the choice. window.dispatchEvent(new CustomEvent('homepilot:use-gpu-node', { detail: { baseUrl: `${cloudUrl}/ollama/v1`, model: modelId }, })) } const onlineCount = devices.filter((d) => d.online).length return (
{/* Header */}

OllaBridge — your remote HomePilot nodes

Models synced from linked machines; inference runs on your own GPU through the relay.

{token && (
)}
{error && (
{error}
)} {!token ? ( /* Connect card */
setEmail(e.target.value)} placeholder="OllaBridge email" autoComplete="email" className="h-11 rounded-xl bg-black/40 border border-white/10 px-3 text-sm text-white placeholder:text-white/30 outline-none focus:border-cyan-400/50" /> setPassword(e.target.value)} placeholder="Password" autoComplete="current-password" className="h-11 rounded-xl bg-black/40 border border-white/10 px-3 text-sm text-white placeholder:text-white/30 outline-none focus:border-cyan-400/50" />

Sign in with your OllaBridge account to sync models from your linked HomePilot machines. Your token stays in this browser only.

) : ( <> {/* Devices strip */}
{devices.length === 0 && !loading && (

No linked machines yet. Pair a PC at{' '} {cloudUrl.replace(/^https?:\/\//, '')}/link {' '}— its models will appear here automatically.

)} {devices.map((d) => (
{d.name}
{d.gpu_name || d.platform || 'node'} {d.vram_mb ? ` · ${Math.round(d.vram_mb / 1024)} GB VRAM` : ''} {d.online ? ' · online' : ' · offline'}
))}
{/* Synced models — filtered to the active Model Type tab. */} {(() => { const shown = models.filter((m) => matchesTab(filterType, guessType(m.id))) if (shown.length === 0) { if (loading || devices.length === 0) return null const lbl = filterType ? (TAB_LABEL[filterType] || filterType) : '' return (

No {lbl} models advertised by your nodes yet. Pull {lbl} models on your linked HomePilot and they’ll sync here automatically.

) } return (
{shown.slice(0, 40).map((m) => { const own = m.source === 'shared_device' const type = guessType(m.id) const isActive = activeModel === m.id return (
{m.id} {own ? `your node${m.device_name ? ` · ${m.device_name}` : ''}` : (m.owned_by || m.source || 'cloud')} · {type} {own && ( GPU node )} {(type === 'chat' || type === 'multimodal') ? ( ) : ( Synced )}
) })}
) })()} )}
) }