"""
Model Repository Browser — modal for importing models from a curated catalog.
Presents model cards organized by category with import animation
and provenance tracking.
"""
from __future__ import annotations
import time
import threading
from typing import TYPE_CHECKING, Dict, Set
import panel as pn
import param
from models.catalog import ModelCatalogEntry, get_model_catalog, FILTER_OPTIONS
if TYPE_CHECKING:
from ui.state import AppState
_CARD_BG = "#FFFFFF"
_CARD_BORDER = "#CBD5E1"
_ANALYTICAL_BADGE = "#0284C7" # sky-600
_GENERATIVE_BADGE = "#7C3AED" # violet-600
_EMBEDDED_BADGE = "#059669" # emerald-600
_API_BADGE = "#D97706" # amber-600
_IMPORTED_BADGE = "#059669" # emerald-600
class ModelRepositoryPanel(param.Parameterized):
"""Model Repository Browser panel."""
def __init__(self, state: "AppState", **params: object) -> None:
super().__init__(**params)
self._state = state
self._catalog = get_model_catalog()
self._imported_names: Set[str] = set()
self._import_status: Dict[str, pn.pane.HTML] = {}
self._active_filter = "All"
self._search_text = ""
self._cards_container = pn.Column(sizing_mode="stretch_width")
def _get_imported_model_names(self) -> Set[str]:
"""Get names of already-imported models."""
names = set()
if self._state.model_registry:
for model_reg in self._state.model_registry.all_models:
names.add(model_reg.model.name)
return names | self._imported_names
def _matches_filter(self, entry: ModelCatalogEntry) -> bool:
"""Check if a catalog entry matches the active filter and search."""
# Search filter
if self._search_text:
search = self._search_text.lower()
searchable = f"{entry.name} {entry.description} {entry.category} {' '.join(entry.tags)}".lower()
if search not in searchable:
return False
# Category filter
f = self._active_filter
if f == "All":
return True
if f == "Analytical":
return entry.model_type == "analytical"
if f == "Generative":
return entry.model_type == "generative"
if f == "Structure":
return "Structure" in entry.category
if f == "Stability":
return "Stability" in entry.category or "Degradation" in entry.category or "Half-life" in entry.category
if f == "Codon":
return "Codon" in entry.category
if f == "UTR":
return "UTR" in entry.category
if f == "Foundation":
return "Foundation" in entry.category
return True
def _build_model_card(self, entry: ModelCatalogEntry) -> pn.Column:
"""Build a single model card."""
imported = entry.name in self._get_imported_model_names()
# Type badge
if entry.model_type == "analytical":
type_badge = f'ANALYTICAL'
else:
type_badge = f'GENERATIVE'
# Deployment badge
if entry.deployment == "embedded":
deploy_badge = f'Embedded'
elif entry.deployment == "api":
deploy_badge = f'API'
else:
deploy_badge = f'Both'
# Repository icon
if "github.com" in entry.repository:
repo_icon = ""
elif "huggingface" in entry.repository:
repo_icon = ""
else:
repo_icon = ""
# Status area
status_pane = pn.pane.HTML("", sizing_mode="stretch_width")
self._import_status[entry.name] = status_pane
# Import button or imported badge
if imported:
action_widget = pn.pane.HTML(
f''
f'Imported',
margin=(4, 0),
)
else:
import_btn = pn.widgets.Button(
name="Import",
button_type="primary",
width=80,
margin=(4, 0),
stylesheets=["""
:host .bk-btn {
font-size: 11px;
padding: 4px 12px;
border-radius: 4px;
}
"""],
)
import_btn.on_click(lambda event, e=entry: self._on_import_model(e))
action_widget = import_btn
card_html = pn.pane.HTML(
f'
'
f'
'
f'
'
f'
'
f'{entry.name} v{entry.version}
'
f'
{type_badge} '
f'{entry.category} '
f'{deploy_badge}
'
f'
{entry.description}
'
f'
{entry.repository}
'
f'
Input: {entry.inputs}
'
f'
'
f'
'
f'
',
sizing_mode="stretch_width",
margin=0,
)
return pn.Column(
card_html,
pn.Row(action_widget, status_pane, sizing_mode="stretch_width", margin=(0, 14, 8, 14)),
sizing_mode="stretch_width",
margin=0,
)
def _on_import_model(self, entry: ModelCatalogEntry) -> None:
"""Handle model import with animated workflow."""
status = self._import_status.get(entry.name)
if not status:
return
def _run_import():
# Step 1: Connecting
status.object = (
''
'Connecting to repository...
'
)
time.sleep(0.8)
# Step 2: Downloading
status.object = (
''
'Downloading model...
'
)
time.sleep(0.8)
# Step 3: Registering
status.object = (
''
'Registering model...
'
)
time.sleep(0.5)
# Register the model as a demo stub
self._register_catalog_model(entry)
# Step 4: Done
status.object = (
f''
f'Successfully imported {entry.name}
'
)
self._imported_names.add(entry.name)
self._state.param.trigger("model_registry")
thread = threading.Thread(target=_run_import, daemon=True)
thread.start()
def _register_catalog_model(self, entry: ModelCatalogEntry) -> None:
"""Register a catalog model as a demo stub in the model registry."""
from models.base import ScoringModel, GenerativeModel, RegisteredModel
if entry.model_type == "analytical":
# Create a demo scoring model stub
class CatalogScorer(ScoringModel):
_entry = entry
@property
def name(self):
return self._entry.name
@property
def description(self):
return self._entry.description
@property
def version(self):
return self._entry.version
def score(self, sequence, metadata=None):
import random
return round(random.uniform(0.3, 0.95), 3)
model = CatalogScorer()
self._state.model_registry._register(
model, "scoring", "catalog",
entry.repository_url,
)
# Store provenance
reg = self._state.model_registry._models[entry.name]
reg.repository = entry.repository
reg.category = entry.category
else:
# Create a demo generative model stub
class CatalogGenerator(GenerativeModel):
_entry = entry
@property
def name(self):
return self._entry.name
@property
def description(self):
return self._entry.description
@property
def version(self):
return self._entry.version
def generate(self, constraints, n=10, seed=None):
return []
model = CatalogGenerator()
self._state.model_registry._register(
model, "generative", "catalog",
entry.repository_url,
)
reg = self._state.model_registry._models[entry.name]
reg.repository = entry.repository
reg.category = entry.category
def _rebuild_cards(self) -> None:
"""Rebuild the cards container based on current filter."""
self._cards_container.clear()
filtered = [e for e in self._catalog if self._matches_filter(e)]
if not filtered:
self._cards_container.append(pn.pane.HTML(
''
'No models match the current filter.
'
))
return
# Group by type
analytical = [e for e in filtered if e.model_type == "analytical"]
generative = [e for e in filtered if e.model_type == "generative"]
if analytical:
self._cards_container.append(pn.pane.HTML(
f''
f'Analytical Models ({len(analytical)})
'
))
for entry in analytical:
self._cards_container.append(self._build_model_card(entry))
if generative:
self._cards_container.append(pn.pane.HTML(
f''
f'Generative Models ({len(generative)})
'
))
for entry in generative:
self._cards_container.append(self._build_model_card(entry))
def panel(self) -> pn.Column:
"""Build the full repository browser panel."""
# Search bar
search_input = pn.widgets.TextInput(
name="",
placeholder="Search models...",
width=300,
margin=(4, 8),
)
def on_search(event):
self._search_text = event.new
self._rebuild_cards()
search_input.param.watch(on_search, "value")
# Filter chips
filter_buttons = []
for f in FILTER_OPTIONS:
btn = pn.widgets.Button(
name=f,
button_type="primary" if f == self._active_filter else "light",
width=90,
margin=(2, 2),
stylesheets=[f"""
:host .bk-btn {{
font-size: 11px;
padding: 4px 10px;
border-radius: 16px;
}}
"""],
)
def on_filter(event, filter_name=f):
self._active_filter = filter_name
for b in filter_buttons:
b.button_type = "primary" if b.name == filter_name else "light"
self._rebuild_cards()
btn.on_click(on_filter)
filter_buttons.append(btn)
filter_row = pn.Row(*filter_buttons, sizing_mode="stretch_width", margin=(4, 0))
# Build initial cards
self._rebuild_cards()
return pn.Column(
pn.pane.HTML(
''
'Model Repository
'
''
'Browse and import public mRNA models. Each model is imported from its '
'source repository and registered for use in analysis and generation workflows.
'
),
pn.Row(search_input, sizing_mode="stretch_width"),
filter_row,
pn.layout.Divider(),
self._cards_container,
sizing_mode="stretch_width",
styles={"padding": "8px 16px", "max-height": "70vh", "overflow-y": "auto"},
)