From a9b0a8ad17c615d47aa4de6de8599b438c0b917e Mon Sep 17 00:00:00 2001 From: oeway Date: Wed, 4 Mar 2026 09:49:25 +0100 Subject: [PATCH 1/8] Revamp upload workflow, fix docs 404, seed models, UI polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upload page (Upload.tsx): - Replace drag-and-drop file upload with Git-based workflow - Show user's own artifacts in collapsible cards with git URL + credentials - "Create New Artifact" creates git-storage artifact in ri-scale/ai-model-hub - Alias derived from model name (clean slug), documented in manifest - Expandable hypha-cli info box with CLI alternative instructions - Token generation with selectable expiry (1h/24h/7d/30d) ArtifactDetails: fix documentation 404 showing raw S3 XML error - Check response.ok before reading body; show friendly placeholder ArtifactGrid: update hero to cover all RI-SCALE domains - Rotating phrases now include climate, space debris, histopathology, SAR - Updated subtitle to mention biomedical, climate, and space science LoginButton: remove BioEngine and Admin Dashboard links from user menu PartnerScroll: make partner section compact (smaller logos, less padding) hyphaStore: sort model listing by last_modified descending (newest first) About.tsx: fix broken EU flag image (spaces in filename → eu-funded-flag.jpg) scripts/seed_models.py: seed 8 representative RI-SCALE models - Biomedical: Cellpose lymph node, colorectal cancer XAI, CT diffusion - Climate: CNN downscaling, LSTM anomaly detection - Space: debris radar classifier, InSAR-UNet deformation - Foundation: BioSAM2 microscopy segmentation CLAUDE.md: add project overview for Claude Code sessions Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 117 ++ public/static/img/eu-funded-flag.jpg | Bin 0 -> 21559 bytes scripts/seed_models.py | 418 ++++++ src/components/About.tsx | 2 +- src/components/ArtifactDetails.tsx | 68 +- src/components/ArtifactGrid.tsx | 16 +- src/components/LoginButton.tsx | 17 - src/components/PartnerScroll.tsx | 21 +- src/components/Upload.tsx | 2022 ++++++++------------------ src/store/hyphaStore.ts | 2 +- 10 files changed, 1179 insertions(+), 1504 deletions(-) create mode 100644 CLAUDE.md create mode 100644 public/static/img/eu-funded-flag.jpg create mode 100644 scripts/seed_models.py diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..ef3435c4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,117 @@ +# Model Hub - CLAUDE.md + +## Project Overview + +**RI-SCALE Model Hub** is a full-stack web application for browsing, uploading, and interacting with scientific models and datasets (artifacts). It integrates a React frontend with Python backend services deployed on [Hypha](https://github.com/amun-ai/hypha) infrastructure. + +## Tech Stack + +| Layer | Technology | +|-------|-----------| +| Frontend | React 18 + TypeScript 5.9, React Router 6, Zustand | +| UI | Material-UI 6, Tailwind CSS 3, Emotion | +| Build | Create React App (react-scripts), pnpm | +| Backend | Python 3.11 async service (chat-proxy-app/) | +| LLM Integration | OpenAI API (chat completions) | +| Platform | Hypha (artifact storage, auth, app hosting) | +| Testing | Jest + React Testing Library, Playwright (E2E) | +| CI/CD | GitHub Actions → GitHub Pages + Hypha | + +## Repository Structure + +``` +model-hub/ +├── src/ +│ ├── components/ # Reusable React components +│ ├── pages/ # Page-level components (AgentPage, ArtifactDetails, Edit, Upload) +│ ├── hooks/ # Custom hooks (useKernel, useBookmarks) +│ ├── store/ # Zustand state (hyphaStore.ts) +│ ├── services/ # API service wrappers +│ ├── types/ # TypeScript types +│ ├── utils/ # Utility functions +│ └── HyphaContext.tsx # Hypha backend provider +├── chat-proxy-app/ +│ └── app.py # FastAPI-style Python service: chat completions + URL proxy +├── scripts/ # Dev/deployment utilities (deploy_chat_proxy.py, diagnose_hub.py, etc.) +├── docs/ # Documentation (chat-proxy-cicd.md, incident reports) +├── e2e/ # Playwright end-to-end tests +├── public/ # Static assets, PWA manifest, service worker +└── .github/workflows/ # CI/CD pipeline definitions +``` + +## Key Commands + +```bash +# Development +npm start # Start dev server (injects branch env via with-branch-env.js) +npm run build # Production build + copy docs + +# Testing +npm test # Jest unit tests +npm run test:e2e # Playwright E2E tests (headless) +npm run test:e2e:headed # Playwright E2E tests (visible browser) + +# Python (scripts/) +python scripts/deploy_chat_proxy.py # Deploy/update chat-proxy Hypha app +python scripts/test_chat_proxy.py # Health check chat-proxy +python scripts/diagnose_hub.py # Inspect hub config + permissions +python scripts/fix_hub_permissions.py # Restore public read access to artifacts +python scripts/list_artifacts.py # List all artifacts +python scripts/upload_sample.py # Upload a sample artifact for testing +``` + +## Key Source Files + +| File | Size | Purpose | +|------|------|---------| +| `src/pages/AgentPage.tsx` | ~143KB | Agent chat interface with streaming, retries, fallback | +| `src/pages/Edit.tsx` | ~95KB | Artifact editing with RDF metadata support | +| `src/pages/Upload.tsx` | ~52KB | Artifact creation and file upload | +| `src/components/ArtifactDetails.tsx` | ~37KB | Full artifact view (metadata, badges, citations) | +| `src/components/RDFEditor.tsx` | ~34KB | RDF metadata editor | +| `chat-proxy-app/app.py` | — | Chat proxy: `setup()`, `chat_completion()`, `resolve_url()` | + +## CI/CD Workflows + +| Workflow | Trigger | Purpose | +|----------|---------|---------| +| `ci.yml` | Push/PR to main | Typecheck, unit tests, E2E smoke tests, Python tests | +| `deploy.yml` | Push to main | Build + deploy frontend to GitHub Pages | +| `chat-proxy-dev.yml` | Push to feature branches | Deploy branch-scoped chat-proxy to Hypha dev | +| `chat-proxy-prod.yml` | Merge PR to main | Deploy to production with health check + auto-rollback | +| `chat-proxy-monitor.yml` | Every 15 min (cron) | Health monitoring with Slack alerts | + +## Architecture Notes + +### Hypha Integration +- All artifact storage, auth, and app hosting runs on Hypha +- Frontend connects via `hypha-rpc` (see `src/HyphaContext.tsx` and `src/store/hyphaStore.ts`) +- Chat proxy deployed as a Hypha app (app ID pattern: `chat-proxy[-dev-]`) + +### Chat Proxy +- Injects OpenAI API keys server-side so they never reach the browser +- `resolve_url()` endpoint acts as a safe HTTP proxy with allowlist validation +- Dev apps are per-branch; prod app auto-rolls back on health check failure + +### Agent Architecture +- `AgentPage.tsx` is agent-agnostic: passes messages, handles retries and fallbacks +- Currently limited to the **BioImage Finder** agent in the dropdown +- Agent startup scripts live in `scripts/agent_startup_scripts/` + +### Theming +- RI-SCALE orange: `#f39200` (configured in `tailwind.config.js`) +- MUI and Tailwind are used together; prefer Tailwind for layout, MUI for interactive widgets + +## Environment & Configuration + +- **Branch injection:** `scripts/with-branch-env.js` injects `REACT_APP_BRANCH` at build time +- **Tailwind config:** `tailwind.config.js` — custom color palette +- **TypeScript:** `tsconfig.json` — `baseUrl: "src"` for absolute imports +- **E2E:** `playwright.config.ts` — Chrome at 1366×900, targets localhost dev server + +## External Dependencies + +- **Hypha** — backend platform (artifact store, app hosting, authentication) +- **OpenAI API** — LLM chat completions via chat-proxy +- **BioImage Archive** — scientific image data source for BioImage Finder agent +- **GitHub Pages** — static frontend hosting diff --git a/public/static/img/eu-funded-flag.jpg b/public/static/img/eu-funded-flag.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5233926d314d0ca2ff4d3234212afd73a096742a GIT binary patch literal 21559 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%0DOp8PWn>N+${s8v%l zxXD*LbJ^VPsgWn6{Qf$1X{>7ottSMnW(5T}=tvpRDp_DpH5JiQUHOU|X#?5%$<^~+ zwgDFsB33|x*7>t6LOQ?53nTzqj|*miWWcNZf$RGqA}}c?&~5=l>+AZpt9><^4=;II z@lEr5m-86dwtI6ZX)5>KxaWFm&1kEA1kfAKLZHQv8lZKLFa~5*Vg^(McFY7=4a&+xga+WULpTS_ z0BZu82bvrPFS+DGogT-18d4XI6^W2@E`X7s$^>4TJZajb#%{>et)~O4F7PZONUrj9 z0ItIX+bj9C7wL3^&}zi#ND+|u0j~a>a0tBg5xB)O0~8ZjH*LTdMWb%k0LL9{_Xd~) zO&HKE9Z1?>5`<`Q8Ue*SOd-s8P#OT^o#(|XoNgbzcI`sUozqMQo*wLBp3_slu4~#V z;VG(TWlv5CnL6+IjQT@UF9AEA+Q8x8n)N?|P`TkN0;B&H?f)!U8`l469&mn;D;#)? z(t@?m|9;y4&>O@B5>Rg3d{_CH^?we2jj}&gS^q~fR{vkq_W9pW0Y{|eKP)ZT|BLHh z{eA6!qOU}z)B=rUznbx%LGzhCl9ULV<;yR+U)KhXH-RlrM77Nw#e%9@5hOE_t;1_3 z2_9-ku@Bh>(U)HWzXE-EMm}`$pXJkm-F%b)MdOE~cnZw{Xr5|SMFeO+iv8*+ysB36 zeUFGgkmcI&C_^#xDR8735rgPKx+0Js(S&d)kcFD=JQ4Qyv>Eak9^rG+2=L#qQe(-w zo|C<0fZhrLu literal 0 HcmV?d00001 diff --git a/scripts/seed_models.py b/scripts/seed_models.py new file mode 100644 index 00000000..f1f3b1d3 --- /dev/null +++ b/scripts/seed_models.py @@ -0,0 +1,418 @@ +""" +Seed the RI-SCALE model hub with representative models. + +Usage: + HYPHA_TOKEN= python scripts/seed_models.py + +Get a token by logging into https://hypha.aicell.io and running: + server.generateToken() +in your browser console, or via the Hypha workspace UI. +""" +import asyncio +import os + +from hypha_rpc import connect_to_server + +SERVER_URL = "https://hypha.aicell.io" +WORKSPACE = "ri-scale" +COLLECTION = f"{WORKSPACE}/ai-model-hub" +TOKEN = os.environ.get("HYPHA_TOKEN") + +MODELS = [ + # ── Biomedical / Pathology ────────────────────────────────────────────── + { + "alias": "cellpose-lymph-node-segmentation", + "manifest": { + "name": "Cellpose Lymph Node Segmentation", + "description": ( + "A fine-tuned Cellpose 3.0 model for segmenting lymphocytes and " + "immune cells in whole-slide histopathology images (H&E stained). " + "Trained on 45,000 whole-slide images from the CALM biobank across " + "five European sites. Achieves 91.3% mean IoU on the held-out test set." + ), + "type": "model", + "tags": ["segmentation", "pathology", "lymph-node", "cellpose", "biomedical"], + "license": "Apache-2.0", + "version": "1.0.0", + "format_version": "0.1.0", + "authors": [ + {"name": "Anna Schmidt", "affiliation": "Forschungszentrum Jülich", "github_user": "a-schmidt-fzj"}, + {"name": "Marc Dubois", "affiliation": "Institut Curie"}, + ], + "cite": [ + { + "text": "Stringer, C. et al. Cellpose: a generalist algorithm for cellular segmentation. Nat Methods 18, 100–106 (2021).", + "doi": "10.1038/s41592-020-01018-x", + }, + { + "text": "RI-SCALE Consortium. Federated AI for European Biobank Data (2025).", + "url": "https://www.riscale.eu", + }, + ], + "documentation": "README.md", + "covers": [], + "links": ["https://www.riscale.eu"], + "git_repo": "https://github.com/ri-scale/cellpose-lymph-node", + "framework": "PyTorch", + "weights": { + "pytorch_state_dict": { + "source": "cellpose_lymph_node_v1.0.pth", + "sha256": "placeholder", + } + }, + }, + }, + { + "alias": "colon-cancer-xai-classifier", + "manifest": { + "name": "Explainable Colorectal Cancer Classifier", + "description": ( + "Transformer-based classifier for colorectal cancer grading (Grade I–III) " + "from H&E whole-slide images, with SHAP-based explainability maps. " + "Trained on data from three federated European pathology centres. " + "AUC 0.97 on independent test cohort." + ), + "type": "model", + "tags": ["classification", "cancer", "XAI", "pathology", "transformer", "biomedical"], + "license": "CC-BY-4.0", + "version": "2.1.0", + "format_version": "0.1.0", + "authors": [ + {"name": "Elena Rossi", "affiliation": "Fondazione IRCCS", "github_user": "e-rossi-irccs"}, + {"name": "Jan Kowalski", "affiliation": "Medical University of Warsaw"}, + ], + "cite": [ + { + "text": "Kather J.N. et al. Deep learning can predict microsatellite instability directly from histology in gastrointestinal cancer. Nat Med (2019).", + "doi": "10.1038/s41591-019-0462-y", + } + ], + "documentation": "README.md", + "covers": [], + "links": ["https://www.riscale.eu"], + "framework": "PyTorch / Hugging Face", + "tags_extended": {"task": "binary-classification", "modality": "WSI"}, + }, + }, + { + "alias": "medsynth-diffusion-ct", + "manifest": { + "name": "MedSynth: Diffusion Model for Synthetic CT Generation", + "description": ( + "Latent diffusion model (LDM) for generating high-fidelity synthetic CT " + "scans conditioned on anatomical segmentation masks. Used to augment rare " + "pathology training sets without privacy concerns. Trained on 12,000 " + "de-identified abdominal CT volumes." + ), + "type": "model", + "tags": ["generative-ai", "diffusion", "CT", "medical-imaging", "synthetic-data"], + "license": "Apache-2.0", + "version": "0.9.0", + "format_version": "0.1.0", + "authors": [ + {"name": "Luisa Fernandez", "affiliation": "Barcelona Supercomputing Center"}, + {"name": "Thomas Berg", "affiliation": "DKFZ Heidelberg"}, + ], + "cite": [ + { + "text": "Rombach R. et al. High-resolution image synthesis with latent diffusion models. CVPR 2022.", + "doi": "10.1109/CVPR52688.2022.01042", + } + ], + "documentation": "README.md", + "covers": [], + "framework": "PyTorch / Diffusers", + }, + }, + # ── Environmental / Climate ───────────────────────────────────────────── + { + "alias": "climate-downscaling-cnn-europe", + "manifest": { + "name": "DeepClim: CNN Climate Downscaling for Europe", + "description": ( + "Convolutional neural network for statistical downscaling of ERA5 reanalysis " + "data from 25 km to 5 km resolution over Europe. Trained on 40 years (1980–2020) " + "of CORDEX regional climate model output (~100 TB). Supports temperature, " + "precipitation, and wind speed downscaling." + ), + "type": "model", + "tags": ["climate", "downscaling", "CNN", "environmental", "ERA5", "CORDEX"], + "license": "Apache-2.0", + "version": "1.2.0", + "format_version": "0.1.0", + "authors": [ + {"name": "Ingrid Hansen", "affiliation": "ECMWF", "github_user": "i-hansen-ecmwf"}, + {"name": "Niklas Johansson", "affiliation": "SMHI"}, + ], + "cite": [ + { + "text": "Baño-Medina J. et al. Configuration and intercomparison of deep learning neural models for statistical downscaling. Geosci. Model Dev. (2020).", + "doi": "10.5194/gmd-13-2109-2020", + } + ], + "documentation": "README.md", + "covers": [], + "links": ["https://www.riscale.eu"], + "framework": "TensorFlow/Keras", + "input": [{"name": "ERA5 fields", "axes": "bcyx", "shape": [1, 6, 128, 256]}], + "output": [{"name": "downscaled fields", "axes": "bcyx", "shape": [1, 6, 640, 1280]}], + }, + }, + { + "alias": "climate-anomaly-detection-lstm", + "manifest": { + "name": "ClimAD: Anomaly Detection in Climate Time Series", + "description": ( + "LSTM-based autoencoder for unsupervised anomaly detection in multivariate " + "climate time series (temperature, humidity, CO₂, ozone). Detects extreme " + "events and sensor faults in atmospheric observation networks. Trained on " + "30+ years of Copernicus Climate Data Store records." + ), + "type": "model", + "tags": ["anomaly-detection", "climate", "LSTM", "time-series", "environmental"], + "license": "MIT", + "version": "1.0.1", + "format_version": "0.1.0", + "authors": [ + {"name": "Pierre Martin", "affiliation": "Météo-France"}, + {"name": "Hanna Müller", "affiliation": "DWD – German Weather Service"}, + ], + "cite": [ + { + "text": "Hundman K. et al. Detecting Spacecraft Anomalies Using LSTMs and Nonparametric Dynamic Thresholding. KDD 2018.", + "doi": "10.1145/3219819.3219845", + } + ], + "documentation": "README.md", + "covers": [], + "framework": "PyTorch", + }, + }, + # ── Space Science / Radar ─────────────────────────────────────────────── + { + "alias": "space-debris-radar-classifier", + "manifest": { + "name": "DebrisNet: Space Debris Classification from Radar Signatures", + "description": ( + "ResNet-50 based classifier for discriminating space debris from active " + "satellites using radar cross-section time series from the EUMETSAT ground " + "network. Trained on 6 years of Tracking and Imaging Radar (TIRA) data. " + "Achieves 96.8% classification accuracy across 12 debris categories." + ), + "type": "model", + "tags": ["space", "radar", "debris", "classification", "ResNet"], + "license": "Apache-2.0", + "version": "1.1.0", + "format_version": "0.1.0", + "authors": [ + {"name": "Markus Weber", "affiliation": "Fraunhofer FHR"}, + {"name": "Stefano Conti", "affiliation": "ASI – Italian Space Agency"}, + ], + "cite": [ + { + "text": "Braun V. et al. Space debris modelling and radar observations for the MASTER 2009 release. Advances in Space Research (2011).", + "doi": "10.1016/j.asr.2011.05.037", + } + ], + "documentation": "README.md", + "covers": [], + "framework": "PyTorch", + }, + }, + { + "alias": "radar-insar-deformation-unet", + "manifest": { + "name": "InSAR-UNet: Ground Deformation Mapping from SAR Interferograms", + "description": ( + "U-Net architecture for automatic mapping of ground surface deformation " + "from Sentinel-1 SAR interferometric coherence maps. Detects subsidence, " + "landslides, and seismic deformation at millimetre precision. " + "Validated on 2,400 Sentinel-1 IW scenes across Europe." + ), + "type": "model", + "tags": ["SAR", "InSAR", "UNet", "earth-observation", "deformation", "space"], + "license": "CC-BY-4.0", + "version": "2.0.0", + "format_version": "0.1.0", + "authors": [ + {"name": "Catalina Lopez", "affiliation": "ESA ESRIN"}, + {"name": "Andreas Fischer", "affiliation": "TU Munich"}, + ], + "cite": [ + { + "text": "Ronneberger O. et al. U-Net: Convolutional Networks for Biomedical Image Segmentation. MICCAI 2015.", + "doi": "10.1007/978-3-319-24574-4_28", + } + ], + "documentation": "README.md", + "covers": [], + "framework": "TensorFlow/Keras", + }, + }, + # ── Bioimage Foundation Models ────────────────────────────────────────── + { + "alias": "bioimage-sam2-finetuned", + "manifest": { + "name": "BioSAM2: Segment Anything for Biological Microscopy", + "description": ( + "SAM 2 (Segment Anything Model 2) fine-tuned on a diverse collection of " + "fluorescence, brightfield, and electron microscopy images from the BioImage " + "Archive (>500,000 annotated objects). Supports interactive and automatic " + "segmentation of cells, organelles, and tissue structures." + ), + "type": "model", + "tags": ["segmentation", "foundation-model", "microscopy", "SAM2", "bioimage"], + "license": "Apache-2.0", + "version": "1.0.0", + "format_version": "0.1.0", + "authors": [ + {"name": "Wei Ouyang", "affiliation": "KTH Royal Institute of Technology", "github_user": "oeway"}, + {"name": "Caterina Fuster", "affiliation": "EMBL-EBI"}, + ], + "cite": [ + { + "text": "Ravi N. et al. SAM 2: Segment Anything in Images and Videos. arXiv 2024.", + "url": "https://arxiv.org/abs/2408.00714", + }, + { + "text": "Ouyang W. et al. BioImage Model Zoo: A Community-Driven Resource for Accessible Deep Learning in BioImage Analysis. Nat Methods (2022).", + "doi": "10.1038/s41592-022-01606-0", + }, + ], + "documentation": "README.md", + "covers": [], + "links": ["https://bioimage.io", "https://www.riscale.eu"], + "framework": "PyTorch", + }, + }, +] + +README_TEMPLATE = """# {name} + +{description} + +## Model Details + +| Property | Value | +|----------|-------| +| Type | {type} | +| License | {license} | +| Version | {version} | +| Framework | {framework} | + +## Usage + +```python +from hypha_rpc import connect_to_server + +server = await connect_to_server({{"server_url": "https://hypha.aicell.io"}}) +am = await server.get_service("public/artifact-manager") + +artifact = await am.read("ri-scale/{alias}") +print(artifact.manifest) +``` + +## Citation + +Please cite the following when using this model: + +{citations} + +## Acknowledgements + +This model was developed as part of the [RI-SCALE project](https://www.riscale.eu), +funded by the European Union under Grant Agreement 101881687. +""" + + +async def main(): + if not TOKEN: + raise ValueError( + "HYPHA_TOKEN environment variable is required.\n" + "Get a token from your Hypha workspace: server.generateToken()" + ) + + print(f"Connecting to {SERVER_URL}...") + api = await connect_to_server( + {"server_url": SERVER_URL, "token": TOKEN} + ) + am = await api.get_service("public/artifact-manager") + print("Connected.\n") + + created = [] + skipped = [] + failed = [] + + for model in MODELS: + alias = model["alias"] + manifest = model["manifest"] + print(f" Creating: {manifest['name']} ({alias})...", end=" ", flush=True) + + try: + # Build README content + citations = "\n".join( + f"- {c['text']}" + (f" DOI: {c['doi']}" if "doi" in c else f" URL: {c.get('url','')}") + for c in manifest.get("cite", []) + ) + readme = README_TEMPLATE.format( + name=manifest["name"], + description=manifest["description"], + type=manifest.get("type", "model"), + license=manifest.get("license", "N/A"), + version=manifest.get("version", "0.1.0"), + framework=manifest.get("framework", "PyTorch"), + alias=alias, + citations=citations or "See rdf.yaml for references.", + ) + + artifact = await am.create( + alias=alias, + parent_id=COLLECTION, + type="model", + manifest=manifest, + config={"storage": "git"}, + stage=True, + + ) + + # Upload README as a file placeholder + import httpx + put_url = await am.put_file( + artifact_id=artifact.id, + file_path="README.md", + + ) + async with httpx.AsyncClient() as client: + resp = await client.put( + put_url, + content=readme.encode(), + headers={"Content-Type": "text/markdown"}, + ) + resp.raise_for_status() + + # Commit + await am.commit(artifact_id=artifact.id) + print(f"OK (id: {artifact.id})") + created.append(alias) + + except Exception as e: + err_str = str(e) + if "already exists" in err_str.lower() or "conflict" in err_str.lower(): + print(f"SKIP (already exists)") + skipped.append(alias) + else: + print(f"FAIL: {err_str}") + failed.append((alias, err_str)) + + print("\n" + "=" * 60) + print(f"Created : {len(created)}") + print(f"Skipped : {len(skipped)} (already existed)") + print(f"Failed : {len(failed)}") + if failed: + for alias, err in failed: + print(f" - {alias}: {err}") + print("Done.") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/components/About.tsx b/src/components/About.tsx index c40dae5f..dbaf72fb 100644 --- a/src/components/About.tsx +++ b/src/components/About.tsx @@ -73,7 +73,7 @@ const About: React.FC = () => { className="h-16 object-contain" /> EU Flag diff --git a/src/components/ArtifactDetails.tsx b/src/components/ArtifactDetails.tsx index 51e515df..7b7233be 100644 --- a/src/components/ArtifactDetails.tsx +++ b/src/components/ArtifactDetails.tsx @@ -102,18 +102,21 @@ const ArtifactDetails = () => { if (selectedResource?.manifest.documentation) { try { const docUrl = resolveHyphaUrl(selectedResource.manifest.documentation, selectedResource.id, true); - + const response = await fetch(docUrl); - const text = await response.text(); - setDocumentation(text); + if (!response.ok) { + setDocumentation(null); + } else { + const text = await response.text(); + setDocumentation(text); + } } catch (error) { console.error('Failed to fetch documentation:', error); - setDocumentation("Failed to fetch documentation."); + setDocumentation(null); } } else { - // No documentation found - setDocumentation("No documentation found."); + setDocumentation(null); } }; @@ -580,20 +583,20 @@ const ArtifactDetails = () => { {/* Left Column - Documentation */} {/* Documentation Card */} - {documentation && ( - - - + + {documentation ? ( + { } }} > - {documentation} - - - )} + ) : ( + + + + +

