HomePilot Deploy Bot commited on
feat(hf): OllaBridge provider class across all Model Type tabs
Browse files- .gitattributes +4 -4
- frontend/.cache-bust +1 -1
- frontend/src/ui/Models.tsx +29 -9
- frontend/src/ui/components/OllaBridgeModels.tsx +57 -22
.gitattributes
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
-
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 2 |
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 3 |
*.svg filter=lfs diff=lfs merge=lfs -text
|
| 4 |
*.ico filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
| 5 |
*.png filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.webp filter=lfs diff=lfs merge=lfs -text
|
| 7 |
*.woff filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.woff2 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.hpersona filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
| 1 |
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.webp filter=lfs diff=lfs merge=lfs -text
|
| 3 |
*.svg filter=lfs diff=lfs merge=lfs -text
|
| 4 |
*.ico filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.hpersona filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.woff2 filter=lfs diff=lfs merge=lfs -text
|
| 8 |
*.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 9 |
*.woff filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
frontend/.cache-bust
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
|
|
|
|
| 1 |
+
1783229390
|
frontend/src/ui/Models.tsx
CHANGED
|
@@ -588,6 +588,10 @@ export default function ModelsView(props: ModelsParams) {
|
|
| 588 |
|
| 589 |
const [modelType, setModelType] = useState<'chat' | 'multimodal' | 'image' | 'video' | 'edit' | 'enhance' | 'addons' | 'lora'>('chat')
|
| 590 |
const [provider, setProvider] = useState<string>(props.providerChat || 'ollama')
|
|
|
|
|
|
|
|
|
|
|
|
|
| 591 |
|
| 592 |
const defaultBaseUrl = useMemo(() => {
|
| 593 |
if (modelType === 'chat') return props.baseUrlChat || ''
|
|
@@ -660,14 +664,18 @@ export default function ModelsView(props: ModelsParams) {
|
|
| 660 |
|
| 661 |
// Filter providers based on model type
|
| 662 |
const availableProviders = useMemo(() => {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 663 |
if (modelType === 'chat') {
|
| 664 |
// For chat: all providers EXCEPT comfyui and civitai
|
| 665 |
-
return providers.filter(p => p.name !== 'comfyui' && p.name !== 'civitai')
|
| 666 |
} else if (modelType === 'multimodal') {
|
| 667 |
-
// Multimodal:
|
| 668 |
-
return providers.filter(p => p.name === 'ollama')
|
| 669 |
} else {
|
| 670 |
-
// For image/video/edit/enhance: comfyui + civitai (if experimental
|
| 671 |
const filtered = providers.filter(p => p.name === 'comfyui')
|
| 672 |
|
| 673 |
// Add Civitai only for image/video (Civitai API only supports these types)
|
|
@@ -679,11 +687,14 @@ export default function ModelsView(props: ModelsParams) {
|
|
| 679 |
})
|
| 680 |
}
|
| 681 |
|
| 682 |
-
return filtered
|
| 683 |
}
|
| 684 |
}, [providers, modelType, props.experimentalCivitai])
|
| 685 |
|
| 686 |
useEffect(() => {
|
|
|
|
|
|
|
|
|
|
| 687 |
// When modelType changes, update provider + baseUrl defaults
|
| 688 |
if (modelType === 'chat') {
|
| 689 |
// Switch to a chat provider (prefer the one from settings, or default to ollama)
|
|
@@ -762,7 +773,9 @@ export default function ModelsView(props: ModelsParams) {
|
|
| 762 |
// Skip fetching installed models for remote API providers and Civitai
|
| 763 |
// Remote providers: they're cloud services, not local installations
|
| 764 |
// Civitai: download-only provider, models get installed to ComfyUI after download
|
| 765 |
-
|
|
|
|
|
|
|
| 766 |
|
| 767 |
if (skipProviders.includes(provider)) {
|
| 768 |
setInstalled([])
|
|
@@ -2060,9 +2073,14 @@ export default function ModelsView(props: ModelsParams) {
|
|
| 2060 |
{/* Body */}
|
| 2061 |
<div className="flex-1 overflow-y-auto px-4 sm:px-8 py-6 scrollbar-hide">
|
| 2062 |
<div className="flex flex-col gap-4 max-w-7xl mx-auto">
|
| 2063 |
-
{/* OllaBridge provider class —
|
| 2064 |
-
|
| 2065 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2066 |
{/* Error messages - hide for Civitai since it's download-only */}
|
| 2067 |
{installedError && provider !== 'civitai' ? (
|
| 2068 |
<div className={`rounded-xl border p-5 ${
|
|
@@ -3061,6 +3079,8 @@ export default function ModelsView(props: ModelsParams) {
|
|
| 3061 |
)}
|
| 3062 |
</div>
|
| 3063 |
)}
|
|
|
|
|
|
|
| 3064 |
</div>
|
| 3065 |
</div>
|
| 3066 |
|
|
|
|
| 588 |
|
| 589 |
const [modelType, setModelType] = useState<'chat' | 'multimodal' | 'image' | 'video' | 'edit' | 'enhance' | 'addons' | 'lora'>('chat')
|
| 590 |
const [provider, setProvider] = useState<string>(props.providerChat || 'ollama')
|
| 591 |
+
// Latest provider for effects that only depend on [modelType] but must not
|
| 592 |
+
// clobber a manual OllaBridge selection when the user switches tabs.
|
| 593 |
+
const providerRef = React.useRef(provider)
|
| 594 |
+
React.useEffect(() => { providerRef.current = provider }, [provider])
|
| 595 |
|
| 596 |
const defaultBaseUrl = useMemo(() => {
|
| 597 |
if (modelType === 'chat') return props.baseUrlChat || ''
|
|
|
|
| 664 |
|
| 665 |
// Filter providers based on model type
|
| 666 |
const availableProviders = useMemo(() => {
|
| 667 |
+
// OllaBridge is a provider *class* available under EVERY Model Type — its
|
| 668 |
+
// models come from the user's linked HomePilot / GPU nodes (relay-synced),
|
| 669 |
+
// so web & mobile can use remote compute. Listed first as the hero option.
|
| 670 |
+
const ollabridge: Provider = { name: 'ollabridge', label: '☁️ OllaBridge (your nodes)', kind: modelType as any }
|
| 671 |
if (modelType === 'chat') {
|
| 672 |
// For chat: all providers EXCEPT comfyui and civitai
|
| 673 |
+
return [ollabridge, ...providers.filter(p => p.name !== 'comfyui' && p.name !== 'civitai')]
|
| 674 |
} else if (modelType === 'multimodal') {
|
| 675 |
+
// Multimodal: Ollama (vision models run locally) + OllaBridge nodes
|
| 676 |
+
return [ollabridge, ...providers.filter(p => p.name === 'ollama')]
|
| 677 |
} else {
|
| 678 |
+
// For image/video/edit/enhance/lora/addons: comfyui + civitai (if experimental) + OllaBridge
|
| 679 |
const filtered = providers.filter(p => p.name === 'comfyui')
|
| 680 |
|
| 681 |
// Add Civitai only for image/video (Civitai API only supports these types)
|
|
|
|
| 687 |
})
|
| 688 |
}
|
| 689 |
|
| 690 |
+
return [ollabridge, ...filtered]
|
| 691 |
}
|
| 692 |
}, [providers, modelType, props.experimentalCivitai])
|
| 693 |
|
| 694 |
useEffect(() => {
|
| 695 |
+
// Keep OllaBridge selected across Model Type tabs — it spans all types and
|
| 696 |
+
// its content is type-filtered, so don't reset it to a local provider.
|
| 697 |
+
if (providerRef.current === 'ollabridge') return
|
| 698 |
// When modelType changes, update provider + baseUrl defaults
|
| 699 |
if (modelType === 'chat') {
|
| 700 |
// Switch to a chat provider (prefer the one from settings, or default to ollama)
|
|
|
|
| 773 |
// Skip fetching installed models for remote API providers and Civitai
|
| 774 |
// Remote providers: they're cloud services, not local installations
|
| 775 |
// Civitai: download-only provider, models get installed to ComfyUI after download
|
| 776 |
+
// OllaBridge renders its own relay-synced list; the local backend has no
|
| 777 |
+
// catalog for it, so skip the fetch (avoids a spurious error banner).
|
| 778 |
+
const skipProviders = ['openai', 'claude', 'watsonx', 'civitai', 'ollabridge']
|
| 779 |
|
| 780 |
if (skipProviders.includes(provider)) {
|
| 781 |
setInstalled([])
|
|
|
|
| 2073 |
{/* Body */}
|
| 2074 |
<div className="flex-1 overflow-y-auto px-4 sm:px-8 py-6 scrollbar-hide">
|
| 2075 |
<div className="flex flex-col gap-4 max-w-7xl mx-auto">
|
| 2076 |
+
{/* OllaBridge provider class — a special provider whose models are
|
| 2077 |
+
synced from the user's linked HomePilot / GPU nodes (relay), shown
|
| 2078 |
+
for whichever Model Type tab is active. When selected it replaces
|
| 2079 |
+
the local catalog for this tab. */}
|
| 2080 |
+
{provider === 'ollabridge' ? (
|
| 2081 |
+
<OllaBridgeModels filterType={modelType} />
|
| 2082 |
+
) : (
|
| 2083 |
+
<>
|
| 2084 |
{/* Error messages - hide for Civitai since it's download-only */}
|
| 2085 |
{installedError && provider !== 'civitai' ? (
|
| 2086 |
<div className={`rounded-xl border p-5 ${
|
|
|
|
| 3079 |
)}
|
| 3080 |
</div>
|
| 3081 |
)}
|
| 3082 |
+
</>
|
| 3083 |
+
)}
|
| 3084 |
</div>
|
| 3085 |
</div>
|
| 3086 |
|
frontend/src/ui/components/OllaBridgeModels.tsx
CHANGED
|
@@ -58,18 +58,39 @@ interface CloudModel {
|
|
| 58 |
device_name?: string
|
| 59 |
}
|
| 60 |
|
| 61 |
-
/** Loose model-type guess from the model id —
|
| 62 |
function guessType(id: string): string {
|
| 63 |
const s = id.toLowerCase()
|
| 64 |
if (/(llava|vision|-vl|minicpm-v|moondream)/.test(s)) return 'multimodal'
|
| 65 |
if (/(sdxl|flux|stable-diffusion|sd15|sd3|pony|dreamshaper)/.test(s)) return 'image'
|
| 66 |
if (/(svd|ltx|wan|video)/.test(s)) return 'video'
|
| 67 |
-
if (/(upscal|esrgan|enhance|restore)/.test(s)) return 'enhance'
|
| 68 |
if (/lora/.test(s)) return 'LoRA'
|
| 69 |
return 'chat'
|
| 70 |
}
|
| 71 |
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
const cloudUrl = getCloudUrl()
|
| 74 |
const [token, setToken] = useState(getCloudToken())
|
| 75 |
const [devices, setDevices] = useState<DeviceInfo[]>([])
|
|
@@ -267,10 +288,22 @@ export default function OllaBridgeModels() {
|
|
| 267 |
))}
|
| 268 |
</div>
|
| 269 |
|
| 270 |
-
{/* Synced models */}
|
| 271 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
<div className="grid gap-1.5">
|
| 273 |
-
{
|
| 274 |
const own = m.source === 'shared_device'
|
| 275 |
const type = guessType(m.id)
|
| 276 |
const isActive = activeModel === m.id
|
|
@@ -290,26 +323,28 @@ export default function OllaBridgeModels() {
|
|
| 290 |
GPU node
|
| 291 |
</span>
|
| 292 |
)}
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
</div>
|
| 306 |
)
|
| 307 |
})}
|
| 308 |
</div>
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
<p className="text-xs text-white/40">No models advertised by your nodes yet — make sure the connector is running and models are pulled.</p>
|
| 312 |
-
)}
|
| 313 |
</>
|
| 314 |
)}
|
| 315 |
</section>
|
|
|
|
| 58 |
device_name?: string
|
| 59 |
}
|
| 60 |
|
| 61 |
+
/** Loose model-type guess from the model id — drives badges + per-tab filter. */
|
| 62 |
function guessType(id: string): string {
|
| 63 |
const s = id.toLowerCase()
|
| 64 |
if (/(llava|vision|-vl|minicpm-v|moondream)/.test(s)) return 'multimodal'
|
| 65 |
if (/(sdxl|flux|stable-diffusion|sd15|sd3|pony|dreamshaper)/.test(s)) return 'image'
|
| 66 |
if (/(svd|ltx|wan|video)/.test(s)) return 'video'
|
| 67 |
+
if (/(upscal|esrgan|enhance|restore|gfpgan|codeformer)/.test(s)) return 'enhance'
|
| 68 |
if (/lora/.test(s)) return 'LoRA'
|
| 69 |
return 'chat'
|
| 70 |
}
|
| 71 |
|
| 72 |
+
/** Does a model's guessed type belong under the given Model Type tab? */
|
| 73 |
+
function matchesTab(filterType: string | undefined, guessed: string): boolean {
|
| 74 |
+
if (!filterType) return true
|
| 75 |
+
switch (filterType) {
|
| 76 |
+
case 'chat': return guessed === 'chat'
|
| 77 |
+
case 'multimodal': return guessed === 'multimodal'
|
| 78 |
+
case 'image':
|
| 79 |
+
case 'edit': return guessed === 'image'
|
| 80 |
+
case 'video': return guessed === 'video'
|
| 81 |
+
case 'enhance': return guessed === 'enhance'
|
| 82 |
+
case 'lora': return guessed === 'LoRA'
|
| 83 |
+
case 'addons': return true
|
| 84 |
+
default: return true
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
const TAB_LABEL: Record<string, string> = {
|
| 89 |
+
chat: 'chat', multimodal: 'multimodal', image: 'image', edit: 'edit',
|
| 90 |
+
video: 'video', enhance: 'enhance', lora: 'LoRA', addons: 'add-on',
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
export default function OllaBridgeModels({ filterType }: { filterType?: string } = {}) {
|
| 94 |
const cloudUrl = getCloudUrl()
|
| 95 |
const [token, setToken] = useState(getCloudToken())
|
| 96 |
const [devices, setDevices] = useState<DeviceInfo[]>([])
|
|
|
|
| 288 |
))}
|
| 289 |
</div>
|
| 290 |
|
| 291 |
+
{/* Synced models — filtered to the active Model Type tab. */}
|
| 292 |
+
{(() => {
|
| 293 |
+
const shown = models.filter((m) => matchesTab(filterType, guessType(m.id)))
|
| 294 |
+
if (shown.length === 0) {
|
| 295 |
+
if (loading || devices.length === 0) return null
|
| 296 |
+
const lbl = filterType ? (TAB_LABEL[filterType] || filterType) : ''
|
| 297 |
+
return (
|
| 298 |
+
<p className="text-xs text-white/40 py-1">
|
| 299 |
+
No {lbl} models advertised by your nodes yet. Pull {lbl} models on your linked
|
| 300 |
+
HomePilot and they’ll sync here automatically.
|
| 301 |
+
</p>
|
| 302 |
+
)
|
| 303 |
+
}
|
| 304 |
+
return (
|
| 305 |
<div className="grid gap-1.5">
|
| 306 |
+
{shown.slice(0, 40).map((m) => {
|
| 307 |
const own = m.source === 'shared_device'
|
| 308 |
const type = guessType(m.id)
|
| 309 |
const isActive = activeModel === m.id
|
|
|
|
| 323 |
GPU node
|
| 324 |
</span>
|
| 325 |
)}
|
| 326 |
+
{(type === 'chat' || type === 'multimodal') ? (
|
| 327 |
+
<button
|
| 328 |
+
type="button"
|
| 329 |
+
onClick={() => useForChat(m.id)}
|
| 330 |
+
className={[
|
| 331 |
+
'shrink-0 h-8 px-3 rounded-lg text-xs font-semibold inline-flex items-center gap-1.5 transition-colors',
|
| 332 |
+
isActive
|
| 333 |
+
? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30'
|
| 334 |
+
: 'bg-white/5 hover:bg-white/10 border border-white/10 text-white/70 hover:text-white',
|
| 335 |
+
].join(' ')}
|
| 336 |
+
>
|
| 337 |
+
<Zap size={12} /> {isActive ? 'Active' : 'Use for Chat'}
|
| 338 |
+
</button>
|
| 339 |
+
) : (
|
| 340 |
+
<span className="shrink-0 text-[10px] text-white/40 px-2 py-1 rounded-md bg-white/5 border border-white/10">Synced</span>
|
| 341 |
+
)}
|
| 342 |
</div>
|
| 343 |
)
|
| 344 |
})}
|
| 345 |
</div>
|
| 346 |
+
)
|
| 347 |
+
})()}
|
|
|
|
|
|
|
| 348 |
</>
|
| 349 |
)}
|
| 350 |
</section>
|