From 7fd2e75a1cb428c26500262713acbcb71afec923 Mon Sep 17 00:00:00 2001 From: jovonni Date: Sat, 7 Mar 2026 16:31:11 -0500 Subject: [PATCH 01/16] viz poc, adding model manager tests, dashboard panels, db work, and docs --- api/src/main.rs | 24 + api/src/routes/mod.rs | 1 + api/src/routes/visualizations.rs | 337 ++++++++ db/init.sql | 40 + db/migrations/003_visualizations.sql | 38 + docs/CLI-REGISTRY.md | 265 +++++++ docs/MODELING.md | 85 ++ docs/VISUALIZATIONS.md | 372 +++++++++ sdk/python/openmodelstudio/__init__.py | 71 +- sdk/python/openmodelstudio/cli.py | 209 +++++ sdk/python/openmodelstudio/config.py | 68 ++ sdk/python/openmodelstudio/registry.py | 204 +++++ sdk/python/openmodelstudio/visualization.py | 446 +++++++++++ sdk/python/pyproject.toml | 3 + web/package.json | 2 + web/pnpm-lock.yaml | 70 ++ web/src/app/dashboards/[id]/page.tsx | 620 +++++++++++++++ web/src/app/dashboards/page.tsx | 296 +++++++ web/src/app/globals.css | 69 ++ web/src/app/registry/[id]/page.tsx | 558 ++++++++++++++ web/src/app/registry/page.tsx | 502 ++++++++++++ web/src/app/visualizations/[id]/page.tsx | 811 ++++++++++++++++++++ web/src/app/visualizations/page.tsx | 421 ++++++++++ web/src/components/layout/sidebar.tsx | 11 + web/src/components/shared/viz-renderer.tsx | 297 +++++++ 25 files changed, 5818 insertions(+), 2 deletions(-) create mode 100644 api/src/routes/visualizations.rs create mode 100644 db/migrations/003_visualizations.sql create mode 100644 docs/CLI-REGISTRY.md create mode 100644 docs/VISUALIZATIONS.md create mode 100644 sdk/python/openmodelstudio/cli.py create mode 100644 sdk/python/openmodelstudio/config.py create mode 100644 sdk/python/openmodelstudio/registry.py create mode 100644 sdk/python/openmodelstudio/visualization.py create mode 100644 web/src/app/dashboards/[id]/page.tsx create mode 100644 web/src/app/dashboards/page.tsx create mode 100644 web/src/app/registry/[id]/page.tsx create mode 100644 web/src/app/registry/page.tsx create mode 100644 web/src/app/visualizations/[id]/page.tsx create mode 100644 web/src/app/visualizations/page.tsx create mode 100644 web/src/components/shared/viz-renderer.tsx diff --git a/api/src/main.rs b/api/src/main.rs index aeb2ba0..9f39331 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -201,6 +201,30 @@ async fn main() { .route("/sdk/sweeps", post(routes::sdk::create_sweep)) .route("/sdk/sweeps/{id}", get(routes::sdk::get_sweep)) .route("/sdk/sweeps/{id}/stop", post(routes::sdk::stop_sweep)) + // SDK Visualizations + .route("/sdk/visualizations", get(routes::visualizations::list_all)) + .route("/sdk/visualizations", post(routes::visualizations::create)) + .route("/sdk/visualizations/{id}", get(routes::visualizations::get)) + .route("/sdk/visualizations/{id}/publish", post(routes::visualizations::publish)) + .route("/sdk/visualizations/{id}/render", post(routes::visualizations::get)) + // SDK Dashboards + .route("/sdk/dashboards", get(routes::visualizations::list_dashboards)) + .route("/sdk/dashboards", post(routes::visualizations::create_dashboard)) + .route("/sdk/dashboards/{id}", get(routes::visualizations::get_dashboard)) + .route("/sdk/dashboards/{id}", put(routes::visualizations::update_dashboard)) + // Visualizations + .route("/visualizations", get(routes::visualizations::list_all)) + .route("/visualizations", post(routes::visualizations::create)) + .route("/visualizations/{id}", get(routes::visualizations::get)) + .route("/visualizations/{id}", put(routes::visualizations::update)) + .route("/visualizations/{id}", delete(routes::visualizations::delete)) + .route("/visualizations/{id}/publish", post(routes::visualizations::publish)) + // Dashboards + .route("/dashboards", get(routes::visualizations::list_dashboards)) + .route("/dashboards", post(routes::visualizations::create_dashboard)) + .route("/dashboards/{id}", get(routes::visualizations::get_dashboard)) + .route("/dashboards/{id}", put(routes::visualizations::update_dashboard)) + .route("/dashboards/{id}", delete(routes::visualizations::delete_dashboard)) // Admin .route("/admin/users", get(routes::admin::list_users)) .route("/admin/users/{id}", put(routes::admin::update_user)) diff --git a/api/src/routes/mod.rs b/api/src/routes/mod.rs index 7993890..88fa2c4 100644 --- a/api/src/routes/mod.rs +++ b/api/src/routes/mod.rs @@ -20,3 +20,4 @@ pub mod monitoring; pub mod automl; pub mod api_keys; pub mod sdk; +pub mod visualizations; diff --git a/api/src/routes/visualizations.rs b/api/src/routes/visualizations.rs new file mode 100644 index 0000000..f8775b2 --- /dev/null +++ b/api/src/routes/visualizations.rs @@ -0,0 +1,337 @@ +use axum::{ + extract::{Path, Query, State}, + Json, +}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; +use uuid::Uuid; + +use crate::error::{AppError, AppResult}; +use crate::middleware::auth::AuthUser; +use crate::AppState; + +#[derive(Deserialize)] +pub struct ListParams { + pub project_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct Visualization { + pub id: Uuid, + pub project_id: Option, + pub name: String, + pub description: Option, + pub backend: String, + pub output_type: String, + pub code: Option, + pub config: Option, + pub rendered_output: Option, + pub refresh_interval: Option, + pub published: bool, + pub created_at: Option>, + pub updated_at: Option>, +} + +#[derive(Deserialize)] +pub struct CreateVisualization { + pub project_id: Option, + pub name: String, + pub description: Option, + pub backend: String, + pub output_type: Option, + pub code: Option, + pub data: Option, + pub config: Option, + pub refresh_interval: Option, +} + +#[derive(Deserialize)] +pub struct UpdateVisualization { + pub name: Option, + pub description: Option, + pub code: Option, + pub data: Option, + pub config: Option, + pub refresh_interval: Option, + pub rendered_output: Option, +} + +pub async fn list_all( + State(state): State, + AuthUser(_claims): AuthUser, + Query(params): Query, +) -> AppResult>> { + let rows: Vec = if let Some(pid) = params.project_id { + sqlx::query_as( + "SELECT id, project_id, name, description, backend, output_type, code, + config, rendered_output, refresh_interval, published, + created_at, updated_at + FROM visualizations WHERE project_id = $1 + ORDER BY updated_at DESC" + ) + .bind(pid) + .fetch_all(&state.db) + .await? + } else { + sqlx::query_as( + "SELECT id, project_id, name, description, backend, output_type, code, + config, rendered_output, refresh_interval, published, + created_at, updated_at + FROM visualizations + ORDER BY updated_at DESC" + ) + .fetch_all(&state.db) + .await? + }; + Ok(Json(rows)) +} + +pub async fn create( + State(state): State, + AuthUser(claims): AuthUser, + Json(body): Json, +) -> AppResult> { + let id = Uuid::new_v4(); + let output_type = body.output_type.unwrap_or_else(|| { + match body.backend.as_str() { + "matplotlib" | "seaborn" | "plotnine" | "networkx" | "geopandas" => "svg".to_string(), + "plotly" => "plotly".to_string(), + "bokeh" => "bokeh".to_string(), + "altair" => "vega-lite".to_string(), + "datashader" => "png".to_string(), + _ => "svg".to_string(), + } + }); + + sqlx::query( + "INSERT INTO visualizations (id, project_id, name, description, backend, output_type, code, data, config, refresh_interval, created_by) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)" + ) + .bind(id) + .bind(body.project_id) + .bind(&body.name) + .bind(&body.description) + .bind(&body.backend) + .bind(&output_type) + .bind(&body.code) + .bind(&body.data) + .bind(&body.config) + .bind(body.refresh_interval.unwrap_or(0)) + .bind(claims.sub) + .execute(&state.db) + .await?; + + Ok(Json(serde_json::json!({ + "id": id, + "name": body.name, + "backend": body.backend, + "output_type": output_type, + }))) +} + +pub async fn get( + State(state): State, + AuthUser(_claims): AuthUser, + Path(id): Path, +) -> AppResult> { + let viz: Visualization = sqlx::query_as( + "SELECT id, project_id, name, description, backend, output_type, code, + config, rendered_output, refresh_interval, published, + created_at, updated_at + FROM visualizations WHERE id = $1" + ) + .bind(id) + .fetch_optional(&state.db) + .await? + .ok_or(AppError::NotFound("Visualization not found".into()))?; + Ok(Json(viz)) +} + +pub async fn update( + State(state): State, + AuthUser(_claims): AuthUser, + Path(id): Path, + Json(body): Json, +) -> AppResult> { + sqlx::query( + "UPDATE visualizations SET + name = COALESCE($2, name), + description = COALESCE($3, description), + code = COALESCE($4, code), + data = COALESCE($5, data), + config = COALESCE($6, config), + refresh_interval = COALESCE($7, refresh_interval), + rendered_output = COALESCE($8, rendered_output), + updated_at = now() + WHERE id = $1" + ) + .bind(id) + .bind(&body.name) + .bind(&body.description) + .bind(&body.code) + .bind(&body.data) + .bind(&body.config) + .bind(body.refresh_interval) + .bind(&body.rendered_output) + .execute(&state.db) + .await?; + Ok(Json(serde_json::json!({"updated": true}))) +} + +pub async fn delete( + State(state): State, + AuthUser(_claims): AuthUser, + Path(id): Path, +) -> AppResult> { + sqlx::query("DELETE FROM visualizations WHERE id = $1") + .bind(id) + .execute(&state.db) + .await?; + Ok(Json(serde_json::json!({"deleted": true}))) +} + +pub async fn publish( + State(state): State, + AuthUser(_claims): AuthUser, + Path(id): Path, +) -> AppResult> { + sqlx::query("UPDATE visualizations SET published = true, updated_at = now() WHERE id = $1") + .bind(id) + .execute(&state.db) + .await?; + Ok(Json(serde_json::json!({"published": true}))) +} + +// ── Dashboards ────────────────────────────────────────────────────── + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct Dashboard { + pub id: Uuid, + pub project_id: Option, + pub name: String, + pub description: Option, + pub layout: Option, + pub published: bool, + pub created_at: Option>, + pub updated_at: Option>, +} + +#[derive(Deserialize)] +pub struct CreateDashboard { + pub project_id: Option, + pub name: String, + pub description: Option, + pub layout: Option, +} + +#[derive(Deserialize)] +pub struct UpdateDashboard { + pub name: Option, + pub description: Option, + pub layout: Option, +} + +pub async fn list_dashboards( + State(state): State, + AuthUser(_claims): AuthUser, + Query(params): Query, +) -> AppResult>> { + let rows: Vec = if let Some(pid) = params.project_id { + sqlx::query_as( + "SELECT id, project_id, name, description, layout, published, created_at, updated_at + FROM dashboards WHERE project_id = $1 + ORDER BY updated_at DESC" + ) + .bind(pid) + .fetch_all(&state.db) + .await? + } else { + sqlx::query_as( + "SELECT id, project_id, name, description, layout, published, created_at, updated_at + FROM dashboards + ORDER BY updated_at DESC" + ) + .fetch_all(&state.db) + .await? + }; + Ok(Json(rows)) +} + +pub async fn create_dashboard( + State(state): State, + AuthUser(claims): AuthUser, + Json(body): Json, +) -> AppResult> { + let id = Uuid::new_v4(); + let layout = body.layout.unwrap_or(serde_json::json!([])); + + sqlx::query( + "INSERT INTO dashboards (id, project_id, name, description, layout, created_by) + VALUES ($1, $2, $3, $4, $5, $6)" + ) + .bind(id) + .bind(body.project_id) + .bind(&body.name) + .bind(&body.description) + .bind(&layout) + .bind(claims.sub) + .execute(&state.db) + .await?; + + Ok(Json(serde_json::json!({ + "id": id, + "name": body.name, + }))) +} + +pub async fn get_dashboard( + State(state): State, + AuthUser(_claims): AuthUser, + Path(id): Path, +) -> AppResult> { + let dash: Dashboard = sqlx::query_as( + "SELECT id, project_id, name, description, layout, published, created_at, updated_at + FROM dashboards WHERE id = $1" + ) + .bind(id) + .fetch_optional(&state.db) + .await? + .ok_or(AppError::NotFound("Dashboard not found".into()))?; + Ok(Json(dash)) +} + +pub async fn update_dashboard( + State(state): State, + AuthUser(_claims): AuthUser, + Path(id): Path, + Json(body): Json, +) -> AppResult> { + sqlx::query( + "UPDATE dashboards SET + name = COALESCE($2, name), + description = COALESCE($3, description), + layout = COALESCE($4, layout), + updated_at = now() + WHERE id = $1" + ) + .bind(id) + .bind(&body.name) + .bind(&body.description) + .bind(&body.layout) + .execute(&state.db) + .await?; + Ok(Json(serde_json::json!({"updated": true}))) +} + +pub async fn delete_dashboard( + State(state): State, + AuthUser(_claims): AuthUser, + Path(id): Path, +) -> AppResult> { + sqlx::query("DELETE FROM dashboards WHERE id = $1") + .bind(id) + .execute(&state.db) + .await?; + Ok(Json(serde_json::json!({"deleted": true}))) +} diff --git a/db/init.sql b/db/init.sql index a5c41c4..a253819 100644 --- a/db/init.sql +++ b/db/init.sql @@ -427,3 +427,43 @@ CREATE TRIGGER trg_projects_updated_at BEFORE UPDATE ON projects FOR EACH ROW EX CREATE TRIGGER trg_models_updated_at BEFORE UPDATE ON models FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER trg_jobs_updated_at BEFORE UPDATE ON jobs FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER trg_workspaces_updated_at BEFORE UPDATE ON workspaces FOR EACH ROW EXECUTE FUNCTION update_updated_at(); + +-- ============================================================ +-- VISUALIZATIONS +-- ============================================================ +CREATE TABLE IF NOT EXISTS visualizations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + project_id UUID REFERENCES projects(id) ON DELETE SET NULL, + name TEXT NOT NULL, + description TEXT, + backend TEXT NOT NULL, -- matplotlib, seaborn, plotly, bokeh, altair, plotnine, datashader, networkx, geopandas + output_type TEXT NOT NULL, -- svg, plotly, bokeh, vega-lite, png + code TEXT, -- Python code with render(ctx) function + data JSONB, -- Data payload + config JSONB, -- Config (width, height, theme, etc.) + rendered_output TEXT, -- Cached rendered output + refresh_interval INT DEFAULT 0, -- 0 = static, >0 = seconds between refreshes + published BOOLEAN DEFAULT false, + created_by UUID NOT NULL REFERENCES users(id), + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS idx_visualizations_project ON visualizations(project_id); + +-- ============================================================ +-- DASHBOARDS +-- ============================================================ +CREATE TABLE IF NOT EXISTS dashboards ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + project_id UUID REFERENCES projects(id) ON DELETE SET NULL, + name TEXT NOT NULL, + description TEXT, + layout JSONB DEFAULT '[]'::jsonb, -- Array of {visualization_id, x, y, w, h} + published BOOLEAN DEFAULT false, + created_by UUID NOT NULL REFERENCES users(id), + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS idx_dashboards_project ON dashboards(project_id); diff --git a/db/migrations/003_visualizations.sql b/db/migrations/003_visualizations.sql new file mode 100644 index 0000000..85011fc --- /dev/null +++ b/db/migrations/003_visualizations.sql @@ -0,0 +1,38 @@ +-- Migration 003: Visualizations and Dashboards +-- Adds tables for visualization rendering and dashboard composition + +-- Visualizations +CREATE TABLE IF NOT EXISTS visualizations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + project_id UUID REFERENCES projects(id) ON DELETE SET NULL, + name TEXT NOT NULL, + description TEXT, + backend TEXT NOT NULL, -- matplotlib, seaborn, plotly, bokeh, altair, plotnine, datashader, networkx, geopandas + output_type TEXT NOT NULL, -- svg, plotly, bokeh, vega-lite, png + code TEXT, -- Python code with render(ctx) function + data JSONB, -- Data payload + config JSONB, -- Config (width, height, theme, etc.) + rendered_output TEXT, -- Cached rendered output + refresh_interval INT DEFAULT 0, -- 0 = static, >0 = seconds between refreshes + published BOOLEAN DEFAULT false, + created_by UUID NOT NULL REFERENCES users(id), + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS idx_visualizations_project ON visualizations(project_id); + +-- Dashboards +CREATE TABLE IF NOT EXISTS dashboards ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + project_id UUID REFERENCES projects(id) ON DELETE SET NULL, + name TEXT NOT NULL, + description TEXT, + layout JSONB DEFAULT '[]'::jsonb, -- Array of {visualization_id, x, y, w, h} + published BOOLEAN DEFAULT false, + created_by UUID NOT NULL REFERENCES users(id), + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS idx_dashboards_project ON dashboards(project_id); diff --git a/docs/CLI-REGISTRY.md b/docs/CLI-REGISTRY.md new file mode 100644 index 0000000..30b3367 --- /dev/null +++ b/docs/CLI-REGISTRY.md @@ -0,0 +1,265 @@ +# CLI & Model Registry + +Install, search, and manage models from the command line using the `openmodelstudio` CLI. Models are published in the [Open Model Registry](https://github.com/GACWR/open-model-registry), a public GitHub repository that acts as a decentralized model package manager. + +## Installation + +```bash +pip install openmodelstudio +``` + +This installs both the Python SDK and the `openmodelstudio` CLI command. + +## Commands + +### Search for Models + +```bash +openmodelstudio search classification +``` + +Output: + +``` +NAME VERSION FRAMEWORK CATEGORY DESCRIPTION +--------------- ------- --------- -------------- ------------------------------------------- +iris-svm 1.0.0 sklearn classification Support Vector Machine classifier for the... +titanic-rf 1.0.0 sklearn classification Random Forest classifier for Titanic surv... +``` + +Filter by framework or category: + +```bash +openmodelstudio search cnn --framework pytorch +openmodelstudio search "" --category nlp +openmodelstudio search "" --framework sklearn --category classification +``` + +### Browse All Registry Models + +```bash +openmodelstudio registry +``` + +Output: + +``` +NAME VERSION FRAMEWORK CATEGORY AUTHOR DESCRIPTION +--------------- ------- --------- -------------- ---------------- --------------------------- +iris-svm 1.0.0 sklearn classification openmodelstudio Support Vector Machine cla... +mnist-cnn 1.0.0 pytorch computer-vision openmodelstudio Convolutional Neural Netwo... +sentiment-lstm 1.0.0 pytorch nlp openmodelstudio Bidirectional LSTM for tex... +timeseries-arima 1.0.0 python time-series openmodelstudio ARIMA model for univariate... +titanic-rf 1.0.0 sklearn classification openmodelstudio Random Forest classifier f... +``` + +### Get Model Details + +```bash +openmodelstudio info mnist-cnn +``` + +Output: + +``` +Name: mnist-cnn +Version: 1.0.0 +Author: openmodelstudio +Framework: pytorch +Category: computer-vision +License: MIT +Description: Convolutional Neural Network for MNIST digit classification. +Tags: image-classification, cnn, mnist, beginner, deep-learning +Dependencies: torch>=2.0, torchvision>=0.15, numpy>=1.24 +Homepage: https://github.com/GACWR/open-model-registry +``` + +### Install a Model + +```bash +openmodelstudio install titanic-rf +``` + +Output: + +``` +Installing 'titanic-rf' from registry... +Installed to /home/user/.openmodelstudio/models/titanic-rf +``` + +This downloads the model files and a `model.json` manifest to your local models directory. The model is then available for import and registration with the platform. + +Force-reinstall an existing model: + +```bash +openmodelstudio install titanic-rf --force +``` + +### List Installed Models + +```bash +openmodelstudio list +``` + +Output: + +``` +NAME VERSION FRAMEWORK PATH +---------- ------- --------- ------------------------------------------- +titanic-rf 1.0.0 sklearn /home/user/.openmodelstudio/models/titanic-rf +mnist-cnn 1.0.0 pytorch /home/user/.openmodelstudio/models/mnist-cnn +``` + +### Uninstall a Model + +```bash +openmodelstudio uninstall titanic-rf +``` + +### Using an Installed Model + +After installing, the model's `model.py` is available locally. Register it with the platform from a notebook: + +```python +import openmodelstudio as oms +from pathlib import Path + +# Read the installed model code +model_dir = Path.home() / ".openmodelstudio" / "models" / "titanic-rf" +code = (model_dir / "model.py").read_text() + +# Register it into your project +handle = oms.register_model("titanic-rf", source_code=code) +print(handle) + +# Train it +job = oms.start_training(handle.model_id, wait=True) +print(f"Training: {job['status']}") +``` + +Or install directly from the UI on the **Model Registry** page (sidebar > Develop > Model Registry). + +## Configuration + +### View Current Config + +```bash +openmodelstudio config +``` + +Output: + +``` +registry_url: https://raw.githubusercontent.com/GACWR/open-model-registry/main/registry/index.json +models_dir: /home/user/.openmodelstudio/models +``` + +### Change Registry URL + +Point to a custom registry (your own fork, a private registry, etc.): + +```bash +openmodelstudio config set registry_url https://raw.githubusercontent.com/myorg/my-registry/main/registry/index.json +``` + +Or set via environment variable: + +```bash +export OPENMODELSTUDIO_REGISTRY_URL="https://raw.githubusercontent.com/myorg/my-registry/main/registry/index.json" +``` + +### Change Models Directory + +```bash +openmodelstudio config set models_dir /opt/models +``` + +Or set via environment variable: + +```bash +export OPENMODELSTUDIO_MODELS_DIR="/opt/models" +``` + +## Python SDK (Programmatic Access) + +All CLI commands are available as Python functions: + +```python +import openmodelstudio as oms + +# Search +results = oms.registry_search("classification") +results = oms.registry_search("cnn", framework="pytorch") +results = oms.registry_search("", category="nlp") + +# List all +models = oms.registry_list() + +# Get info +info = oms.registry_info("titanic-rf") +print(info["description"]) +print(info["dependencies"]) + +# Install +path = oms.registry_install("titanic-rf") +path = oms.registry_install("mnist-cnn", force=True) + +# Uninstall +oms.registry_uninstall("titanic-rf") + +# List installed +installed = oms.list_installed() + +# Switch registry +oms.set_registry("https://raw.githubusercontent.com/myorg/my-registry/main/registry/index.json") +``` + +## How the Registry Works + +The Open Model Registry is a GitHub repository with this structure: + +``` +open-model-registry/ + models/ + iris-svm/ + model.py # Model code (train + infer functions) + mnist-cnn/ + model.py + sentiment-lstm/ + model.py + ... + registry/ + index.json # Aggregated metadata for all models + scripts/ + build_index.py # Generates index.json from model directories +``` + +Each model directory contains: +- `model.py` -- the model code following the `train(ctx)` / `infer(ctx)` interface +- Additional files as needed (configs, weights, etc.) + +The `registry/index.json` is an aggregated index with metadata for every model (name, version, description, framework, category, tags, dependencies, file list). Both the CLI and the web UI read this single JSON file to discover available models. + +### Using a Custom Registry + +1. Fork [open-model-registry](https://github.com/GACWR/open-model-registry) +2. Add your model directories under `models/` +3. Run `python scripts/build_index.py` to regenerate `index.json` +4. Push to your fork +5. Point the CLI or SDK to your fork's raw URL + +## Web UI + +The **Model Registry** page in the sidebar (Develop > Model Registry) provides: + +- Browse all models with search and category/framework filters +- Click any model card to view full details, source code, dependencies, and tags +- Install models directly into a project from the UI +- Link to the model's GitHub page + +Each model detail page shows: +- Full description +- Source code viewer (Monaco editor, read-only) +- Tags, dependencies, license, author +- Quick install command (click to copy) +- Install-to-project dialog diff --git a/docs/MODELING.md b/docs/MODELING.md index 3ba8c64..e121a0e 100644 --- a/docs/MODELING.md +++ b/docs/MODELING.md @@ -164,6 +164,89 @@ print(f"Model type: {type(clf_loaded).__name__}") print(f"Estimators: {clf_loaded.n_estimators}") ``` +## Cell 14 -- Visualize Training Results + +Create a visualization that shows your training metrics. This uses the unified visualization abstraction -- the same `render()` function works for matplotlib, plotly, altair, and 6 other backends. + +```python +import matplotlib.pyplot as plt + +# Create a visualization record on the platform +viz = openmodelstudio.create_visualization("titanic-accuracy", + backend="matplotlib", + description="Random Forest accuracy across experiments") + +# Plot the results +fig, ax = plt.subplots(figsize=(8, 5)) +configs = ["rf-tuned\n(200 trees, depth=8)", "rf-deep\n(500 trees, depth=15)"] +accuracies = [0.94, 0.96] +bars = ax.bar(configs, accuracies, color=["#8b5cf6", "#10b981"], width=0.5) +ax.set_ylabel("Accuracy") +ax.set_title("Titanic RF — Experiment Comparison") +ax.set_ylim(0.9, 1.0) +for bar, acc in zip(bars, accuracies): + ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.002, + f"{acc:.2f}", ha="center", fontsize=12) + +# render() auto-detects matplotlib and converts to SVG +output = openmodelstudio.render(fig) + +# Publish so it appears in dashboards +openmodelstudio.publish_visualization(viz["id"]) +print("Visualization published") +``` + +After this cell, check the **Visualizations** page -- your chart is visible and can be added to any dashboard. + +## Cell 15 -- Interactive Plotly Chart + +For interactive charts with zoom, hover, and pan, use Plotly. JSON-based backends like Plotly and Altair also render live in the in-browser editor. + +```python +viz2 = openmodelstudio.create_visualization("loss-curve", + backend="plotly", + description="Training loss per fold") + +import plotly.graph_objects as go + +fig = go.Figure() +fig.add_trace(go.Scatter( + x=[1, 2, 3, 4, 5], + y=[0.35, 0.22, 0.15, 0.11, 0.08], + mode="lines+markers", + name="rf-tuned (loss)", + line=dict(color="#8b5cf6"), +)) +fig.add_trace(go.Scatter( + x=[1, 2, 3, 4, 5], + y=[0.30, 0.18, 0.10, 0.06, 0.04], + mode="lines+markers", + name="rf-deep (loss)", + line=dict(color="#10b981"), +)) +fig.update_layout(title="Cross-Validation Loss", xaxis_title="Fold", yaxis_title="Loss") + +output = openmodelstudio.render(fig) +openmodelstudio.publish_visualization(viz2["id"]) +print("Interactive Plotly chart published") +``` + +## Cell 16 -- Build a Monitoring Dashboard + +Combine your visualizations into a single dashboard view. + +```python +dashboard = openmodelstudio.create_dashboard("Titanic Experiment Monitor", + description="Training metrics for the Titanic classification experiments") + +print(f"Dashboard created: {dashboard['id']}") +print("Open the Dashboards page to add your visualizations as panels") +``` + +After this cell, open the **Dashboards** page, click your new dashboard, and use **Add Panel** to add the visualizations you created above. Drag and resize panels to build your layout. + +For the full visualization reference including all 9 backends, the in-browser editor, and dashboard configuration, see [Visualizations & Dashboards](VISUALIZATIONS.md). + ## What You Built After running the notebook, everything is visible across the platform: @@ -175,4 +258,6 @@ After running the notebook, everything is visible across the platform: | **Jobs** | Training and inference jobs with status, duration, metrics charts | | **Experiments** | `titanic-tuning` experiment with two runs, parallel coordinates, metric comparison | | **Datasets** | `titanic` dataset with format, size, and version info | +| **Visualizations** | `titanic-accuracy` bar chart and `loss-curve` interactive Plotly chart | +| **Dashboards** | `Titanic Experiment Monitor` with drag-and-drop panels | | **Dashboard** | Updated summary metrics reflecting your new models, jobs, and experiments | diff --git a/docs/VISUALIZATIONS.md b/docs/VISUALIZATIONS.md new file mode 100644 index 0000000..db090d4 --- /dev/null +++ b/docs/VISUALIZATIONS.md @@ -0,0 +1,372 @@ +# Visualizations & Dashboards + +Create, render, and publish data visualizations from notebooks. Combine them into drag-and-drop dashboards for real-time monitoring. OpenModelStudio supports **9 visualization backends** with a unified abstraction that works the same way regardless of which library you choose. + +## Supported Backends + +| Backend | Output Type | Description | +|---------|-------------|-------------| +| **matplotlib** | SVG | Standard Python plotting (line, bar, scatter, heatmap, etc.) | +| **seaborn** | SVG | Statistical visualization built on matplotlib | +| **plotly** | Plotly JSON | Interactive charts with zoom, pan, hover tooltips | +| **bokeh** | Bokeh JSON | Interactive web-ready charts with streaming support | +| **altair** | Vega-Lite JSON | Declarative statistical visualization (Vega-Lite spec) | +| **plotnine** | SVG | ggplot2-style grammar of graphics for Python | +| **datashader** | PNG | Server-side rendering for massive datasets (millions of points) | +| **networkx** | SVG | Network/graph visualizations | +| **geopandas** | SVG | Geospatial map visualizations | + +## Quick Start + +### From a JupyterLab Workspace + +```python +import openmodelstudio as oms +import matplotlib.pyplot as plt +import numpy as np + +# 1. Create a visualization record +viz = oms.create_visualization("training-loss", + backend="matplotlib", + description="Training loss over epochs") + +# 2. Render it +fig, ax = plt.subplots() +epochs = np.arange(1, 21) +loss = 0.9 * np.exp(-0.15 * epochs) + 0.05 +ax.plot(epochs, loss, color="#8b5cf6", linewidth=2) +ax.set_xlabel("Epoch") +ax.set_ylabel("Loss") +ax.set_title("Training Loss") + +# 3. Push rendered output to platform +output = oms.render(fig) # auto-detects matplotlib → SVG +oms.publish_visualization(viz["id"]) +``` + +After running this cell, the visualization appears on the **Visualizations** page and is available for dashboards. + +### Plotly (Interactive, JSON-Based) + +Plotly visualizations are JSON specs that render interactively in the browser with zoom, pan, and hover. + +```python +import openmodelstudio as oms + +viz = oms.create_visualization("accuracy-curve", + backend="plotly", + description="Model accuracy vs epoch") + +# For Plotly, the code is a JSON spec — edit it directly in the browser editor +# or define it programmatically: +import plotly.graph_objects as go + +fig = go.Figure() +fig.add_trace(go.Scatter( + x=list(range(1, 11)), + y=[0.5, 0.62, 0.71, 0.78, 0.82, 0.85, 0.87, 0.89, 0.90, 0.91], + mode="lines+markers", + name="Accuracy", + line=dict(color="#10b981"), +)) +fig.update_layout(title="Model Accuracy", xaxis_title="Epoch", yaxis_title="Accuracy") + +output = oms.render(fig) # auto-detects plotly → Plotly JSON +oms.publish_visualization(viz["id"]) +``` + +### Altair / Vega-Lite (Declarative) + +Altair charts are Vega-Lite JSON specs. You can write them as Python or edit JSON directly in the browser editor. + +```python +import openmodelstudio as oms +import altair as alt +import pandas as pd + +viz = oms.create_visualization("feature-distribution", + backend="altair", + description="Distribution of model features") + +data = pd.DataFrame({ + "feature": ["Age", "Fare", "Pclass", "SibSp", "Parch"], + "importance": [0.28, 0.25, 0.22, 0.15, 0.10], +}) + +chart = alt.Chart(data).mark_bar(cornerRadiusTopLeft=3, cornerRadiusTopRight=3).encode( + x=alt.X("feature", sort="-y"), + y="importance", + color=alt.Color("feature", scale=alt.Scale(scheme="category10")), +) + +output = oms.render(chart) # auto-detects altair → Vega-Lite JSON +oms.publish_visualization(viz["id"]) +``` + +### Seaborn (Statistical) + +```python +import openmodelstudio as oms +import seaborn as sns +import matplotlib.pyplot as plt +import pandas as pd +import numpy as np + +viz = oms.create_visualization("correlation-heatmap", + backend="seaborn", + description="Feature correlation matrix") + +data = pd.DataFrame(np.random.randn(100, 5), columns=["A", "B", "C", "D", "E"]) +fig, ax = plt.subplots(figsize=(8, 6)) +sns.heatmap(data.corr(), annot=True, cmap="coolwarm", ax=ax) + +output = oms.render(fig) +oms.publish_visualization(viz["id"]) +``` + +### Bokeh (Interactive Streaming) + +```python +import openmodelstudio as oms +from bokeh.plotting import figure +from bokeh.models import ColumnDataSource +import numpy as np + +viz = oms.create_visualization("signal-plot", + backend="bokeh", + description="Real-time signal visualization", + refresh_interval=10) # re-render every 10 seconds + +x = np.linspace(0, 4 * np.pi, 200) +y = np.sin(x) +source = ColumnDataSource(data=dict(x=x, y=y)) + +p = figure(title="Signal", width=800, height=400) +p.line("x", "y", source=source, line_width=2, color="#8b5cf6") + +output = oms.render(p) +oms.publish_visualization(viz["id"]) +``` + +### NetworkX (Graphs) + +```python +import openmodelstudio as oms +import networkx as nx +import matplotlib.pyplot as plt + +viz = oms.create_visualization("model-graph", + backend="networkx", + description="Model architecture as a graph") + +G = nx.karate_club_graph() +fig, ax = plt.subplots(figsize=(10, 8)) +pos = nx.spring_layout(G, seed=42) +nx.draw_networkx(G, pos, ax=ax, node_color="#8b5cf6", + edge_color="rgba(200,200,200,0.3)", + font_color="black", node_size=300) + +output = oms.render(fig) +oms.publish_visualization(viz["id"]) +``` + +### Datashader (Large Datasets) + +```python +import openmodelstudio as oms +import datashader as ds +import pandas as pd +import numpy as np + +viz = oms.create_visualization("embedding-scatter", + backend="datashader", + description="1M point embedding visualization") + +n = 1_000_000 +data = pd.DataFrame({"x": np.random.randn(n), "y": np.random.randn(n)}) +canvas = ds.Canvas(plot_width=800, plot_height=600) +agg = canvas.points(data, "x", "y") +img = ds.tf.shade(agg, cmap=["#000000", "#8b5cf6", "#ffffff"]) + +output = oms.render(img) +oms.publish_visualization(viz["id"]) +``` + +### GeoPandas (Maps) + +```python +import openmodelstudio as oms +import geopandas as gpd +import matplotlib.pyplot as plt + +viz = oms.create_visualization("data-coverage", + backend="geopandas", + description="Geographic data distribution") + +world = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres")) +fig, ax = plt.subplots(figsize=(12, 6)) +world.plot(ax=ax, color="#8b5cf6", edgecolor="rgba(255,255,255,0.3)") +ax.set_title("Data Coverage") + +output = oms.render(fig) +oms.publish_visualization(viz["id"]) +``` + +## The `render()` Function + +The `oms.render()` function auto-detects the backend from the object type and converts it to the appropriate output format: + +| Input Object | Detected Backend | Output | +|-------------|-----------------|--------| +| `matplotlib.figure.Figure` | matplotlib | SVG string | +| `plotly.graph_objects.Figure` | plotly | Plotly JSON string | +| `bokeh.model.Model` | bokeh | Bokeh JSON string | +| `altair.Chart` | altair | Vega-Lite JSON string | +| `plotnine.ggplot` | plotnine | SVG string | +| `datashader.transfer_functions.Image` | datashader | Base64 PNG data URL | +| `networkx.Graph` | networkx | SVG string (via matplotlib) | +| `geopandas.GeoDataFrame` | geopandas | SVG string (via matplotlib) | + +You never need to specify the backend manually when calling `render()` -- it inspects the object's class. + +## In-Browser Visualization Editor + +Every visualization has a full editor at `/visualizations/{id}` with: + +- **Monaco code editor** with syntax highlighting (Python for most backends, JSON for Plotly/Altair) +- **Live preview** for JSON-based backends (Plotly, Altair) -- edits render instantly +- **Template insertion** -- pre-built starter code for each backend +- **Data tab** -- attach JSON data that gets passed as `ctx.data` to the render function +- **Config tab** -- set refresh interval, output type, and custom config JSON +- **Publish button** -- make the visualization available for dashboards + +### JSON-Based Backends (Plotly, Altair) + +For Plotly and Altair, the code in the editor IS the visualization spec. Changes render live in the preview pane -- no notebook execution needed. + +### Python-Based Backends (matplotlib, seaborn, etc.) + +For Python backends, the editor shows the `render(ctx)` function. The preview displays the last rendered output from a notebook execution. To update the preview, run `oms.render()` in a notebook. + +## Dashboards + +Dashboards combine multiple visualizations into a single view with drag-and-drop layout. + +### Creating a Dashboard + +```python +import openmodelstudio as oms + +dashboard = oms.create_dashboard("Training Monitor", + description="Real-time training metrics overview") + +print(f"Dashboard: {dashboard['id']}") +``` + +Or create one from the **Dashboards** page in the sidebar. + +### Adding Panels + +From the dashboard page (`/dashboards/{id}`): + +1. Click **Add Panel** +2. Select a visualization from the dropdown +3. Choose initial width (quarter, third, half, two-thirds, full) and height +4. Click **Add Panel** + +Panels can be: +- **Dragged** to rearrange (grab the grip handle on the left) +- **Resized** by dragging corners +- **Removed** with the X button +- **Maximized** to open the full visualization editor + +### Locking the Layout + +Toggle the **Lock/Unlock** button to prevent accidental rearrangement. When locked, drag and resize are disabled. + +### Saving + +Click **Save Layout** when you see the "Unsaved changes" badge. The layout is stored as JSON in the database and persists across sessions. + +### Dashboard SDK + +```python +import openmodelstudio as oms + +# List dashboards +dashboards = oms.list_dashboards() + +# Get a specific dashboard +dash = oms.get_dashboard(dashboard_id) + +# Update layout programmatically +oms.update_dashboard(dashboard_id, + name="Updated Name", + layout=[ + {"visualization_id": "...", "x": 0, "y": 0, "w": 6, "h": 2}, + {"visualization_id": "...", "x": 6, "y": 0, "w": 6, "h": 2}, + ]) + +# Delete a dashboard +oms.delete_dashboard(dashboard_id) +``` + +## API Reference + +All visualization and dashboard operations are available via REST API. + +### Visualizations + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/visualizations` | List all visualizations | +| POST | `/visualizations` | Create a visualization | +| GET | `/visualizations/{id}` | Get visualization details | +| PUT | `/visualizations/{id}` | Update visualization | +| DELETE | `/visualizations/{id}` | Delete visualization | +| POST | `/visualizations/{id}/publish` | Publish for dashboards | + +### Dashboards + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/dashboards` | List all dashboards | +| POST | `/dashboards` | Create a dashboard | +| GET | `/dashboards/{id}` | Get dashboard with layout | +| PUT | `/dashboards/{id}` | Update dashboard layout | +| DELETE | `/dashboards/{id}` | Delete dashboard | + +### Create Visualization Request + +```json +{ + "name": "training-loss", + "backend": "matplotlib", + "description": "Training loss over epochs", + "code": "def render(ctx): ...", + "refresh_interval": 0 +} +``` + +The `output_type` is auto-detected from the backend if not specified. + +## Dynamic Visualizations + +Set `refresh_interval` to a non-zero value (in seconds) to create auto-refreshing visualizations. The platform will re-execute the render function at the specified interval. + +```python +viz = oms.create_visualization("live-metrics", + backend="plotly", + refresh_interval=5) # refresh every 5 seconds +``` + +This is useful for monitoring dashboards that track training progress, system metrics, or streaming data. + +## Tips + +- **Start with Plotly or Altair** for interactive charts -- they render live in the browser editor without needing a notebook +- **Use matplotlib/seaborn** when you need publication-quality static figures +- **Use datashader** for datasets with more than 100k points -- it renders server-side and sends a PNG +- **Set refresh_interval > 0** for live monitoring dashboards +- **Publish visualizations** before adding them to dashboards +- The browser editor loads Plotly.js, Vega-Embed, and BokehJS from CDN on demand -- no frontend bundle bloat diff --git a/sdk/python/openmodelstudio/__init__.py b/sdk/python/openmodelstudio/__init__.py index ba49b1f..842a812 100644 --- a/sdk/python/openmodelstudio/__init__.py +++ b/sdk/python/openmodelstudio/__init__.py @@ -1,4 +1,4 @@ -"""OpenModelStudio SDK — register models, load datasets, and track experiments from workspaces.""" +"""OpenModelStudio SDK — register models, load datasets, track experiments, and visualize from workspaces.""" from .client import Client from .model import ( @@ -47,7 +47,45 @@ delete_experiment, ) -__version__ = "0.0.1" +# Registry +from .registry import ( + registry_search, + registry_list, + registry_info, + registry_install, + registry_uninstall, + list_installed, + set_registry, +) + +# Visualization +from .visualization import ( + create_visualization, + publish_visualization, + render_visualization, + list_visualizations, + delete_visualization, + create_dashboard, + update_dashboard, + list_dashboards, + get_dashboard, + delete_dashboard, + render, + detect_backend, + VisualizationContext, + SUPPORTED_BACKENDS, +) + +# Config +from .config import ( + get_registry_url, + set_registry_url, + get_models_dir, + set_models_dir, + get_config, +) + +__version__ = "0.0.2" __all__ = [ "Client", @@ -97,4 +135,33 @@ "list_experiment_runs", "compare_experiment_runs", "delete_experiment", + # Registry + "registry_search", + "registry_list", + "registry_info", + "registry_install", + "registry_uninstall", + "list_installed", + "set_registry", + # Visualization + "create_visualization", + "publish_visualization", + "render_visualization", + "list_visualizations", + "delete_visualization", + "create_dashboard", + "update_dashboard", + "list_dashboards", + "get_dashboard", + "delete_dashboard", + "render", + "detect_backend", + "VisualizationContext", + "SUPPORTED_BACKENDS", + # Config + "get_registry_url", + "set_registry_url", + "get_models_dir", + "set_models_dir", + "get_config", ] diff --git a/sdk/python/openmodelstudio/cli.py b/sdk/python/openmodelstudio/cli.py new file mode 100644 index 0000000..975a302 --- /dev/null +++ b/sdk/python/openmodelstudio/cli.py @@ -0,0 +1,209 @@ +"""OpenModelStudio CLI — install, search, and manage models from the command line. + +Usage: + openmodelstudio install Install a model from the registry + openmodelstudio uninstall Remove an installed model + openmodelstudio search Search the model registry + openmodelstudio list List installed models + openmodelstudio registry List all models in the registry + openmodelstudio info Show details about a registry model + openmodelstudio config Show current configuration + openmodelstudio config set Set a configuration value +""" + +import argparse +import json +import sys + + +def _print_table(rows: list, headers: list): + """Print a simple aligned table.""" + if not rows: + return + widths = [len(h) for h in headers] + for row in rows: + for i, cell in enumerate(row): + widths[i] = max(widths[i], len(str(cell))) + + fmt = " ".join(f"{{:<{w}}}" for w in widths) + print(fmt.format(*headers)) + print(fmt.format(*["-" * w for w in widths])) + for row in rows: + print(fmt.format(*[str(c) for c in row])) + + +def cmd_install(args): + from .registry import registry_install + name = args.name + print(f"Installing '{name}' from registry...") + try: + path = registry_install(name, force=args.force) + print(f"Installed to {path}") + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_uninstall(args): + from .registry import registry_uninstall + if registry_uninstall(args.name): + print(f"Uninstalled '{args.name}'") + else: + print(f"Model '{args.name}' is not installed") + + +def cmd_search(args): + from .registry import registry_search + query = " ".join(args.query) if args.query else "" + results = registry_search(query, category=args.category, framework=args.framework) + if not results: + print("No models found matching your query.") + return + rows = [] + for m in results: + rows.append([ + m["name"], + m.get("version", "?"), + m.get("framework", "?"), + m.get("category", "?"), + m.get("description", "")[:60], + ]) + _print_table(rows, ["NAME", "VERSION", "FRAMEWORK", "CATEGORY", "DESCRIPTION"]) + + +def cmd_list(args): + from .registry import list_installed + installed = list_installed() + if not installed: + print("No models installed. Use 'openmodelstudio install ' to install one.") + return + rows = [] + for m in installed: + rows.append([ + m["name"], + m.get("version", "?"), + m.get("framework", "?"), + m.get("_installed_path", "?"), + ]) + _print_table(rows, ["NAME", "VERSION", "FRAMEWORK", "PATH"]) + + +def cmd_registry(args): + from .registry import registry_list + models = registry_list() + if not models: + print("Registry is empty or unreachable.") + return + rows = [] + for m in models: + rows.append([ + m["name"], + m.get("version", "?"), + m.get("framework", "?"), + m.get("category", "?"), + m.get("author", "?"), + m.get("description", "")[:50], + ]) + _print_table(rows, ["NAME", "VERSION", "FRAMEWORK", "CATEGORY", "AUTHOR", "DESCRIPTION"]) + + +def cmd_info(args): + from .registry import registry_info + try: + info = registry_info(args.name) + except ValueError as e: + print(str(e), file=sys.stderr) + sys.exit(1) + + print(f"Name: {info['name']}") + print(f"Version: {info.get('version', '?')}") + print(f"Author: {info.get('author', '?')}") + print(f"Framework: {info.get('framework', '?')}") + print(f"Category: {info.get('category', '?')}") + print(f"License: {info.get('license', '?')}") + print(f"Description: {info.get('description', '')}") + if info.get("tags"): + print(f"Tags: {', '.join(info['tags'])}") + if info.get("dependencies"): + print(f"Dependencies: {', '.join(info['dependencies'])}") + if info.get("homepage"): + print(f"Homepage: {info['homepage']}") + + +def cmd_config(args): + from .config import get_config, set_registry_url, set_models_dir + + if args.action == "set": + key = args.key + value = args.value + if key == "registry_url": + set_registry_url(value) + print(f"Set registry_url = {value}") + elif key == "models_dir": + set_models_dir(value) + print(f"Set models_dir = {value}") + else: + print(f"Unknown config key: {key}", file=sys.stderr) + print("Valid keys: registry_url, models_dir") + sys.exit(1) + else: + cfg = get_config() + for k, v in cfg.items(): + print(f"{k}: {v}") + + +def main(): + parser = argparse.ArgumentParser( + prog="openmodelstudio", + description="OpenModelStudio — AI model platform CLI", + ) + subparsers = parser.add_subparsers(dest="command", help="Available commands") + + # install + p_install = subparsers.add_parser("install", help="Install a model from the registry") + p_install.add_argument("name", help="Model name (e.g. titanic-rf)") + p_install.add_argument("--force", "-f", action="store_true", help="Overwrite existing") + p_install.set_defaults(func=cmd_install) + + # uninstall + p_uninstall = subparsers.add_parser("uninstall", help="Remove an installed model") + p_uninstall.add_argument("name", help="Model name") + p_uninstall.set_defaults(func=cmd_uninstall) + + # search + p_search = subparsers.add_parser("search", help="Search the model registry") + p_search.add_argument("query", nargs="*", help="Search terms") + p_search.add_argument("--category", "-c", help="Filter by category") + p_search.add_argument("--framework", "-f", help="Filter by framework") + p_search.set_defaults(func=cmd_search) + + # list + p_list = subparsers.add_parser("list", help="List installed models") + p_list.set_defaults(func=cmd_list) + + # registry + p_registry = subparsers.add_parser("registry", help="List all models in the registry") + p_registry.set_defaults(func=cmd_registry) + + # info + p_info = subparsers.add_parser("info", help="Show details about a registry model") + p_info.add_argument("name", help="Model name") + p_info.set_defaults(func=cmd_info) + + # config + p_config = subparsers.add_parser("config", help="Show or set configuration") + p_config.add_argument("action", nargs="?", default="show", choices=["show", "set"]) + p_config.add_argument("key", nargs="?", help="Config key") + p_config.add_argument("value", nargs="?", help="Config value") + p_config.set_defaults(func=cmd_config) + + args = parser.parse_args() + if not args.command: + parser.print_help() + sys.exit(0) + + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/sdk/python/openmodelstudio/config.py b/sdk/python/openmodelstudio/config.py new file mode 100644 index 0000000..592414b --- /dev/null +++ b/sdk/python/openmodelstudio/config.py @@ -0,0 +1,68 @@ +"""Configuration management for OpenModelStudio SDK. + +Handles user preferences including custom registry URLs, +model install paths, and persistent settings. +""" + +import json +import os +from pathlib import Path + +DEFAULT_REGISTRY_URL = ( + "https://raw.githubusercontent.com/GACWR/open-model-registry/main/registry/index.json" +) + +_CONFIG_DIR = Path.home() / ".openmodelstudio" +_CONFIG_FILE = _CONFIG_DIR / "config.json" + + +def _load_config() -> dict: + if _CONFIG_FILE.exists(): + try: + return json.loads(_CONFIG_FILE.read_text()) + except (json.JSONDecodeError, OSError): + return {} + return {} + + +def _save_config(cfg: dict): + _CONFIG_DIR.mkdir(parents=True, exist_ok=True) + _CONFIG_FILE.write_text(json.dumps(cfg, indent=2)) + + +def get_registry_url() -> str: + env = os.environ.get("OPENMODELSTUDIO_REGISTRY_URL") + if env: + return env + cfg = _load_config() + return cfg.get("registry_url", DEFAULT_REGISTRY_URL) + + +def set_registry_url(url: str): + cfg = _load_config() + cfg["registry_url"] = url + _save_config(cfg) + + +def get_models_dir() -> Path: + env = os.environ.get("OPENMODELSTUDIO_MODELS_DIR") + if env: + return Path(env) + cfg = _load_config() + default = str(Path.home() / ".openmodelstudio" / "models") + return Path(cfg.get("models_dir", default)) + + +def set_models_dir(path: str): + cfg = _load_config() + cfg["models_dir"] = str(path) + _save_config(cfg) + + +def get_config() -> dict: + cfg = _load_config() + return { + "registry_url": get_registry_url(), + "models_dir": str(get_models_dir()), + "api_url": os.environ.get("OPENMODELSTUDIO_API_URL", cfg.get("api_url", "")), + } diff --git a/sdk/python/openmodelstudio/registry.py b/sdk/python/openmodelstudio/registry.py new file mode 100644 index 0000000..65c8162 --- /dev/null +++ b/sdk/python/openmodelstudio/registry.py @@ -0,0 +1,204 @@ +"""OpenModelStudio Registry Client. + +Provides functions to search, list, install, and manage models +from the public OpenModel Registry or a custom registry. +""" + +import json +import os +import shutil +from pathlib import Path + +import requests + +from .config import get_registry_url, get_models_dir + + +def _fetch_index(registry_url: str = None) -> dict: + url = registry_url or get_registry_url() + resp = requests.get(url, timeout=30) + resp.raise_for_status() + return resp.json() + + +def registry_search(query: str, category: str = None, framework: str = None, + registry_url: str = None) -> list: + """Search the model registry. + + Examples:: + + results = oms.registry_search("classification") + results = oms.registry_search("cnn", framework="pytorch") + results = oms.registry_search("", category="nlp") + + Args: + query: Search query (matches name, description, tags) + category: Filter by category + framework: Filter by framework + registry_url: Override default registry URL + + Returns: + List of matching model metadata dicts + """ + index = _fetch_index(registry_url) + models = index.get("models", []) + query_lower = query.lower() + + results = [] + for m in models: + # Category filter + if category and m.get("category", "").lower() != category.lower(): + continue + # Framework filter + if framework and m.get("framework", "").lower() != framework.lower(): + continue + # Text search + if query_lower: + searchable = " ".join([ + m.get("name", ""), + m.get("description", ""), + " ".join(m.get("tags", [])), + m.get("author", ""), + ]).lower() + if query_lower not in searchable: + continue + results.append(m) + + return results + + +def registry_list(registry_url: str = None) -> list: + """List all models in the registry. + + Returns: + List of model metadata dicts + """ + index = _fetch_index(registry_url) + return index.get("models", []) + + +def registry_info(name: str, registry_url: str = None) -> dict: + """Get detailed info about a specific model in the registry. + + Args: + name: Model name (e.g. "titanic-rf") + + Returns: + Model metadata dict + + Raises: + ValueError: If model not found + """ + index = _fetch_index(registry_url) + for m in index.get("models", []): + if m["name"] == name: + return m + raise ValueError(f"Model '{name}' not found in registry") + + +def registry_install(name: str, registry_url: str = None, models_dir: str = None, + force: bool = False) -> Path: + """Install a model from the registry. + + Downloads the model files to the local models directory and makes + them available for import and registration. + + Examples:: + + path = oms.registry_install("titanic-rf") + path = oms.registry_install("mnist-cnn", force=True) + + Args: + name: Model name (e.g. "titanic-rf") + registry_url: Override default registry URL + models_dir: Override default models directory + force: Overwrite existing installation + + Returns: + Path to the installed model directory + """ + info = registry_info(name, registry_url=registry_url) + raw_prefix = info.get("_registry", {}).get("raw_url_prefix", "") + if not raw_prefix: + reg_path = info.get("_registry", {}).get("path", f"models/{name}") + url = registry_url or get_registry_url() + base = url.rsplit("/registry/", 1)[0] + raw_prefix = f"{base}/{reg_path}" + + dest = Path(models_dir) if models_dir else get_models_dir() + model_dir = dest / name + if model_dir.exists() and not force: + return model_dir + + model_dir.mkdir(parents=True, exist_ok=True) + + # Download each file listed in the manifest + files = info.get("files", []) + if not files: + files = ["model.py"] + + for fname in files: + file_url = f"{raw_prefix}/{fname}" + resp = requests.get(file_url, timeout=60) + resp.raise_for_status() + (model_dir / fname).write_bytes(resp.content) + + # Write manifest locally + (model_dir / "model.json").write_text(json.dumps(info, indent=2)) + + return model_dir + + +def registry_uninstall(name: str, models_dir: str = None) -> bool: + """Uninstall a locally installed model. + + Args: + name: Model name + + Returns: + True if model was removed, False if it wasn't installed + """ + dest = Path(models_dir) if models_dir else get_models_dir() + model_dir = dest / name + if model_dir.exists(): + shutil.rmtree(model_dir) + return True + return False + + +def list_installed(models_dir: str = None) -> list: + """List locally installed models. + + Returns: + List of model metadata dicts for installed models + """ + dest = Path(models_dir) if models_dir else get_models_dir() + if not dest.exists(): + return [] + + installed = [] + for d in sorted(dest.iterdir()): + if not d.is_dir(): + continue + manifest = d / "model.json" + if manifest.exists(): + try: + data = json.loads(manifest.read_text()) + data["_installed_path"] = str(d) + installed.append(data) + except (json.JSONDecodeError, OSError): + continue + return installed + + +def set_registry(url: str): + """Set the default registry URL. + + Persists across sessions. Can also be set via the + OPENMODELSTUDIO_REGISTRY_URL environment variable. + + Args: + url: Full URL to registry/index.json + """ + from .config import set_registry_url + set_registry_url(url) diff --git a/sdk/python/openmodelstudio/visualization.py b/sdk/python/openmodelstudio/visualization.py new file mode 100644 index 0000000..0eef257 --- /dev/null +++ b/sdk/python/openmodelstudio/visualization.py @@ -0,0 +1,446 @@ +"""OpenModelStudio Unified Visualization Abstraction. + +Provides a framework-agnostic interface for creating visualizations +that can be saved, served in the dashboard, and composed into dashboards. + +Supported backends: + - matplotlib / seaborn / plotnine (ggplot2) → static SVG/PNG + - plotly / bokeh / altair → interactive JSON + - datashader → static PNG (large data) + - networkx → static SVG (graph layouts) + - geopandas → static SVG (maps) + +Usage from a notebook:: + + import openmodelstudio as oms + + # Quick static visualization + viz = oms.create_visualization("loss-curve", "matplotlib", + code=\"\"\" + import matplotlib.pyplot as plt + def render(ctx): + plt.figure(figsize=(10, 6)) + plt.plot(ctx.data["epochs"], ctx.data["loss"]) + plt.title("Training Loss") + plt.xlabel("Epoch") + plt.ylabel("Loss") + return plt.gcf() + \"\"\", + data={"epochs": [1,2,3], "loss": [0.9, 0.5, 0.2]} + ) + + # Interactive Plotly + viz = oms.create_visualization("metrics-scatter", "plotly", + code=\"\"\" + import plotly.express as px + def render(ctx): + return px.scatter(ctx.data, x="epoch", y="accuracy") + \"\"\", + data=df.to_dict("records") + ) + + # Push to dashboard + oms.publish_visualization(viz["id"]) +""" + +import base64 +import io +import json +from abc import ABC, abstractmethod + + +# ── Visualization Context ──────────────────────────────────────────── + +class VisualizationContext: + """Context object passed to visualization render functions. + + Similar to ModelContext for train(ctx)/infer(ctx), this provides + data access and configuration for rendering. + """ + + def __init__(self, data=None, config=None, params=None): + self.data = data or {} + self.config = config or {} + self.params = params or {} + + @property + def width(self) -> int: + return int(self.config.get("width", 800)) + + @property + def height(self) -> int: + return int(self.config.get("height", 600)) + + @property + def theme(self) -> str: + return self.config.get("theme", "dark") + + +# ── Backend Renderers ──────────────────────────────────────────────── + +def _render_matplotlib(fig) -> dict: + """Convert a matplotlib Figure to SVG string.""" + buf = io.BytesIO() + fig.savefig(buf, format="svg", bbox_inches="tight", transparent=True, + facecolor="none", edgecolor="none") + buf.seek(0) + svg = buf.getvalue().decode("utf-8") + import matplotlib.pyplot as plt + plt.close(fig) + return {"type": "svg", "content": svg} + + +def _render_plotly(fig) -> dict: + """Convert a Plotly figure to JSON spec.""" + return {"type": "plotly", "content": fig.to_json()} + + +def _render_bokeh(fig) -> dict: + """Convert a Bokeh figure to JSON.""" + from bokeh.embed import json_item + return {"type": "bokeh", "content": json.dumps(json_item(fig))} + + +def _render_altair(chart) -> dict: + """Convert an Altair chart to Vega-Lite JSON spec.""" + return {"type": "vega-lite", "content": chart.to_json()} + + +def _render_plotnine(plot) -> dict: + """Convert a plotnine (ggplot) to SVG.""" + buf = io.BytesIO() + plot.save(buf, format="svg", verbose=False) + buf.seek(0) + return {"type": "svg", "content": buf.getvalue().decode("utf-8")} + + +def _render_datashader(img) -> dict: + """Convert a datashader image to base64 PNG.""" + buf = io.BytesIO() + img.to_pil().save(buf, format="PNG") + buf.seek(0) + b64 = base64.b64encode(buf.getvalue()).decode() + return {"type": "png", "content": f"data:image/png;base64,{b64}"} + + +def _render_networkx(fig) -> dict: + """Render NetworkX via matplotlib to SVG.""" + return _render_matplotlib(fig) + + +def _render_geopandas(fig) -> dict: + """Render GeoPandas via matplotlib to SVG.""" + return _render_matplotlib(fig) + + +# Backend dispatch +_RENDERERS = { + "matplotlib": _render_matplotlib, + "seaborn": _render_matplotlib, + "plotnine": _render_plotnine, + "plotly": _render_plotly, + "bokeh": _render_bokeh, + "altair": _render_altair, + "datashader": _render_datashader, + "networkx": _render_networkx, + "geopandas": _render_geopandas, +} + +# Map backends to output types for the API +BACKEND_OUTPUT_TYPES = { + "matplotlib": "svg", + "seaborn": "svg", + "plotnine": "svg", + "plotly": "plotly", + "bokeh": "bokeh", + "altair": "vega-lite", + "datashader": "png", + "networkx": "svg", + "geopandas": "svg", +} + +SUPPORTED_BACKENDS = list(_RENDERERS.keys()) + + +def detect_backend(obj) -> str: + """Auto-detect visualization backend from a figure/chart object.""" + cls_name = type(obj).__module__ + "." + type(obj).__qualname__ + + # Matplotlib + try: + import matplotlib.figure + if isinstance(obj, matplotlib.figure.Figure): + return "matplotlib" + except ImportError: + pass + + # Plotly + try: + import plotly.graph_objs + if isinstance(obj, plotly.graph_objs.Figure): + return "plotly" + except ImportError: + pass + + # Bokeh + try: + from bokeh.model import Model as BokehModel + if isinstance(obj, BokehModel): + return "bokeh" + except ImportError: + pass + + # Altair + try: + import altair + if isinstance(obj, altair.Chart): + return "altair" + except ImportError: + pass + + # plotnine + try: + import plotnine + if isinstance(obj, plotnine.ggplot): + return "plotnine" + except ImportError: + pass + + raise TypeError( + f"Cannot auto-detect visualization backend for {type(obj).__name__}. " + f"Supported backends: {', '.join(SUPPORTED_BACKENDS)}" + ) + + +def render(obj, backend: str = None) -> dict: + """Render a visualization object to its output format. + + Auto-detects the backend if not specified. + + Args: + obj: A figure/chart object (matplotlib Figure, plotly Figure, etc.) + backend: Override backend detection + + Returns: + Dict with 'type' (svg/plotly/bokeh/vega-lite/png) and 'content' + """ + if backend is None: + backend = detect_backend(obj) + renderer = _RENDERERS.get(backend) + if renderer is None: + raise ValueError(f"Unsupported backend: {backend}. Supported: {', '.join(SUPPORTED_BACKENDS)}") + return renderer(obj) + + +# ── SDK Integration Functions ───────────────────────────────────────── + +def create_visualization( + name: str, + backend: str, + code: str = None, + data: dict = None, + config: dict = None, + description: str = None, + refresh_interval: int = None, + _client=None, +) -> dict: + """Create and save a visualization to the platform. + + The code should define a ``render(ctx)`` function that returns + a figure/chart object appropriate for the backend. + + Args: + name: Visualization name + backend: One of: matplotlib, seaborn, plotly, bokeh, altair, plotnine, datashader, networkx, geopandas + code: Python code with a render(ctx) function + data: Data dict to pass to the render context + config: Config dict (width, height, theme, etc.) + description: Optional description + refresh_interval: For dynamic visualizations, seconds between refreshes (0 = static) + + Returns: + Dict with visualization id and metadata + """ + if backend not in SUPPORTED_BACKENDS: + raise ValueError(f"Unsupported backend: {backend}. Supported: {', '.join(SUPPORTED_BACKENDS)}") + + body = { + "name": name, + "backend": backend, + "output_type": BACKEND_OUTPUT_TYPES[backend], + } + if code: + body["code"] = code + if data is not None: + body["data"] = data + if config: + body["config"] = config + if description: + body["description"] = description + if refresh_interval is not None: + body["refresh_interval"] = refresh_interval + + if _client is None: + from .model import _get_client + _client = _get_client() + + if _client.project_id: + body["project_id"] = _client.project_id + + return _client._post("/sdk/visualizations", body) + + +def publish_visualization(viz_id: str, _client=None) -> dict: + """Publish a visualization to the dashboard. + + Makes the visualization visible in the Visualizations section + of the OpenModelStudio dashboard. + + Args: + viz_id: UUID of the visualization + """ + if _client is None: + from .model import _get_client + _client = _get_client() + return _client._post(f"/sdk/visualizations/{viz_id}/publish", {}) + + +def render_visualization(viz_id: str, data: dict = None, _client=None) -> dict: + """Execute a saved visualization and return the rendered output. + + Args: + viz_id: UUID of the visualization + data: Optional data override + + Returns: + Dict with 'type' and 'content' (the rendered output) + """ + if _client is None: + from .model import _get_client + _client = _get_client() + body = {} + if data is not None: + body["data"] = data + return _client._post(f"/sdk/visualizations/{viz_id}/render", body) + + +def list_visualizations(project_id: str = None, _client=None) -> list: + """List all visualizations in the current project. + + Returns: + List of visualization metadata dicts + """ + if _client is None: + from .model import _get_client + _client = _get_client() + params = {} + pid = project_id or _client.project_id + if pid: + params["project_id"] = pid + return _client._get("/sdk/visualizations", params=params) + + +def delete_visualization(viz_id: str, _client=None) -> dict: + """Delete a visualization. + + Args: + viz_id: UUID of the visualization + """ + if _client is None: + from .model import _get_client + _client = _get_client() + return _client._delete(f"/sdk/visualizations/{viz_id}") + + +# ── Dashboard Functions ─────────────────────────────────────────────── + +def create_dashboard( + name: str, + layout: list = None, + description: str = None, + _client=None, +) -> dict: + """Create a dashboard that composes multiple visualizations. + + The layout is a list of panel definitions, each specifying + a visualization and its grid position. + + Args: + name: Dashboard name + layout: List of panel dicts, each with: + - visualization_id: UUID of the visualization + - x, y: Grid position (0-based) + - w, h: Width and height in grid units + description: Optional description + + Returns: + Dict with dashboard id and metadata + + Example:: + + oms.create_dashboard("Training Overview", layout=[ + {"visualization_id": "abc-123", "x": 0, "y": 0, "w": 6, "h": 4}, + {"visualization_id": "def-456", "x": 6, "y": 0, "w": 6, "h": 4}, + ]) + """ + if _client is None: + from .model import _get_client + _client = _get_client() + + body = {"name": name} + if layout: + body["layout"] = layout + if description: + body["description"] = description + if _client.project_id: + body["project_id"] = _client.project_id + + return _client._post("/sdk/dashboards", body) + + +def update_dashboard(dashboard_id: str, layout: list = None, name: str = None, + _client=None) -> dict: + """Update a dashboard's layout or name. + + Args: + dashboard_id: UUID of the dashboard + layout: New layout (replaces existing) + name: New name + """ + if _client is None: + from .model import _get_client + _client = _get_client() + body = {} + if layout is not None: + body["layout"] = layout + if name is not None: + body["name"] = name + return _client._put(f"/sdk/dashboards/{dashboard_id}", body) + + +def list_dashboards(project_id: str = None, _client=None) -> list: + """List all dashboards in the current project.""" + if _client is None: + from .model import _get_client + _client = _get_client() + params = {} + pid = project_id or _client.project_id + if pid: + params["project_id"] = pid + return _client._get("/sdk/dashboards", params=params) + + +def get_dashboard(dashboard_id: str, _client=None) -> dict: + """Get a dashboard by ID including its layout.""" + if _client is None: + from .model import _get_client + _client = _get_client() + return _client._get(f"/sdk/dashboards/{dashboard_id}") + + +def delete_dashboard(dashboard_id: str, _client=None) -> dict: + """Delete a dashboard.""" + if _client is None: + from .model import _get_client + _client = _get_client() + return _client._delete(f"/sdk/dashboards/{dashboard_id}") diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index 032aa21..42fa07e 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -30,6 +30,9 @@ dependencies = [ "requests>=2.28", ] +[project.scripts] +openmodelstudio = "openmodelstudio.cli:main" + [project.optional-dependencies] test = [ "pytest>=7.4.0", diff --git a/web/package.json b/web/package.json index 2885a18..85b7dab 100644 --- a/web/package.json +++ b/web/package.json @@ -11,6 +11,7 @@ "dependencies": { "@apollo/client": "^4.1.5", "@monaco-editor/react": "^4.7.0", + "@types/react-grid-layout": "^2.1.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -23,6 +24,7 @@ "react": "19.2.3", "react-day-picker": "^9.13.2", "react-dom": "19.2.3", + "react-grid-layout": "^2.2.2", "react-markdown": "^10.1.0", "recharts": "^3.7.0", "remark-gfm": "^4.0.1", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index d633e90..2761d1a 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@monaco-editor/react': specifier: ^4.7.0 version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@types/react-grid-layout': + specifier: ^2.1.0 + version: 2.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -50,6 +53,9 @@ importers: react-dom: specifier: 19.2.3 version: 19.2.3(react@19.2.3) + react-grid-layout: + specifier: ^2.2.2 + version: 2.2.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react-markdown: specifier: ^10.1.0 version: 10.1.0(@types/react@19.2.14)(react@19.2.3) @@ -1560,6 +1566,10 @@ packages: peerDependencies: '@types/react': ^19.2.0 + '@types/react-grid-layout@2.1.0': + resolution: {integrity: sha512-pHEjVg9ert6BDFHFQ1IEdLUkd2gasJvyti5lV2kE46N/R07ZiaSZpAXeXJAA1MXy/Qby23fZmiuEgZkITxPXug==} + deprecated: This is a stub types definition. react-grid-layout provides its own type definitions, so you do not need this installed. + '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} @@ -2483,6 +2493,9 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-equals@4.0.3: + resolution: {integrity: sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==} + fast-glob@3.3.1: resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} engines: {node: '>=8.6.0'} @@ -3668,6 +3681,18 @@ packages: peerDependencies: react: ^19.2.3 + react-draggable@4.5.0: + resolution: {integrity: sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==} + peerDependencies: + react: '>= 16.3.0' + react-dom: '>= 16.3.0' + + react-grid-layout@2.2.2: + resolution: {integrity: sha512-yNo9pxQWoxHWRAwHGSVT4DEGELYPyQ7+q9lFclb5jcqeFzva63/2F72CryS/jiTIr/SBIlTaDdyjqH+ODg8oBw==} + peerDependencies: + react: '>= 16.3.0' + react-dom: '>= 16.3.0' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -3709,6 +3734,12 @@ packages: '@types/react': optional: true + react-resizable@3.1.3: + resolution: {integrity: sha512-liJBNayhX7qA4tBJiBD321FDhJxgGTJ07uzH5zSORXoE8h7PyEZ8mLqmosST7ppf6C4zUsbd2gzDMmBCfFp9Lw==} + peerDependencies: + react: '>= 16.3' + react-dom: '>= 16.3' + react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -3774,6 +3805,9 @@ packages: reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resize-observer-polyfill@1.5.1: + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -5831,6 +5865,13 @@ snapshots: dependencies: '@types/react': 19.2.14 + '@types/react-grid-layout@2.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + react-grid-layout: 2.2.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + transitivePeerDependencies: + - react + - react-dom + '@types/react@19.2.14': dependencies: csstype: 3.2.3 @@ -6889,6 +6930,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-equals@4.0.3: {} + fast-glob@3.3.1: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -8290,6 +8333,24 @@ snapshots: react: 19.2.3 scheduler: 0.27.0 + react-draggable@4.5.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + clsx: 2.1.1 + prop-types: 15.8.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + react-grid-layout@2.2.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + clsx: 2.1.1 + fast-equals: 4.0.3 + prop-types: 15.8.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-draggable: 4.5.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react-resizable: 3.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + resize-observer-polyfill: 1.5.1 + react-is@16.13.1: {} react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.3): @@ -8338,6 +8399,13 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + react-resizable@3.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + prop-types: 15.8.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-draggable: 4.5.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.3): dependencies: get-nonce: 1.0.1 @@ -8442,6 +8510,8 @@ snapshots: reselect@5.1.1: {} + resize-observer-polyfill@1.5.1: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} diff --git a/web/src/app/dashboards/[id]/page.tsx b/web/src/app/dashboards/[id]/page.tsx new file mode 100644 index 0000000..6735768 --- /dev/null +++ b/web/src/app/dashboards/[id]/page.tsx @@ -0,0 +1,620 @@ +"use client"; + +import { useState, useEffect, useCallback, useMemo } from "react"; +import { useParams, useRouter } from "next/navigation"; +import { AppShell } from "@/components/layout/app-shell"; +import { AnimatedPage } from "@/components/shared/animated-page"; +import { GlassCard } from "@/components/shared/glass-card"; +import { EmptyState } from "@/components/shared/empty-state"; +import { ErrorState } from "@/components/shared/error-state"; +import { CardSkeleton } from "@/components/shared/loading-skeleton"; +import { VizRenderer } from "@/components/shared/viz-renderer"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { motion, AnimatePresence } from "framer-motion"; +import { + LayoutDashboard, + Plus, + Save, + ArrowLeft, + X, + BarChart3, + GripVertical, + Maximize2, + Lock, + Unlock, +} from "lucide-react"; +import { api } from "@/lib/api"; +import { toast } from "sonner"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { WidthProvider, Responsive } from "react-grid-layout/legacy"; +import type { Layout, LayoutItem } from "react-grid-layout"; +import "react-grid-layout/css/styles.css"; + +const ResponsiveGridLayout = WidthProvider(Responsive); + +interface DashboardLayoutItem { + visualization_id: string; + x: number; + y: number; + w: number; + h: number; +} + +interface Dashboard { + id: string; + name: string; + description: string | null; + layout: DashboardLayoutItem[] | null; + published: boolean; + created_at: string; + updated_at: string; +} + +interface VisualizationFull { + id: string; + name: string; + backend: string; + output_type: string; + description: string | null; + rendered_output: string | null; + published: boolean; +} + +interface VisualizationSummary { + id: string; + name: string; + backend: string; + output_type: string; + description: string | null; +} + +const backendColors: Record = { + matplotlib: "bg-blue-500/10 text-blue-400 border-blue-500/20", + seaborn: "bg-teal-500/10 text-teal-400 border-teal-500/20", + plotly: "bg-purple-500/10 text-purple-400 border-purple-500/20", + bokeh: "bg-green-500/10 text-green-400 border-green-500/20", + altair: "bg-orange-500/10 text-orange-400 border-orange-500/20", + plotnine: "bg-red-500/10 text-red-400 border-red-500/20", + datashader: "bg-cyan-500/10 text-cyan-400 border-cyan-500/20", + networkx: "bg-yellow-500/10 text-yellow-400 border-yellow-500/20", + geopandas: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20", +}; + +const ROW_HEIGHT = 120; + +export default function DashboardDetailPage() { + const params = useParams(); + const router = useRouter(); + const id = params.id as string; + + const [dashboard, setDashboard] = useState(null); + const [allVisualizations, setAllVisualizations] = useState([]); + const [vizDetails, setVizDetails] = useState>(new Map()); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [saving, setSaving] = useState(false); + const [hasChanges, setHasChanges] = useState(false); + const [locked, setLocked] = useState(false); + + // Dashboard panel layout + const [panels, setPanels] = useState([]); + + // Add panel dialog + const [addPanelOpen, setAddPanelOpen] = useState(false); + const [selectedVizId, setSelectedVizId] = useState(""); + const [panelWidth, setPanelWidth] = useState("6"); + const [panelHeight, setPanelHeight] = useState("2"); + + // Fetch the full visualization detail (with rendered_output) for a given ID + const fetchVizDetail = useCallback(async (vizId: string) => { + try { + const detail = await api.get(`/visualizations/${vizId}`); + setVizDetails((prev) => { + const next = new Map(prev); + next.set(vizId, detail); + return next; + }); + } catch { + // Visualization may have been deleted; skip + } + }, []); + + const fetchDashboard = useCallback(() => { + setLoading(true); + setError(null); + Promise.all([ + api.get(`/dashboards/${id}`), + api.get("/visualizations"), + ]) + .then(([dash, vizs]) => { + setDashboard(dash); + const items: DashboardLayoutItem[] = Array.isArray(dash.layout) ? dash.layout : []; + setPanels(items); + setAllVisualizations(vizs); + + // Fetch full detail for each panel's visualization + const uniqueIds = [...new Set(items.map((p) => p.visualization_id))]; + uniqueIds.forEach(fetchVizDetail); + }) + .catch((err) => + setError(err instanceof Error ? err.message : "Failed to load dashboard") + ) + .finally(() => setLoading(false)); + }, [id, fetchVizDetail]); + + useEffect(() => { + fetchDashboard(); + }, [fetchDashboard]); + + // Convert our panels to react-grid-layout format + const rglLayout: LayoutItem[] = useMemo( + () => + panels.map((p, i) => ({ + i: String(i), + x: p.x, + y: p.y, + w: p.w, + h: p.h, + minW: 2, + minH: 1, + maxW: 12, + })), + [panels] + ); + + // Handle layout change from drag/resize + const handleLayoutChange = useCallback( + (newLayout: Layout) => { + const updated = panels.map((panel, i) => { + const item = newLayout.find((l) => l.i === String(i)); + if (!item) return panel; + return { + ...panel, + x: item.x, + y: item.y, + w: item.w, + h: item.h, + }; + }); + setPanels(updated); + setHasChanges(true); + }, + [panels] + ); + + const handleAddPanel = () => { + if (!selectedVizId) { + toast.error("Select a visualization"); + return; + } + const w = parseInt(panelWidth) || 6; + const h = parseInt(panelHeight) || 2; + const maxY = panels.reduce((max, p) => Math.max(max, p.y + p.h), 0); + + const newPanel: DashboardLayoutItem = { + visualization_id: selectedVizId, + x: 0, + y: maxY, + w, + h, + }; + setPanels([...panels, newPanel]); + setHasChanges(true); + setAddPanelOpen(false); + setSelectedVizId(""); + setPanelWidth("6"); + setPanelHeight("2"); + + // Fetch viz detail if not already loaded + if (!vizDetails.has(selectedVizId)) { + fetchVizDetail(selectedVizId); + } + + toast.success("Panel added"); + }; + + const handleRemovePanel = (index: number) => { + setPanels(panels.filter((_, i) => i !== index)); + setHasChanges(true); + toast.success("Panel removed"); + }; + + const handleSave = async () => { + setSaving(true); + try { + await api.put(`/dashboards/${id}`, { + name: dashboard?.name, + description: dashboard?.description, + layout: panels, + }); + toast.success("Dashboard saved"); + setHasChanges(false); + } catch (err) { + toast.error( + err instanceof Error ? err.message : "Failed to save dashboard" + ); + } finally { + setSaving(false); + } + }; + + const getViz = (vizId: string) => vizDetails.get(vizId); + const getVizSummary = (vizId: string) => + allVisualizations.find((v) => v.id === vizId); + + // Available visualizations not yet in this dashboard + const availableVizs = allVisualizations.filter( + (v) => !panels.some((p) => p.visualization_id === v.id) + ); + + if (loading) { + return ( + + +
+ +
+
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ +
+ ))} +
+
+
+ ); + } + + if (error) { + return ( + + + + + + ); + } + + return ( + + + {/* Header */} +
+
+ + + +
+

+ {dashboard?.name || "Dashboard"} +

+

+ {dashboard?.description || + "Drag and drop panels to arrange your dashboard"} +

+
+
+
+ + + + + + + + {hasChanges && ( + + + + )} + +
+
+ + {/* Info bar */} +
+ + {panels.length} {panels.length === 1 ? "panel" : "panels"} + + + 12-column grid + + + {locked ? "Drag disabled" : "Drag to rearrange"} + + {hasChanges && ( + + Unsaved changes + + )} +
+ + {/* Grid Layout */} + {panels.length === 0 ? ( + setAddPanelOpen(true)} + /> + ) : ( +
+ handleLayoutChange(layout)} + margin={[16, 16]} + containerPadding={[0, 0]} + useCSSTransforms + compactType="vertical" + > + {panels.map((panel, index) => { + const vizFull = getViz(panel.visualization_id); + const vizSummary = getVizSummary(panel.visualization_id); + const vizName = vizFull?.name || vizSummary?.name || "Unknown"; + const vizBackend = vizFull?.backend || vizSummary?.backend || ""; + const outputType = vizFull?.output_type || vizSummary?.output_type || "svg"; + const renderedOutput = vizFull?.rendered_output || null; + + return ( +
+ + {/* Panel header */} +
+
+ + + {vizName} + + {vizBackend && ( + + {vizBackend} + + )} +
+
+ + {!locked && ( + + )} +
+
+ + {/* Visualization content */} +
+
+ +
+
+
+
+ ); + })} +
+
+ )} + + {/* Add Panel Dialog */} + + + + Add Panel + + Select a visualization to add to the dashboard. + + +
+
+ + {availableVizs.length === 0 && allVisualizations.length === 0 ? ( +

+ No visualizations available. Create one first on the + Visualizations page. +

+ ) : availableVizs.length === 0 ? ( +

+ All visualizations are already on this dashboard. You can + still add duplicates. +

+ ) : null} + +
+ +
+
+ + +
+
+ + +
+
+ + {/* Grid preview */} +
+ +
+
+ + {panelWidth} x {panelHeight} + +
+
+
+ + +
+
+
+
+
+ ); +} diff --git a/web/src/app/dashboards/page.tsx b/web/src/app/dashboards/page.tsx new file mode 100644 index 0000000..f944fd6 --- /dev/null +++ b/web/src/app/dashboards/page.tsx @@ -0,0 +1,296 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { AppShell } from "@/components/layout/app-shell"; +import { AnimatedPage, staggerContainer, staggerItem } from "@/components/shared/animated-page"; +import { GlassCard } from "@/components/shared/glass-card"; +import { EmptyState } from "@/components/shared/empty-state"; +import { ErrorState } from "@/components/shared/error-state"; +import { CardSkeleton } from "@/components/shared/loading-skeleton"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { motion } from "framer-motion"; +import { LayoutDashboard, Search, Plus, ChevronRight, Layers } from "lucide-react"; +import Link from "next/link"; +import { api } from "@/lib/api"; +import { toast } from "sonner"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; + +interface DashboardLayout { + visualization_id: string; + x: number; + y: number; + w: number; + h: number; +} + +interface Dashboard { + id: string; + name: string; + description: string | null; + layout: DashboardLayout[]; + created_at: string; + updated_at: string; +} + +export default function DashboardsPage() { + const [dashboards, setDashboards] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [search, setSearch] = useState(""); + const [searchFocused, setSearchFocused] = useState(false); + + // Create dialog state + const [createOpen, setCreateOpen] = useState(false); + const [newName, setNewName] = useState(""); + const [newDescription, setNewDescription] = useState(""); + const [submitting, setSubmitting] = useState(false); + + const fetchDashboards = () => { + setLoading(true); + setError(null); + api + .get("/dashboards") + .then(setDashboards) + .catch((err) => + setError(err instanceof Error ? err.message : "Failed to load dashboards") + ) + .finally(() => setLoading(false)); + }; + + useEffect(() => { + fetchDashboards(); + }, []); + + const handleCreate = async () => { + if (!newName.trim()) { + toast.error("Dashboard name is required"); + return; + } + setSubmitting(true); + try { + await api.post("/dashboards", { + name: newName.trim(), + description: newDescription.trim() || null, + }); + toast.success("Dashboard created"); + setCreateOpen(false); + setNewName(""); + setNewDescription(""); + fetchDashboards(); + } catch (err) { + toast.error( + err instanceof Error ? err.message : "Failed to create dashboard" + ); + } finally { + setSubmitting(false); + } + }; + + const filtered = dashboards.filter( + (d) => + d.name.toLowerCase().includes(search.toLowerCase()) || + (d.description || "").toLowerCase().includes(search.toLowerCase()) + ); + + return ( + + + {/* Header */} +
+
+

Dashboards

+

+ Create and manage custom dashboards with visualization panels +

+
+ + + + + + + + + New Dashboard + + Create a new dashboard to compose visualization panels. + + +
+
+ + setNewName(e.target.value)} + className="border bg-muted input-glow" + /> +
+
+ + setNewDescription(e.target.value)} + className="border bg-muted" + /> +
+ +
+
+
+
+ + {/* Animated search bar */} + + + setSearch(e.target.value)} + onFocus={() => setSearchFocused(true)} + onBlur={() => setSearchFocused(false)} + className="border bg-card/50 pl-10 input-glow transition-all" + /> + + + {/* Content */} + {loading ? ( +
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+ ) : error ? ( + + ) : filtered.length === 0 ? ( + setCreateOpen(true)} + /> + ) : ( + + {filtered.map((dashboard) => { + const panelCount = dashboard.layout?.length || 0; + return ( + + + + {/* Header */} +
+
+
+ +
+
+

+ {dashboard.name} +

+
+ + + {panelCount}{" "} + {panelCount === 1 ? "panel" : "panels"} + +
+
+
+
+ + {/* Description */} + {dashboard.description && ( +

+ {dashboard.description} +

+ )} + {!dashboard.description &&
} + + {/* Mini grid preview */} + {panelCount > 0 && ( +
+ {dashboard.layout.slice(0, 6).map((panel, idx) => ( +
+ ))} +
+ )} + + {/* Footer */} +
+
+ + {panelCount}{" "} + {panelCount === 1 ? "panel" : "panels"} + +
+ +
+ + + + ); + })} + + )} + + + ); +} diff --git a/web/src/app/globals.css b/web/src/app/globals.css index 630d6aa..d3a13a9 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -408,3 +408,72 @@ .pill-transition { transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } + +/* ===== REACT GRID LAYOUT OVERRIDES ===== */ +.react-grid-layout { + position: relative; + transition: height 300ms ease; +} + +.react-grid-item { + transition: all 300ms ease; + transition-property: left, top, width, height; +} + +.react-grid-item.cssTransforms { + transition-property: transform, width, height; +} + +.react-grid-item.react-draggable-dragging { + transition: none; + z-index: 50; + will-change: transform; + opacity: 0.9; +} + +.react-grid-item.dropping { + visibility: hidden; +} + +.react-grid-item > .react-resizable-handle { + position: absolute; + width: 16px; + height: 16px; + bottom: 2px; + right: 2px; + cursor: se-resize; + opacity: 0; + transition: opacity 0.2s ease; +} + +.react-grid-item:hover > .react-resizable-handle { + opacity: 1; +} + +.react-grid-item > .react-resizable-handle::after { + content: ''; + position: absolute; + right: 3px; + bottom: 3px; + width: 8px; + height: 8px; + border-right: 2px solid rgba(255, 255, 255, 0.2); + border-bottom: 2px solid rgba(255, 255, 255, 0.2); + border-radius: 0 0 2px 0; +} + +.react-grid-placeholder { + background: rgba(255, 255, 255, 0.06) !important; + border: 1px dashed rgba(255, 255, 255, 0.15) !important; + border-radius: 12px; + opacity: 1 !important; + transition: all 200ms ease; +} + +/* SVG visualization styles */ +.viz-svg svg { + width: 100%; + height: 100%; + max-width: 100%; + max-height: 100%; +} diff --git a/web/src/app/registry/[id]/page.tsx b/web/src/app/registry/[id]/page.tsx new file mode 100644 index 0000000..439fe31 --- /dev/null +++ b/web/src/app/registry/[id]/page.tsx @@ -0,0 +1,558 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useParams, useRouter } from "next/navigation"; +import dynamic from "next/dynamic"; +import { AppShell } from "@/components/layout/app-shell"; +import { AnimatedPage, staggerContainer, staggerItem } from "@/components/shared/animated-page"; +import { GlassCard } from "@/components/shared/glass-card"; +import { ErrorState } from "@/components/shared/error-state"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { motion } from "framer-motion"; +import { + Package, + ArrowLeft, + Download, + User, + Tag, + ExternalLink, + FileCode, + BookOpen, + GitBranch, + Shield, + Loader2, + Copy, + Check, +} from "lucide-react"; +import { api } from "@/lib/api"; +import { toast } from "sonner"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs"; + +const MonacoEditor = dynamic(() => import("@monaco-editor/react"), { + ssr: false, + loading: () => ( +
+ +
+ ), +}); + +interface RegistryMeta { + path: string; + raw_url_prefix: string; +} + +interface RegistryModel { + name: string; + description: string; + framework: string; + category: string; + version: string; + author: string; + tags: string[]; + files: string[]; + license: string; + dependencies: string[]; + homepage: string; + _registry: RegistryMeta; +} + +interface RegistryIndex { + version: string; + models: RegistryModel[]; +} + +const REGISTRY_URL = + "https://raw.githubusercontent.com/GACWR/open-model-registry/main/registry/index.json"; + +const frameworkColors: Record = { + pytorch: "bg-orange-500/10 text-orange-400 border-orange-500/20", + sklearn: "bg-blue-500/10 text-blue-400 border-blue-500/20", + tensorflow: "bg-yellow-500/10 text-yellow-400 border-yellow-500/20", + jax: "bg-green-500/10 text-green-400 border-green-500/20", + python: "bg-violet-500/10 text-violet-400 border-violet-500/20", + rust: "bg-amber-500/10 text-amber-400 border-amber-500/20", +}; + +const categoryColors: Record = { + classification: "bg-emerald-500/10 text-emerald-400", + "computer-vision": "bg-pink-500/10 text-pink-400", + nlp: "bg-cyan-500/10 text-cyan-400", + "time-series": "bg-indigo-500/10 text-indigo-400", + generative: "bg-fuchsia-500/10 text-fuchsia-400", + regression: "bg-teal-500/10 text-teal-400", + clustering: "bg-rose-500/10 text-rose-400", + "anomaly-detection": "bg-red-500/10 text-red-400", +}; + +export default function RegistryModelDetailPage() { + const params = useParams(); + const router = useRouter(); + const modelName = decodeURIComponent(params.id as string); + + const [model, setModel] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Source code for each file + const [fileSources, setFileSources] = useState>({}); + const [loadingCode, setLoadingCode] = useState(false); + const [activeFile, setActiveFile] = useState(""); + + // Install dialog + const [installOpen, setInstallOpen] = useState(false); + const [installProject, setInstallProject] = useState(""); + const [installing, setInstalling] = useState(false); + const [projects, setProjects] = useState<{ id: string; name: string }[]>([]); + + // Copy state + const [copied, setCopied] = useState(false); + + useEffect(() => { + setLoading(true); + setError(null); + + Promise.all([ + fetch(REGISTRY_URL).then((r) => { + if (!r.ok) throw new Error(`Registry fetch failed (${r.status})`); + return r.json() as Promise; + }), + api.get<{ id: string; name: string }[]>("/projects").catch(() => []), + ]) + .then(([data, projs]) => { + const found = data.models?.find((m) => m.name === modelName); + if (!found) { + setError(`Model "${modelName}" not found in registry`); + return; + } + setModel(found); + setProjects(projs); + + // Fetch source code for all files + const files = found.files || ["model.py"]; + setActiveFile(files[0]); + setLoadingCode(true); + Promise.all( + files.map(async (fname) => { + const url = `${found._registry.raw_url_prefix}/${fname}`; + try { + const res = await fetch(url); + return [fname, res.ok ? await res.text() : `# Failed to load ${fname}`] as const; + } catch { + return [fname, `# Failed to load ${fname}`] as const; + } + }) + ).then((results) => { + const sources: Record = {}; + for (const [fname, code] of results) { + sources[fname] = code; + } + setFileSources(sources); + setLoadingCode(false); + }); + }) + .catch((err) => + setError(err instanceof Error ? err.message : "Failed to load model") + ) + .finally(() => setLoading(false)); + }, [modelName]); + + const handleInstall = async () => { + if (!installProject || !model) { + toast.error("Select a project"); + return; + } + setInstalling(true); + try { + const mainFile = model.files?.[0] || "model.py"; + const source_code = fileSources[mainFile] || ""; + + await api.post("/sdk/register-model", { + project_id: installProject, + name: model.name, + description: model.description, + framework: model.framework, + source_code, + }); + toast.success(`Installed ${model.name} successfully`); + setInstallOpen(false); + } catch (err) { + toast.error( + err instanceof Error ? err.message : "Failed to install model" + ); + } finally { + setInstalling(false); + } + }; + + const handleCopyInstall = () => { + navigator.clipboard.writeText(`openmodelstudio install ${modelName}`); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + if (loading) { + return ( + + + + + + ); + } + + if (error || !model) { + return ( + + + router.push("/registry")} + /> + + + ); + } + + const lang = activeFile.endsWith(".rs") + ? "rust" + : activeFile.endsWith(".py") + ? "python" + : "plaintext"; + + return ( + + + {/* Header */} +
+ + + +
+ +
+
+
+

+ {model.name} +

+ + {model.framework} + + + {model.category} + + + v{model.version} + +
+
+ + + {model.author} + + + + {model.license} + +
+
+
+ {model.homepage && ( + + + + )} + + + +
+
+ + {/* Content grid */} + + {/* Left: Description + Meta */} + + {/* Description */} + +

+ + About +

+

+ {model.description} +

+
+ + {/* Quick install */} + +

+ + Quick Install +

+
+ + openmodelstudio install {model.name} + + + {copied ? ( + + ) : ( + + )} + +
+

+ Or install from the UI with the Install button above. +

+
+ + {/* Tags */} + {model.tags && model.tags.length > 0 && ( + +

+ + Tags +

+
+ {model.tags.map((tag) => ( + + {tag} + + ))} +
+
+ )} + + {/* Dependencies */} + {model.dependencies && model.dependencies.length > 0 && ( + +

+ + Dependencies +

+
+ {model.dependencies.map((dep) => ( +
+ + {dep} + +
+ ))} +
+
+ )} + + {/* Files */} + +

+ + Files +

+
+ {model.files.map((fname) => ( + + ))} +
+
+
+ + {/* Right: Source Code viewer (spans 2 cols) */} + + +
+
+ + + {activeFile} + +
+
+ {model.files.length > 1 && ( + + + {model.files.map((f) => ( + + {f} + + ))} + + + )} +
+
+
+ {loadingCode ? ( +
+ +
+ ) : ( + + )} +
+
+
+
+ + {/* Install Dialog */} + + + + Install Model + + Install{" "} + + {model.name} + {" "} + into a project. + + +
+
+
+ + + {model.name} + + + {model.framework} + +
+

+ {model.description} +

+
+
+ + +
+ +
+
+
+
+
+ ); +} diff --git a/web/src/app/registry/page.tsx b/web/src/app/registry/page.tsx new file mode 100644 index 0000000..8ad4b72 --- /dev/null +++ b/web/src/app/registry/page.tsx @@ -0,0 +1,502 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { AppShell } from "@/components/layout/app-shell"; +import { AnimatedPage, staggerContainer, staggerItem } from "@/components/shared/animated-page"; +import { GlassCard } from "@/components/shared/glass-card"; +import { EmptyState } from "@/components/shared/empty-state"; +import { ErrorState } from "@/components/shared/error-state"; +import { CardSkeleton } from "@/components/shared/loading-skeleton"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { motion } from "framer-motion"; +import { Package, Search, Download, Tag, User, ExternalLink, ChevronRight } from "lucide-react"; +import Link from "next/link"; +import { api } from "@/lib/api"; +import { toast } from "sonner"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +interface RegistryMeta { + path: string; + raw_url_prefix: string; +} + +interface RegistryModel { + name: string; + description: string; + framework: string; + category: string; + version: string; + author: string; + tags: string[]; + files: string[]; + license: string; + dependencies: string[]; + homepage: string; + _registry: RegistryMeta; +} + +interface RegistryIndex { + version: string; + models: RegistryModel[]; +} + +const REGISTRY_URL = + "https://raw.githubusercontent.com/GACWR/open-model-registry/main/registry/index.json"; + +const frameworkColors: Record = { + pytorch: "bg-orange-500/10 text-orange-400 border-orange-500/20", + sklearn: "bg-blue-500/10 text-blue-400 border-blue-500/20", + tensorflow: "bg-yellow-500/10 text-yellow-400 border-yellow-500/20", + jax: "bg-green-500/10 text-green-400 border-green-500/20", + python: "bg-violet-500/10 text-violet-400 border-violet-500/20", + rust: "bg-amber-500/10 text-amber-400 border-amber-500/20", +}; + +const categoryColors: Record = { + classification: "bg-emerald-500/10 text-emerald-400", + "computer-vision": "bg-pink-500/10 text-pink-400", + nlp: "bg-cyan-500/10 text-cyan-400", + "time-series": "bg-indigo-500/10 text-indigo-400", + generative: "bg-fuchsia-500/10 text-fuchsia-400", + regression: "bg-teal-500/10 text-teal-400", + clustering: "bg-rose-500/10 text-rose-400", + "anomaly-detection": "bg-red-500/10 text-red-400", +}; + +const ALL_CATEGORIES = [ + "All", + "Classification", + "Computer Vision", + "NLP", + "Time Series", + "Generative", + "Regression", + "Clustering", + "Anomaly Detection", +]; + +const ALL_FRAMEWORKS = [ + "All", + "PyTorch", + "sklearn", + "TensorFlow", + "JAX", + "Python", + "Rust", +]; + +function categoryKey(label: string): string { + return label.toLowerCase().replace(/\s+/g, "-"); +} + +export default function RegistryPage() { + const [models, setModels] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [search, setSearch] = useState(""); + const [searchFocused, setSearchFocused] = useState(false); + const [activeCategory, setActiveCategory] = useState("All"); + const [activeFramework, setActiveFramework] = useState("All"); + + // Install dialog state + const [installOpen, setInstallOpen] = useState(false); + const [installModel, setInstallModel] = useState(null); + const [installProject, setInstallProject] = useState(""); + const [installing, setInstalling] = useState(false); + const [projects, setProjects] = useState<{ id: string; name: string }[]>([]); + + const fetchRegistry = () => { + setLoading(true); + setError(null); + fetch(REGISTRY_URL) + .then((res) => { + if (!res.ok) throw new Error(`Failed to fetch registry (${res.status})`); + return res.json(); + }) + .then((data: RegistryIndex) => { + setModels(data.models || []); + }) + .catch((err) => setError(err instanceof Error ? err.message : "Failed to load registry")) + .finally(() => setLoading(false)); + }; + + useEffect(() => { + fetchRegistry(); + api + .get<{ id: string; name: string }[]>("/projects") + .then(setProjects) + .catch(() => {}); + }, []); + + const handleInstallClick = (model: RegistryModel) => { + setInstallModel(model); + setInstallOpen(true); + }; + + const handleInstall = async () => { + if (!installProject) { + toast.error("Select a project"); + return; + } + if (!installModel) return; + setInstalling(true); + try { + // Fetch the model code from the registry raw URL + const mainFile = installModel.files?.[0] || "model.py"; + const codeUrl = `${installModel._registry.raw_url_prefix}/${mainFile}`; + const codeRes = await fetch(codeUrl); + const source_code = codeRes.ok ? await codeRes.text() : ""; + + await api.post("/sdk/register-model", { + project_id: installProject, + name: installModel.name, + description: installModel.description, + framework: installModel.framework, + source_code, + }); + toast.success(`Installed ${installModel.name} successfully`); + setInstallOpen(false); + setInstallModel(null); + setInstallProject(""); + } catch (err) { + toast.error( + err instanceof Error ? err.message : "Failed to install model" + ); + } finally { + setInstalling(false); + } + }; + + const filtered = models.filter((m) => { + const matchesSearch = + m.name.toLowerCase().includes(search.toLowerCase()) || + m.description.toLowerCase().includes(search.toLowerCase()) || + m.tags?.some((t) => t.toLowerCase().includes(search.toLowerCase())); + const matchesCategory = + activeCategory === "All" || + m.category.toLowerCase() === categoryKey(activeCategory); + const matchesFramework = + activeFramework === "All" || + m.framework.toLowerCase() === activeFramework.toLowerCase(); + return matchesSearch && matchesCategory && matchesFramework; + }); + + return ( + + + {/* Header */} +
+
+

+ Model Registry +

+

+ Browse and install community models from the Open Model Registry +

+
+ + + +
+ + {/* Category filter tabs */} +
+ {ALL_CATEGORIES.map((cat) => ( + setActiveCategory(cat)} + className={`rounded-full px-3 py-1.5 text-xs font-medium transition-all duration-200 ${ + activeCategory === cat + ? "bg-white text-black shadow-lg shadow-white/10" + : "bg-white/5 text-muted-foreground hover:bg-white/10 hover:text-foreground" + }`} + > + {cat} + + ))} +
+ + {/* Framework filter badges */} +
+ + Framework: + + {ALL_FRAMEWORKS.map((fw) => ( + setActiveFramework(fw)} + className={`rounded-md px-2.5 py-1 text-[11px] font-medium border transition-all duration-200 ${ + activeFramework === fw + ? "bg-white/10 text-foreground border-white/20" + : "bg-transparent text-muted-foreground border-border/50 hover:bg-white/5 hover:text-foreground" + }`} + > + {fw} + + ))} +
+ + {/* Animated search bar */} + + + setSearch(e.target.value)} + onFocus={() => setSearchFocused(true)} + onBlur={() => setSearchFocused(false)} + className="border bg-card/50 pl-10 input-glow transition-all" + /> + + + {/* Content */} + {loading ? ( +
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+ ) : error ? ( + + ) : filtered.length === 0 ? ( + + ) : ( + + {filtered.map((model) => ( + + + + {/* Header */} +
+
+
+ +
+
+

+ {model.name} +

+
+ + + {model.author} + +
+
+
+
+ + {/* Description */} +

+ {model.description} +

+ + {/* Tags */} + {model.tags && model.tags.length > 0 && ( +
+ {model.tags.slice(0, 4).map((tag) => ( + + + {tag} + + ))} + {model.tags.length > 4 && ( + + +{model.tags.length - 4} more + + )} +
+ )} + + {/* Badges + Actions */} +
+
+ + {model.framework} + + + {model.category} + + + v{model.version} + +
+
+ + + + +
+
+
+ +
+ ))} +
+ )} + + {/* Install Dialog */} + + + + Install Model + + Install{" "} + + {installModel?.name} + {" "} + into a project. + + +
+ {installModel && ( +
+
+ + + {installModel.name} + + + {installModel.framework} + +
+

+ {installModel.description} +

+
+ )} +
+ + +
+ +
+
+
+
+
+ ); +} diff --git a/web/src/app/visualizations/[id]/page.tsx b/web/src/app/visualizations/[id]/page.tsx new file mode 100644 index 0000000..31c2446 --- /dev/null +++ b/web/src/app/visualizations/[id]/page.tsx @@ -0,0 +1,811 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { useParams, useRouter } from "next/navigation"; +import dynamic from "next/dynamic"; +import { AppShell } from "@/components/layout/app-shell"; +import { AnimatedPage } from "@/components/shared/animated-page"; +import { GlassCard } from "@/components/shared/glass-card"; +import { ErrorState } from "@/components/shared/error-state"; +import { VizRenderer } from "@/components/shared/viz-renderer"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { motion, AnimatePresence } from "framer-motion"; +import { + ArrowLeft, + Save, + Eye, + EyeOff, + Play, + Upload, + Code2, + Database, + Settings2, + BarChart3, + Loader2, + Check, +} from "lucide-react"; +import { api } from "@/lib/api"; +import { toast } from "sonner"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs"; + +// Dynamic import Monaco to avoid SSR issues +const MonacoEditor = dynamic(() => import("@monaco-editor/react"), { + ssr: false, + loading: () => ( +
+ +
+ ), +}); + +interface Visualization { + id: string; + project_id: string | null; + name: string; + description: string | null; + backend: string; + output_type: string; + code: string | null; + config: Record | null; + refresh_interval: number | null; + published: boolean; + rendered_output: string | null; + created_at: string; + updated_at: string; +} + +const backendColors: Record = { + matplotlib: "bg-blue-500/10 text-blue-400 border-blue-500/20", + seaborn: "bg-teal-500/10 text-teal-400 border-teal-500/20", + plotly: "bg-purple-500/10 text-purple-400 border-purple-500/20", + bokeh: "bg-green-500/10 text-green-400 border-green-500/20", + altair: "bg-orange-500/10 text-orange-400 border-orange-500/20", + plotnine: "bg-red-500/10 text-red-400 border-red-500/20", + datashader: "bg-cyan-500/10 text-cyan-400 border-cyan-500/20", + networkx: "bg-yellow-500/10 text-yellow-400 border-yellow-500/20", + geopandas: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20", +}; + +// Map backend to language for Monaco syntax highlighting +const backendLanguage: Record = { + matplotlib: "python", + seaborn: "python", + plotnine: "python", + plotly: "json", + bokeh: "python", + altair: "json", + datashader: "python", + networkx: "python", + geopandas: "python", +}; + +// Template code for each backend +const TEMPLATES: Record = { + matplotlib: `import matplotlib.pyplot as plt +import numpy as np + +def render(ctx): + """Render a matplotlib visualization.""" + fig, ax = plt.subplots(figsize=(ctx.width / 100, ctx.height / 100)) + fig.patch.set_alpha(0) + ax.set_facecolor("none") + + # Example: line chart + x = np.linspace(0, 10, 100) + y = np.sin(x) + ax.plot(x, y, color="#8b5cf6", linewidth=2) + + ax.set_title("Sine Wave", color="white") + ax.tick_params(colors="white") + for spine in ax.spines.values(): + spine.set_color("rgba(255,255,255,0.2)") + + return fig +`, + seaborn: `import seaborn as sns +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd + +def render(ctx): + """Render a seaborn visualization.""" + fig, ax = plt.subplots(figsize=(ctx.width / 100, ctx.height / 100)) + fig.patch.set_alpha(0) + ax.set_facecolor("none") + + # Example: scatter plot + data = pd.DataFrame({ + "x": np.random.randn(100), + "y": np.random.randn(100), + "category": np.random.choice(["A", "B", "C"], 100), + }) + sns.scatterplot(data=data, x="x", y="y", hue="category", ax=ax) + ax.tick_params(colors="white") + ax.set_title("Scatter Plot", color="white") + + return fig +`, + plotly: `{ + "data": [ + { + "type": "scatter", + "mode": "lines+markers", + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [0.9, 0.75, 0.6, 0.45, 0.35, 0.28, 0.22, 0.18, 0.15, 0.12], + "name": "Training Loss", + "line": { "color": "#8b5cf6", "width": 2 }, + "marker": { "size": 6 } + }, + { + "type": "scatter", + "mode": "lines+markers", + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [0.5, 0.6, 0.68, 0.74, 0.78, 0.82, 0.85, 0.87, 0.89, 0.91], + "name": "Accuracy", + "line": { "color": "#10b981", "width": 2 }, + "marker": { "size": 6 }, + "yaxis": "y2" + } + ], + "layout": { + "title": { "text": "Training Metrics", "font": { "color": "white" } }, + "xaxis": { "title": "Epoch" }, + "yaxis": { "title": "Loss" }, + "yaxis2": { "title": "Accuracy", "overlaying": "y", "side": "right" }, + "legend": { "x": 0, "y": 1.15, "orientation": "h" } + } +} +`, + altair: `{ + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "mark": { "type": "bar", "cornerRadiusTopLeft": 3, "cornerRadiusTopRight": 3 }, + "encoding": { + "x": { + "field": "category", + "type": "nominal", + "axis": { "labelAngle": 0 } + }, + "y": { + "field": "value", + "type": "quantitative" + }, + "color": { + "field": "category", + "type": "nominal", + "scale": { "scheme": "category10" } + } + }, + "data": { + "values": [ + { "category": "A", "value": 28 }, + { "category": "B", "value": 55 }, + { "category": "C", "value": 43 }, + { "category": "D", "value": 91 }, + { "category": "E", "value": 81 }, + { "category": "F", "value": 53 } + ] + }, + "width": "container", + "height": 300, + "title": "Category Distribution" +} +`, + bokeh: `from bokeh.plotting import figure +from bokeh.models import ColumnDataSource +import numpy as np + +def render(ctx): + """Render a Bokeh visualization.""" + x = np.linspace(0, 4 * np.pi, 100) + y = np.sin(x) + + source = ColumnDataSource(data=dict(x=x, y=y)) + p = figure(title="Sine Wave", width=ctx.width, height=ctx.height, + background_fill_alpha=0, border_fill_alpha=0) + p.line("x", "y", source=source, line_width=2, color="#8b5cf6") + + return p +`, + plotnine: `from plotnine import * +import pandas as pd +import numpy as np + +def render(ctx): + """Render a plotnine (ggplot2) visualization.""" + data = pd.DataFrame({ + "x": np.random.randn(200), + "y": np.random.randn(200), + "group": np.random.choice(["Alpha", "Beta"], 200), + }) + return ( + ggplot(data, aes("x", "y", color="group")) + + geom_point(alpha=0.6) + + theme_minimal() + + labs(title="Scatter Plot") + ) +`, + datashader: `import datashader as ds +import pandas as pd +import numpy as np + +def render(ctx): + """Render a datashader image for large datasets.""" + n = 1_000_000 + data = pd.DataFrame({ + "x": np.random.randn(n), + "y": np.random.randn(n) + np.random.randn(n) * 0.5, + }) + canvas = ds.Canvas(plot_width=ctx.width, plot_height=ctx.height) + agg = canvas.points(data, "x", "y") + return ds.tf.shade(agg, cmap=["#000000", "#8b5cf6", "#ffffff"]) +`, + networkx: `import networkx as nx +import matplotlib.pyplot as plt + +def render(ctx): + """Render a NetworkX graph.""" + fig, ax = plt.subplots(figsize=(ctx.width / 100, ctx.height / 100)) + fig.patch.set_alpha(0) + ax.set_facecolor("none") + + G = nx.karate_club_graph() + pos = nx.spring_layout(G, seed=42) + nx.draw_networkx(G, pos, ax=ax, node_color="#8b5cf6", + edge_color="rgba(255,255,255,0.2)", + font_color="white", node_size=200) + ax.set_title("Karate Club Graph", color="white") + + return fig +`, + geopandas: `import geopandas as gpd +import matplotlib.pyplot as plt + +def render(ctx): + """Render a GeoPandas map.""" + fig, ax = plt.subplots(figsize=(ctx.width / 100, ctx.height / 100)) + fig.patch.set_alpha(0) + ax.set_facecolor("none") + + world = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres")) + world.plot(ax=ax, color="#8b5cf6", edgecolor="rgba(255,255,255,0.3)") + ax.set_title("World Map", color="white") + ax.tick_params(colors="white") + + return fig +`, +}; + +export default function VisualizationDetailPage() { + const params = useParams(); + const router = useRouter(); + const id = params.id as string; + + const [viz, setViz] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [saving, setSaving] = useState(false); + const [publishing, setPublishing] = useState(false); + const [hasChanges, setHasChanges] = useState(false); + + // Editor state + const [code, setCode] = useState(""); + const [dataJson, setDataJson] = useState("{}"); + const [configJson, setConfigJson] = useState("{}"); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [refreshInterval, setRefreshInterval] = useState("0"); + const [showPreview, setShowPreview] = useState(true); + const [activeTab, setActiveTab] = useState("code"); + + // Live preview state for interactive backends (plotly, vega-lite) + const [previewOutput, setPreviewOutput] = useState(null); + const [previewType, setPreviewType] = useState("svg"); + + const fetchViz = useCallback(() => { + setLoading(true); + setError(null); + api + .get(`/visualizations/${id}`) + .then((v) => { + setViz(v); + setCode(v.code || TEMPLATES[v.backend] || ""); + setName(v.name); + setDescription(v.description || ""); + setRefreshInterval(String(v.refresh_interval || 0)); + setPreviewOutput(v.rendered_output); + setPreviewType(v.output_type); + if (v.config) { + try { + setConfigJson(JSON.stringify(v.config, null, 2)); + } catch { + setConfigJson("{}"); + } + } + }) + .catch((err) => + setError( + err instanceof Error ? err.message : "Failed to load visualization" + ) + ) + .finally(() => setLoading(false)); + }, [id]); + + useEffect(() => { + fetchViz(); + }, [fetchViz]); + + // Live preview for JSON-based backends (plotly, altair/vega-lite) + useEffect(() => { + if (!viz) return; + const backend = viz.backend.toLowerCase(); + if (backend === "plotly" || backend === "altair") { + // For JSON-based backends, the code IS the spec + try { + JSON.parse(code); + setPreviewOutput(code); + setPreviewType(backend === "plotly" ? "plotly" : "vega-lite"); + } catch { + // Invalid JSON, don't update preview + } + } + }, [code, viz]); + + const handleSave = async () => { + if (!viz) return; + setSaving(true); + try { + const body: Record = { + name, + description: description || null, + code, + refresh_interval: parseInt(refreshInterval) || 0, + }; + + // For JSON-based backends, save the code as rendered_output too + const backend = viz.backend.toLowerCase(); + if (backend === "plotly" || backend === "altair") { + try { + JSON.parse(code); + body.rendered_output = code; + } catch { + // Not valid JSON, skip + } + } + + try { + const parsed = JSON.parse(configJson); + body.config = parsed; + } catch { + // Skip invalid JSON + } + + try { + const parsed = JSON.parse(dataJson); + body.data = parsed; + } catch { + // Skip invalid JSON + } + + await api.put(`/visualizations/${id}`, body); + toast.success("Visualization saved"); + setHasChanges(false); + fetchViz(); + } catch (err) { + toast.error( + err instanceof Error ? err.message : "Failed to save" + ); + } finally { + setSaving(false); + } + }; + + const handlePublish = async () => { + setPublishing(true); + try { + await api.post(`/visualizations/${id}/publish`, {}); + toast.success("Visualization published to dashboard"); + fetchViz(); + } catch (err) { + toast.error( + err instanceof Error ? err.message : "Failed to publish" + ); + } finally { + setPublishing(false); + } + }; + + const handleCodeChange = (value: string | undefined) => { + setCode(value || ""); + setHasChanges(true); + }; + + const handleInsertTemplate = () => { + if (!viz) return; + const template = TEMPLATES[viz.backend]; + if (template) { + setCode(template); + setHasChanges(true); + toast.success("Template inserted"); + } + }; + + if (loading) { + return ( + + + + + + ); + } + + if (error || !viz) { + return ( + + + + + + ); + } + + const isJsonBackend = viz.backend === "plotly" || viz.backend === "altair"; + const editorLanguage = isJsonBackend ? "json" : "python"; + + return ( + + + {/* Header */} +
+
+ + + +
+
+ +
+
+
+ { + setName(e.target.value); + setHasChanges(true); + }} + className="h-7 border-none bg-transparent text-lg font-bold p-0 focus-visible:ring-0 text-foreground" + /> + + {viz.backend} + + + + {viz.published ? "Published" : "Draft"} + +
+ { + setDescription(e.target.value); + setHasChanges(true); + }} + placeholder="Add a description..." + className="h-5 mt-0.5 border-none bg-transparent text-xs text-muted-foreground p-0 focus-visible:ring-0" + /> +
+
+
+ +
+ + + + + {!viz.published && ( + + + + )} + + + {hasChanges && ( + + + + )} + +
+
+ + {/* Editor + Preview split */} +
+ {/* Left: Code Editor */} + + + + + + {isJsonBackend ? "Spec (JSON)" : "Code (Python)"} + + + + Data + + + + Config + + + + +
+
+

+ {isJsonBackend + ? `Paste or edit a ${viz.backend === "plotly" ? "Plotly" : "Vega-Lite"} JSON spec. It renders live in the preview.` + : `Write a render(ctx) function that returns a ${viz.backend} figure object.`} +

+ +
+
+ +
+
+
+ + +
+
+

+ JSON data passed as ctx.data to the render function. +

+
+
+ { + setDataJson(v || "{}"); + setHasChanges(true); + }} + options={{ + minimap: { enabled: false }, + fontSize: 13, + lineHeight: 20, + padding: { top: 8 }, + scrollBeyondLastLine: false, + wordWrap: "on", + automaticLayout: true, + tabSize: 2, + }} + /> +
+
+
+ + +
+
+ + { + setRefreshInterval(e.target.value); + setHasChanges(true); + }} + className="border bg-muted h-8 text-xs" + /> +

+ Set to 0 for static. For dynamic visualizations, this + controls how often the render function re-executes. +

+
+ +
+ +
+ { + setConfigJson(v || "{}"); + setHasChanges(true); + }} + options={{ + minimap: { enabled: false }, + fontSize: 12, + lineHeight: 18, + scrollBeyondLastLine: false, + wordWrap: "on", + automaticLayout: true, + tabSize: 2, + }} + /> +
+
+ +
+ + + {viz.output_type} + +

+ Auto-detected from backend. SVG for matplotlib/seaborn/plotnine/networkx/geopandas, + Plotly JSON for plotly, Vega-Lite JSON for altair, Bokeh JSON for bokeh, + PNG for datashader. +

+
+
+
+
+
+ + {/* Right: Preview */} + {showPreview && ( + + +
+
+ + + Preview + +
+ {isJsonBackend && ( + + + Live + + )} + {!isJsonBackend && ( + + Rendered from notebook + + )} +
+
+
+ +
+
+
+
+ )} +
+
+
+ ); +} diff --git a/web/src/app/visualizations/page.tsx b/web/src/app/visualizations/page.tsx new file mode 100644 index 0000000..e38020b --- /dev/null +++ b/web/src/app/visualizations/page.tsx @@ -0,0 +1,421 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { AppShell } from "@/components/layout/app-shell"; +import { AnimatedPage, staggerContainer, staggerItem } from "@/components/shared/animated-page"; +import { GlassCard } from "@/components/shared/glass-card"; +import { EmptyState } from "@/components/shared/empty-state"; +import { ErrorState } from "@/components/shared/error-state"; +import { CardSkeleton } from "@/components/shared/loading-skeleton"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { motion } from "framer-motion"; +import { BarChart3, Search, Plus, Eye, Clock, Trash2, ChevronRight } from "lucide-react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { api } from "@/lib/api"; +import { toast } from "sonner"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +interface Visualization { + id: string; + name: string; + backend: string; + output_type: string; + description: string | null; + refresh_interval: number; + published: boolean; + created_at: string; + updated_at: string; +} + +const backendColors: Record = { + matplotlib: "bg-blue-500/10 text-blue-400 border-blue-500/20", + seaborn: "bg-teal-500/10 text-teal-400 border-teal-500/20", + plotly: "bg-purple-500/10 text-purple-400 border-purple-500/20", + bokeh: "bg-green-500/10 text-green-400 border-green-500/20", + altair: "bg-orange-500/10 text-orange-400 border-orange-500/20", + plotnine: "bg-red-500/10 text-red-400 border-red-500/20", + datashader: "bg-cyan-500/10 text-cyan-400 border-cyan-500/20", + networkx: "bg-yellow-500/10 text-yellow-400 border-yellow-500/20", + geopandas: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20", +}; + +const BACKENDS = [ + "matplotlib", + "seaborn", + "plotly", + "bokeh", + "altair", + "plotnine", + "datashader", + "networkx", + "geopandas", +]; + +export default function VisualizationsPage() { + const router = useRouter(); + const [visualizations, setVisualizations] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [search, setSearch] = useState(""); + const [searchFocused, setSearchFocused] = useState(false); + const [deleting, setDeleting] = useState(null); + + // Create dialog state + const [createOpen, setCreateOpen] = useState(false); + const [newName, setNewName] = useState(""); + const [newBackend, setNewBackend] = useState(""); + const [newDescription, setNewDescription] = useState(""); + const [newRefreshInterval, setNewRefreshInterval] = useState("0"); + const [submitting, setSubmitting] = useState(false); + + const fetchVisualizations = () => { + setLoading(true); + setError(null); + api + .get("/visualizations") + .then(setVisualizations) + .catch((err) => + setError(err instanceof Error ? err.message : "Failed to load visualizations") + ) + .finally(() => setLoading(false)); + }; + + useEffect(() => { + fetchVisualizations(); + }, []); + + const handleCreate = async () => { + if (!newName.trim()) { + toast.error("Name is required"); + return; + } + if (!newBackend) { + toast.error("Select a backend"); + return; + } + setSubmitting(true); + try { + const result = await api.post<{ id: string }>("/visualizations", { + name: newName.trim(), + backend: newBackend, + description: newDescription.trim() || null, + refresh_interval: parseInt(newRefreshInterval) || 0, + }); + toast.success("Visualization created"); + setCreateOpen(false); + setNewName(""); + setNewBackend(""); + setNewDescription(""); + setNewRefreshInterval("0"); + // Navigate directly to the editor + router.push(`/visualizations/${result.id}`); + } catch (err) { + toast.error( + err instanceof Error ? err.message : "Failed to create visualization" + ); + } finally { + setSubmitting(false); + } + }; + + const handleDelete = async (vizId: string, e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setDeleting(vizId); + try { + await api.delete(`/visualizations/${vizId}`); + toast.success("Visualization deleted"); + setVisualizations(visualizations.filter((v) => v.id !== vizId)); + } catch (err) { + toast.error( + err instanceof Error ? err.message : "Failed to delete" + ); + } finally { + setDeleting(null); + } + }; + + const filtered = visualizations.filter( + (v) => + v.name.toLowerCase().includes(search.toLowerCase()) || + (v.description || "").toLowerCase().includes(search.toLowerCase()) || + v.backend.toLowerCase().includes(search.toLowerCase()) + ); + + function getStatus(v: Visualization): "Published" | "Draft" { + return v.published ? "Published" : "Draft"; + } + + return ( + + + {/* Header */} +
+
+

+ Visualizations +

+

+ Create and manage data visualizations +

+
+ + + + + + + + + New Visualization + + Create a new visualization with your preferred backend. + + +
+
+ + setNewName(e.target.value)} + className="border bg-muted input-glow" + /> +
+
+ + +
+
+ + setNewDescription(e.target.value)} + className="border bg-muted" + /> +
+
+ + setNewRefreshInterval(e.target.value)} + className="border bg-muted" + /> +

+ Set to 0 for a static visualization, or enter seconds for + auto-refresh. +

+
+ +
+
+
+
+ + {/* Animated search bar */} + + + setSearch(e.target.value)} + onFocus={() => setSearchFocused(true)} + onBlur={() => setSearchFocused(false)} + className="border bg-card/50 pl-10 input-glow transition-all" + /> + + + {/* Content */} + {loading ? ( +
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+ ) : error ? ( + + ) : filtered.length === 0 ? ( + setCreateOpen(true)} + /> + ) : ( + + {filtered.map((viz) => { + const status = getStatus(viz); + return ( + + + + {/* Header */} +
+
+
+ +
+
+

+ {viz.name} +

+
+ + {viz.backend} + +
+
+
+ + + {status} + +
+ + {/* Description */} + {viz.description && ( +

+ {viz.description} +

+ )} + {!viz.description &&
} + + {/* Footer */} +
+
+ {viz.refresh_interval > 0 && ( + + + {viz.refresh_interval}s refresh + + )} + + {new Date(viz.created_at).toLocaleDateString()} + +
+
+ + +
+
+ + + + ); + })} + + )} + + + ); +} diff --git a/web/src/components/layout/sidebar.tsx b/web/src/components/layout/sidebar.tsx index 5fc4ecc..eed9095 100644 --- a/web/src/components/layout/sidebar.tsx +++ b/web/src/components/layout/sidebar.tsx @@ -18,6 +18,9 @@ import { SlidersHorizontal, Cloud, Activity, + Package, + BarChart3, + LayoutDashboard, Users, Box, Settings, @@ -48,6 +51,7 @@ const sections = [ items: [ { name: "Workspaces", href: "/workspaces", icon: Terminal }, { name: "Models", href: "/models", icon: Brain }, + { name: "Model Registry", href: "/registry", icon: Package }, { name: "Datasets", href: "/datasets", icon: Database }, { name: "Data Sources", href: "/data-sources", icon: Plug }, { name: "Feature Store", href: "/features", icon: Layers }, @@ -69,6 +73,13 @@ const sections = [ { name: "Monitoring", href: "/monitoring", icon: Activity }, ], }, + { + label: "ANALYZE", + items: [ + { name: "Visualizations", href: "/visualizations", icon: BarChart3 }, + { name: "Dashboards", href: "/dashboards", icon: LayoutDashboard }, + ], + }, { label: "ADMIN", admin: true, diff --git a/web/src/components/shared/viz-renderer.tsx b/web/src/components/shared/viz-renderer.tsx new file mode 100644 index 0000000..8a1f377 --- /dev/null +++ b/web/src/components/shared/viz-renderer.tsx @@ -0,0 +1,297 @@ +"use client"; + +import { useEffect, useRef, useState, useCallback } from "react"; +import { BarChart3, Loader2 } from "lucide-react"; + +interface VizRendererProps { + outputType: string; + renderedOutput?: string | null; + className?: string; + autoResize?: boolean; +} + +/** + * Universal visualization renderer. + * + * Renders visualization output based on its type: + * - "svg" → inline SVG via dangerouslySetInnerHTML + * - "plotly" → Plotly.js (loaded from CDN on demand) + * - "vega-lite" → vega-embed (loaded from CDN on demand) + * - "bokeh" → BokehJS (loaded from CDN on demand) + * - "png" → tag (expects base64 data URL) + */ +export function VizRenderer({ + outputType, + renderedOutput, + className = "", + autoResize = true, +}: VizRendererProps) { + const containerRef = useRef(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // ── SVG ── + if (outputType === "svg" && renderedOutput) { + return ( +
+ ); + } + + // ── PNG ── + if (outputType === "png" && renderedOutput) { + return ( +
+ Visualization +
+ ); + } + + // ── Plotly ── + if (outputType === "plotly" && renderedOutput) { + return ( + + ); + } + + // ── Vega-Lite ── + if (outputType === "vega-lite" && renderedOutput) { + return ( + + ); + } + + // ── Bokeh ── + if (outputType === "bokeh" && renderedOutput) { + return ( + + ); + } + + // ── Empty state ── + return ( +
+ +

+ {renderedOutput + ? `Unsupported output type: ${outputType}` + : "Not rendered yet — run from a notebook or click Preview"} +

+
+ ); +} + +// ── SVG Sanitizer ────────────────────────────────────────────────── + +function sanitizeSvg(svg: string): string { + // Strip \ No newline at end of file + \ No newline at end of file diff --git a/tests/e2e/project-filter-flow.spec.ts b/tests/e2e/project-filter-flow.spec.ts new file mode 100644 index 0000000..214a1c7 --- /dev/null +++ b/tests/e2e/project-filter-flow.spec.ts @@ -0,0 +1,299 @@ +/** + * OpenModelStudio — Project Filter Integration Flow + * + * Verifies the complete project-scoped workflow: + * 1. Login + * 2. Create a project + * 3. Verify project appears in the global project filter dropdown + * 4. Upload a dataset scoped to the project + * 5. Create a workspace scoped to the project + * 6. Create a model scoped to the project + * 7. Set the project filter and verify pages scope correctly + * 8. Verify dashboard reflects the created entities + * 9. Cleanup + */ +import { test, expect } from './helpers/fixtures'; +import { apiLogin, apiPost, apiGet, apiDelete, DEFAULT_ADMIN } from './helpers/api-client'; + +test.describe('Project Filter Flow', () => { + test('full project-scoped workflow across all pages', async ({ authenticatedPage: page }) => { + test.setTimeout(180_000); + + let token: string; + let projectId: string; + let projectName: string; + let datasetId: string; + let modelId: string; + let workspaceId: string; + + // ─── Step 0: Authenticate ───────────────────────────────── + await test.step('Authenticate via API', async () => { + token = await apiLogin(DEFAULT_ADMIN); + expect(token).toBeTruthy(); + }); + + // ─── Step 1: Create Project via UI ──────────────────────── + await test.step('Create Project via UI', async () => { + projectName = `Filter Test ${Date.now()}`; + await page.goto('/projects'); + await page.waitForTimeout(2000); + + const createBtn = page.locator('button:has-text("New Project"), button:has-text("Create"), button:has(svg.lucide-plus)').first(); + await expect(createBtn).toBeVisible({ timeout: 10000 }); + await createBtn.click(); + await expect(page.locator('[role="dialog"]')).toBeVisible({ timeout: 5000 }); + + // Fill project name + const nameInput = page.locator('[role="dialog"] input').first(); + await nameInput.fill(projectName); + + // Fill description if present + const descInput = page.locator('[role="dialog"] textarea').first(); + if (await descInput.isVisible({ timeout: 1000 }).catch(() => false)) { + await descInput.fill('E2E project filter test'); + } + + // Select stage if combobox is present + const stageSelect = page.locator('[role="dialog"] button[role="combobox"]').first(); + if (await stageSelect.isVisible({ timeout: 1000 }).catch(() => false)) { + await stageSelect.click(); + await page.locator('[role="option"]').first().click(); + } + + // Navigate wizard steps + const nextBtn = page.locator('[role="dialog"] button:has-text("Next")').first(); + while (await nextBtn.isVisible({ timeout: 1000 }).catch(() => false)) { + await nextBtn.click(); + await page.waitForTimeout(300); + } + + // Submit + const submitBtn = page.locator('[role="dialog"] button:has-text("Create"), [role="dialog"] button:has-text("Submit")').first(); + if (await submitBtn.isVisible({ timeout: 1000 }).catch(() => false)) { + await submitBtn.click(); + } + + await expect(page.locator('[role="dialog"]')).toBeHidden({ timeout: 10000 }); + await page.waitForTimeout(2000); + + // Get project ID + const projects = await apiGet(token, '/projects'); + const proj = (projects as any[]).find((p: any) => p.name === projectName); + expect(proj).toBeTruthy(); + projectId = proj.id; + }); + + // ─── Step 2: Verify project appears in global filter ───── + await test.step('Verify project appears in global project filter', async () => { + await page.goto('/datasets'); + await page.waitForTimeout(2000); + + // The project filter is in the topbar — look for a combobox or select in header + const filterBtn = page.locator('header button[role="combobox"], header button:has-text("All Projects"), header button:has-text("Select Project")').first(); + if (await filterBtn.isVisible({ timeout: 5000 }).catch(() => false)) { + await filterBtn.click(); + await page.waitForTimeout(500); + + // Our project should appear in the dropdown options + const projectOption = page.locator(`[role="option"]:has-text("${projectName}")`).first(); + const hasOption = await projectOption.isVisible({ timeout: 5000 }).catch(() => false); + expect(hasOption).toBeTruthy(); + + // Select it + await projectOption.click(); + await page.waitForTimeout(1000); + } + }); + + // ─── Step 3: Create dataset via API ─────────────────────── + await test.step('Create dataset scoped to project', async () => { + const dataset = await apiPost(token, '/datasets', { + project_id: projectId, + name: `Filter Test Dataset ${Date.now()}`, + description: 'Dataset for project filter test', + format: 'csv', + }); + datasetId = dataset.id; + expect(datasetId).toBeTruthy(); + }); + + // ─── Step 4: Verify dataset appears on datasets page ───── + await test.step('Verify dataset appears on datasets page', async () => { + await page.goto('/datasets'); + await page.waitForTimeout(3000); + + // Datasets page should show content (cards or list) + const content = page.locator('main [class*="card"], main [class*="Card"], main a[href*="/datasets/"]'); + const empty = page.locator('text=/no datasets|upload your first/i'); + const hasContent = await content.first().isVisible({ timeout: 8000 }).catch(() => false); + const hasEmpty = await empty.first().isVisible({ timeout: 2000 }).catch(() => false); + expect(hasContent || hasEmpty).toBeTruthy(); + }); + + // ─── Step 5: Open upload dialog and verify project dropdown ─ + await test.step('Upload dialog shows project in dropdown', async () => { + await page.goto('/datasets'); + await page.waitForTimeout(2000); + + const uploadBtn = page.locator('button:has-text("Upload"), button:has-text("New Dataset"), button:has-text("Create"), button:has(svg.lucide-upload)').first(); + if (await uploadBtn.isVisible({ timeout: 5000 }).catch(() => false)) { + await uploadBtn.click(); + await expect(page.locator('[role="dialog"]')).toBeVisible({ timeout: 5000 }); + + // Look for project dropdown in the dialog + const projectSelect = page.locator('[role="dialog"] button[role="combobox"]').first(); + if (await projectSelect.isVisible({ timeout: 3000 }).catch(() => false)) { + await projectSelect.click(); + await page.waitForTimeout(500); + + // Our project should be in the options + const projectOption = page.locator(`[role="option"]`).first(); + const hasOptions = await projectOption.isVisible({ timeout: 3000 }).catch(() => false); + expect(hasOptions).toBeTruthy(); + + await page.keyboard.press('Escape'); + } + + await page.keyboard.press('Escape'); + } + }); + + // ─── Step 6: Create model scoped to project ────────────── + await test.step('Create model scoped to project via API', async () => { + const model = await apiPost(token, '/sdk/register-model', { + name: `filter-test-model-${Date.now()}`, + framework: 'sklearn', + project_id: projectId, + source_code: ` +def train(ctx): + ctx.log_metric("progress", 100) + +def infer(ctx): + ctx.set_output({"result": "ok"}) +`, + }); + expect(model.model_id).toBeTruthy(); + modelId = model.model_id; + }); + + // ─── Step 7: Verify model appears on models page ───────── + await test.step('Verify model appears on models page', async () => { + await page.goto('/models'); + await page.waitForTimeout(3000); + + const content = page.locator('main [class*="card"], main [class*="Card"], main a[href*="/models/"]'); + const empty = page.locator('text=/no models|register|get started/i'); + const hasContent = await content.first().isVisible({ timeout: 8000 }).catch(() => false); + const hasEmpty = await empty.first().isVisible({ timeout: 2000 }).catch(() => false); + expect(hasContent || hasEmpty).toBeTruthy(); + }); + + // ─── Step 8: Launch workspace scoped to project ────────── + await test.step('Launch workspace for project', async () => { + await page.goto('/workspaces'); + await page.waitForTimeout(2000); + + const launchBtn = page.locator('button:has-text("Launch"), button:has-text("New Workspace"), button:has-text("Create"), button:has(svg.lucide-plus)').first(); + if (await launchBtn.isVisible({ timeout: 5000 }).catch(() => false)) { + await launchBtn.click(); + await expect(page.locator('[role="dialog"]')).toBeVisible({ timeout: 5000 }); + + // Select JupyterLab IDE + const jupyterOption = page.locator('[role="dialog"] text=/JupyterLab/i').first(); + if (await jupyterOption.isVisible({ timeout: 2000 }).catch(() => false)) { + await jupyterOption.click(); + } + + // Select project in workspace dialog + const projectSelect = page.locator('[role="dialog"] button[role="combobox"]').first(); + if (await projectSelect.isVisible({ timeout: 1000 }).catch(() => false)) { + await projectSelect.click(); + await page.waitForTimeout(500); + + // Look for our project option + const projectOption = page.locator(`[role="option"]`).first(); + if (await projectOption.isVisible({ timeout: 3000 }).catch(() => false)) { + await projectOption.click(); + } + } + + // Fill workspace name + const nameInput = page.locator('[role="dialog"] input[placeholder*="name" i], [role="dialog"] input').first(); + if (await nameInput.isVisible({ timeout: 1000 }).catch(() => false)) { + await nameInput.fill(`Filter WS ${Date.now()}`); + } + + // Submit + const submitBtn = page.locator('[role="dialog"] button:has-text("Launch"), [role="dialog"] button:has-text("Create")').first(); + if (await submitBtn.isVisible({ timeout: 1000 }).catch(() => false)) { + await submitBtn.click(); + await page.waitForTimeout(5000); + } + } + + // Get workspace ID via API + try { + const workspaces = await apiGet(token, '/workspaces'); + const ws = (workspaces as any[]).find((w: any) => w.name?.includes('Filter WS')); + if (ws) workspaceId = ws.id; + } catch { + // Workspace creation may fail without K8s + } + }); + + // ─── Step 9: Dashboard shows created entities ──────────── + await test.step('Dashboard reflects created entities', async () => { + await page.goto('/'); + await page.waitForTimeout(3000); + + // Dashboard should show KPI cards with counts + const kpiCards = page.locator('[data-slot="card"], [class*="card"], [class*="Card"]'); + await expect(kpiCards.first()).toBeVisible({ timeout: 10000 }); + + // Look for entity type labels on dashboard + const hasModels = await page.locator('text=/model/i').first().isVisible({ timeout: 3000 }).catch(() => false); + const hasDatasets = await page.locator('text=/dataset/i').first().isVisible({ timeout: 2000 }).catch(() => false); + const hasProjects = await page.locator('text=/project/i').first().isVisible({ timeout: 2000 }).catch(() => false); + + // At least some entity types should be visible on dashboard + expect(hasModels || hasDatasets || hasProjects).toBeTruthy(); + }); + + // ─── Step 10: Project detail shows associated entities ─── + await test.step('Project detail shows associated resources', async () => { + await page.goto(`/projects/${projectId}`); + await page.waitForTimeout(3000); + + // Project name should be visible + await expect(page.locator(`text=${projectName}`).first()).toBeVisible({ timeout: 10000 }); + + // Click through tabs to check associated entities + const tabs = page.locator('main button[role="tab"]'); + if (await tabs.first().isVisible({ timeout: 5000 }).catch(() => false)) { + const tabCount = await tabs.count(); + for (let i = 0; i < Math.min(tabCount, 6); i++) { + if (await tabs.nth(i).isVisible().catch(() => false)) { + await tabs.nth(i).click(); + await page.waitForTimeout(800); + } + } + } + }); + + // ─── Step 11: Cleanup ───────────────────────────────────── + await test.step('Cleanup created resources', async () => { + if (workspaceId) { + try { await apiDelete(token, `/workspaces/${workspaceId}`); } catch { /* ok */ } + } + if (modelId) { + try { await apiDelete(token, `/models/${modelId}`); } catch { /* ok */ } + } + if (datasetId) { + try { await apiDelete(token, `/datasets/${datasetId}`); } catch { /* ok */ } + } + if (projectId) { + try { await apiDelete(token, `/projects/${projectId}`); } catch { /* ok */ } + } + }); + }); +}); diff --git a/web/src/providers/project-filter-provider.tsx b/web/src/providers/project-filter-provider.tsx index 832418a..f2b5838 100644 --- a/web/src/providers/project-filter-provider.tsx +++ b/web/src/providers/project-filter-provider.tsx @@ -1,6 +1,7 @@ "use client"; import { createContext, useContext, useState, useEffect, useCallback } from "react"; +import { usePathname } from "next/navigation"; import { api } from "@/lib/api"; import { useAuth } from "@/providers/auth-provider"; @@ -14,6 +15,7 @@ interface ProjectFilterContextValue { setSelectedProjectId: (id: string | null) => void; projects: Project[]; loading: boolean; + refetchProjects: () => void; } const ProjectFilterContext = createContext({ @@ -21,22 +23,24 @@ const ProjectFilterContext = createContext({ setSelectedProjectId: () => {}, projects: [], loading: true, + refetchProjects: () => {}, }); export function ProjectFilterProvider({ children }: { children: React.ReactNode }) { const [selectedProjectId, setSelectedState] = useState(null); const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(true); + const { user } = useAuth(); + const pathname = usePathname(); useEffect(() => { const stored = localStorage.getItem("oms_project_filter"); if (stored && stored !== "null") setSelectedState(stored); }, []); - const { user } = useAuth(); - - useEffect(() => { - if (!user) { + const fetchProjects = useCallback(() => { + const token = typeof window !== "undefined" ? localStorage.getItem("auth_token") : null; + if (!token) { setLoading(false); return; } @@ -44,7 +48,12 @@ export function ProjectFilterProvider({ children }: { children: React.ReactNode .then(setProjects) .catch(() => {}) .finally(() => setLoading(false)); - }, [user]); + }, []); + + // Refetch whenever user logs in or navigates to a different page + useEffect(() => { + fetchProjects(); + }, [user, pathname, fetchProjects]); const setSelectedProjectId = useCallback((id: string | null) => { setSelectedState(id); @@ -52,7 +61,7 @@ export function ProjectFilterProvider({ children }: { children: React.ReactNode }, []); return ( - + {children} ); From 653dba6d9c20090e60ae70f68a8cd423a8cbae1c Mon Sep 17 00:00:00 2001 From: jovonni Date: Sun, 8 Mar 2026 08:45:37 -0400 Subject: [PATCH 09/16] fixing model injection and global search --- api/src/main.rs | 2 + api/src/routes/models.rs | 25 ++++++ api/src/routes/sdk.rs | 19 +++++ docs/CLI-REGISTRY.md | 22 ++--- sdk/python/openmodelstudio/__init__.py | 7 +- sdk/python/openmodelstudio/cli.py | 6 +- sdk/python/openmodelstudio/client.py | 108 +++++++++++++++++++++++++ sdk/python/openmodelstudio/model.py | 17 +++- sdk/python/openmodelstudio/registry.py | 33 +++++++- web/src/app/(auth)/layout.tsx | 2 +- web/src/app/registry/page.tsx | 8 +- 11 files changed, 231 insertions(+), 18 deletions(-) diff --git a/api/src/main.rs b/api/src/main.rs index 6054cd0..8a34ab9 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -97,6 +97,7 @@ async fn main() { .route("/models", get(routes::models::list_all)) .route("/models", post(routes::models::create)) .route("/models/registry-status", get(routes::models::registry_status)) + .route("/models/registry-uninstall", post(routes::models::registry_uninstall)) .route("/models/{id}", get(routes::models::get)) .route("/models/{id}", put(routes::models::update)) .route("/models/{id}", delete(routes::models::delete)) @@ -181,6 +182,7 @@ async fn main() { .route("/sdk/datasets/{id}/upload", post(routes::sdk::dataset_upload)) .route("/sdk/datasets/{id}/content", get(routes::sdk::dataset_content)) .route("/sdk/models/resolve/{name_or_id}", get(routes::sdk::resolve_model)) + .route("/sdk/models/resolve-registry/{name}", get(routes::sdk::resolve_registry_model)) .route("/sdk/models/{id}/artifact", get(routes::sdk::model_artifact)) // SDK Feature Store .route("/sdk/features", post(routes::sdk::create_features)) diff --git a/api/src/routes/models.rs b/api/src/routes/models.rs index 7512164..a6b3fa8 100644 --- a/api/src/routes/models.rs +++ b/api/src/routes/models.rs @@ -42,6 +42,31 @@ pub async fn registry_status( Ok(Json(result)) } +/// POST /models/registry-uninstall +/// Marks a registry model as uninstalled by clearing its registry_name. +#[derive(Debug, serde::Deserialize)] +pub struct RegistryUninstallRequest { + pub name: String, +} + +pub async fn registry_uninstall( + State(state): State, + AuthUser(_claims): AuthUser, + Json(req): Json, +) -> AppResult> { + let updated = sqlx::query( + "UPDATE models SET registry_name = NULL, updated_at = NOW() WHERE registry_name = $1" + ) + .bind(&req.name) + .execute(&state.db) + .await?; + + Ok(Json(serde_json::json!({ + "uninstalled": true, + "rows_affected": updated.rows_affected() + }))) +} + pub async fn list( State(state): State, AuthUser(_claims): AuthUser, diff --git a/api/src/routes/sdk.rs b/api/src/routes/sdk.rs index bb8a180..fefff88 100644 --- a/api/src/routes/sdk.rs +++ b/api/src/routes/sdk.rs @@ -421,6 +421,25 @@ pub async fn resolve_model( Ok(Json(model)) } +/// GET /sdk/models/resolve-registry/{registry_name} +/// Resolve a model by its registry_name column. Returns the full Model JSON +/// including source_code. Used by SDK use_model(). +pub async fn resolve_registry_model( + State(state): State, + AuthUser(_claims): AuthUser, + Path(registry_name): Path, +) -> AppResult> { + let model: crate::models::model::Model = sqlx::query_as( + "SELECT * FROM models WHERE registry_name = $1 ORDER BY created_at DESC LIMIT 1" + ) + .bind(®istry_name) + .fetch_optional(&state.db) + .await? + .ok_or_else(|| AppError::NotFound(format!("Registry model not found: {registry_name}")))?; + + Ok(Json(model)) +} + /// GET /sdk/models/{id}/artifact /// Serve the latest checkpoint artifact for a model, or extract the embedded /// base64 blob from the model's source_code (for SDK-registered models). diff --git a/docs/CLI-REGISTRY.md b/docs/CLI-REGISTRY.md index 30b3367..7824d3d 100644 --- a/docs/CLI-REGISTRY.md +++ b/docs/CLI-REGISTRY.md @@ -118,18 +118,16 @@ openmodelstudio uninstall titanic-rf ### Using an Installed Model -After installing, the model's `model.py` is available locally. Register it with the platform from a notebook: +After installing, use `oms.use_model()` to load the model and register it in your project: ```python import openmodelstudio as oms -from pathlib import Path -# Read the installed model code -model_dir = Path.home() / ".openmodelstudio" / "models" / "titanic-rf" -code = (model_dir / "model.py").read_text() +# Load the installed registry model +iris = oms.use_model("iris-svm") -# Register it into your project -handle = oms.register_model("titanic-rf", source_code=code) +# Register it in your project under any name +handle = oms.register_model("my-iris", model=iris) print(handle) # Train it @@ -137,7 +135,9 @@ job = oms.start_training(handle.model_id, wait=True) print(f"Training: {job['status']}") ``` -Or install directly from the UI on the **Model Registry** page (sidebar > Develop > Model Registry). +`use_model()` resolves models via the platform API, so it works inside workspace containers (K8s pods) without requiring filesystem access. If the model isn't installed yet, it auto-installs from the registry. + +You can also install directly from the UI on the **Model Registry** page (sidebar > Develop > Model Registry). Each model card shows an **Installed** or **Not Installed** badge. ## Configuration @@ -204,7 +204,11 @@ print(info["dependencies"]) path = oms.registry_install("titanic-rf") path = oms.registry_install("mnist-cnn", force=True) -# Uninstall +# Use an installed model (works in workspace containers) +iris = oms.use_model("iris-svm") +handle = oms.register_model("my-iris", model=iris) + +# Uninstall (removes locally + unregisters from platform) oms.registry_uninstall("titanic-rf") # List installed diff --git a/sdk/python/openmodelstudio/__init__.py b/sdk/python/openmodelstudio/__init__.py index 842a812..1363d5b 100644 --- a/sdk/python/openmodelstudio/__init__.py +++ b/sdk/python/openmodelstudio/__init__.py @@ -1,6 +1,6 @@ """OpenModelStudio SDK — register models, load datasets, track experiments, and visualize from workspaces.""" -from .client import Client +from .client import Client, RegistryModel from .model import ( register_model, publish_version, @@ -10,6 +10,8 @@ upload_dataset, create_dataset, load_model, + # Registry Model + use_model, # Feature Store create_features, load_features, @@ -89,6 +91,7 @@ __all__ = [ "Client", + "RegistryModel", # Model registration "register_model", "publish_version", @@ -100,6 +103,8 @@ "create_dataset", # Model loading "load_model", + # Registry Model + "use_model", # Feature Store "create_features", "load_features", diff --git a/sdk/python/openmodelstudio/cli.py b/sdk/python/openmodelstudio/cli.py index 2ca9933..a6a6265 100644 --- a/sdk/python/openmodelstudio/cli.py +++ b/sdk/python/openmodelstudio/cli.py @@ -60,12 +60,14 @@ def cmd_install(args): def cmd_uninstall(args): - from .config import require_project_root, get_project_models_dir + from .config import require_project_root, get_project_models_dir, get_config from .registry import registry_uninstall require_project_root() models_dir = get_project_models_dir() - if registry_uninstall(args.name, models_dir=str(models_dir)): + cfg = get_config() + if registry_uninstall(args.name, models_dir=str(models_dir), + api_url=cfg.get("api_url")): print(f"Uninstalled '{args.name}'") else: print(f"Model '{args.name}' is not installed") diff --git a/sdk/python/openmodelstudio/client.py b/sdk/python/openmodelstudio/client.py index d39c41a..db87917 100644 --- a/sdk/python/openmodelstudio/client.py +++ b/sdk/python/openmodelstudio/client.py @@ -483,6 +483,28 @@ def __repr__(self): return f"ModelHandle(id={self.model_id!r}, name={self.name!r}, version={self.version})" +class RegistryModel: + """A model loaded from the registry, ready to pass to register_model(). + + Usage:: + + iris = oms.use_model("iris-svm") + handle = oms.register_model("my-iris", model=iris) + """ + + def __init__(self, name: str, source_code: str, framework: str, + description: str = None, registry_name: str = None): + self.name = name + self.source_code = source_code + self.framework = framework + self.description = description + self.registry_name = registry_name or name + self._is_registry_model = True + + def __repr__(self): + return f"RegistryModel(name={self.name!r}, framework={self.framework!r})" + + class Client: """OpenModelStudio API client. @@ -563,6 +585,15 @@ def register_model( source_code: Python source code with a train(ctx) function file: Path to a .py file with train(ctx)/infer(ctx) functions """ + # Handle RegistryModel instances (from use_model()) + if hasattr(model, '_is_registry_model') and model._is_registry_model: + return self.register_model( + name, + source_code=model.source_code, + framework=model.framework, + description=model.description or description, + ) + # If a file path is provided, read source code from it if file is not None: if not os.path.isfile(file): @@ -894,6 +925,83 @@ def load_model(self, name_or_id: str, version: int = None, device: str = None): raise ValueError(f"Unsupported framework for loading: {framework}") + # ── Registry Model Loading ─────────────────────────────────────── + + def use_model(self, registry_name: str) -> RegistryModel: + """Load an installed registry model, ready for register_model(). + + Tries the platform API first (works in workspace containers), + falls back to local filesystem, and auto-installs from registry + if not found anywhere. + + Examples:: + + iris = oms.use_model("iris-svm") + handle = oms.register_model("my-iris", model=iris) + + Args: + registry_name: The registry model name (e.g. "iris-svm") + + Returns: + RegistryModel instance usable with register_model() + """ + # 1. Try resolving from platform API (works inside workspace containers) + try: + model_info = self._get(f"/sdk/models/resolve-registry/{registry_name}") + return RegistryModel( + name=model_info["name"], + source_code=model_info.get("source_code", ""), + framework=model_info.get("framework", "pytorch"), + description=model_info.get("description"), + registry_name=registry_name, + ) + except Exception: + pass + + # 2. Try local filesystem (for host-side CLI usage) + from .config import get_models_dir + local_dir = get_models_dir() / registry_name + model_file = local_dir / "model.py" + manifest = local_dir / "model.json" + if model_file.exists(): + import json as _json + info = {} + if manifest.exists(): + try: + info = _json.loads(manifest.read_text()) + except Exception: + pass + return RegistryModel( + name=registry_name, + source_code=model_file.read_text(), + framework=info.get("framework", "pytorch"), + description=info.get("description"), + registry_name=registry_name, + ) + + # 3. Auto-install from registry + from .registry import registry_install + registry_install( + registry_name, + api_url=self.api_url, + token=self.token, + ) + # Retry API resolve after install + try: + model_info = self._get(f"/sdk/models/resolve-registry/{registry_name}") + return RegistryModel( + name=model_info["name"], + source_code=model_info.get("source_code", ""), + framework=model_info.get("framework", "pytorch"), + description=model_info.get("description"), + registry_name=registry_name, + ) + except Exception: + raise ValueError( + f"Model '{registry_name}' not found. Install it first:\n" + f" openmodelstudio install {registry_name}" + ) + # ── Feature Store ──────────────────────────────────────────────── def create_features( diff --git a/sdk/python/openmodelstudio/model.py b/sdk/python/openmodelstudio/model.py index 137d9d1..c0914e5 100644 --- a/sdk/python/openmodelstudio/model.py +++ b/sdk/python/openmodelstudio/model.py @@ -1,6 +1,6 @@ """Module-level convenience functions that use a default Client instance.""" -from .client import Client, ModelHandle +from .client import Client, ModelHandle, RegistryModel _client = None @@ -120,6 +120,21 @@ def load_model(name_or_id: str, version: int = None, device: str = None): return _get_client().load_model(name_or_id, version=version, device=device) +def use_model(registry_name: str) -> RegistryModel: + """Load an installed registry model, ready to register. + + Works inside workspace containers (resolves via API) and on the host + (falls back to local filesystem). Auto-installs from registry if + not yet installed. + + Examples:: + + iris = openmodelstudio.use_model("iris-svm") + handle = openmodelstudio.register_model("my-iris", model=iris) + """ + return _get_client().use_model(registry_name) + + # ── Feature Store ──────────────────────────────────────────────────── def create_features(df, feature_names=None, group_name=None, entity="default", transforms=None) -> dict: diff --git a/sdk/python/openmodelstudio/registry.py b/sdk/python/openmodelstudio/registry.py index 837ed86..6790ef0 100644 --- a/sdk/python/openmodelstudio/registry.py +++ b/sdk/python/openmodelstudio/registry.py @@ -196,21 +196,48 @@ def _load_api_url() -> str: return "" -def registry_uninstall(name: str, models_dir: str = None) -> bool: +def registry_uninstall(name: str, models_dir: str = None, + api_url: str = None, token: str = None) -> bool: """Uninstall a locally installed model. + Removes local files and unregisters from the platform API so the + dashboard reflects the change. + Args: name: Model name + models_dir: Override models directory + api_url: Override API URL + token: Override auth token Returns: True if model was removed, False if it wasn't installed """ + removed = False dest = Path(models_dir) if models_dir else get_models_dir() model_dir = dest / name if model_dir.exists(): shutil.rmtree(model_dir) - return True - return False + removed = True + + # Also unregister from platform API + _api_url = api_url or os.environ.get("OPENMODELSTUDIO_API_URL") or _load_api_url() + _token = token or os.environ.get("OPENMODELSTUDIO_TOKEN") + if _api_url: + try: + headers = {"Content-Type": "application/json"} + if _token: + headers["Authorization"] = f"Bearer {_token}" + resp = requests.post( + f"{_api_url}/models/registry-uninstall", + json={"name": name}, headers=headers, timeout=10, + ) + if resp.ok: + print(f" Unregistered '{name}' from platform") + removed = True + except Exception: + pass # best-effort + + return removed def list_installed(models_dir: str = None) -> list: diff --git a/web/src/app/(auth)/layout.tsx b/web/src/app/(auth)/layout.tsx index 4005ec6..6e6f696 100644 --- a/web/src/app/(auth)/layout.tsx +++ b/web/src/app/(auth)/layout.tsx @@ -23,7 +23,7 @@ export default function AuthLayout({ children }: { children: React.ReactNode })
{children}

- Powered by K8s + PyTorch + Rust + Created with ❤️ by GACWR

diff --git a/web/src/app/registry/page.tsx b/web/src/app/registry/page.tsx index 63c28e8..376f346 100644 --- a/web/src/app/registry/page.tsx +++ b/web/src/app/registry/page.tsx @@ -185,6 +185,8 @@ export default function RegistryPage() { }); toast.success(`Installed ${installModel.name} successfully`); setInstallStatus((prev) => ({ ...prev, [installModel.name]: true })); + // Refetch full status to ensure consistency + fetchInstallStatus(models.map((m) => m.name)); setInstallOpen(false); setInstallModel(null); setInstallProject(""); @@ -407,10 +409,14 @@ export default function RegistryPage() { > v{model.version} - {installStatus[model.name] && ( + {installStatus[model.name] ? ( Installed + ) : ( + + Not Installed + )}
From 77e79a8c4a21951e2952a08222e3a86b613f2b1b Mon Sep 17 00:00:00 2001 From: jovonni Date: Sun, 8 Mar 2026 09:07:25 -0400 Subject: [PATCH 10/16] search is working --- api/src/routes/sdk.rs | 60 ++- tests/e2e/playwright-report/index.html | 2 +- tests/e2e/search-and-registry.spec.ts | 390 +++++++++++++++++++ web/src/components/shared/search-overlay.tsx | 10 +- web/src/components/ui/command.tsx | 4 +- 5 files changed, 441 insertions(+), 25 deletions(-) create mode 100644 tests/e2e/search-and-registry.spec.ts diff --git a/api/src/routes/sdk.rs b/api/src/routes/sdk.rs index fefff88..4d43991 100644 --- a/api/src/routes/sdk.rs +++ b/api/src/routes/sdk.rs @@ -41,26 +41,44 @@ pub async fn register_model( Json(req): Json, ) -> AppResult> { let framework = req.framework.unwrap_or_else(|| "pytorch".into()); - let project_id = req.project_id.unwrap_or_else(Uuid::nil); + let project_id: Option = req.project_id.filter(|id| !id.is_nil()); let workspace_id: Option = None; - // Check if a model with the same name (or registry_name) already exists in this project + // Check if a model with the same name (or registry_name) already exists let existing: Option = if req.registry_name.is_some() { - sqlx::query_as( - "SELECT * FROM models WHERE registry_name = $1 AND project_id = $2 ORDER BY version DESC LIMIT 1" - ) - .bind(&req.registry_name) - .bind(project_id) - .fetch_optional(&state.db) - .await? + if let Some(pid) = project_id { + sqlx::query_as( + "SELECT * FROM models WHERE registry_name = $1 AND project_id = $2 ORDER BY version DESC LIMIT 1" + ) + .bind(&req.registry_name) + .bind(pid) + .fetch_optional(&state.db) + .await? + } else { + sqlx::query_as( + "SELECT * FROM models WHERE registry_name = $1 AND project_id IS NULL ORDER BY version DESC LIMIT 1" + ) + .bind(&req.registry_name) + .fetch_optional(&state.db) + .await? + } } else { - sqlx::query_as( - "SELECT * FROM models WHERE name = $1 AND project_id = $2 ORDER BY version DESC LIMIT 1" - ) - .bind(&req.name) - .bind(project_id) - .fetch_optional(&state.db) - .await? + if let Some(pid) = project_id { + sqlx::query_as( + "SELECT * FROM models WHERE name = $1 AND project_id = $2 ORDER BY version DESC LIMIT 1" + ) + .bind(&req.name) + .bind(pid) + .fetch_optional(&state.db) + .await? + } else { + sqlx::query_as( + "SELECT * FROM models WHERE name = $1 AND project_id IS NULL ORDER BY version DESC LIMIT 1" + ) + .bind(&req.name) + .fetch_optional(&state.db) + .await? + } }; let from_registry = req.registry_name.is_some(); @@ -351,7 +369,7 @@ pub async fn create_dataset( let dataset_id = Uuid::new_v4(); let format = req.format.unwrap_or_else(|| "csv".into()); - let project_id = req.project_id.unwrap_or_else(Uuid::nil); + let project_id: Option = req.project_id.filter(|id| !id.is_nil()); // Decode base64 let bytes = base64::engine::general_purpose::STANDARD @@ -592,12 +610,12 @@ pub async fn create_features( AuthUser(claims): AuthUser, Json(req): Json, ) -> AppResult> { - let project_id = req.project_id.unwrap_or_else(Uuid::nil); + let project_id: Option = req.project_id.filter(|id| !id.is_nil()); let entity = req.entity.unwrap_or_else(|| "default".into()); // Create or find feature group let group_id: Uuid = match sqlx::query_scalar::<_, Uuid>( - "SELECT id FROM feature_groups WHERE name = $1 AND project_id = $2" + "SELECT id FROM feature_groups WHERE name = $1 AND (project_id = $2 OR ($2::uuid IS NULL AND project_id IS NULL))" ) .bind(&req.group_name) .bind(project_id) @@ -708,7 +726,7 @@ pub async fn create_hyperparameters( Json(req): Json, ) -> AppResult> { let id = Uuid::new_v4(); - let project_id = req.project_id.unwrap_or_else(Uuid::nil); + let project_id: Option = req.project_id.filter(|id| !id.is_nil()); let hp: HyperparameterSet = sqlx::query_as( "INSERT INTO hyperparameter_sets (id, project_id, name, description, parameters, model_id, created_by, created_at, updated_at) @@ -1117,7 +1135,7 @@ pub async fn create_pipeline( Json(req): Json, ) -> AppResult> { let pipeline_id = Uuid::new_v4(); - let project_id = req.project_id.unwrap_or_else(Uuid::nil); + let project_id: Option = req.project_id.filter(|id| !id.is_nil()); let pipeline: Pipeline = sqlx::query_as( "INSERT INTO pipelines (id, project_id, name, description, status, created_by, created_at, updated_at) diff --git a/tests/e2e/playwright-report/index.html b/tests/e2e/playwright-report/index.html index 4e30ed0..6410bf9 100644 --- a/tests/e2e/playwright-report/index.html +++ b/tests/e2e/playwright-report/index.html @@ -82,4 +82,4 @@
- \ No newline at end of file + \ No newline at end of file diff --git a/tests/e2e/search-and-registry.spec.ts b/tests/e2e/search-and-registry.spec.ts new file mode 100644 index 0000000..5c984f2 --- /dev/null +++ b/tests/e2e/search-and-registry.spec.ts @@ -0,0 +1,390 @@ +/** + * OpenModelStudio — Search & Registry Install Integration Tests + * + * Tests two critical flows: + * 1. Search: typing in search input returns results from the API + * 2. Registry Install: CLI install registers model in platform, + * use_model() resolves it, uninstall clears it + */ +import { test, expect } from './helpers/fixtures'; +import { apiLogin, apiPost, apiGet, apiDelete, DEFAULT_ADMIN, API_URL } from './helpers/api-client'; + +// ─── Search Tests ──────────────────────────────────────────────────── + +test.describe('Search — Full Page', () => { + test('search returns results for existing project', async ({ authenticatedPage: page }) => { + const token = await apiLogin(DEFAULT_ADMIN); + + // Create a uniquely named project + const searchName = `SearchTest${Date.now()}`; + const project = await apiPost(token, '/projects', { + name: searchName, + description: 'Project for search validation', + }); + + // Navigate to search page + await page.goto('/search'); + await page.waitForTimeout(2000); + + // Type in search input + const searchInput = page.locator('input[placeholder*="search" i]').first(); + await expect(searchInput).toBeVisible({ timeout: 10000 }); + await searchInput.fill(searchName); + + // Wait for debounce + API response + await page.waitForTimeout(2000); + + // Should show results count + const resultsText = page.locator(`text=/${searchName}/`).first(); + await expect(resultsText).toBeVisible({ timeout: 10000 }); + + // Should show the project in results + const resultCount = page.locator('text=/\\d+ results/i').first(); + const hasResults = await resultCount.isVisible({ timeout: 5000 }).catch(() => false); + expect(hasResults).toBeTruthy(); + + // Cleanup + try { await apiDelete(token, `/projects/${project.id}`); } catch { /* ok */ } + }); + + test('search shows "No results" for gibberish query', async ({ authenticatedPage: page }) => { + await page.goto('/search'); + await page.waitForTimeout(2000); + + const searchInput = page.locator('input[placeholder*="search" i]').first(); + await searchInput.fill('zzzznonexistent99999xyz'); + await page.waitForTimeout(2000); + + // Should show "0 results" or "No results" + const noResults = page.locator('text=/no results|0 results/i').first(); + await expect(noResults).toBeVisible({ timeout: 5000 }); + }); +}); + +test.describe('Search — Command Palette (⌘K)', () => { + test('⌘K shows search results from API', async ({ authenticatedPage: page }) => { + const token = await apiLogin(DEFAULT_ADMIN); + + // Create a uniquely named project + const searchName = `CmdKTest${Date.now()}`; + const project = await apiPost(token, '/projects', { + name: searchName, + description: 'Project for ⌘K search validation', + }); + + await page.goto('/'); + await page.waitForTimeout(2000); + + // Open ⌘K + await page.keyboard.press('Meta+k'); + await page.waitForTimeout(500); + + // Dialog should be open + const dialog = page.locator('[cmdk-dialog], [role="dialog"]').first(); + await expect(dialog).toBeVisible({ timeout: 5000 }); + + // Type search query + const input = page.locator('[cmdk-input], input[placeholder*="search" i]').first(); + await input.fill(searchName); + await page.waitForTimeout(1500); // debounce + API + + // Should show the project in results (cmdk items) + const result = page.locator(`[cmdk-item]:has-text("${searchName}"), [role="option"]:has-text("${searchName}")`).first(); + const hasResult = await result.isVisible({ timeout: 5000 }).catch(() => false); + + // Close + await page.keyboard.press('Escape'); + + // Even if specific result not found, verify the search attempt was made + // by checking for any search-related content + expect(hasResult).toBeTruthy(); + + // Cleanup + try { await apiDelete(token, `/projects/${project.id}`); } catch { /* ok */ } + }); + + test('⌘K shows quick navigation when no query', async ({ authenticatedPage: page }) => { + await page.goto('/'); + await page.waitForTimeout(2000); + + await page.keyboard.press('Meta+k'); + await page.waitForTimeout(500); + + // Should show quick nav items + const navItems = page.locator('[cmdk-item], [role="option"]'); + await expect(navItems.first()).toBeVisible({ timeout: 5000 }); + + await page.keyboard.press('Escape'); + }); +}); + +// ─── Search API Tests (no browser needed) ──────────────────────────── + +test.describe('Search — API Endpoint', () => { + test('GET /search returns categorized results', async () => { + const token = await apiLogin(DEFAULT_ADMIN); + + // Create test data + const name = `APISearchTest${Date.now()}`; + const project = await apiPost(token, '/projects', { + name: name, + description: 'API search test', + }); + + // Search via API + const results = await apiGet(token, `/search?q=${encodeURIComponent(name)}`); + + // Verify response structure + expect(results).toHaveProperty('projects'); + expect(results).toHaveProperty('models'); + expect(results).toHaveProperty('datasets'); + expect(results).toHaveProperty('experiments'); + expect(results).toHaveProperty('training'); + expect(results).toHaveProperty('workspaces'); + expect(results).toHaveProperty('features'); + expect(results).toHaveProperty('visualizations'); + expect(results).toHaveProperty('data_sources'); + + // Should find our project + expect(results.projects.length).toBeGreaterThanOrEqual(1); + const found = results.projects.find((p: any) => p.name === name); + expect(found).toBeTruthy(); + expect(found.href).toContain('/projects/'); + + // Cleanup + try { await apiDelete(token, `/projects/${project.id}`); } catch { /* ok */ } + }); + + test('GET /search with limit parameter works', async () => { + const token = await apiLogin(DEFAULT_ADMIN); + const results = await apiGet(token, '/search?q=test&limit=3'); + expect(results).toHaveProperty('projects'); + // Each category should have at most 3 results + for (const key of Object.keys(results)) { + expect(results[key].length).toBeLessThanOrEqual(3); + } + }); + + test('GET /search with no matches returns empty arrays', async () => { + const token = await apiLogin(DEFAULT_ADMIN); + const results = await apiGet(token, '/search?q=zzzznonexistent99999xyz'); + const total = Object.values(results).reduce((a: number, b: any) => a + b.length, 0); + expect(total).toBe(0); + }); +}); + +// ─── Registry Install Tests (API-only) ────────────────────────────── + +test.describe('Model Registry — CLI Install Integration', () => { + test('register-model with no project_id succeeds (NULL project)', async () => { + const token = await apiLogin(DEFAULT_ADMIN); + + // This simulates what CLI install does — POST /sdk/register-model + // WITHOUT a project_id (previously caused 500 due to Uuid::nil FK violation) + const name = `registry-test-${Date.now()}`; + const result = await apiPost(token, '/sdk/register-model', { + name: name, + framework: 'sklearn', + description: 'Test model registered without project_id', + source_code: 'def train(ctx): pass\ndef infer(ctx): pass', + registry_name: name, + // NOTE: no project_id — this is the critical test + }); + + expect(result.model_id).toBeTruthy(); + expect(result.name).toBe(name); + expect(result.version).toBe(1); + + // Verify the model can be resolved via registry name + const resolved = await apiGet(token, `/sdk/models/resolve-registry/${name}`); + expect(resolved.name).toBe(name); + expect(resolved.registry_name).toBe(name); + expect(resolved.source_code).toContain('def train(ctx)'); + + // Verify registry-status shows as installed + const status = await apiGet(token, `/models/registry-status?names=${name}`); + expect(status[name]).toBe(true); + + // Cleanup + try { await apiDelete(token, `/models/${result.model_id}`); } catch { /* ok */ } + }); + + test('register-model with valid project_id succeeds', async () => { + const token = await apiLogin(DEFAULT_ADMIN); + + // Create a project first + const project = await apiPost(token, '/projects', { + name: `Registry Proj ${Date.now()}`, + description: 'For registry test', + }); + + const name = `proj-registry-test-${Date.now()}`; + const result = await apiPost(token, '/sdk/register-model', { + name: name, + framework: 'pytorch', + source_code: 'def train(ctx): pass\ndef infer(ctx): pass', + registry_name: name, + project_id: project.id, + }); + + expect(result.model_id).toBeTruthy(); + + // Verify resolve works + const resolved = await apiGet(token, `/sdk/models/resolve-registry/${name}`); + expect(resolved.name).toBe(name); + expect(resolved.project_id).toBe(project.id); + + // Cleanup + try { await apiDelete(token, `/models/${result.model_id}`); } catch { /* ok */ } + try { await apiDelete(token, `/projects/${project.id}`); } catch { /* ok */ } + }); + + test('registry-uninstall clears registry_name', async () => { + const token = await apiLogin(DEFAULT_ADMIN); + + // Register a model with registry_name + const name = `uninstall-test-${Date.now()}`; + const result = await apiPost(token, '/sdk/register-model', { + name: name, + framework: 'sklearn', + source_code: 'def train(ctx): pass\ndef infer(ctx): pass', + registry_name: name, + }); + + // Verify it's installed + const statusBefore = await apiGet(token, `/models/registry-status?names=${name}`); + expect(statusBefore[name]).toBe(true); + + // Uninstall + const uninstallResult = await apiPost(token, '/models/registry-uninstall', { name }); + expect(uninstallResult.uninstalled).toBe(true); + expect(uninstallResult.rows_affected).toBeGreaterThanOrEqual(1); + + // Verify it's no longer installed + const statusAfter = await apiGet(token, `/models/registry-status?names=${name}`); + expect(statusAfter[name]).toBe(false); + + // Cleanup + try { await apiDelete(token, `/models/${result.model_id}`); } catch { /* ok */ } + }); + + test('resolve-registry returns 404 for non-existent model', async () => { + const token = await apiLogin(DEFAULT_ADMIN); + + try { + await apiGet(token, '/sdk/models/resolve-registry/nonexistent-model-999'); + // Should not reach here + expect(true).toBe(false); + } catch (err: any) { + expect(err.message).toContain('404'); + } + }); + + test('full install → resolve → uninstall cycle', async () => { + const token = await apiLogin(DEFAULT_ADMIN); + + const name = `full-cycle-${Date.now()}`; + const sourceCode = ` +def train(ctx): + ctx.log_metric("accuracy", 0.95) + +def infer(ctx): + ctx.set_output({"prediction": "positive"}) +`; + + // 1. Install (register with registry_name, no project_id) + const installed = await apiPost(token, '/sdk/register-model', { + name: name, + framework: 'sklearn', + description: 'Full cycle test', + source_code: sourceCode, + registry_name: name, + }); + expect(installed.model_id).toBeTruthy(); + + // 2. Resolve by registry name + const resolved = await apiGet(token, `/sdk/models/resolve-registry/${name}`); + expect(resolved.name).toBe(name); + expect(resolved.source_code).toContain('def train(ctx)'); + expect(resolved.source_code).toContain('def infer(ctx)'); + + // 3. Verify in registry-status + const status1 = await apiGet(token, `/models/registry-status?names=${name}`); + expect(status1[name]).toBe(true); + + // 4. Re-register (should update version, not create duplicate) + const updated = await apiPost(token, '/sdk/register-model', { + name: name, + framework: 'sklearn', + source_code: sourceCode + '\n# updated', + registry_name: name, + }); + expect(updated.model_id).toBe(installed.model_id); + expect(updated.version).toBe(2); + + // 5. Uninstall + await apiPost(token, '/models/registry-uninstall', { name }); + + // 6. Verify no longer in registry-status + const status2 = await apiGet(token, `/models/registry-status?names=${name}`); + expect(status2[name]).toBe(false); + + // 7. Resolve should now fail + try { + await apiGet(token, `/sdk/models/resolve-registry/${name}`); + expect(true).toBe(false); // should not reach + } catch (err: any) { + expect(err.message).toContain('404'); + } + + // Cleanup + try { await apiDelete(token, `/models/${installed.model_id}`); } catch { /* ok */ } + }); +}); + +// ─── Registry Badge UI Tests ───────────────────────────────────────── + +test.describe('Model Registry — UI Badge', () => { + test('registry page shows install status badges', async ({ authenticatedPage: page }) => { + await page.goto('/registry'); + await page.waitForTimeout(3000); + + // Should show model cards from the registry + const cards = page.locator('main [class*="card"], main [class*="Card"]'); + const hasCards = await cards.first().isVisible({ timeout: 10000 }).catch(() => false); + + if (hasCards) { + // Each card should have either "Installed" or "Not Installed" badge + const installedBadge = page.locator('text=/Installed/').first(); + const notInstalledBadge = page.locator('text=/Not Installed/').first(); + const hasBadge = await installedBadge.isVisible({ timeout: 3000 }).catch(() => false) + || await notInstalledBadge.isVisible({ timeout: 3000 }).catch(() => false); + expect(hasBadge).toBeTruthy(); + } + }); + + test('installing model updates badge to Installed', async ({ authenticatedPage: page }) => { + const token = await apiLogin(DEFAULT_ADMIN); + + // Register a model as if CLI installed it + const name = `iris-svm`; // use a known registry model name + await apiPost(token, '/sdk/register-model', { + name: name, + framework: 'sklearn', + source_code: 'def train(ctx): pass\ndef infer(ctx): pass', + registry_name: name, + }).catch(() => {}); // may already exist + + // Navigate to registry page + await page.goto('/registry'); + await page.waitForTimeout(3000); + + // Look for "Installed" badge + const installedBadge = page.locator('text=Installed').first(); + const hasInstalled = await installedBadge.isVisible({ timeout: 5000 }).catch(() => false); + // At minimum, the page should load and show badges + const notInstalledBadge = page.locator('text=/Not Installed/').first(); + const hasNotInstalled = await notInstalledBadge.isVisible({ timeout: 3000 }).catch(() => false); + expect(hasInstalled || hasNotInstalled).toBeTruthy(); + }); +}); diff --git a/web/src/components/shared/search-overlay.tsx b/web/src/components/shared/search-overlay.tsx index 40ec3c1..2c1ae3d 100644 --- a/web/src/components/shared/search-overlay.tsx +++ b/web/src/components/shared/search-overlay.tsx @@ -14,6 +14,7 @@ import { BarChart3, Layers, Plug, } from "lucide-react"; import { useRouter } from "next/navigation"; +import { toast } from "sonner"; import { api } from "@/lib/api"; interface SearchContextType { @@ -137,7 +138,12 @@ function SearchOverlay() { debounceRef.current = setTimeout(() => { api.get(`/search?q=${encodeURIComponent(query)}&limit=5`) .then(setResults) - .catch(() => setResults(null)) + .catch((err) => { + setResults(null); + if (err instanceof Error && err.message !== "Unauthorized") { + toast.error("Search failed"); + } + }) .finally(() => setLoading(false)); }, 200); @@ -149,7 +155,7 @@ function SearchOverlay() { const hasResults = results && Object.values(results).some((arr) => arr.length > 0); return ( - + & { title?: string description?: string className?: string showCloseButton?: boolean + shouldFilter?: boolean }) { return ( @@ -52,7 +54,7 @@ function CommandDialog({ className={cn("overflow-hidden p-0", className)} showCloseButton={showCloseButton} > - + {children} From a2e03fa2b0d7f768774b3eccfa2035af51ddcbb3 Mon Sep 17 00:00:00 2001 From: jovonni Date: Sun, 8 Mar 2026 09:34:19 -0400 Subject: [PATCH 11/16] model injection working --- api/src/models/dataset.rs | 4 +-- api/src/models/model.rs | 2 +- api/src/models/pipeline.rs | 2 +- api/src/routes/sdk.rs | 2 +- sdk/python/openmodelstudio/registry.py | 38 ++++++++++++++++++++++++++ web/src/app/registry/[id]/page.tsx | 30 ++++++++++++++++++-- 6 files changed, 71 insertions(+), 7 deletions(-) diff --git a/api/src/models/dataset.rs b/api/src/models/dataset.rs index fd12ba7..2a2ebc9 100644 --- a/api/src/models/dataset.rs +++ b/api/src/models/dataset.rs @@ -6,7 +6,7 @@ use uuid::Uuid; #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct Dataset { pub id: Uuid, - pub project_id: Uuid, + pub project_id: Option, pub name: String, pub description: Option, pub format: String, @@ -45,7 +45,7 @@ pub struct UploadUrlResponse { #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct DataSource { pub id: Uuid, - pub project_id: Uuid, + pub project_id: Option, pub name: String, pub source_type: String, pub connection_string: Option, diff --git a/api/src/models/model.rs b/api/src/models/model.rs index 42ec808..3da2d08 100644 --- a/api/src/models/model.rs +++ b/api/src/models/model.rs @@ -6,7 +6,7 @@ use uuid::Uuid; #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct Model { pub id: Uuid, - pub project_id: Uuid, + pub project_id: Option, pub name: String, pub description: Option, pub framework: String, diff --git a/api/src/models/pipeline.rs b/api/src/models/pipeline.rs index 0223952..ff2b6fa 100644 --- a/api/src/models/pipeline.rs +++ b/api/src/models/pipeline.rs @@ -6,7 +6,7 @@ use uuid::Uuid; #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct Pipeline { pub id: Uuid, - pub project_id: Uuid, + pub project_id: Option, pub name: String, pub description: Option, pub config: serde_json::Value, diff --git a/api/src/routes/sdk.rs b/api/src/routes/sdk.rs index 4d43991..9f4a7a4 100644 --- a/api/src/routes/sdk.rs +++ b/api/src/routes/sdk.rs @@ -1374,7 +1374,7 @@ pub async fn create_sweep( Json(req): Json, ) -> AppResult> { let model = resolve_model_id(&state.db, &req.model_id).await?; - let project_id = req.project_id.unwrap_or(model.project_id); + let project_id = req.project_id.or(model.project_id); let dataset_id = if let Some(ref ds) = req.dataset_id { Some(resolve_dataset_id(&state.db, ds).await?) diff --git a/sdk/python/openmodelstudio/registry.py b/sdk/python/openmodelstudio/registry.py index 6790ef0..7f03a2d 100644 --- a/sdk/python/openmodelstudio/registry.py +++ b/sdk/python/openmodelstudio/registry.py @@ -154,6 +154,14 @@ def registry_install(name: str, registry_url: str = None, models_dir: str = None _api_url = api_url or os.environ.get("OPENMODELSTUDIO_API_URL") or _load_api_url() _token = token or os.environ.get("OPENMODELSTUDIO_TOKEN") + # Auto-detect local platform if no api_url is configured + if not _api_url: + _api_url = _auto_detect_api_url() + + # Auto-login if we have an api_url but no token + if _api_url and not _token: + _token = _auto_login(_api_url) + if _api_url: try: main_file = files[0] @@ -196,6 +204,32 @@ def _load_api_url() -> str: return "" +def _auto_detect_api_url() -> str: + """Auto-detect local platform API (K8s NodePort at localhost:31001).""" + try: + resp = requests.get("http://localhost:31001/healthz", timeout=2) + if resp.ok: + return "http://localhost:31001" + except Exception: + pass + return "" + + +def _auto_login(api_url: str) -> str: + """Auto-login with default credentials to get a token for registration.""" + try: + resp = requests.post( + f"{api_url}/auth/login", + json={"email": "test@openmodel.studio", "password": "Test1234"}, + timeout=10, + ) + if resp.ok: + return resp.json().get("access_token", "") + except Exception: + pass + return "" + + def registry_uninstall(name: str, models_dir: str = None, api_url: str = None, token: str = None) -> bool: """Uninstall a locally installed model. @@ -222,6 +256,10 @@ def registry_uninstall(name: str, models_dir: str = None, # Also unregister from platform API _api_url = api_url or os.environ.get("OPENMODELSTUDIO_API_URL") or _load_api_url() _token = token or os.environ.get("OPENMODELSTUDIO_TOKEN") + if not _api_url: + _api_url = _auto_detect_api_url() + if _api_url and not _token: + _token = _auto_login(_api_url) if _api_url: try: headers = {"Content-Type": "application/json"} diff --git a/web/src/app/registry/[id]/page.tsx b/web/src/app/registry/[id]/page.tsx index e78d899..f5da802 100644 --- a/web/src/app/registry/[id]/page.tsx +++ b/web/src/app/registry/[id]/page.tsx @@ -14,6 +14,7 @@ import { Package, ArrowLeft, Download, + Trash2, User, Tag, ExternalLink, @@ -219,6 +220,16 @@ export default function RegistryModelDetailPage() { } }; + const handleUninstall = async () => { + try { + await api.post("/models/registry-uninstall", { name: modelName }); + toast.success(`Uninstalled ${modelName}`); + setIsInstalled(false); + } catch (err) { + toast.error(err instanceof Error ? err.message : "Failed to uninstall"); + } + }; + const handleCopyInstall = () => { navigator.clipboard.writeText(`openmodelstudio install ${modelName}`); setCopied(true); @@ -294,10 +305,14 @@ export default function RegistryModelDetailPage() { v{model.version} - {isInstalled && ( + {isInstalled ? ( Installed + ) : ( + + Not Installed + )}
@@ -324,12 +339,23 @@ export default function RegistryModelDetailPage() { )} + {isInstalled && ( + + + + )}
From 5a0ccdcaa7bc32d1bbd38328a1a81a332e84d452 Mon Sep 17 00:00:00 2001 From: jovonni Date: Sun, 8 Mar 2026 10:16:06 -0400 Subject: [PATCH 12/16] updating injected notebook tutorials --- deploy/Dockerfile.workspace | 8 +- web/src/app/training/[id]/page.tsx | 153 +++--- .../components/shared/notification-panel.tsx | 12 +- workspace/registry.ipynb | 307 ++++++++++++ workspace/visualization.ipynb | 462 ++++++++++++++++++ workspace/welcome.ipynb | 158 +++++- 6 files changed, 997 insertions(+), 103 deletions(-) create mode 100644 workspace/registry.ipynb create mode 100644 workspace/visualization.ipynb diff --git a/deploy/Dockerfile.workspace b/deploy/Dockerfile.workspace index f1d5e6f..aa15d29 100644 --- a/deploy/Dockerfile.workspace +++ b/deploy/Dockerfile.workspace @@ -31,10 +31,14 @@ RUN mkdir -p /opt/conda/share/jupyter/lab/settings && \ echo '{ "@jupyterlab/apputils-extension:themes": { "theme": "JupyterLab Dark" } }' \ > /opt/conda/share/jupyter/lab/settings/overrides.json -# Create workspace directory and copy welcome notebook +# Create workspace directory and copy notebooks RUN mkdir -p /workspace/models && chown -R ${NB_UID}:${NB_GID} /workspace COPY workspace/welcome.ipynb /workspace/welcome.ipynb -RUN chown ${NB_UID}:${NB_GID} /workspace/welcome.ipynb +COPY workspace/visualization.ipynb /workspace/visualization.ipynb +COPY workspace/registry.ipynb /workspace/registry.ipynb +RUN chown ${NB_UID}:${NB_GID} /workspace/welcome.ipynb \ + /workspace/visualization.ipynb \ + /workspace/registry.ipynb USER ${NB_UID} WORKDIR /workspace diff --git a/web/src/app/training/[id]/page.tsx b/web/src/app/training/[id]/page.tsx index ea7881c..a27af69 100644 --- a/web/src/app/training/[id]/page.tsx +++ b/web/src/app/training/[id]/page.tsx @@ -118,87 +118,92 @@ export default function TrainingDetailPage() { return Math.max(0, Math.floor((end - start) / 1000)); }, []); - useEffect(() => { - let cancelled = false; - - async function fetchAll() { - try { - const jobRes = await api.get(`/training/${jobId}`); - if (cancelled) return; - setJob(jobRes); - setElapsedSec(computeElapsed(jobRes.started_at, jobRes.completed_at)); - - // Fetch metrics and artifacts in parallel - const [metricsRes, artifactsRes] = await Promise.all([ - api.get(`/training/${jobId}/metrics`).catch(() => [] as MetricRecord[]), - api.get(`/jobs/${jobId}/artifacts`).catch(() => [] as Artifact[]), - ]); - - if (cancelled) return; - - // Split metrics by metric_name - const loss: { name: string; value: number }[] = []; - const acc: { name: string; value: number }[] = []; - - if (metricsRes && metricsRes.length > 0) { - for (const record of metricsRes) { - const point = { - name: (record.step ?? record.epoch ?? 0).toString(), - value: record.value, - }; - if (record.metric_name === "loss") { - loss.push(point); - } else if (record.metric_name === "accuracy") { - acc.push(point); - } + const fetchAll = useCallback(async (isInitial = false) => { + try { + const jobRes = await api.get(`/training/${jobId}`); + setJob(jobRes); + setElapsedSec(computeElapsed(jobRes.started_at, jobRes.completed_at)); + + // Fetch metrics and artifacts in parallel + const [metricsRes, artifactsRes] = await Promise.all([ + api.get(`/training/${jobId}/metrics`).catch(() => [] as MetricRecord[]), + api.get(`/jobs/${jobId}/artifacts`).catch(() => [] as Artifact[]), + ]); + + // Split metrics by metric_name + const loss: { name: string; value: number }[] = []; + const acc: { name: string; value: number }[] = []; + + if (metricsRes && metricsRes.length > 0) { + for (const record of metricsRes) { + const point = { + name: (record.step ?? record.epoch ?? 0).toString(), + value: record.value, + }; + if (record.metric_name === "loss") { + loss.push(point); + } else if (record.metric_name === "accuracy") { + acc.push(point); } } + } - setLossData(loss); - setAccData(acc); - setArtifacts(artifactsRes ?? []); - } catch (err) { - if (!cancelled) { - toast.error(err instanceof Error ? err.message : "Failed to load training job"); - - // Set a fallback job so the full UI always renders (e.g. for E2E tests) - const fallbackJob: Job = { - id: jobId, - project_id: "", - model_id: "", - dataset_id: null, - job_type: "Training Job", - status: "unknown", - k8s_job_name: null, - hardware_tier: "N/A", - hyperparameters: {}, - metrics: null, - started_at: null, - completed_at: null, - error_message: null, - created_by: "", - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - progress: 0, - epoch_current: null, - epoch_total: null, - loss: null, - learning_rate: null, - gpu_config: null, - }; - setJob(fallbackJob); - setIsFallback(true); - setElapsedSec(0); - } - } finally { - if (!cancelled) setLoading(false); + setLossData(loss); + setAccData(acc); + setArtifacts(artifactsRes ?? []); + } catch (err) { + if (isInitial) { + toast.error(err instanceof Error ? err.message : "Failed to load training job"); + + // Set a fallback job so the full UI always renders (e.g. for E2E tests) + const fallbackJob: Job = { + id: jobId, + project_id: "", + model_id: "", + dataset_id: null, + job_type: "Training Job", + status: "unknown", + k8s_job_name: null, + hardware_tier: "N/A", + hyperparameters: {}, + metrics: null, + started_at: null, + completed_at: null, + error_message: null, + created_by: "", + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + progress: 0, + epoch_current: null, + epoch_total: null, + loss: null, + learning_rate: null, + gpu_config: null, + }; + setJob(fallbackJob); + setIsFallback(true); + setElapsedSec(0); } + } finally { + if (isInitial) setLoading(false); } - - fetchAll(); - return () => { cancelled = true; }; }, [jobId, computeElapsed]); + // Initial fetch + useEffect(() => { + fetchAll(true); + }, [fetchAll]); + + // Poll job + metrics every 3s while job is active + useEffect(() => { + if (!job) return; + const isActive = job.status === "running" || job.status === "pending"; + if (!isActive) return; + + const t = setInterval(() => fetchAll(false), 3000); + return () => clearInterval(t); + }, [job?.status, fetchAll]); + // Tick elapsed timer every second while job is running useEffect(() => { if (!job) return; diff --git a/web/src/components/shared/notification-panel.tsx b/web/src/components/shared/notification-panel.tsx index 63f79c0..6f7eb3c 100644 --- a/web/src/components/shared/notification-panel.tsx +++ b/web/src/components/shared/notification-panel.tsx @@ -168,11 +168,11 @@ export function NotificationPanel() { {/* Header */} -
+

Notifications

{unreadCount > 0 && ( @@ -197,15 +197,15 @@ export function NotificationPanel() {
- {/* Body */} - + {/* Body — fixed max height, self-contained scrolling */} + {notifications.length === 0 ? (

No notifications yet

) : ( -
+
{renderGroup("Today", groups.today, handleClick)} {renderGroup("This Week", groups.thisWeek, handleClick)} {renderGroup("Earlier", groups.earlier, handleClick)} @@ -225,7 +225,7 @@ function renderGroup( if (items.length === 0) return null; return (
-
+
{label} diff --git a/workspace/registry.ipynb b/workspace/registry.ipynb new file mode 100644 index 0000000..f9e4462 --- /dev/null +++ b/workspace/registry.ipynb @@ -0,0 +1,307 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Model Registry\n", + "\n", + "Search, install, and manage models from the [Open Model Registry](https://github.com/GACWR/open-model-registry).\n", + "\n", + "All CLI commands are also available as Python SDK functions. This notebook covers the SDK equivalents." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 1. Search for Models\n", + "\n", + "Search by name, description, or tags. Filter by framework and category." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import openmodelstudio as oms\n", + "\n", + "# Search by keyword\n", + "results = oms.registry_search(\"classification\")\n", + "for m in results:\n", + " print(f\"{m['name']:<20} {m.get('framework',''):<10} {m.get('category',''):<16} {m.get('description','')[:50]}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Filter by framework\n", + "results = oms.registry_search(\"cnn\", framework=\"pytorch\")\n", + "for m in results:\n", + " print(f\"{m['name']:<20} {m.get('description','')[:60]}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Filter by category (empty query = all in category)\n", + "results = oms.registry_search(\"\", category=\"nlp\")\n", + "for m in results:\n", + " print(f\"{m['name']:<20} {m.get('description','')[:60]}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 2. Browse All Registry Models" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import openmodelstudio as oms\n", + "\n", + "models = oms.registry_list()\n", + "print(f\"{'NAME':<18} {'VERSION':<9} {'FRAMEWORK':<10} {'CATEGORY':<16} {'DESCRIPTION'}\")\n", + "print(\"-\" * 80)\n", + "for m in models:\n", + " print(f\"{m['name']:<18} {m.get('version',''):<9} {m.get('framework',''):<10} {m.get('category',''):<16} {m.get('description','')[:30]}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 3. Get Model Details" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import openmodelstudio as oms\n", + "\n", + "info = oms.registry_info(\"iris-svm\")\n", + "print(f\"Name: {info['name']}\")\n", + "print(f\"Version: {info.get('version', '')}\")\n", + "print(f\"Author: {info.get('author', '')}\")\n", + "print(f\"Framework: {info.get('framework', '')}\")\n", + "print(f\"Category: {info.get('category', '')}\")\n", + "print(f\"License: {info.get('license', '')}\")\n", + "print(f\"Description: {info.get('description', '')}\")\n", + "print(f\"Tags: {', '.join(info.get('tags', []))}\")\n", + "print(f\"Dependencies: {', '.join(info.get('dependencies', []))}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 4. Install a Model\n", + "\n", + "Downloads the model files and registers it with the platform." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import openmodelstudio as oms\n", + "\n", + "# Install a model\n", + "path = oms.registry_install(\"iris-svm\")\n", + "print(f\"Installed to: {path}\")\n", + "\n", + "# Force-reinstall an existing model\n", + "# path = oms.registry_install(\"iris-svm\", force=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 5. List Installed Models" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import openmodelstudio as oms\n", + "\n", + "installed = oms.list_installed()\n", + "print(f\"{'NAME':<18} {'VERSION':<9} {'FRAMEWORK':<10} {'PATH'}\")\n", + "print(\"-\" * 70)\n", + "for m in installed:\n", + " print(f\"{m['name']:<18} {m.get('version',''):<9} {m.get('framework',''):<10} {m.get('_installed_path','')[:40]}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 6. Use an Installed Model\n", + "\n", + "`use_model()` loads a registry model and returns a `RegistryModel` object ready for `register_model()`. It resolves via the platform API, so it works inside workspace containers (K8s pods) without filesystem access. If the model isn\u2019t installed yet, it auto-installs from the registry." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import openmodelstudio as oms\n", + "\n", + "# Load the installed registry model\n", + "iris = oms.use_model(\"iris-svm\")\n", + "print(f\"Model: {iris.name}\")\n", + "print(f\"Framework: {iris.framework}\")\n", + "print(f\"Description: {iris.description}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 7. Register and Train a Registry Model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import openmodelstudio as oms\n", + "\n", + "# Load and register under your own name\n", + "iris = oms.use_model(\"iris-svm\")\n", + "handle = oms.register_model(\"my-iris\", model=iris)\n", + "print(handle)\n", + "\n", + "# Train it\n", + "job = oms.start_training(handle.model_id, wait=True)\n", + "print(f\"Training: {job['status']}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 8. Uninstall a Model\n", + "\n", + "Removes local files **and** unregisters from the platform, so the UI reflects the change." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import openmodelstudio as oms\n", + "\n", + "removed = oms.registry_uninstall(\"iris-svm\")\n", + "print(f\"Removed: {removed}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## CLI Commands Reference\n", + "\n", + "All of the above are also available as CLI commands:\n", + "\n", + "```bash\n", + "openmodelstudio search classification\n", + "openmodelstudio search cnn --framework pytorch\n", + "openmodelstudio search \"\" --category nlp\n", + "openmodelstudio registry\n", + "openmodelstudio info iris-svm\n", + "openmodelstudio install iris-svm\n", + "openmodelstudio install iris-svm --force\n", + "openmodelstudio list\n", + "openmodelstudio uninstall iris-svm\n", + "openmodelstudio config\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## How the Registry Works\n", + "\n", + "The Open Model Registry is a GitHub repository:\n", + "\n", + "```\n", + "open-model-registry/\n", + " models/\n", + " iris-svm/\n", + " model.py # train(ctx) + infer(ctx)\n", + " mnist-cnn/\n", + " model.py\n", + " ...\n", + " registry/\n", + " index.json # Aggregated metadata\n", + "```\n", + "\n", + "Each model has a `model.py` following the `train(ctx)` / `infer(ctx)` interface. The `index.json` is read by both the CLI and the web UI to discover available models.\n", + "\n", + "You can also browse and install models from the **Model Registry** page in the sidebar (Develop > Model Registry).\n", + "\n", + "For full documentation, see [CLI & Model Registry docs](../docs/CLI-REGISTRY.md)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbformat_minor": 5, + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/workspace/visualization.ipynb b/workspace/visualization.ipynb new file mode 100644 index 0000000..2605941 --- /dev/null +++ b/workspace/visualization.ipynb @@ -0,0 +1,462 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Visualizations & Dashboards\n", + "\n", + "Create, render, and publish data visualizations from notebooks. Combine them into drag-and-drop dashboards for real-time monitoring.\n", + "\n", + "OpenModelStudio supports **9 visualization backends** with a unified `render()` abstraction.\n", + "\n", + "| Backend | Output | Description |\n", + "|---------|--------|-------------|\n", + "| **matplotlib** | SVG | Standard Python plotting |\n", + "| **seaborn** | SVG | Statistical visualization |\n", + "| **plotly** | JSON | Interactive charts (zoom, pan, hover) |\n", + "| **bokeh** | JSON | Interactive streaming charts |\n", + "| **altair** | JSON | Declarative (Vega-Lite) |\n", + "| **plotnine** | SVG | ggplot2-style grammar of graphics |\n", + "| **datashader** | PNG | Server-side for millions of points |\n", + "| **networkx** | SVG | Network/graph visualizations |\n", + "| **geopandas** | SVG | Geospatial maps |" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 1. Matplotlib (SVG)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import openmodelstudio as oms\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "# 1. Create a visualization record\n", + "viz = oms.create_visualization(\"training-loss\",\n", + " backend=\"matplotlib\",\n", + " description=\"Training loss over epochs\")\n", + "\n", + "# 2. Render it\n", + "fig, ax = plt.subplots()\n", + "epochs = np.arange(1, 21)\n", + "loss = 0.9 * np.exp(-0.15 * epochs) + 0.05\n", + "ax.plot(epochs, loss, color=\"#8b5cf6\", linewidth=2)\n", + "ax.set_xlabel(\"Epoch\")\n", + "ax.set_ylabel(\"Loss\")\n", + "ax.set_title(\"Training Loss\")\n", + "\n", + "# 3. Push rendered output to platform\n", + "output = oms.render(fig, viz_id=viz[\"id\"]) # auto-detects matplotlib \u2192 SVG\n", + "oms.publish_visualization(viz[\"id\"])\n", + "print(\"Matplotlib visualization published\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 2. Plotly (Interactive JSON)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import openmodelstudio as oms\n", + "import plotly.graph_objects as go\n", + "\n", + "viz = oms.create_visualization(\"accuracy-curve\",\n", + " backend=\"plotly\",\n", + " description=\"Model accuracy vs epoch\")\n", + "\n", + "fig = go.Figure()\n", + "fig.add_trace(go.Scatter(\n", + " x=list(range(1, 11)),\n", + " y=[0.5, 0.62, 0.71, 0.78, 0.82, 0.85, 0.87, 0.89, 0.90, 0.91],\n", + " mode=\"lines+markers\",\n", + " name=\"Accuracy\",\n", + " line=dict(color=\"#10b981\"),\n", + "))\n", + "fig.update_layout(title=\"Model Accuracy\", xaxis_title=\"Epoch\", yaxis_title=\"Accuracy\")\n", + "\n", + "output = oms.render(fig, viz_id=viz[\"id\"]) # auto-detects plotly \u2192 JSON\n", + "oms.publish_visualization(viz[\"id\"])\n", + "print(\"Plotly visualization published\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 3. Altair / Vega-Lite (Declarative)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import openmodelstudio as oms\n", + "import altair as alt\n", + "import pandas as pd\n", + "\n", + "viz = oms.create_visualization(\"feature-distribution\",\n", + " backend=\"altair\",\n", + " description=\"Distribution of model features\")\n", + "\n", + "data = pd.DataFrame({\n", + " \"feature\": [\"Age\", \"Fare\", \"Pclass\", \"SibSp\", \"Parch\"],\n", + " \"importance\": [0.28, 0.25, 0.22, 0.15, 0.10],\n", + "})\n", + "\n", + "chart = alt.Chart(data).mark_bar(cornerRadiusTopLeft=3, cornerRadiusTopRight=3).encode(\n", + " x=alt.X(\"feature\", sort=\"-y\"),\n", + " y=\"importance\",\n", + " color=alt.Color(\"feature\", scale=alt.Scale(scheme=\"category10\")),\n", + ")\n", + "\n", + "output = oms.render(chart, viz_id=viz[\"id\"]) # auto-detects altair \u2192 Vega-Lite JSON\n", + "oms.publish_visualization(viz[\"id\"])\n", + "print(\"Altair visualization published\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 4. Seaborn (Statistical)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import openmodelstudio as oms\n", + "import seaborn as sns\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "import numpy as np\n", + "\n", + "viz = oms.create_visualization(\"correlation-heatmap\",\n", + " backend=\"seaborn\",\n", + " description=\"Feature correlation matrix\")\n", + "\n", + "data = pd.DataFrame(np.random.randn(100, 5), columns=[\"A\", \"B\", \"C\", \"D\", \"E\"])\n", + "fig, ax = plt.subplots(figsize=(8, 6))\n", + "sns.heatmap(data.corr(), annot=True, cmap=\"coolwarm\", ax=ax)\n", + "\n", + "output = oms.render(fig, viz_id=viz[\"id\"])\n", + "oms.publish_visualization(viz[\"id\"])\n", + "print(\"Seaborn heatmap published\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 5. Bokeh (Interactive Streaming)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import openmodelstudio as oms\n", + "from bokeh.plotting import figure\n", + "from bokeh.models import ColumnDataSource\n", + "import numpy as np\n", + "\n", + "viz = oms.create_visualization(\"signal-plot\",\n", + " backend=\"bokeh\",\n", + " description=\"Real-time signal visualization\",\n", + " refresh_interval=10) # re-render every 10 seconds\n", + "\n", + "x = np.linspace(0, 4 * np.pi, 200)\n", + "y = np.sin(x)\n", + "source = ColumnDataSource(data=dict(x=x, y=y))\n", + "\n", + "p = figure(title=\"Signal\", width=800, height=400)\n", + "p.line(\"x\", \"y\", source=source, line_width=2, color=\"#8b5cf6\")\n", + "\n", + "output = oms.render(p, viz_id=viz[\"id\"])\n", + "oms.publish_visualization(viz[\"id\"])\n", + "print(\"Bokeh visualization published\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 6. NetworkX (Graphs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import openmodelstudio as oms\n", + "import networkx as nx\n", + "import matplotlib.pyplot as plt\n", + "\n", + "viz = oms.create_visualization(\"model-graph\",\n", + " backend=\"networkx\",\n", + " description=\"Model architecture as a graph\")\n", + "\n", + "G = nx.karate_club_graph()\n", + "fig, ax = plt.subplots(figsize=(10, 8))\n", + "pos = nx.spring_layout(G, seed=42)\n", + "nx.draw_networkx(G, pos, ax=ax, node_color=\"#8b5cf6\",\n", + " edge_color=(0.78, 0.78, 0.78, 0.3),\n", + " font_color=\"black\", node_size=300)\n", + "\n", + "output = oms.render(fig, viz_id=viz[\"id\"])\n", + "oms.publish_visualization(viz[\"id\"])\n", + "print(\"NetworkX graph published\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 7. Datashader (Large Datasets)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import openmodelstudio as oms\n", + "import datashader as ds\n", + "import pandas as pd\n", + "import numpy as np\n", + "\n", + "viz = oms.create_visualization(\"embedding-scatter\",\n", + " backend=\"datashader\",\n", + " description=\"1M point embedding visualization\")\n", + "\n", + "n = 1_000_000\n", + "data = pd.DataFrame({\"x\": np.random.randn(n), \"y\": np.random.randn(n)})\n", + "canvas = ds.Canvas(plot_width=800, plot_height=600)\n", + "agg = canvas.points(data, \"x\", \"y\")\n", + "img = ds.tf.shade(agg, cmap=[\"#000000\", \"#8b5cf6\", \"#ffffff\"])\n", + "\n", + "output = oms.render(img, viz_id=viz[\"id\"])\n", + "oms.publish_visualization(viz[\"id\"])\n", + "print(\"Datashader visualization published\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 8. GeoPandas (Maps)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import openmodelstudio as oms\n", + "import geopandas as gpd\n", + "import matplotlib.pyplot as plt\n", + "\n", + "viz = oms.create_visualization(\"data-coverage\",\n", + " backend=\"geopandas\",\n", + " description=\"Geographic data distribution\")\n", + "\n", + "url = \"https://naciscdn.org/naturalearth/110m/cultural/ne_110m_admin_0_countries.zip\"\n", + "world = gpd.read_file(url)\n", + "fig, ax = plt.subplots(figsize=(12, 6))\n", + "world.plot(ax=ax, color=\"#8b5cf6\", edgecolor=(1, 1, 1, 0.3))\n", + "ax.set_title(\"Data Coverage\")\n", + "\n", + "output = oms.render(fig, viz_id=viz[\"id\"])\n", + "oms.publish_visualization(viz[\"id\"])\n", + "print(\"GeoPandas map published\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## The `render()` Function\n", + "\n", + "`oms.render()` auto-detects the backend from the object type:\n", + "\n", + "| Input Object | Backend | Output |\n", + "|-------------|---------|--------|\n", + "| `matplotlib.figure.Figure` | matplotlib | SVG |\n", + "| `plotly.graph_objects.Figure` | plotly | Plotly JSON |\n", + "| `bokeh.model.Model` | bokeh | Bokeh JSON |\n", + "| `altair.Chart` | altair | Vega-Lite JSON |\n", + "| `plotnine.ggplot` | plotnine | SVG |\n", + "| `datashader Image` | datashader | Base64 PNG |\n", + "| `networkx.Graph` | networkx | SVG (via matplotlib) |\n", + "| `geopandas.GeoDataFrame` | geopandas | SVG (via matplotlib) |\n", + "\n", + "Pass `viz_id=` to push the output to the platform:\n", + "\n", + "```python\n", + "output = oms.render(fig, viz_id=viz[\"id\"])\n", + "```\n", + "\n", + "Without `viz_id`, `render()` returns the output dict locally but doesn't save it." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Dynamic Visualizations\n", + "\n", + "Set `refresh_interval` to create auto-refreshing visualizations:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import openmodelstudio as oms\n", + "\n", + "viz = oms.create_visualization(\"live-metrics\",\n", + " backend=\"plotly\",\n", + " refresh_interval=5) # refresh every 5 seconds\n", + "\n", + "print(f\"Created live visualization: {viz['id']}\")\n", + "print(\"The platform will re-execute the render function every 5 seconds\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Dashboards\n", + "\n", + "Combine multiple visualizations into a single view with drag-and-drop layout." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import openmodelstudio as oms\n", + "\n", + "# Create a dashboard\n", + "dashboard = oms.create_dashboard(\"Training Monitor\",\n", + " description=\"Real-time training metrics overview\")\n", + "\n", + "print(f\"Dashboard: {dashboard['id']}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Dashboard SDK" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import openmodelstudio as oms\n", + "\n", + "# List dashboards\n", + "dashboards = oms.list_dashboards()\n", + "print(f\"Total dashboards: {len(dashboards)}\")\n", + "\n", + "# Get a specific dashboard\n", + "# dash = oms.get_dashboard(dashboard_id)\n", + "\n", + "# Update layout programmatically\n", + "# oms.update_dashboard(dashboard_id,\n", + "# name=\"Updated Name\",\n", + "# layout=[\n", + "# {\"visualization_id\": \"...\", \"x\": 0, \"y\": 0, \"w\": 6, \"h\": 2},\n", + "# {\"visualization_id\": \"...\", \"x\": 6, \"y\": 0, \"w\": 6, \"h\": 2},\n", + "# ])\n", + "\n", + "# Delete a dashboard\n", + "# oms.delete_dashboard(dashboard_id)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## Tips\n", + "\n", + "- **Start with Plotly or Altair** for interactive charts \u2014 they render live in the browser editor\n", + "- **Use matplotlib/seaborn** for publication-quality static figures\n", + "- **Use datashader** for datasets with more than 100k points\n", + "- **Set `refresh_interval > 0`** for live monitoring dashboards\n", + "- **Publish visualizations** before adding them to dashboards\n", + "- The in-browser editor at `/visualizations/{id}` has a Monaco code editor, live preview (for JSON backends), template insertion, and data/config tabs\n", + "\n", + "For the full API reference, see [Visualizations & Dashboards docs](../docs/VISUALIZATIONS.md)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbformat_minor": 5, + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/workspace/welcome.ipynb b/workspace/welcome.ipynb index f9dbddb..f217651 100644 --- a/workspace/welcome.ipynb +++ b/workspace/welcome.ipynb @@ -6,11 +6,11 @@ "source": [ "# Welcome to OpenModelStudio\n", "\n", - "Your workspace is pre-configured with the **`openmodelstudio` SDK**. Everything is auto-connected — your project, auth token, and API endpoint are already set.\n", + "Your workspace is pre-configured with the **`openmodelstudio` SDK**. Everything is auto-connected \u2014 your project, auth token, and API endpoint are already set.\n", "\n", "This notebook walks through a **complete end-to-end ML workflow**:\n", "\n", - "**Load data → Engineer features → Store hyperparams → Register model → Train → Infer → Experiment tracking**\n", + "**Load data \u2192 Engineer features \u2192 Store hyperparams \u2192 Register model \u2192 Train \u2192 Infer \u2192 Experiment tracking**\n", "\n", "Run each cell one by one." ] @@ -20,7 +20,7 @@ "metadata": {}, "source": [ "---\n", - "## Cell 1 — Imports" + "## Cell 1 \u2014 Imports" ] }, { @@ -39,7 +39,7 @@ "metadata": {}, "source": [ "---\n", - "## Cell 2 — Load and prep data" + "## Cell 2 \u2014 Load and prep data" ] }, { @@ -59,7 +59,7 @@ "metadata": {}, "source": [ "---\n", - "## Cell 3 — Register features in the Feature Store" + "## Cell 3 \u2014 Register features in the Feature Store" ] }, { @@ -83,7 +83,7 @@ "metadata": {}, "source": [ "---\n", - "## Cell 4 — Store hyperparameters" + "## Cell 4 \u2014 Store hyperparameters" ] }, { @@ -106,7 +106,7 @@ "metadata": {}, "source": [ "---\n", - "## Cell 5 — Register model (no local training needed)" + "## Cell 5 \u2014 Register model (no local training needed)" ] }, { @@ -128,7 +128,7 @@ "metadata": {}, "source": [ "---\n", - "## Cell 6 — Train through the system" + "## Cell 6 \u2014 Train through the system" ] }, { @@ -150,7 +150,7 @@ "metadata": {}, "source": [ "---\n", - "## Cell 7 — View training logs" + "## Cell 7 \u2014 View training logs" ] }, { @@ -169,7 +169,7 @@ "metadata": {}, "source": [ "---\n", - "## Cell 8 — Run inference through the system" + "## Cell 8 \u2014 Run inference through the system" ] }, { @@ -191,7 +191,7 @@ "metadata": {}, "source": [ "---\n", - "## Cell 9 — Create an experiment and record the run" + "## Cell 9 \u2014 Create an experiment and record the run" ] }, { @@ -216,7 +216,7 @@ "metadata": {}, "source": [ "---\n", - "## Cell 10 — Run a second config and add to experiment" + "## Cell 10 \u2014 Run a second config and add to experiment" ] }, { @@ -244,7 +244,7 @@ " parameters={\"n_estimators\": 500, \"max_depth\": 15, \"min_samples_split\": 2},\n", " metrics={\"accuracy\": 0.96})\n", "\n", - "print(f\"Second run recorded — status: {job2['status']}\")" + "print(f\"Second run recorded \u2014 status: {job2['status']}\")" ] }, { @@ -252,7 +252,7 @@ "metadata": {}, "source": [ "---\n", - "## Cell 11 — Compare experiment runs" + "## Cell 11 \u2014 Compare experiment runs" ] }, { @@ -277,7 +277,7 @@ "metadata": {}, "source": [ "---\n", - "## Cell 12 — Monitor all jobs" + "## Cell 12 \u2014 Monitor all jobs" ] }, { @@ -298,7 +298,7 @@ "metadata": {}, "source": [ "---\n", - "## Cell 13 — Load trained model back into notebook (optional)" + "## Cell 13 \u2014 Load trained model back into notebook (optional)" ] }, { @@ -312,6 +312,122 @@ "print(f\"Estimators: {clf_loaded.n_estimators}\")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Cell 14 \u2014 Visualize Training Results\n", + "\n", + "Create a visualization that shows your training metrics. This uses the unified visualization abstraction \u2014 the same `render()` function works for matplotlib, plotly, altair, and 6 other backends." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "# Create a visualization record on the platform\n", + "viz = openmodelstudio.create_visualization(\"titanic-accuracy\",\n", + " backend=\"matplotlib\",\n", + " description=\"Random Forest accuracy across experiments\")\n", + "\n", + "# Plot the results\n", + "fig, ax = plt.subplots(figsize=(8, 5))\n", + "configs = [\"rf-tuned\\n(200 trees, depth=8)\", \"rf-deep\\n(500 trees, depth=15)\"]\n", + "accuracies = [0.94, 0.96]\n", + "bars = ax.bar(configs, accuracies, color=[\"#8b5cf6\", \"#10b981\"], width=0.5)\n", + "ax.set_ylabel(\"Accuracy\")\n", + "ax.set_title(\"Titanic RF \u2014 Experiment Comparison\")\n", + "ax.set_ylim(0.9, 1.0)\n", + "for bar, acc in zip(bars, accuracies):\n", + " ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.002,\n", + " f\"{acc:.2f}\", ha=\"center\", fontsize=12)\n", + "\n", + "# render() auto-detects matplotlib, converts to SVG, and pushes to the platform\n", + "output = openmodelstudio.render(fig, viz_id=viz[\"id\"])\n", + "\n", + "# Publish so it appears in dashboards\n", + "openmodelstudio.publish_visualization(viz[\"id\"])\n", + "print(\"Visualization published\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Cell 15 \u2014 Interactive Plotly Chart\n", + "\n", + "For interactive charts with zoom, hover, and pan, use Plotly. JSON-based backends like Plotly and Altair also render live in the in-browser editor." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "viz2 = openmodelstudio.create_visualization(\"loss-curve\",\n", + " backend=\"plotly\",\n", + " description=\"Training loss per fold\")\n", + "\n", + "import plotly.graph_objects as go\n", + "\n", + "fig = go.Figure()\n", + "fig.add_trace(go.Scatter(\n", + " x=[1, 2, 3, 4, 5],\n", + " y=[0.35, 0.22, 0.15, 0.11, 0.08],\n", + " mode=\"lines+markers\",\n", + " name=\"rf-tuned (loss)\",\n", + " line=dict(color=\"#8b5cf6\"),\n", + "))\n", + "fig.add_trace(go.Scatter(\n", + " x=[1, 2, 3, 4, 5],\n", + " y=[0.30, 0.18, 0.10, 0.06, 0.04],\n", + " mode=\"lines+markers\",\n", + " name=\"rf-deep (loss)\",\n", + " line=dict(color=\"#10b981\"),\n", + "))\n", + "fig.update_layout(title=\"Cross-Validation Loss\", xaxis_title=\"Fold\", yaxis_title=\"Loss\")\n", + "\n", + "output = openmodelstudio.render(fig, viz_id=viz2[\"id\"])\n", + "openmodelstudio.publish_visualization(viz2[\"id\"])\n", + "print(\"Interactive Plotly chart published\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Cell 16 \u2014 Build a Monitoring Dashboard\n", + "\n", + "Combine your visualizations into a single dashboard view with panels." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dashboard = openmodelstudio.create_dashboard(\"Titanic Experiment Monitor\",\n", + " description=\"Training metrics for the Titanic classification experiments\")\n", + "\n", + "# Add both visualizations as panels\n", + "openmodelstudio.update_dashboard(dashboard[\"id\"], layout=[\n", + " {\"visualization_id\": viz[\"id\"], \"x\": 0, \"y\": 0, \"w\": 6, \"h\": 3},\n", + " {\"visualization_id\": viz2[\"id\"], \"x\": 6, \"y\": 0, \"w\": 6, \"h\": 3},\n", + "])\n", + "\n", + "print(f\"Dashboard created: {dashboard['id']}\")\n", + "print(\"Open the Dashboards page to see your panels\")" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -321,10 +437,10 @@ "## SDK Reference\n", "\n", "The SDK reads these environment variables automatically (set by the workspace pod):\n", - "- `OPENMODELSTUDIO_API_URL` — API endpoint\n", - "- `OPENMODELSTUDIO_TOKEN` — Auth token\n", - "- `OPENMODELSTUDIO_WORKSPACE_ID` — Current workspace\n", - "- `OPENMODELSTUDIO_PROJECT_ID` — Current project\n", + "- `OPENMODELSTUDIO_API_URL` \u2014 API endpoint\n", + "- `OPENMODELSTUDIO_TOKEN` \u2014 Auth token\n", + "- `OPENMODELSTUDIO_WORKSPACE_ID` \u2014 Current workspace\n", + "- `OPENMODELSTUDIO_PROJECT_ID` \u2014 Current project\n", "\n", "See the full SDK docs at [sdk/python/README.md](../sdk/python/README.md)." ] @@ -343,4 +459,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file From b980e97c6fe9a7adf4ef2f761f15374d098a5809 Mon Sep 17 00:00:00 2001 From: jovonni Date: Sun, 8 Mar 2026 11:08:29 -0400 Subject: [PATCH 13/16] fixed duration --- api/src/routes/training.rs | 11 ++++++++++ model-runner/python/runner.py | 3 ++- web/src/app/training/page.tsx | 39 +++++++++++++++++++++++++---------- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/api/src/routes/training.rs b/api/src/routes/training.rs index 4ca49cd..c82b7a9 100644 --- a/api/src/routes/training.rs +++ b/api/src/routes/training.rs @@ -194,6 +194,17 @@ pub async fn post_metrics( // Check for training completion (progress >= 100) if event.metric_name == "progress" && event.value >= 100.0 { + // Mark job as completed with final timestamp + sqlx::query( + "UPDATE jobs SET status = 'completed', completed_at = COALESCE(completed_at, NOW()), updated_at = NOW() WHERE id = $1 AND status != 'completed'" + ) + .bind(job_id) + .execute(&state.db) + .await + .ok(); + + state.metrics.remove(&job_id).await; + // Look up the job owner to notify them let owner: Option<(Uuid,)> = sqlx::query_as("SELECT created_by FROM jobs WHERE id = $1") .bind(job_id) diff --git a/model-runner/python/runner.py b/model-runner/python/runner.py index fd0c332..e0a9d0f 100644 --- a/model-runner/python/runner.py +++ b/model-runner/python/runner.py @@ -60,7 +60,8 @@ def update_job_status(conn, job_id, status, error_message=None, progress=None): with conn.cursor() as cur: if status == "running": cur.execute( - "UPDATE jobs SET status = 'running', started_at = NOW(), " + "UPDATE jobs SET status = 'running', " + "started_at = COALESCE(started_at, NOW()), " "updated_at = NOW() WHERE id = %s", (job_id,), ) diff --git a/web/src/app/training/page.tsx b/web/src/app/training/page.tsx index e593cc4..f9dbcc5 100644 --- a/web/src/app/training/page.tsx +++ b/web/src/app/training/page.tsx @@ -58,9 +58,12 @@ function timeSince(date: string | null): string { function duration(start: string | null, end: string | null): string { if (!start) return "—"; const endTime = end ? new Date(end).getTime() : Date.now(); - const diff = endTime - new Date(start).getTime(); - const mins = Math.floor(diff / 60000); - if (mins < 60) return `${mins}m`; + const diff = Math.max(0, endTime - new Date(start).getTime()); + const totalSec = Math.floor(diff / 1000); + if (totalSec < 60) return `${totalSec}s`; + const mins = Math.floor(totalSec / 60); + const secs = totalSec % 60; + if (mins < 60) return `${mins}m ${secs}s`; const hrs = Math.floor(mins / 60); return `${hrs}h ${mins % 60}m`; } @@ -92,7 +95,8 @@ const statusColors: Record = { export default function TrainingPage() { const { selectedProjectId } = useProjectFilter(); - const [jobs, setJobs] = useState([]); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [rawJobs, setRawJobs] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [stopJobId, setStopJobId] = useState(null); @@ -101,21 +105,34 @@ export default function TrainingPage() { const [newJobTier, setNewJobTier] = useState(""); const [submitting, setSubmitting] = useState(false); const [models, setModels] = useState<{ id: string; name: string; framework: string }[]>([]); + const [, setTick] = useState(0); // force re-render for live durations - const fetchJobs = () => { - setLoading(true); - setError(null); + const fetchJobs = (initial = false) => { + if (initial) { setLoading(true); setError(null); } api.getFiltered("/training/jobs", selectedProjectId) - .then((data) => setJobs(data.map(mapJob))) - .catch((err) => setError(err instanceof Error ? err.message : "Failed to load training jobs")) - .finally(() => setLoading(false)); + .then((data) => setRawJobs(data)) + .catch((err) => { if (initial) setError(err instanceof Error ? err.message : "Failed to load training jobs"); }) + .finally(() => { if (initial) setLoading(false); }); }; + // Map raw jobs on every render so Date.now() stays fresh for running-job durations + const jobs = rawJobs.map(mapJob); + useEffect(() => { - fetchJobs(); + fetchJobs(true); api.getFiltered<{ id: string; name: string; framework: string }[]>("/models", selectedProjectId).then(setModels).catch(() => {}); }, [selectedProjectId]); + // Poll every 5s when there are active jobs + tick every second for live durations + const hasActiveJobs = rawJobs.some((j: any) => j.status === "running" || j.status === "pending"); + + useEffect(() => { + if (!hasActiveJobs) return; + const poll = setInterval(() => fetchJobs(false), 5000); + const tick = setInterval(() => setTick(t => t + 1), 1000); + return () => { clearInterval(poll); clearInterval(tick); }; + }, [hasActiveJobs, selectedProjectId]); + const handleNewJob = async () => { if (!newModelId) { toast.error("Select a model"); return; } setSubmitting(true); From 241838ddf51e5b5af20a5bb046b1a22d713401ec Mon Sep 17 00:00:00 2001 From: jovonni Date: Sun, 8 Mar 2026 11:47:43 -0400 Subject: [PATCH 14/16] updated readme --- README.md | 129 +++++++++++++++++++++++---- docs/screenshots/oms-screenshot3.png | Bin 0 -> 375742 bytes 2 files changed, 114 insertions(+), 15 deletions(-) create mode 100644 docs/screenshots/oms-screenshot3.png diff --git a/README.md b/README.md index 6669bdb..96b2721 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,16 @@ ### For Data Scientists - **Project Management** -- Organize experiments with stage-based workflow (Ideation, Development, Production) +- **Project-Scoped Filtering** -- Global project selector in the topbar scopes every page (models, datasets, experiments, jobs, workspaces, features, visualizations) to a single project - **Model Editor** -- Write and edit models directly in the browser with Monaco (Python + Rust) -- **Real-Time Training** -- Watch loss curves update live via SSE during training +- **Model Registry & CLI** -- Search, install, and manage models from the [Open Model Registry](https://github.com/GACWR/open-model-registry) via CLI (`openmodelstudio install iris-svm`) or the in-app registry browser. Install status syncs bidirectionally between CLI and UI +- **Real-Time Training** -- Watch loss curves, accuracy, and all metrics auto-update live during training with second-level duration accuracy - **Generative Output Viewer** -- See video/image/audio outputs as models train - **Experiment Tracking** -- Compare runs with parallel coordinates and sortable tables -- **JupyterLab Workspaces** -- Launch cloud-native notebooks with one click +- **Visualizations & Dashboards** -- 9 visualization backends (matplotlib, seaborn, plotly, bokeh, altair, plotnine, datashader, networkx, geopandas) with a unified `render()` abstraction. Combine visualizations into drag-and-drop dashboards with persistent layout +- **Global Search** -- Cmd+K command palette searches across models, datasets, experiments, training jobs, projects, and visualizations with instant navigation +- **Notifications** -- Real-time notification bell with unread count, grouped timeline (Today / This Week / Earlier), mark-all-read, and context-aware icons +- **JupyterLab Workspaces** -- Launch cloud-native notebooks pre-loaded with tutorial notebooks (Welcome, Visualizations, Registry) - **LLM Assistant** -- Natural language control of the entire platform - **AutoML** -- Automated hyperparameter search - **Feature Store** -- Reusable features across projects @@ -49,6 +54,7 @@ ### For ML Engineers - **Kubernetes-Native** -- Every model trains in its own ephemeral pod - **Rust API** -- High-performance backend built with Axum + SQLx +- **Python SDK & CLI** -- `pip install openmodelstudio` gives you both a Python SDK (`import openmodelstudio as oms`) and a CLI for registry management, model install/uninstall, and configuration - **GraphQL** -- Auto-generated from PostgreSQL via PostGraphile - **Streaming Data** -- Never load full datasets to disk - **One-Command Deploy** -- `make k8s-deploy` sets up everything @@ -68,6 +74,60 @@ OpenModelStudio Workspaces and Model Metrics

+### Visualizations & Dashboards + +Create, render, and publish data visualizations from notebooks or the in-browser editor. OpenModelStudio supports **9 visualization backends** with a unified `render()` function that auto-detects the library: + +| Backend | Output | Use Case | +|---------|--------|----------| +| matplotlib | SVG | Standard plots, publication-quality figures | +| seaborn | SVG | Statistical visualization, heatmaps | +| plotly | JSON | Interactive charts with zoom, pan, hover | +| bokeh | JSON | Interactive streaming charts | +| altair | JSON | Declarative Vega-Lite specifications | +| plotnine | SVG | ggplot2-style grammar of graphics | +| datashader | PNG | Server-side rendering for millions of points | +| networkx | SVG | Network/graph visualizations | +| geopandas | SVG | Geospatial maps | + +```python +import openmodelstudio as oms + +viz = oms.create_visualization("loss-curve", backend="plotly") +output = oms.render(fig, viz_id=viz["id"]) # auto-detects backend +oms.publish_visualization(viz["id"]) # available for dashboards +``` + +Combine visualizations into **drag-and-drop dashboards** with resizable panels, lock/unlock layout, and persistent configuration. Each visualization also has a full **in-browser editor** (`/visualizations/{id}`) with Monaco, live preview for JSON backends, template insertion, and data/config tabs. + +

+ OpenModelStudio Visualization Framework +

+ +### Model Registry + +Browse, install, and manage models from the [Open Model Registry](https://github.com/GACWR/open-model-registry) -- a public GitHub repo that acts as a decentralized model package manager. + +**From the CLI:** +```bash +openmodelstudio search classification # Search by keyword +openmodelstudio install iris-svm # Install a model +openmodelstudio list # List installed models +``` + +**From a notebook or script:** +```python +import openmodelstudio as oms + +iris = oms.use_model("iris-svm") # Load from registry +handle = oms.register_model("my-iris", model=iris) # Register in project +job = oms.start_training(handle.model_id, wait=True) # Train it +``` + +`use_model()` resolves via the platform API, so it works inside workspace containers (K8s pods) without filesystem access. If the model isn't installed yet, it auto-installs from the registry. The web UI registry page shows **Installed** / **Not Installed** badges that stay in sync with CLI operations. + +--- + ## Quick Start ### Prerequisites @@ -142,9 +202,9 @@ This will: | **Frontend** | Next.js 16, shadcn/ui, Tailwind, Recharts | App Router, Monaco editor, SSE streaming, Cmd+K search | | **API** | Rust, Axum, SQLx | JWT auth, RBAC, K8s client, SSE metrics, LLM integration | | **PostGraphile** | Node.js | Auto-generated GraphQL from PostgreSQL schema | -| **PostgreSQL 16** | SQL | Primary data store: users, projects, models, jobs, datasets, experiments | +| **PostgreSQL 16** | SQL | Primary data store: users, projects, models, jobs, datasets, experiments, visualizations, dashboards, notifications | | **Model Runner** | Python/Rust | Ephemeral K8s pods per training job, streaming metrics | -| **JupyterHub** | Python | Per-user JupyterLab with pre-configured SDK and datasets | +| **JupyterHub** | Python | Per-user JupyterLab with pre-configured SDK, tutorial notebooks, and datasets | ### Training Job Lifecycle @@ -161,15 +221,18 @@ User clicks "Train" --> API creates training_job record ### Database Schema (Key Tables) ```sql -users (id, email, name, password_hash, role, created_at) -projects (id, name, description, stage, owner_id, created_at) -models (id, project_id, name, framework, created_at) -model_versions (id, model_id, version, code, created_at) -jobs (id, project_id, model_id, job_type, status, config, metrics, started_at, completed_at) -datasets (id, project_id, name, path, format, size_bytes, created_at) -experiments (id, project_id, name, description, created_at) -experiment_runs (id, experiment_id, parameters, metrics, created_at) -workspaces (id, user_id, status, jupyter_url, created_at) +users (id, email, name, password_hash, role, created_at) +projects (id, name, description, stage, owner_id, created_at) +models (id, project_id, name, framework, registry_name, created_at) +model_versions (id, model_id, version, code, created_at) +jobs (id, project_id, model_id, job_type, status, config, metrics, started_at, completed_at) +datasets (id, project_id, name, path, format, size_bytes, created_at) +experiments (id, project_id, name, description, created_at) +experiment_runs (id, experiment_id, parameters, metrics, created_at) +workspaces (id, user_id, status, jupyter_url, created_at) +visualizations (id, project_id, name, backend, code, output_type, output_data, published, created_at) +dashboards (id, project_id, name, description, layout, created_at) +notifications (id, user_id, title, message, notification_type, read, link, created_at) ``` > See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full architecture documentation. @@ -181,7 +244,9 @@ workspaces (id, user_id, status, jupyter_url, created_at) Follow these guides to go from zero to a fully tracked ML experiment: 1. **[Usage Guide](docs/USAGE.md)** -- Log in, create a project, upload a dataset, launch a workspace -2. **[Modeling Guide](docs/MODELING.md)** -- Train, evaluate, and track models using the SDK (13-cell notebook walkthrough) +2. **[Modeling Guide](docs/MODELING.md)** -- Train, evaluate, and track models using the SDK (16-cell notebook walkthrough including visualizations and dashboards) +3. **[Visualization Guide](docs/VISUALIZATIONS.md)** -- All 9 backends, `render()` function, dashboards, and the in-browser editor (pre-loaded as `visualization.ipynb` in workspaces) +4. **[Registry & CLI Guide](docs/CLI-REGISTRY.md)** -- Install, use, and manage models from the Open Model Registry (pre-loaded as `registry.ipynb` in workspaces) --- @@ -222,6 +287,38 @@ Follow these guides to go from zero to a fully tracked ML experiment: | `GET` | `/training/:id` | Get training job status | | `GET` | `/training/:id/metrics` | SSE stream of training metrics | +### Visualizations & Dashboards + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/visualizations` | List visualizations (supports `?project_id=`) | +| `POST` | `/visualizations` | Create a visualization | +| `GET` | `/visualizations/:id` | Get visualization details | +| `PUT` | `/visualizations/:id` | Update visualization code/config | +| `POST` | `/visualizations/:id/render` | Render a visualization | +| `POST` | `/visualizations/:id/publish` | Publish for dashboard use | +| `GET` | `/dashboards` | List dashboards | +| `POST` | `/dashboards` | Create a dashboard | +| `PUT` | `/dashboards/:id` | Update dashboard layout | + +### Notifications & Search + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/notifications` | Get user notifications (supports `?unread=true`) | +| `POST` | `/notifications/:id/read` | Mark notification as read | +| `POST` | `/notifications/read-all` | Mark all notifications as read | +| `GET` | `/search?q=` | Global search across models, datasets, experiments, jobs, projects | + +### Model Registry + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/models/registry-status?names=` | Check install status for registry models | +| `POST` | `/models/registry-install` | Register a model from the registry | +| `POST` | `/models/registry-uninstall` | Unregister a registry model | +| `GET` | `/sdk/models/resolve-registry/:name` | Resolve a registry model by name (used by SDK `use_model()`) | + ### Other Endpoints | Method | Endpoint | Description | @@ -304,7 +401,9 @@ Run `make help` to see all available targets. Key ones: | Doc | Description | |-----|-------------| | [Usage Guide](docs/USAGE.md) | UI walkthrough: login, projects, datasets, workspaces | -| [Modeling Guide](docs/MODELING.md) | End-to-end SDK notebook: train, evaluate, track | +| [Modeling Guide](docs/MODELING.md) | End-to-end SDK notebook: train, evaluate, visualize, track | +| [Visualizations Guide](docs/VISUALIZATIONS.md) | 9 backends, `render()`, dashboards, in-browser editor | +| [CLI & Registry Guide](docs/CLI-REGISTRY.md) | Model registry: search, install, `use_model()`, uninstall | | [Architecture](docs/ARCHITECTURE.md) | System design, component diagram, data flow | | [Model Authoring](docs/MODEL-AUTHORING.md) | How to write models for OpenModelStudio | | [Dataset Guide](docs/DATASET-GUIDE.md) | Preparing and uploading datasets | diff --git a/docs/screenshots/oms-screenshot3.png b/docs/screenshots/oms-screenshot3.png new file mode 100644 index 0000000000000000000000000000000000000000..4ea63ef4ee83a5c3ea4ee497bdb0701fe3553265 GIT binary patch literal 375742 zcmbrlcUY546F3@>Do7E9ARRmu5lBFK0O=r2MFFLT5Q-2&Cv=diG!+n#s-UPSMUW!B z1wkM*=@1}5=n#6SH|qJ$cfQ~A`{O?M=6UmMd1q&Lc4y|Do!xnI+fe($`77rE0Kf%Z z9gVvH0Cgw;Kp{s%MXFgGXi_I7<#01ydtKdY02)$(3UHC^9Dt0J`}My}## z&Rdc4!hge%lkzO&|AlvoruYqSPpbd*vmyO|;rUNW>h&2lfPz#5k_3aup8v^{kuU&q z|E>pvKxDapFrJ+O0OYxUFoseC0O$VB%l$$?`oRP!mq-b%o6ZAI0Dz(O*Pkp!lz{^P zptN?nXXa&QpsxT!BE+F~NL#o#3gPy<8AJu;28B7py?AZm4o(>QAR$1&y`a1(go~@E0!m5fH(mu&{#Uhx z5DCb`&R*fJhSuL8q>_@5qnDSPf`o*xudleTlsMAEK>{o3fP2C`oZP&ekgmMHa6)a7-d;*VLg#t^My=}Q<>6%e4|Z2iap>fmcwq^ZZr((}^+M{deI{$;Ih+Pi{~TPxw7QH@K3} zUAQOG+XDvwjp0|LzmceWz@c7nJ7r0bq>LCyN(>|ik@!De{p#bt4K$!$P~`wYcWfQ4Zhyc0U-)}SZCcM&F8sY@|FrnCo__%U#WVl!`N$6T4-dI{d$|1OPdk_d+y#z+ zyL$cNAn@NDWCv65a`JM4|CfJMUA+F6e@KF%-~x4ZP!d9k*}?6h-Y#B3%J*G=H?!jv zCJoTp3+cvd3UzUDgSx>zM0mw`kqA#Q60gJIuAYuaFRA|v&3|_Tk@(jL{(n&H_AjCQ zB{six!!Lm$y&#b*^*6bsl7gzYmm|_c`4-X#>FVmld)pD};Q{#t_n$W;Y5lz{|DUFw zq)z^s`~MjT3hMSR$t8g){FXSR$2}y{MOhu{>I3B^iILL3M2Gj6xG4QgM0kITh0?!7 z0V45#V*0<1{trPU9Y)e-694EtQsE!133nx_N)M7w)Y_I8004Lax*DqYP?@V))F{rM zKToy4xQu?8Zo9^mb!PQ0_d(jyUBY~M@>hEoK{zu zj69;ZUuVn6s6G%&+}OC)PXA;5k?BJxpSSx#+54lDqv_-Lo;rLT-f-}%vuR;rAxj%W z_t@A&jvJ28E^x~ADtIs3-MREYQ!`Td>6_mVS42umimjqe|C-J*B;8v3F)z*Ur(EnR zAodM~&7Dh{zn;}a{fAYT{sL5*YF!MaRAr(dJ+IgJ$jnG}v41exYCH5dzZE8{s*`*H zxc&zV4T6y^l)2I7IKa8&FZZuk5n|n@cimHQUlCls;4$R>`s9C`Q``SvnkOzVFSF7H zy&2&aCiV4uD3=svl32wxX&cv4o>SQT8H%c7fDV9_RQG2nZ$|pk%iTn70bGmYt^(-( z(4;n=r_3S+e`e-8;fZ+86vI4CeX@PpwXMhYXddK zyQ@=WGiC-Ygja)C^97~JmP`gd@AmUP^?SA#bMWJ7z<(IYR{mlbEwlSb33RB?wD@at zVj2%~`wKyD6KiXp2%7~Ft72O}x< z(C1`|6um~g7b|_H1ZW%o=EdGU#|va5lvCVa>s$`s$FJohFlFQQZaJ3at9LUL9wq!7 z+ATelFLOC?(jH5bN-QCx!#~`gstcF17%;Fc8QDt)T>m5WFBRE``A_#&N2O52zr^0Z z0w@Gadd-Pn)wt@#@*`d2Nb?7K=TLx?_1q1d2(%bNYEfVH~)sj_p7 zkY1~RHR*u$YWym-%0XBbDd)5FHg)Xm7;`p)J>8QX_rxd-`!mU2X3VbZavHsxX$hD< zAmG9mIfUAo!rK$LIL6r5if&8b)PTAG)<2>VQ4vkos5!1s=%%yA~mC;RP46DfGq&z+)5 z*PoUw*i*e#v(>S$h5wQC(odrWMmf{)wDM;Ui}W8Jd%s|<4P_>z6>7xK=9o>9Z%$Nu zu1SOt^)^>eHbTxgDuV0hUz)ETt`t|2WDhel*}$lLkjoW(Ftc}nON{oV5FqFA>Q#Mx zVeX2#3Ev+|lIe^o{B^)`w4$s!kO3zfD*Gh7?K4 z;FtPr^SDaho2we$Tv5DrDuyHj-i>w{h?VudN>;V1Y13ccM8z?JR6aFe3OyrD0QHRT z?()yQaqOuKO{0;@NqNvtpSI7+=gP4Tp&ZUu>#D%fEkR0~R%~CMw!@cka4N(r^U+E#H#ZpH@%P{3VN1U(}?QG{hUDJnN&G${bH;E=vNbQxOyh?A;FLF=X zrC6A(M>{n>eWtiQOQ5|iIP-Hr>lKqu21_)O<7J|0WzI?iX(~iG1XGumc|(rKn~MmZ-!UVOtS1Wg9E@#g(kvePOjcC6EFeNCBM^ zuv#U$T_rR7{RvQI8sioRv`5xe^$y?Qz!ZNdvl`eh?TFnd*Q7&M%+qa@<0`5#;uA~E zik>w!E;5T@JocKb)Lp`84}p+Qfb5zKP$#PZRgW zpC(`TDNUq;U8VZB{Vr0mI0|a%f3e48a|xR0R|p!sx+M6MNb{_^6%$z0{=Q@;iX-ZC zj#*Yt@JY-}3nDLG=KPX+TK8pB|Az;L2W*yJ&nJWSC;7OSWs;cU4!Qz@dz6n{j+RXu zu9&=(N?#qX+!*^ZN+R54=+gt}7M>pzMbp{YHvVplM+GhYVZnYJ*-)o+xR_Tl?pib2 zl`gf`EsLd3vecLf+{*YIY(A!S6(D4k;%D!Htk4>rb7pcbn)!4o-t}#l3;gmtvA!~J z`Qu+}@1K3US4Xll1Y6W7Z5C+jMVYWcD>GLgxXgU(-F(3sWUdD=+~+iSn2Zof#~(J& z2I{aM`sO%P?w#nQC-odu#h?-umfx4`N{uTCY`$Yjw8rvZk(eP3O{r_3%qcLkRp7-CMIM8$9DAbVmrp8LW%~yl5L#lE6rtS)Cv*KFX=Z&&~in?JL?uJV5 z!Yr6{2A8d+p#|(`p~Tm3vEpTXid;xs)EY7z&*@;vU;(=vZ3Vw9T2$HzA2u6SDlT7N zS#z9xwEW2#=hGy{9LlVO?@DKBZBJA_m2)ZEiB>sovN~&%Sk0FR1_N*PQ?|X+_-@fY z&_Ut-{TUyh>(r5Fr$|8{db2ryrpEt|w9YMzm5d^X1^ z?%T=UIL5VVhRVFeMm33ObY@^>+}+KfxLR_1T`1E{jR{M;@l>>khQ%GrI!1Y9OIt&H zR%NaZFl1AEqlGglZAQ|m+ZMr2>qh2(a*~NW0$mFfRC+6}QB5YAq$>1iVj(_@45G50 zp5v%7cg<|k)#i--a&Pd$S*;{<^ z=Vf^%aZGW+U)dpN>ow|gkB)+^{DH?kUbVLmeNLtXadFtn(Pe(@Uh-6M=4VT7+jhI^ zfEQ0}73i2l(`8F6f9Z3cm}%VbCcA?_ zt74!F_sOABc?i*}HRLo(Ika@}oq=$ zucM=$@UqO(vIv^xC#( z+w4Bq*9RjMuFS4Nad+FF#HKVEDV>y`EZ6P3^vR4zJmF6Yif|}u{~k^jwgt?2HFAfW zrsp`ieWU%u7r%p;orX`R}wV{P43c#$7qMh+_x*^_l zY=F4|t*2rDPo<=gWl`~3_e>9*S#q>ax~@yNx1q7qwHu5R)P$edz`F%N@OKVx9;{&< zlYr`Rpak34HA0_>(v*uev^t|NBo+3wZ@j>*rn7mHgV<9EzMVsLQ77!B!+iR!)TL`AmQZEd@)*#)aXvZZpljNOVh)k)=vN z%Y|?Uon%3Je5dII&5v!*(JydlG|d?O`~t!`d=R{DUi3OxE?V+!T_1yz#d74~Yo$9> zu3qs4ZQEK_!iad(^-B{(8^{@2RCn9M!wK6&=kP2s~xwsG(<1CG*L zhQYjQv|7ex+TY~o+iRDthaV4o&yS=X;EU#d6mmF_vsxdpfnX9aZchiSlQs`~>oprx z;7ff@zf8+`WME5z;<{YT;d0?x2KM;fr1!8T^i8W>(i0B!ivZ&p$o_=qUP^Ib8v|ss zk++^-jrWD}!E`vE1;r#X1h42b<%g$aW)G*gl_CeJl>{u=qDG{p!N)&~gD+Ez%1#AJ z>3>gmEPHf-&Tu_H^Q>q9BPRgFR3~&ybtV}xY*70j6WZ5e1QJTvtWZr;N4v!c_iHZA zlYaOt(oP;(xsl)BDc}BXOH(0jBxudqx-8^)JBM#F;wXb;61(WmXNLi?Mv$d^5VUsv z+sf+NAjK{tY160TM}#X1c%PCeSkaOYyHq>ou7AlQracXNu^r z<)`CrOqNK(krsb|w42HX(F$2=bNl5h?QFK}k#>#II24zZyLP5iuwu7Czu|%F>a~{u zee>6oHZCz|(VMKYfYxG>8wqMXZE27BuI3z9A4J$hy5XU26OruA9!9(G`WG6?+J3m# z8pR|YUCiUjsqQg4GaRczg1Ft|j9S$&$agp9GkO9$$Kr1hGdZNMI439{F1nq0sI=C3 zO@8<6Ri%zR3iJ7fBFs*UVNRlk0aiOZNP@!l3e8a(ETL4|d5FR)%$jHCp!ZV>Nr4;`-hd2mXjI|+9ZmUv{YKx zDe;+}hKmI{4i*&ekkz|G=IYk-g#LnFw&xNR-FW{voAg<6J2JIH!uvZ_Dod>_>SH}d zDIH&jtvKi(yW<4+FM8J;aR)>=&vG{;^Z6yj?k>5z)3>}+DSV^Fm+g^@IqQp+i1Pj6 zZXlTVsN(oriGBvGl4@OVj1pcUct4fRI?x?eR8n^CWMB6Ko{~5%e=Gg2Z2dV3M5bib z+zUN5Ga$(pkmI%2lW+fe3^41REJ_5qUhPL zWEWVda|UOfVUyiB*j~~Ri=`=SmM?=}lqzm>vNnS8uMN{3__o@{jo#HK)V4>$(BOIy zNICerUF{_yzq}I=I`>l$XR?^mUx#*b8Jo>Tj0#(dsn(PU-rDpdE z)T?cJUY_K&(sphRw0^zbd7Q(#7d{>G}MpRWDi%_?viSvYD6JAUi>9r7kXmZ$Ze{3RG>p%@*iKNQrhiu z4Y`#HBL9k4bBc+3?@yD`ap@t?zFisb9~>@a#NpB8jvvM>1Z5K?KheSwy#Gr1*;kXG z&7T#cvp;=K^IH&Vj1Xt)k-0n3W8Ywjm4q_W@vf}(5~AMvmn@$u%--aTs-jD~Hexs) zQ^q~Hk|9R^qUVQ11B51#n&bc&Lp-NtRNv0CK6$iBp|v=BQvFMfuN9~qJpd`RV9!qX z_c}$3DZo#jEEN{lUx|QHyd;TuD@m(=28CtIB4Ghb?JA`zCnOJp9DlcSah-k0F^;F@ zOn+-C_y}=C`Ur4P*ibxw-`BG8anQE{Laq#Pzgf~Ny ziD30FpGb>IKM=+3zxevL2?~lMi+dIm4Sp<}1n<`z%P}<2*WJ)lId%We@rH3pSe7i! zzb%8Dk(|w`**4H0I`x24MShlMb~OT`kAvOB`n$8&G^g9rWl@!*Qn9M!o)0 zQ*S6)gl-+3ST%jjF@^}7IOghU_1kQsjy|G-QLQnxoNIp|9I6`QqYj)S%ey=1nAr%L znX!q(aZLd29c4LjitV7MP`6yGs;HFZh!v~IDTn92rveNKP&s}?jG;TDH`|Q|a;gXA zzHEI=H+6-qqVjoQ&r&Zpb7+YlM((U7Nu@|2qGfUtX}Zp0o&!M}-&WRby7Di)FkA-S zx!fXZyI^djbeX;k=n9&?Q@`@If5+ovqFVXY+P)8x)2C2pzs?vpN8jXgcu$q7;BiFL zxrN7c;xw@uG#OhnwI};n6`Hzo)2&-4wpVATJOyUL86iyc?=QzA(zngJZgQ_1Ok#zK zE!ej-uPk{o5vvk{>6_9I-!kTNicyERnAtP}{p%%;(smp_yeoQApACxKq+>&!TXMuUlHkQG| z7;A5+qI9esuodv4LIbOL0*#Bk!p6@VXk=iie#+cFx}M{+p{3EE(3epk<`6k7;7*wE z6W{IPMebw(aZ4(-@qH4#GMiN-51B-Cz2c)wp~T>pc-g2duJ^vOaxV<&*9)?J9txjp zz%Ow?8j_jkRmTt9KT6aW5GJ=K_LCK_#?!2npUalFKHW-HSL(B8y1ckwEdgHG4iu0` zCJSQqA_L~#@DmhYS4}W>eYWzTwsE_{j7p~zRtEg#&o90AAO_F{48<$Y^25Zl8RKLF zwHdO#1N*fF&KCr9`^!PtfbCn??ZMaZl2bMo8DbY8)aFiQpWifxpC|HrrOpz>mXU7v5WYU=>gvX+to&7_EFr) zgR0fo?!e)f?TYv1Y2Iit5hYveqZ_4It+D9QK`xrOWYe<^X=?G3W#_kQf_2r)YCqL(C_&>R5>D!F| zMb8AJ09~;NnDmC~2~rqoj9VYivOy);rHf3oRvQISr^l3gxSZpwF793VNh{38%%Oy+ z7MswjADvbZ>yXWGo==ktaT_!s-~i#GpcVbB%UMLP-otdJEtNNtNq!LOGtya$1W+u^ zO~94FLoSMD(zqC5FPNb(-XW!hf>flv54lWsz;i|u#Vt4kT=8RMHaB9X(h8~QuvfdK zsM>lkmt?fscv)0!iaCWTS499&(4&3T>Ki^=l>++15we|T2cst)?!~l=#_KAOZ?1@Z z<28`yeZ;IxPPQQxJRL@j7%6!T{Xy_x2^9&5Ga}%`0mj^)s0u#YTx|lhxW|D}N;W|C z#wHrg@z$8I@6oCeH)@_~nYY$pD%-kK1h=AcJ6e@9zAKa zz<8#&-_Lp9QvS|y_I&;Q9XYMsEkt$)3*s4UP-=<4A?AUF-qE}3F{xX*2DeSdZ@40Q zgs9Xs%;^Iy&M{*^TYdH&LbjlNs$p1M6K}-#R_}-wp|!Ls`P41J&lY<19Rh^g+QdP5 zzm_VsYU%rIJB$-j4q4tNpw?ch59aGaCinrP<5dThGKqrVB$E&$nPL91y5p@MP0wVU zw&qHN&pls#m*M&a8FVzxcfl$;6BAM{a>xM@RXNyf;kEJL-WTGo1%cyD=PZerBAIX{5ACuS$`C)H6J5w!PY~n>``- zJxG=Fmj%$W>q6YOyEZv^?s$G28GV}V$y6ocyso&ODvR|O-I_vWvV-_6v(qV zzTMO;*$kr}VI>FQ7&-^o657tTOKIUvD3Wn1NS5}^A(atL6o2KBNno8-gd0_t#5~_? z=GR(|0`tI-Tg~M12~t)zT^fG<=rq~eQ=BmR4si1uWy!j{RO}LWqcX-0(qa)>%{-3| z4BPt-39ZnPXEg@&>|E!)aD6=t*vCyS{9;hize%56h2zR?0WTh7e0%D*~IA_GVuej40|El`%5Nx4lGE zHES`B4$GvMbg~*Mk1xb4^~vM}e&ni}pY$2a#*F<%U2DfC7SJ@rGhAZnF}z&lSCAM8 z4qMK8wMv-6VviSOE3_Y}Eu9fMB7o|0%_2z;=?~8ihZ`P;O+|)=GboRo5-}lB2nq0Q zTx4w1s|}i$wVrGnCKL&&BzBu_>kJ5^&7r;CEM@-S8}(+q>=dv^a{|s^+rAz!LifQ9 zR0|=_$u^t(0MZxL&OYV+?oxwtPeVbEc`rmXUFF<(SP`Es=hQz&%R9?B%V2w+1ybuy z_^4ic^Y~{T}-(!%f1gfo*n4hr)RL``Hc!D!*C z=FLp#r-AgegR)UC-}Q4#`&%9c-1B5Fp^f$6i_d>@!&axM?C}c77CN0fHTo^1d^4-o zo$HL~Qw}WAkx=7YOEc=x^qRHGb)`*}%w_nV7{L0Bwg0L6_e#E&18ojI`gM+oTm8}6 zCCFke?k7pxn^zlqo7_7S{2Lu38#NUspJ|xG!4!TAa>L)kiayB9Xz?ze@9Qx=z<-M> zsm3z3%kc;R_d_}8M-5L!KVa|V^@%^)$zC|M3!@qCMKf{MO)$HG1Bt|BvRLH_j*Krh z+UGWomQ+9hs9a0qlNjxK=2<2tC*+RtL~M2ss{XxVt7boLUQbR@rUD8HSSn=V=n!q@ z=Po?f#;S+tVD+%!fXolwv(-&+*!yDufh1Q&(WF&6B?Zk_n!M`|XDqff{d%px4Hq{Y zDXr_OX1wlUz75tvuY8f+;K){1N|uH1yL@cGp9+MOgIj~?v#^1CqxX2~5TIKyFTwcZ zv&M6b6u7t8B#1W3ikCA9X+)hZIk6u%QSpLT+9PmMeGf?-ZqaZ`zXq_Ow?I=4f*{6sLa=5v{_! z(Spi6vRDm$ohhToU8N10_+m_P6~R$3J1w`{Sa{CSIHsxGx)rwkq|G>{EkFGG(K^PD88N-cQRkFjL$)C%K zqhoIV@;0o*EVR`u4X?oe7sf6-D4oJ4Wd7Xt4YWMO6jii+eOCTEFwKEwc(cpq(_@mx zmCY7|1au6He(-$26f@oZ#=2P}ZHF?7bKiY}tpVl1)CA1&5Vr+kg0iy=Xd44(-EU<^ z;AbVE#9c%wW%+PHL$g2ucBD-+CzJ(5l%3f10unp9% zCO`@hARj(%)LW7rB}&8OT6OBACUb_DwSU;Y|z){ z!sImqKdr({)-QCvd%N9*s!A4H`zk^?&l_wNrJbYXBzvmWlGfD65!O|T| z3c-)PiOqc`-piCGew)`~S-I@`Ohi9z_5S$eF@IuGy7j}veeT4h`Yg+&=I3a|cKPMK zmO8^P*kXmYQ;&{YM>9_CXs_ibWl9vRmfR}P6j+jKdz;6vLnTZuqZqHRHh~m zyR8;@u$Y(l*llLqj5&lU)Odb@yOE|bhB-z6l}j8N(1*16jAN8J1S`5N-#^<>Ba5?W zyAHe@6_J=Apbs zA;5cv`>AFD>;`4dlIfYb=DHV?EvNZDFWYR!y)_Iz?G&Ai~~+OVe7p2k{JxF?(~ zGBRIawgTtcLX>7>1W0yw67``D#L*N@F`Yr0MK}i8I%ZvPs84T@9~;TE<9Vz-Mw=sv z&i&3nTW%0iSsF`HXier}0TIef$U9>KS|asHKCEQCY5bt0*>e$wSTslRL&#@vvLCVJ zJzyrqKLNY%db0p=S7yh#;bd~-u{JXzF@Uk}g+~F;Lj7END78eCLtNj^5|HdPO?<(%lAl~dmU13$^ zI!+#>jGrW_$tILRjYf>t^#T=#cB)n={X)6eI^C9LuwO!-Q4N3-`|yhG1YXW*UUn@8 z;%Vf$aYqP<_p8WE>D&m|sB>t!>#fD{wn&%JTlJxE!9mbWl+$`Z^X9&ko+C?a(-2eW zxKvV+MCs<9C21cm72(c`VMVXRi2^hF6SsL_!fc^_+zuB**~1AIUd+I)@jCH=x)47> z{{EBcXcc-4pN8eamDZN+3uI{ovqRS2{AdUAc4qz)Zyrd2r5kL;0Zkqk?H9b|q6M%Q zk61(vPx?k%ijyqUQK|ZQMI0IWeq0S7Z1jGi;RSdrlEOZFGNRqA?1Nb5iAr{NoF&kC z3u1r0_vHrQookTzi(sX-NCnLU?c}Z@6_1I^qP_j@eiNFcQf@>zUK%I9rJEMV4rHr3 z=mev0&*k71cjxJD1wEI-IV@*Ewln0ZcQdlZgGtnU5FO)0q#wepepo~t?pr>c$g3M6 zFG;FCzrCto9L|?Bmw1Eta$M{!MwWDb{2>3AhoRZ*pG#S{(VM!LSQN0q-y87l;zRIR zyOnahvPtRo-B{K&*UL5B_9<*39oLY@37V-Yu*(h*`z+H5sKC1%$Fg?LN3v@{k57n) zDSL@WrlZ!}7VuPVk2C}J>cN-#)`)f&pE9m$LcGs`mg#s{+3-m4inSd_{Mh)lYkBv^ zKGNx%@HV0{kZq6dz&vk(B*a9 zbSyZFmea~Kh8HLe%HHg;f*{lFq@Ke?4I}RMx5+rUn8ufe)5@ZQDeP+li8;QHX5^zl zI4hU-95*#k97q*Cuvr{(WDa`93xUR{$VtoNCm-f%C6r6`K%{DofqfCjyb63IH>)%` zyH&l*WdqLS%d=F_FY!0#RsHz(rh+g^GKwia^n$3Unalc9;8tFZJ3{)Pjp}`?0MaRB zOGM7MX-*0k3JH3a(J}iO#NPI>RH;}6Ag(G*czL75qu&Pi(3NF za$%ttNT;I=Ol9;P#Av7G9)$u5268K|7M+4jAv<5i;x zMw**6o)eyPIi6q(_slPDO|N7kz+IK7yz8Knw*df{91gM$@7FMI-E1dj>ZRX)DGoSd zolRz;5?&XLy9?~CwPNBEk!aqeymZ=gZ(SP0LY%g0My0GMHAd)tTQrRk;@Pl{A#>ba zE*q*p2P9*_3GK!cVk;7(m7sU`XXHhV-p7=%vW-;^_21jr=>thT8erNJ!=P0*S_4dZ+g58ne~+ zQqHG(u{3aeu9g6G!F0MEL^bYoMeEXiod2G`qJRBU5Ze}m4SK6SNE~xHJNlFd;~pa9 zB*SR)eVeOqh%JL3u&w{v2y+#{!h`Y3)`m#=8E$r|Kbs}Caz6Gd6`(j#xoQb%p~-RQ z!d+^%zurFb9V?1K#b;>)v4l_1Fcut*hIMvQe!PP`Oa(2}imA$UOJ^(v#e?x9rMPA_ z6ErTyIsXaBZ%B%Pmy!6iDvlCZ+*;Y|D#~b+ot2+as(lz3@g*+~D0{HNB^k*qy{@dD z^@24*6&)T&IT4$Qo9pw{h8{N;G!#WurLP>0M0>I;g3)g76D=MjUwr3P{cXh{I>!)M z@RA*;n*TwMT)fw2#sN^TU32=^HJCeNJn$rg+kfT6;FGi3$6p*x;-3;-$kxa~vBHk{ z2_z|RkhRS9pZlq|(Keu|i-b>}_fx!=jFFbUW_FegDkC2m6b7%ojKS@c?v`tM3QIjdhA~+bwpV#pa)Tdx(P3v*08BqYdAlikzvSk78pel?~H!!LqT}_*eR?Nj3?kvNa;1eTj5)gEL}~bX+cqD(4U7 zod8ugog+EujtfW!8^)?Sq(c$e$2zg9#Ezdg(k{gC)lDE`-JKF5BU4H zc+}35pWom_n>Ha@owq|zpu9_UZ!eZ+y1a+^426&xx5n0o`2^GeqF?t}=IT+oZC89K!Br~s-AGjT36K?9+LJ=lC@2ab;QPj(vVk?}6J-E8JnCtj=wpcJ?-1(V@9@eL~Us- zpc$klz<<^6O%W<}lkbB2GJI))<)!zL{yKh#0$o*m@V{8b)Zr-u#HyoS(zw;r_I&h`^k zgR9SFSmLQ>m4MdzeK7Cuk)j~WKHU8#$OCk3$XOQERq-o0v;Jb&-dnLWEFyJC9TC(S zgOcm+lF_c{?&tyF@(`r-%V(2c1rh?YO0F_YO-{%syxuNhUD04-DKy#VMf@KvvW8H!b4m z{Ok#}e{n8wlfFFr0Q)ZVL9aDtW@BtVy~Q)j;bOZ^VWJ+2O0vRsn6BA@^Se}TkeEXVxc2hSo9M^K%3Tq91cGzy`X=1LH)m z#;G{c(NsjtD=NExmbmugFF;MT!3%YrynW0I(fAT$F@< zus>g8K79GeJT+^td^s!iLANyHgUXtmN>w2xI}QVLxMELj>s}gZD=T?KRkJd;(iMz) zeX<*XAI0R(#LC#uiB~u--|^{rld*aV$Chg|`cH|E&S-Hc+S~A!@j%zHFgqr->mH8^$I9uox9$thL2akCxyA{dLf@vJ@$FXqGWl z@`)XPo1z;=X`|MS+g;$cC8|%;xzY?KdcW1yT0Y@s)bp(UOP&|V$*j=&cdN5$N0_5| zTum*5yhrR)p5!5$+K4g6G1@LhbwpNkiZIG^kfeYuk`=i5fM{61Z!rI4g_Z|;A)F@a zf?&VC&I8RvC=X7FMMDs8C6mu4Ku4(NqiMPTeY&}CT@TUsXI7YSOjM~_fRKtUiEc+5e6V1N=eQkIX{fIo002NC(s#CPOGI1=tCqfGg4sO|gD zu{4Q3${d8%dbD7=(7s+58PMmN-&lZK-CUD3M|NT2`Ila)&`Xs^9=wE?bBqb?v#N31 zicbaPPu9I+zN^b4x>-FHACk`z&bUvFmRoMbR-=P%Wy>e}#4%BWIf*{*6V4Wi;Afy? zt`49G4Tt(wC&AzzAuEkL&3t0Vdu||?Z_HHB(*wdQ;$O$g(PA!WR4=NT@xgx-Q30of zYYr4sm(&eO*Mx5STnF+hpWcXsdAt~9~tW_tFE z<^74=N{}~+clpHo8>YDo9^7sKQZ3OjNeksKDABPR& zZROnik-Rkg$L__|rCmy!;rMQ+f=KKV+67ybC1dDERQw{Zl>rTKtc_kgs$#3f?r-u1 zv3XySc4aig8esLOhza008NB(I{gv@yll%m}1Y^Nx9$FObKWJ4s^SFcLW|)VyNZ01F zE-r4E^){cVna!!}BtbdW1eAsH2kOl}-3%JAe!+d{vmEW2Xt2Aolg{?Gd0|u?+u@y` zS`X|?CRvL4k&wfoy&ddPeztsV(rEmDDCOyti)+euriVGLRg1Q#wjZg#}XI>CWr3n}TQ)Zp<+t}7c76x;{6pOZr z{t)`J>?ub0#n6lvl7*Em)Id#>A)^kNN|=SSC017N*HqH3&%dh;wd{q{FVS^xGlvE# zuDtAtSE@reF8n8gj0^;HX#Q#YPSES;ZrvyUO`F2?`#cfxt}R**_npcRhlA%_Hsps# zR5P$n(a7DBHR+A>8DM|vn*G}Oy&>PBkx3&92kHr*$P1HhVctsMnuqETph!Ke^CqQh_3l5cq zhJiMIqr9^XD4Me7pt=W@8EL_zGTi2Elo%^WID^z%_$3SoZ7soHtaGs&1-#pwD7l?1 z^D2@X38tJzO-arVC%#QJ_qT-CD4TK`uv3}8%F%C-`WoiaUiLNpU6HeEpqSW^T4&|k zwfqlXI%Kg+Gjoqpi7y{iNDAj$;PwNQ_w|$s@J{D#=f8L$zL&Y`4)s10Mr26-f*x^ zl;kE*IvNHmK9FO>*2{5Bj{l^@Sm23jW(C&xH<{5XBF8(M6E#EIKagx8%+qFet+q}2 zZ=ewRB0`A;zy}FCTc459?lQOwNWdUwVW0%Qz_Mv@J+GuWuowS47!qW)j7Q_A34T!H z;kGl(5H;9$d$0fEA^)W9ZX!Wm8j;u*UhDrX}XYX8&9l&dZYfaq~g?q2=(`sfD=7?uv}yPu*AQS`YU* z;aS%TT_$M-R>P==M%WW4ou$^RvC9mQeud$|2byr4#%n?u&?z=HNEsY!)M~+r9*;$s z63|;`C!CD2D?yrNNaOjL?Qv7Nqw3>5&sltp@Y+m!@%~v^+x*PYF2VCS;2U+Ol&Qti zS-^w0?Rcv)f|tBf1A)|BBC*OrX6G;_I#uM<#e4jP%q$DRmaftD_IK~=lhe=FIM1@h zGl|}xPlCH#(6lNqUCVVF#7|Ji6Z(DzjKdws;tbU2+f-d+c$QJnyk{_Y(J!_=qh6=! zXvu5C+X(sXwHUYII|w?_Ar^y+aD0%5n%-f&*FCVSF$@VWM1FTCI;`c_U9g05jQ?9OL^e0P!^dY(ql27@}ax+C}u}e z2V*{1rNe9f;EAoC*>&|0ZTo(2;RRPL@q8ml*`cG&#j575VP_+rm1s3!GV5x7zLDTi z63xO>GZV>SEo{kfwYz(l+=sbAdRw1(*LSr`E%5Vb<)N#-0V!zWB3W8VVBxYY_}}+- zsc_w_fdfQ&!slz8g&H)UK32H?^d72aDbz7edeb?0_Tfp-OFh=n(|r}YaAeTL#~#1l zkHh9R_wze0Ol3Xf)xnEgLAU*7(p=VE)UfeRdRCme!jbQ0dY@(#$;0K-bKM|2G{DU0N(EfuppT2cNWjoaRT zATXgoI{WYxE9UHUfZFr@JH9r&31uGRvcw{zGvU1(%wuk$RCo5xt5el`Ymf1nWl>Ueb}jbPW)DF|=(<$X0AW7)&uuiY$ws zBzvmF^Y1V^Qb+yd7T!TBE8lwjhx~S~uRy%M-S=K$uMJN!OdSFL9V<-2EYlzRxjlc} zMC6%=&2>e`-dtYNX}35j?(NH033D24Qcu@+0(kmZF_WagB1rUP0FaP+6bg#{pZ@rD z0{ibiNPks+Pb25cT`GmrwV3H2`N@ceKf%kkNAP|aL#cPV*U_z4an=M@%^NO^=CZza zWNGD_3?`6JKhw{L5~U*jT4p0%v-PgsOxL?{G~4=XSNT5&Yvu*xBb+<({qiOpIB zpR(U3My=kVMkWV06L2uuwZcm=W^#cCZX6(+P)O(xGeA^*R^_2M*xFyEx6A=EeF5ULI)XtF?Md#QH0lB0 z@&(R+ch8HPhMcBv5CZEf=f;K(v&Dk$v|N4rVAyAH>(Tnx$c5@O_}bs0q-&|V91Cp~ zEGh~L%)!z0>=z+AqsnR6S|byeSM}aLck}BHNm0M*(F_;1FKSfhZ-|y2>!>saggF^? zxi2wp3Tm&;v$&^e@~;9Lhx!nG68{c+X==^=_U+*%MMye@-tWlkw!N18_)XjKSZ-VO z8tpEP9o5Ad7UbPhr@JP)7dH?Z3JX`ej9%0iv~IoKr|v?GUU}6rIU+PBR9Vk_3}%{sj7t)Tfn`S?OzSGI4xAc!`K#Sy>5Z%QZV}b><*s6LtFclSY)& zK8M8E)@1!ZY@KyjRBP1sM?gWz5hSFfQM!>7WI&_@ln!Z-8A7@d>6C6zTIm|P2apz! zhM_@X=pN$Rp7(gpd9UyN3ob4$*n9Tc&$HHg?)$f@Z~yDZ{e+`PBFgKf*6VEj+iXn& zDfi;G%5KmSk(QGKLIPTD<`A2+%L4p#!<)5O^3U%5B_jIF^chKPj!O737px!sM+-pk zm)Ralt2n*D=?wkyoZXOlE)Y^asd$m`{@=@|12alc5Op$c5S#pc@e)Z@ZTj1j(m`6y ziuL$m#^uRJ;;a98FM;soeH1D`!qWBcReo%kEDs?Z2`;tqIq!xbduXTm$Y~tke^`>m*Zp{3YAN=Xs$z@;Ps0BgFHs-mDQ7z)IUIvm`*LQ=SV>dm=hX-Q)l@M0 zAy_c8k{G>=0hP&LzyYlcMRzD2Gw^a}XdxBruR@Kjxi`>HWd9Um)8rv4FJyS+^Ke_= zym=FnFaMYQb&by|icJ-1W9-Jeal#f_+y=ztfBeV)+<7S2H;91xo!7+?_^$^WK2>9D zSdPiN;mndC8Eey#r3Gd7Qh5pl3e!J)0gM-VzQn+TVaraA8y_FnSszUQYpo?wh|#Zi z>^T#^7QJ#UxfzWzL-#v+ORTR@`lxU8o?c4#7Jgj@2nQ4`E>|vjyBiax7jtPIbpjVj zdPdC2CF886(#?rF^9fap3V)kNMdjfWh?oMYnZK<9Q@)%{XpEidYzQawVW2FtM`vr>hwlZ|Np;P-zN9BZ{HY` zXPhNH@cm2RuNQYGAWyxUaHEsk2Tqy=3p+Vr|k^Y9LfonT+A zR0!D;ePgZ-;SMSF@E^;`2dOC67$*Z|DAzeFlR4Wc6eYCbSPk*UZAy$q*4I`~zOExt zGss0o3UmD*|3m(t{~>j^^}cUkJc@}nSLSYOrX=Srt$(!SIIxddcdh&%SX^qs9p3B5 zqHd1tQt$E2)FqFwIE!86y#D9I!37h&RE4tMhBFqg#;KHFYT2L)i$_y;mtxdV=$F;r zx}ybuU<}gcRa(y&@R%u@+>a|3JA!6fKVTNu*NXu9{BgjKUQN%|;&HM~!3=8hI3-vo zc*y_#;&j&*2%5W#N2nPWRQb+aLy-0P%4Ju`$728qamn02Jw3fAIe*s!Ko2Zl+`_!$ z@3=?1M00bP>Ue<_Sp~M`>6L;~n-``Uk0qpZat+xh@3BYPhgth-=&jqI;P(5(6-qydPbt_>%Bg2bX;b?J z)_lGuXwrpO2%t1Db^&83k<9eBZ%%}sjVp_yOb^!vDxW&6VSRA< z4c2hoH{Jq_{1FcYdf|X>{t?FWNQQ?j9PdNxZtHA5^$wq>;?g*K_4mV(2hbB(3-5D# zKR5QWZJaG{d;+omiXXh z`GbSOrpZ|SnUeE)Dw#A`2H$yPreW|YX8#Y*!d{4(e*a|W`P+mg&DRaK2X6=4PFGn; zX|0L2@7|$=mI*3G+Ft{XXX~qr<4XBh=Gz}Aj8Yt-@F{aTWDZ1`#|3>L*vc~vra@je zqO}^?~9g7~JPhjpVj;PXb$JMS<`z{^t*-Vy_uZD`nHTv&sXIcKN zTcaPRxw)Db*~DitSk)8D67)eLi;bqib(x-oVdp({J2wUwO(QXjGTsayLo0_)6{{-| z!#4d7`hGBhF^^G6<)BF0Xrk*LRwsJm_S^P#_jX2u#?Ysca{IOR%kBGh4(rpu$qldi zopJ*9Vo60i`%z&R&p~;}t<6n8yCK2(h{9l9s-S=3j8(Agb!VX31HqR9`hSsIY8}xl z(n$ql|EyoEI_y8nIruQjaI10Zpc~-@eYTOd)ngh`o)B_b)3ArP7p5wO@Q}5x>-1~& zp#GBq=y|^Y)%xX^=RtwX!Hp)}k@)-hxUH_B=g6C|9{{|CbU%r|0>(ch&u|^XjO3dg z;U&_|hlE@k)38h&-P`Rv`1{bA-|iDissfMwdu0AlC@Gw*t=dS0jUn!T(Y zoWFTV1;y`&w|cJohB#%YPO;2a3^D-YPQnACD0QnMyf5>mHnKStLmr-O%$MO|71jrlyKdV(=WX z-g5R>Wrvad_1I>pmqPQYzMQ#3Z=t{pc%8}W=|S2)zKP0A z^cI`9<4sTH&?`3?Wy$@Y1WT2Ka#EvR-(fUfsmwCO@--*0SH7l*BkU_K(P3*kXCM4N zW=rxsXTV8JyhL&1MZNdGIfNC1{VnC+so|en`0HI#x!ann5HEFJHoN%}SlRwSkBCx& z5VwVYr%{jNL3$U8^~J$37fAr|0@s8i?B)|>Z;r}R?n{l7K8C*<8BQvs!hLG6iE1Jl zd1Lv|VN8?G?33eEmD6QiFWsn5Cb2jhWwI3BgRrq(e(w2eR`sjZZ|gbuhJV;I;ON>m z!@sXAQIO{$fD;N5xj)15)lleR=dRN4>=&gSEc`P;T1~=OSAyP3!9P5w@AK=a5#Co6 z3D~g(`D&Jw$rnLTFD3ZyId1vEALt9#7vaBChgcwW$n6_U7u8i#iWmbjD1H6(NoDyf zwV|Iv$22XHKsF_M4|t+8BfsX=_A7~`7-#k)`}NFJUpy6bQjS5@z22$J8A)=SVx;zw z*kF5&rzFWq+9KJjF|Jh27RdrT`{zLh|L1c{aoiWOvR8wOc0m?AwN$ms6XN5G86I{o z7g#D&Jt)hH?wQx*Fo%e#Fnyv?h066a_3|@Kk^m9KSfscv$n@~+R1ZT%&b*|8HHRuy zgr(<1@5!mQnw4Tb6xdHQk5)&K4k0nd`{-<_+1%@fCD;Eh-v7Dr%!=yX+NT?z&SrSz zn}lFViq)mPtT+{O-!_YUCR7Ep(EOC@LZe+rNpJWgOVW)#T6gIVab6XDt8fUeeETKY zEEsS=Q@!z_nNx9I7N?ebR_8j98tH!*tg^ z%~`5Ex1*(_*3nI~QyQoO@p$d8V+xPUerGXFX@#ZoDN`fKDoCAdn|}=>1diFz)=!evIEc)N-r$48Z3pRICQBIgVr<{aQZ%sJ4hBAcq z8VRUFjiVy`$n|FGkWWM&*x4HGJ=C)uMtmWSpf!H*9Ie&qYrr&6;4?H!r8fWQjtaGn zY6}fr)O|mo!~&pJz~25&4)wMFWwF7_5BGild42qTi2a>V%3H~i`ZiG!>3JOeGP**a z*3dl<1V{zXb!FNgv9~jBAMx5gQTi!2LZ6d_XT_)A;5;y8Q17^D&hRdOYO;PxDyMjt zck;rK!r1FuP_A%QL6J&;uAw+W_NQX?U2(w*1N}QXadfZ^*|N~hCt+Q96_&j?h2)cc zPcFu_AuTizBeRKNM@m)(~EswrHH?rkz*unjLqo4iriOJkbx@4%pf5=}j);V8`)8C-KY?jFZ+qu<$GMp~zLXdeWTNdcw zVNdR($?bNq+#JCVNXC}~gL-%fu`ixdg#|52^RIY_K?j+N&d{NC@UN$29^?ozLZJ); z!jC3t-7MW_D|&&mCTb`|IPH zo%GG(yo#FR9p3ME94XDv1LC81#>17bXrKb=?g2OZC#x;xUgp|SoLA7fHr+`H2j$A? zUiYmx0^I&H+20It80JipT)R(`teXn4%>@0NHlV}bukqWh>sf5QiXpLqI*zRvg_9;3 zZ^CJQJi@Rz+=l$p=bd?y05f_ZYGe7pp$nS_UQwd^@!3rN_>(gUk2FX_Ka42Ey70N{ zaJv)7G&B^l6ZO@krobyml_;XXlxv^GFG2nRFB216Fgj5^w9Ke}mIA92Z3 zHvh@c80i0J@24ad$pYbdobEK8cs0SKmW<=w)HkwhxVPU13tt2F-ukVQ$__%w*^8qM zVnmzw)x}gZddC{@faazs>H4YEjvk#bZ3#{9_uGK1g6nh}xg>7+@Now%`3`$n`J zjTHbwtZw!~Pk(z;2yMAKsRXEh!;!*E!Jh`xou?lo+=d}!Q(cs{RN-{~k}f-X4S-d2 zJ4S)CJOGz0u(dHNl z)E~CWduL6?rMrJ^I=MGuqTYq6sDj%QlY8~@@WUgM?F%!>a(#0eTU-{-N?d2Jk#S0~ zc|pnsxhJ!9@U8G=nCJY|c*Egmm6{*|dwc?e*5}sD0(l%9>DBri55-%vJ~WNOjY}%; zA?B?K;lBo~*)R4VhBep5z6sw>yb^DCzR6KU8-X`kQ-zz0`7!o>NEosG>Q#<+Co5xx zW0KMnFwy0i-Dh3~gc&Gk*Q_$f4M_QL)9BsN4lB@+n2laC~+9s z`oDTOz?Mtw+aqd_U3`w5*W1Np-f=y*;JH7Ea+x(MZM^vQTY_~0IML{W>&bP9p81^j z8BU+J+<2h@lHPAb0In=9VnS1W-Qf~IG*r?yon`%QF*pN7_HWFK#d22!?!@5@zR)a@ zzscaun8)sv0#}o%(7dMuk)zLknBn9^^ND~55IHE1d9(qMcg^%WHV<*9OAE=Hx~8Oi zr|Vyb{-R1EGCdFEiTqg*)()tv9q@N2=e40EQS0DEOt$yZ{8v?d%D+^-NoU zt)oHYt66FQE6p49_)Qp@bD78YBJ^1T1&ou)E;zY|f~L zO)AnG4jG)c6DFpNQuWQd*&C5}oM=tRcK4NZ^S$%Z%f>k1%kFa1_PR*ZAY1PMSpA@l zbMKPJ$6pO^+mX)KHY(FaM!L86UDa;toEL58%fRCKSZzW(x2Ptak|MsQCco4ivlt;C z39%G0U$AI$-=9WJ4Ll*3tCza;I_C{Ds2j;zAF2#hVit0p&a>?ZZxtuU;{jvuA(bRHes*d`W5J)JU5W_ z=4)Ay&l4@p3y%=+hkSyX-oZDSg*Bdd^_|x(v*9yYzn}vid0HDUB)Mq!d1hS7mw9yO z@@w)vXH*?#V2@`J6|17%m64cTOp)tw%00~;obJf8MibIy`1*8J5$MwfJ2t@w$C5P!$FEE25zel z(B4nBAFJp2kL~_$)95=-G5H6v?4oyDX#H0!hn{vLFIKr=O~>Q7GdbyACsV&!vUe6w z0mg#L9!-}h*)aL8F}wi@&>{Rzc@`rSb3IPyTxwb_k4v2=je>4>Jn-W}cHs2;N91$K zCIKxSw%{Ktn6|6G891Kx1)ie=LNl5)Zj5?ClODg3Z)Ze4K%9;*2zUZK$J<~xU^n>! zOB@ybpbv_y0prPC?uL zwha@^d^fzW;P-vz5y;kZI691D+VTBIGA?BE*WCxBvivypUxz}8U8?Ri2Dm5$oW{pe ziUkZ!^<}Lda_CgfJ_$Uo4&|7r{N>Ly1y~P-r9hExpmknuAu~^#o||0bNlvG4X)mLS z-G)fms5jO-At{Oz>ld8PV&;YDqmwvW(RVft904BndLv`g8{gv+j-1!<156;a54jF+ z`e<5l_A-W@x|Pf?nMc2Alzy$~wjkO&rLzFqYRVc#XF}F2f)e{1p5LO zZI~FpBSpq#fk4`NAEvf_j+O`gwwX&CLPRIcQ(@cF)}{DM`_wK-3akZyQPpbGxWVjk zKsm34rZH*n`tJV$D{A34G%<(F)>LK%0NLK=TBN=DC3n)#H#FJKQG;i^+j4znJ7XHF z1Ls04$|g z=)M*+YGngR5b$T-CeEOJ^+DI+bY&Hb+J1YYU)E>KAcy(8Yt-h)WB3?*-#SkxIr*Z> ziVx+{2Rx^D zNWE?_!kX>tA=sQyB29EY>?XWmQ}>ynw=cwSGLNSpiBG>y*fp{Gn&USy)k zXdla!2i$>|TBW}EWi@rY!gcSL?Ryc#7al&^M9J286gN=R{yTTQb8oIU56o|r6;=73~TiF4l|y> zjhYwQaX&7E7NEZ~ME%B3!Vcmdw<)*)D-c|PtASmw($f z&Ru(A8fmT>ep~oPp7XYS_Y|7e^R2?^G@Ji1kfUNr#q;m&7$JqWb=4{%d=vXNhBC(C z>XZZNl!F#C=8f?#08(t>R=8c?iIXeFZp+QJAlsN>MHByw$f3jn043$P%$wc6gsgPp zK70=DBENVevkHKld4rc$$Zn;fzV16`h3n}l(eA&RhZ0acZx4|x; zUinxb5dj;RUC0&9S~u0LL!~=+5g5MoLc(SgC4O8uQD|q2-$IR3|KV%XX=XC7pZc^u`O;hG^P8$ek~D!iJeQx!%|+p27BTXiT#= z^JZFYwmh5hmv8B*&pjF^_&UVC8L50#a~@X~??B|k*K{LvO|Xz%V=dewH;|8nv+P2L zZ=-b_*Jz{`il5br`_P6dPjzOEx)9Pd50_ACSGguYbX96BTR0vf(H^rWZCgSjP|Ct2 zZsB0WgJtzh^iYjuH$Ej|mDmMJ^fhtFcGB-+5AwD4M4_`fAHoK&3jN|G!2*?IxX_O4 z)pW(SyL)60bRMahK#6xo`{Xd}w(<*KA8HbqX(p%N;`&!W!Q)9cawAiZ(DkJ2vI)QzZrBN~w z{nVSLaMz+g3B0o}4gVC0+nPfc8{K;Q?I3zF%9}(?e`=hZj+8ELIX}bw9gz)K($MG1 zsbOo!LptSj+QZgkKR0o^yqmUWU2RAjs;m@De%!ZGD3 zNERgF-X;Ja07Qc6-lH98pN@tP9!ht2x2cmZ#PfC?DMu~pk9P&>sVJu%{P0mcbeBR| zrzDhWqJt-?9(mLi8xX--^d!-V1=d0dehbU#EaIK$1h(phgDls@*%vSNP*{AUbHhrl zP?_y<+Qv<-rC8Q{zVZ8;F6*a)R$3hmfnd{fpA}FcmC*Mm;}ky^9&dLC9a#~jU>_6E ze2*s%61)`pY|-I7i0LkHN$Wo+Qb;cFsFAkT*z_h)_&jRfU$oJ-i(hUzh#zXAzkiOd ztCL+xxXk=n-PxGO0Tecy-=SM@?eMY+@xD}%n#ZcVv1O0ohIut1im0qlyD+EHs*|SI z_cmUpAUX8_8c%xj?J6H$xu=)G4DA+|Y13WlAKpsPvo3F*xn|6BidCNR={FvidrM?- zTD{%5d$*Fwn8Qlmhw!~Z&+Hw-w+Dv(%|b>E{~QRtAW?e3z>YMv<`%P`UesyrD1DA3>!M*td`#kDt=l5CF(@jU8G z&$Ew21HQapJIXS)f0&d8tKZ1`k`_ZM(K$4(`4y)Xd17SU9ovUVPDfI4%%gkPVHYv9 z+}ya^jBXc&)s^aV1LD`gb?VmHuvv6|rguFxfGLMV&kq(jW`GlCqID#|WL!^oF(7)S z4Y+6vdoEYiU6uoD;R-I{rCXP^rZ!0lGEt1P-pb&(G+o&_716VbHU*fh1l zB^Eg)^Y8mOI&O}v$hH1DZ(+Ha#4eSt*WNZBs|J}~ncPPBiqsAl9O{J3g0l=$r>SOa66l%Ef(k=|L6^%<|d) z1m)|$#0UNlWr&dqJrm#PoZhvq;*8##re&WS@(FTj z8t+B7i*-^lXhyP}v51scX(%nZ`Ptf9muYiK+7h_C!_;~;$g`t*fhF-YbJu=3tfe8% zp($#C{Kk!T0dR_N z^U!EEDlX3~#1k){qxI0mipPiX(Z{K$0uw=9&MamM<*6`O%pov+qs1@jP z+STOB7j50}=0uOOD}Oz&IbT9^RGd}_i z9vG^(9A1?|ZXBpo)^$Se!y5Z|`XFV4#egntOeom5s3O7)cdCYfpkFxIoWVN6UdNop= zgUtud4~&%^dYBUZZz@AF{W}B<&)Uh_w={)iXW7dT%pa6KPPB5L8ik_{!^KY~8Q331 z*h;2nNDP|rTmXY8poHEV3eH1Kr+0L*9T*JzHw?N5kLGD;F!&v&vLi*FY`cXreC6-ml;aC@?~5_(r-k`8n)bw$!M1?rC1oRdG9Ttid z;0y`Y=6Bmj4N{*BTH<+ehBK*n65JtgELL)QxJN$wyBuaQ z{2g)b7~H2|)TdK~`F+eVtHymaTOu}hW*ydB__Hpe_Lr`t=08pj=Lke-Z%uaU&cEkq zK7M#Kf0NlHbPN7XWc0cekJm%>l`I&Gi?z!w!;Z$7%9v`I z$mD*kCBKytKhNP0o^lhH`q-VLqc{2MB`@o5=59*W^sAc>b!Sh_>*hQ%z496c>(<@A zWr(1Qn%2ZiPN#NtdbsvF_`iU=+B@dCGk>^}rC0+$ zv~0hal0awj+XXl&^odS!70aNBSautCCu%ZJ&(e9fp9y*IIo5Qy$|J+b+5}Y=I9Nao z7fWEM{n?yJtBjEO4+)!HBXom&$C^9g3iXoh{N+};;XF0{Ubwwz9#j^mM6Ce!p=PFgdRxS=-vvhRgpN3^RYFoeJfSNVyC8O zR%E<;!tBQz_Cw!gZZ$x)ORAq9(QuOX)ZtRmctX!1BI>#4iBXGhctnv+<9r1_eqURf z?l;_J33v^3YDuy=T@aR{(!ph-4hY`|!?_CB;I|J?%qVC^_Kd`6v+sBLEUD8%iGCay z(>gKH&^|P2cySN$FN);!3-f;e2p_qW*s3JgDIO{LYwl+zPZ+O>qizv>q`4yr>6QIF zU2C6h=<^dHa$qd-`i?l1;b=*nz&&G&fa&Hy=0QS zV;Dxpb+S~AwXpVuf&!4ili%C>yzXhTkWK}iXb_vu5V~FE269lM1F_`$3KA|HZ%v@L z7JPl7hdSt!~RfKeOc%k|mn@jRkQqRS{ zS!koqJd2JmlO^-MN26e8tqJzReGS|mLuqvm zl2r-Zri}+bo)fz1-a@vse?0)E8==kKLZ6a%@pZ_&%JS)RwA%@F`sPqy(PLu z!S!}2MktF1lU2;~anKb`J3?sag>_4@?vq=!ZkB-orGoke29B+_boc4xSU~^|dw%~;ko©=1(94``xZHgnc?HNI2 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

a23|THbsjS>ykKq-EOv|$jJyWK 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

kCPpR&_^gU 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

UU!RRkd!G&d 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 Q9vq^1Rrd-?)jH(=0Xb)-c>n+a literal 0 HcmV?d00001 From ed5fd02c486c8d8a46b4a4d0c049aaf2b77398b8 Mon Sep 17 00:00:00 2001 From: jovonni Date: Sun, 8 Mar 2026 11:57:15 -0400 Subject: [PATCH 15/16] fixed rust map_or, and bumped pypi version --- api/src/routes/search.rs | 2 +- sdk/python/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/routes/search.rs b/api/src/routes/search.rs index af16440..0c52d09 100644 --- a/api/src/routes/search.rs +++ b/api/src/routes/search.rs @@ -51,7 +51,7 @@ pub async fn search( let pattern = format!("%{}%", query.q); let cat = query.category.as_deref(); - let should = |name: &str| cat.map_or(true, |c| c == name); + let should = |name: &str| cat.is_none_or(|c| c == name); // Projects: name, description, tags::text, stage let projects = if should("projects") { diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index 42fa07e..9cd5e8d 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "openmodelstudio" -version = "0.0.1" +version = "0.0.2" description = "OpenModelStudio SDK — register models, log metrics, and manage artifacts from JupyterLab workspaces" readme = "README.md" requires-python = ">=3.9" From 03872ac68d3fe1e3f68d28ec0b17753d83b79834 Mon Sep 17 00:00:00 2001 From: jovonni Date: Sun, 8 Mar 2026 12:34:58 -0400 Subject: [PATCH 16/16] clippy fixes for cicd --- api/src/routes/experiments.rs | 2 +- api/src/routes/inference.rs | 2 +- api/src/routes/sdk.rs | 34 +++++++++++++++++----------------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/api/src/routes/experiments.rs b/api/src/routes/experiments.rs index 2511a5d..0a4b378 100644 --- a/api/src/routes/experiments.rs +++ b/api/src/routes/experiments.rs @@ -91,7 +91,7 @@ pub async fn add_run( .bind(&req.metrics) .fetch_one(&state.db) .await?; - notify(&state.db, claims.sub, "Experiment Run Logged", &format!("New run added to experiment"), NotifyType::Info, Some(&format!("/experiments/{}", experiment_id))).await; + notify(&state.db, claims.sub, "Experiment Run Logged", "New run added to experiment", NotifyType::Info, Some(&format!("/experiments/{}", experiment_id))).await; Ok(Json(run)) } diff --git a/api/src/routes/inference.rs b/api/src/routes/inference.rs index b25b0f9..9dd0b22 100644 --- a/api/src/routes/inference.rs +++ b/api/src/routes/inference.rs @@ -73,7 +73,7 @@ pub async fn run( .fetch_one(&state.db) .await?; - notify(&state.db, claims.sub, "Inference Started", &format!("Inference job started for model"), NotifyType::Info, Some(&format!("/inference/{}", job_id))).await; + notify(&state.db, claims.sub, "Inference Started", "Inference job started for model", NotifyType::Info, Some(&format!("/inference/{}", job_id))).await; Ok(Json(job)) } diff --git a/api/src/routes/sdk.rs b/api/src/routes/sdk.rs index 9f4a7a4..d9f1336 100644 --- a/api/src/routes/sdk.rs +++ b/api/src/routes/sdk.rs @@ -62,23 +62,21 @@ pub async fn register_model( .fetch_optional(&state.db) .await? } + } else if let Some(pid) = project_id { + sqlx::query_as( + "SELECT * FROM models WHERE name = $1 AND project_id = $2 ORDER BY version DESC LIMIT 1" + ) + .bind(&req.name) + .bind(pid) + .fetch_optional(&state.db) + .await? } else { - if let Some(pid) = project_id { - sqlx::query_as( - "SELECT * FROM models WHERE name = $1 AND project_id = $2 ORDER BY version DESC LIMIT 1" - ) - .bind(&req.name) - .bind(pid) - .fetch_optional(&state.db) - .await? - } else { - sqlx::query_as( - "SELECT * FROM models WHERE name = $1 AND project_id IS NULL ORDER BY version DESC LIMIT 1" - ) - .bind(&req.name) - .fetch_optional(&state.db) - .await? - } + sqlx::query_as( + "SELECT * FROM models WHERE name = $1 AND project_id IS NULL ORDER BY version DESC LIMIT 1" + ) + .bind(&req.name) + .fetch_optional(&state.db) + .await? }; let from_registry = req.registry_name.is_some(); @@ -133,8 +131,10 @@ pub async fn register_model( .bind(workspace_id) .bind(if from_registry { if new_version == 1 { "Installed from registry" } else { "Updated from registry" } + } else if new_version == 1 { + "Initial version from workspace" } else { - if new_version == 1 { "Initial version from workspace" } else { "Updated from workspace" } + "Updated from workspace" }) .execute(&state.db) .await?;