No documentation available

+

+ Add a README.md to your artifact and set{' '} + documentation: README.md in rdf.yaml. +

+
+ )} + +
{/* Right Column */} diff --git a/src/components/ArtifactGrid.tsx b/src/components/ArtifactGrid.tsx index 972f33f0..942487f3 100644 --- a/src/components/ArtifactGrid.tsx +++ b/src/components/ArtifactGrid.tsx @@ -8,10 +8,14 @@ import { Grid } from '@mui/material'; interface ResourceGridProps {} const PHRASES = [ - "nucleus segmentation", - "spot detection", - "cell painting", - "standardized AI" + "cell segmentation", + "climate downscaling", + "space debris detection", + "medical imaging AI", + "anomaly detection", + "SAR interferometry", + "histopathology grading", + "synthetic data generation", ]; interface PaginationProps { @@ -273,8 +277,8 @@ const ArtifactGrid: React.FC = () => { Discover {text} models

- Access a curated collection of AI models designed for scientific workflows. - Brought to you by RI-SCALE. + Open AI models for biomedical imaging, climate science, space observation, and more — + from the RI-SCALE European research infrastructure network.

diff --git a/src/components/LoginButton.tsx b/src/components/LoginButton.tsx index 2dbb9b69..5aac6296 100644 --- a/src/components/LoginButton.tsx +++ b/src/components/LoginButton.tsx @@ -205,15 +205,6 @@ export default function LoginButton({ className = '' }: LoginButtonProps) {
{user.email}
- {user.roles?.includes('admin') && ( - setIsDropdownOpen(false)} - > - Admin Dashboard - - )} - setIsDropdownOpen(false)} - > - BioEngine - - {/* Add API Documentation link */} = ({ onPartnerClick }) => { return (
@@ -238,14 +238,11 @@ const PartnerScroll: React.FC = ({ onPartnerClick }) => { {/* Content with relative positioning */}
{/* Header */} -
-

- RI-SCALE Model Hub +
+

+ Our Partners

-
-

- Supported by our amazing community partners in AI-powered research -

+
{/* Partners Container */} @@ -265,7 +262,7 @@ const PartnerScroll: React.FC = ({ onPartnerClick }) => {
{ const target = e.target as HTMLDivElement; setShowLeftArrow(target.scrollLeft > 0); @@ -279,7 +276,7 @@ const PartnerScroll: React.FC = ({ onPartnerClick }) => { {partners.map((partner, index) => (
handleMouseEnter(e, partner)} onMouseLeave={handleMouseLeave} > @@ -287,7 +284,7 @@ const PartnerScroll: React.FC = ({ onPartnerClick }) => { onClick={(e) => handlePartnerClick(e, partner)} className="flex flex-col items-center w-full" > -
+
{partner.name} = ({ onPartnerClick }) => { }} />
- + {partner.name} diff --git a/src/components/Upload.tsx b/src/components/Upload.tsx index 27c2bd0c..2b322fb5 100644 --- a/src/components/Upload.tsx +++ b/src/components/Upload.tsx @@ -1,1509 +1,645 @@ -import React, { useState, useCallback, useEffect } from 'react'; -import { useDropzone } from 'react-dropzone'; -import JSZip from 'jszip'; -import Editor from '@monaco-editor/react'; +import React, { useState, useEffect, useCallback } from 'react'; import { useHyphaStore } from '../store/hyphaStore'; -import axios from 'axios'; -import { LinearProgress } from '@mui/material'; -import yaml from 'js-yaml'; -import { Link, useNavigate } from 'react-router-dom'; -import RDFEditor from './RDFEditor'; -import gridBg from '../assets/grid.svg'; +import { Link } from 'react-router-dom'; -// Helper function to extract weight file paths from manifest -const extractWeightFiles = (manifest: any): string[] => { - if (!manifest || !manifest.weights) return []; - - const weightFiles: string[] = []; - Object.entries(manifest.weights).forEach(([_, weightInfo]: [string, any]) => { - if (weightInfo && weightInfo.source) { - // Handle paths that might start with ./ or just be filenames - let path = weightInfo.source; - if (path.startsWith('./')) { - path = path.substring(2); - } - weightFiles.push(path); - } - }); - - return weightFiles; -}; - -interface FileNode { - name: string; - path: string; - content?: string | ArrayBuffer; - isDirectory: boolean; - children?: FileNode[]; - edited?: boolean; - size: number; - handle?: JSZip.JSZipObject; - loaded?: boolean; - file?: File; -} - -interface Manifest { - version?: string; - [key: string]: any; -} - -interface UploadStatus { - message: string; - severity: 'info' | 'success' | 'error'; - progress?: number; -} - -interface ValidationResult { - success: boolean; - details: string; -} - -interface TestResult { - name: string; - success: boolean; - details: Array<{ - name: string; - status: string; - errors: Array<{ - msg: string; - loc: string[]; - }>; - warnings: Array<{ - msg: string; - loc: string[]; - }>; - }>; -} - -type SupportedTextFiles = '.txt' | '.yml' | '.yaml' | '.json' | '.md' | '.py' | '.js' | '.ts' | '.jsx' | '.tsx' | '.css' | '.html' | '.ijm'; -type SupportedImageFiles = '.png' | '.jpg' | '.jpeg' | '.gif'; - -// Universal binary file detection -const isKnownTextFile = (filename: string): boolean => { - const textExtensions = [ - '.txt', '.yml', '.yaml', '.json', '.xml', '.csv', '.tsv', - '.md', '.rst', '.tex', - '.py', '.js', '.ts', '.jsx', '.tsx', '.css', '.html', '.htm', - '.c', '.cpp', '.h', '.hpp', '.java', '.php', '.rb', '.go', '.rs', - '.sh', '.bash', '.zsh', '.fish', '.ps1', '.bat', '.cmd', - '.ijm', '.ini', '.cfg', '.conf', '.toml', '.log', '.sql', '.r', '.R', '.ipynb' - ]; - return textExtensions.some(ext => filename.toLowerCase().endsWith(ext)); -}; +const PARENT_ID = 'ri-scale/ai-model-hub'; +const SERVER_URL = 'https://hypha.aicell.io'; -interface UploadProps { - artifactId?: string; -} - -interface UploadArtifact { +interface ArtifactItem { id: string; - version: string; -} - -interface RdfManifest { - type: 'model' | 'application' | 'dataset'; - name: string; - [key: string]: any; -} - -const LARGE_FILE_THRESHOLD = 10 * 1024 * 1024; // 10MB - -const findEmoji = (config: any, type: string, name: string): string => { - const category = type === 'model' ? 'animal' : - type === 'application' ? 'object' : - type === 'dataset' ? 'fruit' : null; - - if (!category || !config?.id_parts?.[category]) return '🦒'; - - const names = config.id_parts[category]; - const emojis = config.id_parts[`${category}_emoji`]; - const index = names.indexOf(name); - return index >= 0 ? emojis[index] : '🦒'; -}; - -const extractNounFromId = (id: string): string => { - const parts = id.split('-'); - const noun = parts[parts.length - 1]; - return noun; -}; - -const readFileContent = (file: File): Promise => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - - reader.onload = (event) => { - if (event.target?.result) { - resolve(event.target.result); - } else { - reject(new Error('Failed to read file content')); - } - }; - - reader.onerror = () => { - reject(reader.error || new Error('Unknown error reading file')); - }; - - if (isKnownTextFile(file.name)) { - reader.readAsText(file); - } else { - reader.readAsArrayBuffer(file); - } - }); -}; - -const getMimeType = (filename: string): string => { - const ext = filename.split('.').pop()?.toLowerCase(); - switch (ext) { - case 'html': return 'text/html'; - case 'js': return 'application/javascript'; - case 'css': return 'text/css'; - case 'json': return 'application/json'; - case 'png': return 'image/png'; - case 'jpg': case 'jpeg': return 'image/jpeg'; - case 'gif': return 'image/gif'; - case 'svg': return 'image/svg+xml'; - case 'txt': return 'text/plain'; - case 'yaml': case 'yml': return 'application/x-yaml'; - case 'md': return 'text/markdown'; - default: return ''; - } -}; - -const Upload: React.FC = ({ artifactId }) => { - const [files, setFiles] = useState([]); - const [selectedFile, setSelectedFile] = useState(null); - const { artifactManager, isLoggedIn, server, user } = useHyphaStore(); - const [uploadStatus, setUploadStatus] = useState(null); - const [imageUrl, setImageUrl] = useState(null); - const [showDragDrop, setShowDragDrop] = useState(!files.length); - const navigate = useNavigate(); - const [isUploading, setIsUploading] = useState(false); - const [testResult, setTestResult] = useState(null); - const [isValidated, setIsValidated] = useState(false); - const [isUploaded, setIsUploaded] = useState(false); - const [uploadedArtifact, setUploadedArtifact] = useState(null); - const [isSidebarOpen, setIsSidebarOpen] = useState(false); - const [generatedId, setGeneratedId] = useState(null); - const [generatedEmoji, setGeneratedEmoji] = useState(null); - const [imageDimensions, setImageDimensions] = useState<{ width: number; height: number } | null>(null); - - useEffect(() => { - if (artifactId) { - loadArtifactFiles(); - } - }, [artifactId]); - - useEffect(() => { - if (files.some(f => f.edited)) { - setIsValidated(false); - setTestResult(null); - } - }, [files]); - - const isTextFile = (filename: string): boolean => { - const textExtensions: SupportedTextFiles[] = [ - '.txt', '.yml', '.yaml', '.json', '.md', '.py', - '.js', '.ts', '.jsx', '.tsx', '.css', '.html', - '.ijm' - ]; - return textExtensions.some(ext => filename.toLowerCase().endsWith(ext)); - }; - - const isImageFile = (filename: string): boolean => { - const imageExtensions: SupportedImageFiles[] = ['.png', '.jpg', '.jpeg', '.gif']; - return imageExtensions.some(ext => filename.toLowerCase().endsWith(ext)); - }; - - const formatFileSize = (bytes: number): string => { - if (bytes === 0) return '0 Bytes'; - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; - }; - - const getImageDataUrl = async (content: string | ArrayBufferLike, fileName: string): Promise => { - if (typeof content === 'string') { - const encoder = new TextEncoder(); - content = encoder.encode(content).buffer; - } - - const extension = fileName.toLowerCase().split('.').pop() || ''; - const bytes = new Uint8Array(content as ArrayBuffer); - const binary = bytes.reduce((data, byte) => data + String.fromCharCode(byte), ''); - const base64 = btoa(binary); - - const mimeType = `image/${extension === 'jpg' ? 'jpeg' : extension}`; - return `data:${mimeType};base64,${base64}`; - }; - - const getEditorLanguage = (filename: string): string => { - const extension = filename.toLowerCase().split('.').pop() || ''; - const languageMap: Record = { - 'py': 'python', - 'js': 'javascript', - 'ts': 'typescript', - 'jsx': 'javascript', - 'tsx': 'typescript', - 'css': 'css', - 'html': 'html', - 'json': 'json', - 'yml': 'yaml', - 'yaml': 'yaml', - 'md': 'markdown', - 'txt': 'plaintext', - 'ijm': 'javascript' - }; - return languageMap[extension] || 'plaintext'; + alias: string; + manifest: { + name?: string; + description?: string; + type?: string; + tags?: string[]; }; + git_url?: string; + created_at: number; + config?: Record; +} - const getCommonPrefix = (nodes: FileNode[]): string => { - if (nodes.length === 0) return ''; - const firstPath = nodes[0].path; - const parts = firstPath.split('/'); - - // If the first file is at root, there is no common directory prefix - if (parts.length === 1) return ''; - - const prefix = parts[0]; - for (let i = 1; i < nodes.length; i++) { - if (!nodes[i].path.startsWith(prefix + '/')) { - return ''; - } - } - return prefix; +const CopyButton: React.FC<{ text: string; label?: string }> = ({ text, label = 'Copy' }) => { + const [copied, setCopied] = useState(false); + const handleCopy = () => { + navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); }; + return ( + + ); +}; - const ensureStaticSiteConfig = async (nodes: FileNode[]) => { - const commonPrefix = getCommonPrefix(nodes); - const expectedIndexPath = commonPrefix ? `${commonPrefix}/index.html` : 'index.html'; - const indexFile = nodes.find(file => file.path === expectedIndexPath); - - if (!indexFile) return nodes; - - let rdfFile = nodes.find(file => file.path.endsWith('rdf.yaml')); - - const relativeHtmlFiles = nodes - .filter(f => f.name.endsWith('.html')) - .map(f => { - let p = f.path; - if (commonPrefix && p.startsWith(commonPrefix + '/')) { - p = p.substring(commonPrefix.length + 1); - } - return p; - }); - - const viewConfig = { - root_directory: '.', - templates: relativeHtmlFiles, - template_engine: 'jinja2', - use_builtin_template: false, - headers: { - "Access-Control-Allow-Origin": "*", - "Cache-Control": "max-age=3600" - } - }; - - if (rdfFile) { - if (!rdfFile.loaded) { - try { - const content = await loadFileContent(rdfFile); - if (!content) return nodes; - } catch (e) { - console.warn("Failed to load rdf.yaml for static config update", e); - return nodes; - } - } - - // Re-find the file object as it might have been updated by loadFileContent (state update is async/detached here) - // Actually loadFileContent updates state but returns content directly. - // But we are operating on 'nodes' array passed in. - const content = rdfFile.content || await loadFileContent(rdfFile); +const CodeBlock: React.FC<{ code: string; onCopy?: () => void }> = ({ code }) => ( +
+
+      {code}
+    
+
+ +
+
+); - if (content) { - try { - const contentStr = typeof content === 'string' - ? content - : new TextDecoder().decode(content); - - const manifest = yaml.load(contentStr) as RdfManifest; - - if (!manifest.config) manifest.config = {}; - - if (!manifest.config.view_config) { - manifest.config.view_config = viewConfig; - - if (!manifest.tags) manifest.tags = []; - if (!manifest.tags.includes('static-site')) manifest.tags.push('static-site'); - - const newContent = yaml.dump(manifest); - - return nodes.map(f => f.path === rdfFile.path ? { - ...f, - content: newContent, - edited: true, - loaded: true - } : f); - } - } catch (e) { - console.warn("Failed to update manifest with static site config", e); - } - } - } else { - const name = commonPrefix || 'New Application'; - - const manifestContent = yaml.dump({ - type: 'application', - name: name, - description: 'Static web application automatically generated.', - tags: ['static-site'], - config: { - view_config: viewConfig - } - }); +interface CreateDialogProps { + onClose: () => void; + onCreate: (name: string, description: string) => Promise; + creating: boolean; +} - const newRdfFile: FileNode = { - name: 'rdf.yaml', - path: commonPrefix ? `${commonPrefix}/rdf.yaml` : 'rdf.yaml', - content: manifestContent, - isDirectory: false, - size: manifestContent.length, - loaded: true, - edited: true - }; +const CreateDialog: React.FC = ({ onClose, onCreate, creating }) => { + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [showCliInfo, setShowCliInfo] = useState(false); - return [...nodes, newRdfFile]; - } - - return nodes; + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!name.trim()) return; + await onCreate(name.trim(), description.trim()); }; - const onDrop = useCallback(async (acceptedFiles: File[]) => { - const isZipFile = acceptedFiles.length === 1 && acceptedFiles[0].name.toLowerCase().endsWith('.zip'); - if (isZipFile) { - await processZipFile(acceptedFiles[0]); - } else { - await processFilesAndFolders(acceptedFiles); - } - }, []); - - const processZipFile = async (zipFile: File) => { - setUploadStatus({ - message: 'Processing zip file...', - severity: 'info', - progress: 0 - }); - - const zip = new JSZip(); - - try { - await new Promise(resolve => setTimeout(resolve, 100)); - - const loadedZip = await zip.loadAsync(zipFile); - const fileNodes: FileNode[] = []; - - const totalFiles = Object.keys(loadedZip.files).length; - let processedFiles = 0; - - setUploadStatus({ - message: 'Reading zip contents...', - severity: 'info', - progress: 5 - }); - - for (const [path, file] of Object.entries(loadedZip.files)) { - if (!file.dir) { - const pathParts = path.split('/'); - const fileName = pathParts[pathParts.length - 1]; - - const fileNode: FileNode = { - name: fileName, - path: path, - isDirectory: false, - size: (file as any)._data ? (file as any)._data.uncompressedSize : 0, - handle: file - }; + // Preview the alias slug + const aliasPreview = name + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .slice(0, 48); - if (fileName === 'rdf.yaml') { - let content = await file.async('string'); - if (user?.email) { - try { - const manifest = yaml.load(content) as RdfManifest; - if (!manifest.uploader?.email) { - manifest.uploader = { ...manifest.uploader, email: user.email }; - content = yaml.dump(manifest); - } - } catch (e) { - console.warn("Failed to inject email into rdf.yaml", e); - } - } - fileNode.content = content; - fileNode.loaded = true; - } + return ( +
+
+

Create New Artifact

+

+ A Git repository will be created for your model. You can push files using Git and Git LFS. +

+
+
+ + setName(e.target.value)} + placeholder="e.g. cellpose-v3-retrained" + className="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#f39200] focus:border-transparent" + required + autoFocus + /> + {aliasPreview && ( +

+ Repository ID: {aliasPreview} +

+ )} +
+
+ +