# Project export: OkaiLoRa.ai

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2025
- Tagline: OKaiLora.ai is the Shopify of ML for healthcare tasks. Our platform offers training, testing, and sharing for state-of-the-art healthcare tasks.
- Devpost: https://devpost.com/software/okailora-ai
- GitHub: https://github.com/xinlei55555/okailora
- Video: https://www.youtube.com/embed/wMk-V3AVUN8?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Qile0317 (61 commits), klokailo (22 commits), Xin Lei Lin (15 commits)

## Devpost submission (written by the team)

### Inspiration

In the medical AI space, clinicians and researchers often sit on high-value datasets but lack the tools and the time to turn them into actionable machine learning models. The current paradigm involves outsourcing to middlemen, which are ML engineers, who are far removed from the data collection and intent. Yet, even when the models are trained, they need to remain proprietary due to patient data privacy issues, which stop medical healthcare professionals from sharing their data, and enabling wide-spread sharing of their training pipeline. We wanted to flip that model: what if any medical professional could train and deploy state-of-the-art Vision Transformer (ViTs) models themselves, in minutes, no code required? What if, instead of sharing data, they could encode their hundreds of GB of data into low-level representation model adapters (LoRa adapters), which could be added for fast-shareable inference?

### What it does

OKaiLoRa.ai is a no-code platform that simplifies healthcare model training, fine-tuning, and model sharing platform which allows medical professionals to: Upload image data for classification, segmentation, generation, or object detection, within our deployment server. Upload image data for classification, segmentation, generation, or object detection, within our deployment server. Select from a curated set of pre-trained models, which encompass the mainstream medical healthcare tasks, such as image classification, image segmentation, bounding box detection and generation. Select from a curated set of pre-trained models, which encompass the mainstream medical healthcare tasks, such as image classification, image segmentation, bounding box detection and generation. Train lightweight LoRa adapters on limited hardware (even with just 6GB VRAM)! Train lightweight LoRa adapters on limited hardware (even with just 6GB VRAM)! Track training metrics in real-time, no code, no setup! Track training metrics in real-time, no code, no setup! Share inference-ready models via secure Tailscale-powered links which point to the LoRa weight and fine-tuned weight checkpoints, keeping patient private data secure. Share inference-ready models via secure Tailscale-powered links which point to the LoRa weight and fine-tuned weight checkpoints, keeping patient private data secure.

### How we built it

Frontend: Built in React, the UI supports drag-and-drop data uploads, dynamic sliders for model configuration, and real-time progress displays for accuracies, loss, and epoch. Backend: Powered by SpringBoot in Kotlin, we provide a robust REST API layer with the following key endpoints: /train/upload_data for zipped datasets. /train/start, /train/progress to manage and monitor training jobs. /inference/start, /inference/weights for remote model access. Training Engine: Uses PyTorch for clean training logic. LoRa adapters enable rapid fine-tuning with minimal GPU memory Hyperparameter tuning: Learning Rate Scheduler: Integrates the ScheduleFree optimizer, from the FAIR lab, which offers scheduler free learning rate optimizers for training pipeline (see NeurIPS 2025 paper: https://arxiv.org/pdf/2405.15682) Batch size is adapted automatically to fit the GPUs. Epoch number is customizable both by the user, or can be left at the default of 50 The classification pipeline supports any number of classes, determined through the files uploaded. Networking: Tailscale makes it easy to securely run inference from a shared link, ideal for labs or remote teams. Data Handling: Supports user-defined deployment_ids to track versions and LoRA adapter checkpoints for each model and dataset.

### Accomplishments we're proud of

End-to-end drag-and-drop model training and inference with zero coding. Fully operational cross-stack integration (React ↔ Spring Boot ↔ PyTorch). Remote inference sharing via Tailscale and LoRA-based portability. Demonstrated rapid overfitting on small datasets to prove model training is functional and correct.

### What we learned

Kotlin + Spring Boot provides a surprisingly clean and scalable backend for ML workflows. LoRa adapters are a game-changer for low-resource training and are so lightweight that we can use them to share fine-tuned models instead of full heavy ViT checkpoints!

### What's next

for OKaiLoRa.ai We are currently hosted on our local servers, and would love to deploy to LoRa model hub: Upload and download LoRa weights like plugins for base models. Vision-language support: Let users describe tasks (e.g. “classify tumor types”) and auto-generate configurations. Research collaboration platform: Encourage ML researchers to submit healthcare models to our template zoo, following our standard.

## README (from the GitHub repository)

# OkaiLoRa
## Inspiration
In the medical AI space, clinicians and researchers often sit on high-value datasets but lack the tools and the time to turn them into actionable machine learning models. 

The current paradigm involves outsourcing to middlemen, which are ML engineers, who have no idea what the medical imaging data represents. Yet, even when the models are trained, they often need to remain proprietary due to data privacy issues, which stop medical healthcare professionals from sharing their data, and enabling wide-spread sharing of their training pipeline.

We wanted to flip that model: what if any medical professional could train and deploy state-of-the-art models themselves, in minutes, no code required?
What if, instead of sharing data, they could encode their hundreds of GB of data into a low-level representations, which could be added to pretrained models for fast-shareable inference?

## What it does
OKaiLoRa.ai is a no-code platform that simplifies healthcare model training, fine-tuning, and model sharing platform which allows medical professionals to:

1. Upload image data for classification, segmentation, generation, or object detection, within our deployment server.

2. Select from a curated set of pre-trained models, which encompass the mainstream medical healthcare tasks, such as image classification, image segmentation, bounding box detection and generation.

3. Train lightweight LoRa adapters on limited hardware (even with just 6GB VRAM)!

4. Track training metrics in real-time, no code, no setup!

5. Share inference-ready models via secure Tailscale-powered links which point to the LoRa weight  and fine-tuned weight checkpoints, keeping patient private data secure.

## How we built it
**Frontend**: Built in React, the UI supports drag-and-drop data uploads, dynamic sliders for model configuration, and real-time progress displays for accuracies, loss, and epoch.

**Backend**: Powered by SpringBoot in Kotlin, we provide a robust REST API layer with the following key endpoints:
/train/upload_data for zipped datasets.
/train/start, /train/progress to manage and monitor training jobs.
/inference/start, /inference/weights for remote model access.

**Training Engine**: Uses PyTorch for clean training logic. LoRa adapters enable rapid fine-tuning with minimal GPU memory

**Hyperparameter tuning**:
- Learning Rate Scheduler: Integrates the ScheduleFree optimizer, from the FAIR lab, which offers scheduler free learning rate optimizers for training pipeline (see NeurIPS 2025 paper: https://arxiv.org/pdf/2405.15682)
- Batch size is adapted automatically to fit the GPUs.
- Epoch number is customizable both by the user, or can be left at the default of 50
- The classification pipeline supports any number of classes, determined through the files uploaded.

**Networking**: Tailscale makes it easy to securely run inference from a shared link, ideal for labs or remote teams.

**Data Handling**: Supports user-defined deployment_ids to track versions and LoRA adapter checkpoints for each model and dataset.

## Accomplishments that we're proud of
End-to-end drag-and-drop model training and inference with zero coding.

Fully operational cross-stack integration (React ↔ Spring Boot ↔ PyTorch).

Remote inference sharing via Tailscale and LoRA-based portability.

Demonstrated rapid overfitting on small datasets to prove model training is functional and correct.

## What we learned
Kotlin + Spring Boot provides a surprisingly clean and scalable backend for ML workflows.

LoRa adapters are a game-changer for low-resource training and are so lightweight that we can use them to share fine-tuned models instead of full heavy ViT checkpoints!

## What's next for OKaiLoRa.ai
We are currently hosted on our local servers, and would love to deploy to 
LoRa model hub: Upload and download LoRa weights like plugins for base models.

Vision-language support: Let users describe tasks (e.g. “classify tumor types”) and auto-generate configurations.

Research collaboration platform: Encourage ML researchers to submit healthcare models to our template zoo, following our standard.



## Detected evidence (automated analysis)

Indexed codebase: 73 recognized source files, 266 KB.
- CSS (language) — detected in the code
- Kotlin (language) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- PyTorch (technology) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (86 of 86)

```
.gitignore
backend/.gitignore
backend/build.gradle.kts
backend/gradle/wrapper/gradle-wrapper.properties
backend/gradlew
backend/gradlew.bat
backend/inference.py
backend/src/main/kotlin/ca/kailo/berkeley/Application.kt
backend/src/main/kotlin/ca/kailo/berkeley/DeploymentRegistry.kt
backend/src/main/kotlin/ca/kailo/berkeley/InferenceRestController.kt
backend/src/main/kotlin/ca/kailo/berkeley/Storage.kt
backend/src/main/kotlin/ca/kailo/berkeley/TrainRestController.kt
backend/src/main/kotlin/ca/kailo/berkeley/WebConfig.kt
backend/src/main/resources/application.properties
backend/src/main/resources/openapi.yml
backend/train.py
frontend/.gitignore
frontend/eslint.config.mjs
frontend/next.config.ts
frontend/notes.txt
frontend/package.json
frontend/postcss.config.mjs
frontend/README.md
frontend/src/app/finetune-loop/[sessionId]/ConfigModal.tsx
frontend/src/app/finetune-loop/[sessionId]/ConfusionMatrix.tsx
frontend/src/app/finetune-loop/[sessionId]/ControlsPanel.tsx
frontend/src/app/finetune-loop/[sessionId]/index.ts
frontend/src/app/finetune-loop/[sessionId]/LogsModal.tsx
frontend/src/app/finetune-loop/[sessionId]/MetricChart.tsx
frontend/src/app/finetune-loop/[sessionId]/page.tsx
frontend/src/app/finetune-loop/[sessionId]/QuickActionsPanel.tsx
frontend/src/app/finetune-loop/[sessionId]/ROCCurve.tsx
frontend/src/app/finetune-loop/[sessionId]/Sidebar.tsx
frontend/src/app/finetune-loop/[sessionId]/SystemResourcesPanel.tsx
frontend/src/app/finetune-loop/[sessionId]/TrainingHeader.tsx
frontend/src/app/finetune-loop/[sessionId]/TrainingProgressPanel.tsx
frontend/src/app/finetune-loop/[sessionId]/types.ts
frontend/src/app/finetune-loop/[sessionId]/utils.ts
frontend/src/app/finetune-results/[sessionId]/page.tsx
frontend/src/app/finetune/[sessionId]/page.tsx
frontend/src/app/globals.css
frontend/src/app/inference-results/[sessionId]/page.tsx
frontend/src/app/inference/[sessionId]/page.tsx
frontend/src/app/layout.tsx
frontend/src/app/page.tsx
frontend/src/components/ChatWidget.tsx
frontend/src/components/shared/FileUpload.tsx
frontend/src/components/shared/FilterDropdown.tsx
frontend/src/components/shared/index.ts
frontend/src/components/shared/ModelSelection.tsx
frontend/src/components/shared/NavigationBar.tsx
frontend/src/components/shared/NotificationBanner.tsx
frontend/src/components/shared/PageLayout.tsx
frontend/src/components/shared/StepSidebar.tsx
frontend/src/components/shared/UploadedFilesList.tsx
frontend/src/utils/models.ts
frontend/src/utils/navigation.ts
frontend/src/utils/types.ts
frontend/tsconfig.json
model_zoo/configs/config.py
model_zoo/configs/yamls/bbox.yaml
model_zoo/configs/yamls/classification.yaml
model_zoo/configs/yamls/generation.yaml
model_zoo/configs/yamls/segmentation.yaml
model_zoo/data/dataloader/__init__.py
model_zoo/data/dataloader/bbox_dataset.py
model_zoo/data/dataloader/classification_dataset.py
model_zoo/data/dataloader/generation_dataset.py
model_zoo/data/dataloader/segmentation_dataset.py
model_zoo/loss/__init__.py
model_zoo/loss/bbox_loss.py
model_zoo/loss/classification_loss.py
model_zoo/loss/generation_loss.py
model_zoo/loss/segmentation_loss.py
model_zoo/models/__init__.py
model_zoo/models/bbox.py
model_zoo/models/classification.py
model_zoo/models/generation.py
model_zoo/models/lora.py
model_zoo/models/segmentation.py
model_zoo/requirements.txt
model_zoo/scripts/classification.sh
model_zoo/train_utils/misc_tools.py
model_zoo/train.py
README.md
run.sh
```

### Dependencies

- frontend/package.json: @eslint/eslintrc@^3, @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, @types/uuid@^10.0.0, eslint@^9, eslint-config-next@15.3.4, next@15.3.4, openapi-typescript-codegen@^0.29.0, react@^19.0.0, react-dom@^19.0.0, recharts@^2.15.4, tailwindcss@^4, typescript@^5, uuid@^11.1.0
- model_zoo/requirements.txt: datasets, einops, loralib, matplotlib, numpy, pandas, peft, Pillow, pyarrow, PyYAML, Requests, schedulefree, scipy, timm, torch, torchvision, tqdm, train, wandb, xtcocotools, yacs

### Recent commits (newest first)

- live auc and confusion updates
- url param for the inference
- Update inference to be sync
- fix inferene
- fix results for inference
- feat: final changes
- Added base64 of images
- call inference result backend
- fixed missing type: object swagger
- Added classifications to /inference/status
- call inference backend
- smaller titles again
- smaller titles
- improve charts
- Merge branch 'main' of https://github.com/xinlei55555/okailora
- Added /train/elaborate
- fix button
- Added logging for inference script
- make scrollable monitoring
- Merge branch 'main' of https://github.com/xinlei55555/okailora

## Key source files (fetched from GitHub, selected and truncated for size)

### model_zoo/requirements.txt

```
datasets #==3.0.0
einops #==0.8.0
loralib #==0.1.2
matplotlib #==3.9.2
numpy #==2.1.1
pandas #==2.2.2
peft
Pillow #==10.4.0
pyarrow #==17.0.0
PyYAML #==6.0.2
Requests #==2.32.3
schedulefree #==1.2.7
scipy #==1.14.1
timm #==1.0.9
torch #==2.4.1
torchvision #==0.19.1
tqdm #==4.66.5
train #==0.0.5
wandb #==0.18.0
xtcocotools #==1.14.3
yacs
```

### frontend/package.json

```
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev --turbopack",
    "prebuild": "npm run swagger-generate",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "swagger-generate": "npx openapi-typescript-codegen --input ../backend/src/main/resources/openapi.yml --output ./src/api --client fetch"
  },
  "dependencies": {
    "@types/uuid": "^10.0.0",
    "next": "15.3.4",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "recharts": "^2.15.4",
    "uuid": "^11.1.0"
  },
  "devDependencies": {
    "@eslint/eslintrc": "^3",
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "15.3.4",
    "openapi-typescript-codegen": "^0.29.0",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

```

### frontend/src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ChatProvider } from "@/components/ChatWidget";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "OkaiLora",
  description: "AI training and inference simplified",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <ChatProvider>
          <div className="flex h-screen">
            <div className="flex-1 overflow-auto">
              {children}
            </div>
          </div>
        </ChatProvider>
      </body>
    </html>
  );
}

```

### frontend/src/components/shared/index.ts

```typescript
export { default as ModelSelection } from './ModelSelection';
export { default as FileUpload } from './FileUpload';
export { default as NavigationBar } from './NavigationBar';
export { default as NotificationBanner } from './NotificationBanner';
export { default as FilterDropdown } from './FilterDropdown';
export { default as UploadedFilesList } from './UploadedFilesList';
export { default as StepSidebar } from './StepSidebar';
export { default as PageLayout } from './PageLayout';

export type { NavigationStep } from './NavigationBar';
export type { SidebarStep } from './StepSidebar';

```

### frontend/src/app/page.tsx

```typescript
"use client";

import Image from "next/image";
import { useChatContext } from "@/components/ChatWidget";
import { useState, useEffect, useRef } from "react";
import { useRouter } from "next/navigation";
import { handleFineTuneClick, handleTrainClick, handleInferenceClick, handleShareClick } from "@/utils/navigation";

// Mock data - replace with real data later
const recentProjects = [
  {
    id: "1",
    name: "Radiology Report Classifier",
    status: "completed",
    type: "training",
    progress: 100,
    startTime: "2 hours ago",
    duration: "45 min",
    privacy: "PHI Compliant",
    dataSource: "De-identified X-rays",
    accuracy: "94.2%",
    trainedDate: "Dec 15, 2024",
    modelType: "Medical Imaging",
    compliance: "FDA Pre-submission",
  },
  {
    id: "2",
    name: "Drug Interaction Predictor",
    status: "running",
    type: "training",
    progress: 65,
    startTime: "30 min ago",
    duration: "~20 min remaining",
    privacy: "HIPAA Secure",
    dataSource: "Anonymized EHR Data",
    accuracy: "91.8%",
    trainedDate: "Dec 10, 2024",
    modelType: "Clinical Decision Support",
    compliance: "HIPAA Validated",
  },
  {
    id: "3",
    name: "Symptom Checker Assistant",
    status: "completed",
    type: "inference",
    progress: 100,
    startTime: "1 day ago",
    duration: "2 sec",
    privacy: "Local Processing",
    dataSource: "Clinical Notes",
    accuracy: "88.5%",
    trainedDate: "Dec 5, 2024",
    modelType: "NLP - Clinical",
    compliance: "IRB Approved",
  },
  {
    id: "4",
    name: "Chest X-Ray Pneumonia Detector",
    status: "completed",
    type: "model",
    progress: 100,
    startTime: "3 days ago",
    duration: "2 hours",
    privacy: "PHI Compliant",
    dataSource: "Medical Images",
    accuracy: "96.1%",
    trainedDate: "Dec 12, 2024",
    modelType: "Medical Imaging",
    compliance: "FDA Ready",
  },
  {
    id: "5",
    name: "Clinical Note Summarizer",
    status: "fine-tuning",
    type: "fine-tune",
    progress: 40,
    startTime: "1 hour ago",
    duration: "~45 min remaining",
    privacy: "De-identified",
    dataSource: "Patient Records",
    accuracy: "89.3%",
    trainedDate: "Dec 8, 2024",
    modelType: "NLP - Clinical",
    compliance: "HIPAA Validated",
  },
];

export default function Home() {
  const { isChatOpen } = useChatContext();
  const router = useRouter();
  const [showNotifications, setShowNotifications] = useState(false);
  const [showProfileMenu, setShowProfileMenu] = useState(false);
  const notificationsRef = useRef<HTMLDivElement>(null);
  const profileRef = useRef<HTMLDivElement>(null);

  // Close dropdowns when clicking outside
  useEffect(() => {
    function handleClickOutside(event: MouseEvent) {
      if (notificationsRef.current && !notificationsRef.current.contains(event.target as Node)) {
        setShowNotifications(false);
      }
      if (profileRef.current && !profileRef.current.contains(event.target as Node)) {
        setShowProfileMenu(false);
      }
    }

    document.addEventListener('mousedown', handleClickOutside);
    return () => {
      document.removeEventListener('mousedown', handleClickOutside);
    };
  }, []);
  
  return (
    <div className="min-h-screen bg-gray-950 text-white">
      {/* Top Navigation Bar */}
      <nav className="border-b border-gray-800 bg-gray-900/50 backdrop-blur-sm">
        <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
          <div className="flex justify-between items-center h-16">
            {/* Logo */}
            <div className="flex items-center">
              <div className="text-xl font-bold text-blue-400">Okailora</div>
            </div>
            
            {/* Search Bar */}
            <div className="flex-1 max-w-lg mx-8">
              <div className="relative">
                <input
                  type="text"
                  placeholder="Search models, datasets, runs..."
                  className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2 pl-10 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
                />
                <div className="absolute inset-y-0 left-0 pl-3 flex items-center">
                  <svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
                  </svg>
                </div>
              </div>
            </div>

            {/* User Menu */}
            <div className="flex items-center space-x-4">
              {/* Notifications */}
              <div className="relative" ref={notificationsRef}>
                <button 
                  className="relative p-2 text-gray-400 hover:text-white transition-colors hover:bg-gray-800 rounded-lg"
                  onClick={() => setShowNotifications(!showNotifications)}
                  title="Notifications"
                >
                  <svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-5 5v-5zM10.5 17H3a1 1 0 01-1-1V4a1 1 0 011-1h7.5M17 7v10" />
                  </svg>
                  <span className="absolute top-0 right-0 block h-2 w-2 rounded-full bg-red-400"></span>
                </button>
                
                {/* Notifications Dropdown */}
                {showNotifications && (
                  <div className="fixed right-4 top-16 w-80 bg-gray-800 border border-gray-700 rounded-lg shadow-xl z-[9999]">
                    <div className="p-4 border-b border-gray-700">
                      <div className="flex items-center justify-between">
                        <h3 className="text-white font-semibold">Notifications</h3>
                        <button className="text-sm text-blue-400 hover:text-blue-300">Mark all as read</button>

[truncated — 17420 more characters]
```

### frontend/src/app/finetune-loop/[sessionId]/index.ts

```typescript
export { default as TrainingHeader } from './TrainingHeader';
export { default as Sidebar } from './Sidebar';
export { default as ControlsPanel } from './ControlsPanel';
export { default as MetricChart, PlaceholderChart } from './MetricChart';
export { default as LogsModal } from './LogsModal';
export { default as ConfigModal } from './ConfigModal';
export { default as TrainingProgressPanel } from './TrainingProgressPanel';
export { default as SystemResourcesPanel } from './SystemResourcesPanel';
export { default as QuickActionsPanel } from './QuickActionsPanel';
export * from './types';
export * from './utils';

```

### frontend/src/app/inference-results/[sessionId]/page.tsx

```typescript
"use client";

import { useEffect, useState } from 'react';
import { useParams, useRouter, useSearchParams } from 'next/navigation';
import { InferenceService } from '@/api/services/InferenceService';

interface BackendResult {
  image?: string; // image name
  classification?: string;
  base64?: string; // base64 image data
}

export default function InferenceResultsPage() {
  const params = useParams();
  const router = useRouter();
  const searchParams = useSearchParams();
  const sessionId = params.sessionId as string; // This is the workflow session ID from the URL
  
  // Get the deployment ID from URL parameters
  const [deploymentId, setDeploymentId] = useState<string | null>(null);
  const [results, setResults] = useState<BackendResult[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [selectedTab, setSelectedTab] = useState<'gallery' | 'analytics' | 'export'>('gallery');

  useEffect(() => {
    // Get deployment ID from URL parameters
    const urlDeploymentId = searchParams.get('deploymentId');
    
    if (!urlDeploymentId) {
      console.error('[InferenceResults] No deployment ID found in URL parameters for session:', sessionId);
      setError('No deployment ID found. Please start inference again.');
      setLoading(false);
      return;
    }
    
    // Decode the deployment ID in case it was URL encoded
    const decodedDeploymentId = decodeURIComponent(urlDeploymentId);
    setDeploymentId(decodedDeploymentId);
    
    console.log(`[InferenceResults] Starting to fetch results for deployment: ${decodedDeploymentId}`);
    setLoading(true);
    setError(null);
    
    InferenceService.inferenceStatus(decodedDeploymentId)
      .then((res) => {
        console.log('[InferenceResults] Raw API response:', res);
        console.log('[InferenceResults] Response type:', typeof res);
        console.log('[InferenceResults] Response structure:', Object.keys(res || {}));
        
        if (res && Array.isArray(res.result)) {
          console.log(`[InferenceResults] Found ${res.result.length} results in response`);
          console.log('[InferenceResults] First few results:', res.result.slice(0, 3));
          
          // Log details about each result
          res.result.forEach((result, index) => {
            console.log(`[InferenceResults] Result ${index}:`, {
              hasImage: !!result.image,
              imageName: result.image,
              hasClassification: !!result.classification,
              classification: result.classification,
              hasBase64: !!result.base64,
              base64Length: result.base64 ? result.base64.length : 0
            });
          });
          
          setResults(res.result);
        } else {
          console.warn('[InferenceResults] Response does not contain valid result array:', {
            hasRes: !!res,
            hasResult: !!(res && res.result),
            resultType: res && res.result ? typeof res.result : 'undefined',
            isArray: res && res.result ? Array.isArray(res.result) : false
          });
          setResults([]);
        }
        setLoading(false);
        console.log('[InferenceResults] Successfully completed fetching results');
      })
      .catch((e) => {
        console.error('[InferenceResults] Error fetching results:', e);
        console.error('[InferenceResults] Error details:', {
          message: e.message,
          stack: e.stack,
          name: e.name
        });
        setError('Failed to fetch results.');
        setLoading(false);
      });
  }, [sessionId, searchParams]); // Updated dependencies

  // Gallery Tab: Scrollable grid of image results
  const renderGalleryTab = () => (
    <div className="space-y-6">
      {loading && (
        <div className="flex justify-center items-center h-40">
          <span className="text-gray-400 text-lg">Loading...</span>
        </div>
      )}
      {error && (
        <div className="flex justify-center items-center h-40">
          <span className="text-red-400 text-lg">{error}</span>
        </div>
      )}
      {!loading && !error && (
        <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8">
          {results.map((item, idx) => (
            <div key={idx} className="bg-gray-900 rounded-xl shadow-lg flex flex-col items-center p-4 border border-gray-800 hover:shadow-2xl transition-shadow">
              {item.base64 ? (
                <img
                  src={`data:image/jpeg;base64,${item.base64}`}
                  alt={item.image ? item.image : `result-${idx}`}
                  className="w-48 h-48 object-cover rounded-lg mb-4 border border-gray-700"
                />
              ) : (
                <div className="w-48 h-48 flex items-center justify-center bg-gray-800 rounded-lg mb-4 text-gray-500">
                  No Image
                </div>
              )}
              <div className="text-lg font-semibold text-center truncate w-full">
                {item.classification || <span className="text-gray-500">No label</span>}
              </div>
              <div className="text-sm text-gray-400 mt-1 truncate w-full text-center">
                {item.image || <span className="text-gray-600">No image name</span>}
              </div>
            </div>
          ))}
          {results.length === 0 && (
            <div className="col-span-full text-center text-gray-400">No results to display.</div>
          )}
        </div>
      )}
    </div>
  );

  // Analytics Tab: Placeholder for future analytics
  const renderAnalyticsTab = () => (
    <div className="flex flex-col items-center justify-center h-64 text-gray-400">
      <span className="text-2xl mb-2">📈</span>
      <span>Analytics and charts coming soon.</span>
    </div>
  );

  // Export Tab: Placeholder for future export options
  const renderExportTab = () => (
    <div className="flex flex-col items-center justify-center
[truncated — 4248 more characters]
```

### frontend/src/app/finetune-loop/[sessionId]/page.tsx

```typescript
"use client";

import { useState, useEffect, useMemo } from 'react';
import { useParams } from 'next/navigation';
import { useChatContext } from '@/components/ChatWidget';
import { TrainService } from '@/api';
import { MetricPoint, TrainingStatus, SystemResources, ChartDataset } from './types';
import { formatTime, formatNumber } from './utils';
import TrainingHeader from './TrainingHeader';
import Sidebar from './Sidebar';
import MetricChart from './MetricChart';
import ConfusionMatrix from './ConfusionMatrix';
import ROCCurve from './ROCCurve';
import LogsModal from './LogsModal';
import ConfigModal from './ConfigModal';

// --- Add types for ROC and ConfusionMatrix props ---
interface ROCPoint {
  fpr: number;
  tpr: number;
}

interface ConfusionMatrixProps {
  confusionData: number[][];
  labels: string[];
}

interface ROCCurveProps {
  rocData: ROCPoint[];
  auc: number;
}

export default function FinetuneLoopPage() {
  const params = useParams();
  const { isChatOpen, openChatWithMessage } = useChatContext();
  const sessionId = params.sessionId as string;
  
  // Training status
  const [trainingStatus, setTrainingStatus] = useState<TrainingStatus>({
    isRunning: true,
    currentEpoch: 1,
    totalEpochs: 3,
    currentStep: 0,
    totalSteps: 120, // Assuming total steps, adjust if available from backend
    startTime: Date.now(),
    elapsedTime: 0,
    estimatedTimeRemaining: 0,
    learningRate: 0.00001
  });

  // Metrics data
  const [trainLossData, setTrainLossData] = useState<MetricPoint[]>([]);
  const [valLossData, setValLossData] = useState<MetricPoint[]>([]);
  const [trainAccData, setTrainAccData] = useState<MetricPoint[]>([]);
  const [valAccData, setValAccData] = useState<MetricPoint[]>([]);

  // UI state
  const [showConfig, setShowConfig] = useState(false);
  const [showLogs, setShowLogs] = useState(false);
  const [logMessages, setLogMessages] = useState<string[]>([]);

  // Training completed state
  const [isCompleted, setIsCompleted] = useState(false);

  // System resources state
  const [systemResources, setSystemResources] = useState({
    gpuMemory: 14.2,
    cpuUsage: 67,
    diskIO: 24,
    networkIO: 12
  });

  // Fetch real-time data from the backend
  useEffect(() => {
    if (!sessionId || !trainingStatus.isRunning) return;

    const intervalId = setInterval(async () => {
      try {
        const status = await TrainService.trainStatus(sessionId);

        const trainLoss = status.train_loss || [];
        const valLoss = status.val_loss || [];
        const trainAcc = status.train_acc || [];
        const valAcc = status.val_acc || [];

        setTrainLossData(trainLoss.map((value, index) => ({
            step: index + 1, value, epoch: 0, timestamp: Date.now()
        })));
        setValLossData(valLoss.map((value, index) => ({
            step: index + 1, value, epoch: 0, timestamp: Date.now()
        })));
        setTrainAccData(trainAcc.map((value, index) => ({
            step: index + 1, value, epoch: 0, timestamp: Date.now()
        })));
        setValAccData(valAcc.map((value, index) => ({
            step: index + 1, value, epoch: 0, timestamp: Date.now()
        })));

        const currentStep = trainLoss.length;
        const elapsedTime = Date.now() - trainingStatus.startTime;
        let estimatedTimeRemaining = 0;

        if (currentStep > 0 && trainingStatus.totalSteps > 0) {
            const totalTimeEstimate = (elapsedTime / currentStep) * trainingStatus.totalSteps;
            estimatedTimeRemaining = Math.max(0, totalTimeEstimate - elapsedTime);
        }

        setTrainingStatus(prev => ({
            ...prev,
            currentStep: currentStep,
            currentEpoch: prev.totalSteps > 0 && prev.totalEpochs > 0 ? Math.min(prev.totalEpochs, Math.floor(currentStep / (prev.totalSteps / prev.totalEpochs)) + 1) : 1,
            elapsedTime: elapsedTime,
            estimatedTimeRemaining: estimatedTimeRemaining
        }));

        if (status.finished) {
          setTrainingStatus(prev => ({ ...prev, isRunning: false, estimatedTimeRemaining: 0 }));
          setIsCompleted(true);
          setLogMessages(prev => [...prev, "Training completed successfully! 🎉"]);
          clearInterval(intervalId);
        }
      } catch (error) {
        console.error("Failed to fetch training status:", error);
        setTrainingStatus(prev => ({ ...prev, isRunning: false }));
        clearInterval(intervalId);
      }
    }, 1000);

    return () => clearInterval(intervalId);
  }, [sessionId, trainingStatus.isRunning, trainingStatus.startTime, trainingStatus.totalSteps, trainingStatus.totalEpochs]);

  // System resource fluctuation effect
  useEffect(() => {
    if (!trainingStatus.isRunning) return;

    const resourceInterval = setInterval(() => {
      setSystemResources(prev => ({
        gpuMemory: Math.max(12.0, Math.min(15.8, prev.gpuMemory + (Math.random() - 0.5) * 0.4)),
        cpuUsage: Math.max(45, Math.min(85, prev.cpuUsage + (Math.random() - 0.5) * 8)),
        diskIO: Math.max(5, Math.min(65, prev.diskIO + (Math.random() - 0.5) * 15)),
        networkIO: Math.max(2, Math.min(35, prev.networkIO + (Math.random() - 0.5) * 8))
      }));
    }, 2000);

    return () => clearInterval(resourceInterval);
  }, [trainingStatus.isRunning]);

  const getProgressPercentage = () => {
    return (trainingStatus.currentStep / trainingStatus.totalSteps) * 100;
  };

  const getCurrentEpoch = () => {
    // Infer epoch from training data length and steps per epoch
    const currentStep = trainLossData.length;
    const stepsPerEpoch = Math.floor(trainingStatus.totalSteps / trainingStatus.totalEpochs);
    return Math.min(trainingStatus.totalEpochs, Math.floor(currentStep / stepsPerEpoch) + 1);
  };

  const getCurrentLoss = () => {
    return trainLossData.length > 0 ? trainLossData[trainLossData.length - 1].value : 0;
  };

  const getBestAccuracy = () => {
    return valAccData.length > 0 ? Math.max(...valAccData.map(d => 
[truncated — 4474 more characters]
```

### frontend/src/app/inference/[sessionId]/page.tsx

```typescript
"use client";

import { useState, useRef } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { useChatContext } from '@/components/ChatWidget';
import { InferenceService } from '@/api/services/InferenceService';
import { UploadedFile } from '@/utils/types';
import { allModels } from '@/utils/models';
import {
	ModelSelection,
	FileUpload,
	NavigationBar,
	NotificationBanner,
	PageLayout,
	NavigationStep,
	SidebarStep
} from '@/components/shared';

export default function InferencePage() {
	const params = useParams();
	const router = useRouter();
	const { isChatOpen, openChatWithMessage } = useChatContext();
	const sessionId = params.sessionId as string;
	
	// State management
	const [selectedModel, setSelectedModel] = useState<string>('');
	const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
	const [dragActive, setDragActive] = useState(false);
	const [currentStep, setCurrentStep] = useState(1);
	
	const fileInputRef = useRef<HTMLInputElement>(null);
	const dropRef = useRef<HTMLDivElement>(null);

	// Navigation and steps configuration
	const steps: SidebarStep[] = [
		{ step: 1, title: "Select Model", icon: "🤖" },
		{ step: 2, title: "Upload Data", icon: "📁" },
		{ step: 3, title: "Review & Start", icon: "🔮" },
	];

	// Handle file upload (just store locally, don't upload to backend yet)
	const handleFileUpload = async (files: FileList) => {
		Array.from(files).forEach((file) => {
			// Check if file is a zip file
			if (!file.name.toLowerCase().endsWith('.zip') && file.type !== 'application/zip' && file.type !== 'application/x-zip-compressed') {
				alert(`File "${file.name}" is not a ZIP file. Please upload ZIP files only.`);
				return;
			}

			const fileId = Math.random().toString(36).substr(2, 9);
			const uploadedFile: UploadedFile = {
				id: fileId,
				name: file.name,
				size: file.size,
				type: file.type,
				lastModified: file.lastModified,
				status: 'ready',
				progress: 0,
				file: file // Store the actual file object
			};

			setUploadedFiles(prev => [...prev, uploadedFile]);
		});
	};

	// Upload files to backend when transitioning to review step
	const uploadFilesToBackend = async () => {
		const filesToUpload = uploadedFiles.filter(f => f.status === 'ready');
		
		if (filesToUpload.length === 0) return;

		// Get the deployment ID for the selected model
		const deploymentId = await getDeploymentId(selectedModel);
		
		if (!deploymentId) {
			console.error('Could not find deployment ID for selected model:', selectedModel);
			alert('Failed to upload files: Model deployment not found. Please try selecting a different model.');
			return;
		}

		console.log(`Starting upload of ${filesToUpload.length} files to backend using deployment ID: ${deploymentId}...`);

		for (const fileData of filesToUpload) {
			if (!fileData.file) continue;

			console.log(`Uploading file: ${fileData.name} (${formatFileSize(fileData.size)})`);

			// Update status to uploading
			setUploadedFiles(prev => 
				prev.map(f => f.id === fileData.id ? { ...f, status: 'uploading', progress: 0 } : f)
			);

			try {
				// Simulate progress for better UX
				const progressInterval = setInterval(() => {
					setUploadedFiles(prev => 
						prev.map(f => {
							if (f.id === fileData.id && f.progress < 90) {
								return { ...f, progress: f.progress + 10 };
							}
							return f;
						})
					);
				}, 100);

				// Create a FormData with the file for the API call
				const formData = {
					file: fileData.file
				};

				// Call the actual API with deployment ID
				await InferenceService.inferenceUploadData(deploymentId, formData);

				// Clear progress interval and update status to completed
				clearInterval(progressInterval);
				console.log(`✅ Successfully uploaded: ${fileData.name}`);
				setUploadedFiles(prev => 
					prev.map(f => f.id === fileData.id ? { ...f, status: 'completed', progress: 100 } : f)
				);
			} catch (error) {
				console.error(`❌ Upload failed for ${fileData.name}:`, error);
				// Update status to error on failure
				setUploadedFiles(prev => 
					prev.map(f => f.id === fileData.id ? { ...f, status: 'error', progress: 0 } : f)
				);
			}
		}
		
		console.log('🎉 All file uploads completed!');
	};

	// Drag and drop handlers
	const handleDragEnter = (e: React.DragEvent) => {
		e.preventDefault();
		e.stopPropagation();
		setDragActive(true);
	};

	const handleDragLeave = (e: React.DragEvent) => {
		e.preventDefault();
		e.stopPropagation();
		setDragActive(false);
	};

	const handleDragOver = (e: React.DragEvent) => {
		e.preventDefault();
		e.stopPropagation();
	};

	const handleDrop = (e: React.DragEvent) => {
		e.preventDefault();
		e.stopPropagation();
		setDragActive(false);

		const files = e.dataTransfer.files;
		if (files && files.length > 0) {
			handleFileUpload(files);
		}
	};

	const formatFileSize = (bytes: number) => {
		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 removeFile = (fileId: string) => {
		setUploadedFiles(prev => prev.filter(f => f.id !== fileId));
	};

	// Function to determine model type based on selected model
	const getModelType = (modelId: string): 'classification' | 'segmentation' | 'generation' | 'bbox' => {
		const model = allModels.find(m => m.id === modelId);
		if (!model) return 'generation'; // default fallback
		
		// Map based on model tags and name
		if (model.tags.includes('bert') || model.tags.includes('classification') || model.name.toLowerCase().includes('clinical')) {
			return 'classification';
		}
		if (model.tags.includes('segmentation') || model.name.toLowerCase().includes('segment')) {
			return 'segmentation';
		}
		if (model.tags.includes('bbox') || model.name.toLowerCase().includes('detection')) {
			return 'bbox';
		}
		// Default to generation for GPT-like mod
[truncated — 9635 more characters]
```

### frontend/src/app/finetune/[sessionId]/page.tsx

```typescript
"use client";

import { useState, useRef } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { useChatContext } from '@/components/ChatWidget';
import { TrainService } from '@/api/services/TrainService';
import { UploadedFile } from '@/utils/types';
import { allModels } from '@/utils/models';
import {
	ModelSelection,
	FileUpload,
	NavigationBar,
	NotificationBanner,
	PageLayout,
	NavigationStep,
	SidebarStep
} from '@/components/shared';

export default function FinetunePage() {
	const params = useParams();
	const router = useRouter();
	const { isChatOpen, openChatWithMessage } = useChatContext();
	const sessionId = params.sessionId as string;
	
	// State management
	const [selectedModel, setSelectedModel] = useState<string>('');
	const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
	const [dragActive, setDragActive] = useState(false);
	const [currentStep, setCurrentStep] = useState(1);
	
	const fileInputRef = useRef<HTMLInputElement>(null);
	const dropRef = useRef<HTMLDivElement>(null);

	// Navigation and steps configuration
	const steps: SidebarStep[] = [
		{ step: 1, title: "Select Base Model", icon: "🤖" },
		{ step: 2, title: "Upload Training Data", icon: "📁" },
		{ step: 3, title: "Configure Parameters", icon: "⚙️" },
		{ step: 4, title: "Review & Start", icon: "🚀" },
	];

	// Handle file upload (just store locally, don't upload to backend yet)
	const handleFileUpload = async (files: FileList) => {
		Array.from(files).forEach((file) => {
			// Check if file is a zip file
			if (!file.name.toLowerCase().endsWith('.zip') && file.type !== 'application/zip' && file.type !== 'application/x-zip-compressed') {
				alert(`File "${file.name}" is not a ZIP file. Please upload ZIP files only.`);
				return;
			}

			const fileId = Math.random().toString(36).substr(2, 9);
			const uploadedFile: UploadedFile = {
				id: fileId,
				name: file.name,
				size: file.size,
				type: file.type,
				lastModified: file.lastModified,
				status: 'ready',
				progress: 0,
				file: file // Store the actual file object
			};

			setUploadedFiles(prev => [...prev, uploadedFile]);
		});
	};

	// Upload files to backend when transitioning to parameters step
	const uploadFilesToBackend = async () => {
		const filesToUpload = uploadedFiles.filter(f => f.status === 'ready');
		
		if (filesToUpload.length === 0) return;

		console.log(`Starting upload of ${filesToUpload.length} files to backend...`);

		for (const fileData of filesToUpload) {
			if (!fileData.file) continue;

			console.log(`Uploading file: ${fileData.name} (${formatFileSize(fileData.size)})`);

			// Update status to uploading
			setUploadedFiles(prev => 
				prev.map(f => f.id === fileData.id ? { ...f, status: 'uploading', progress: 0 } : f)
			);

			try {
				// Simulate progress for better UX
				const progressInterval = setInterval(() => {
					setUploadedFiles(prev => 
						prev.map(f => {
							if (f.id === fileData.id && f.progress < 90) {
								return { ...f, progress: f.progress + 10 };
							}
							return f;
						})
					);
				}, 100);

				// Create a FormData with the file for the API call
				const formData = {
					file: fileData.file
				};

				// Call the actual API
				await TrainService.trainUploadData(sessionId, formData);

				// Clear progress interval and update status to completed
				clearInterval(progressInterval);
				console.log(`✅ Successfully uploaded: ${fileData.name}`);
				setUploadedFiles(prev => 
					prev.map(f => f.id === fileData.id ? { ...f, status: 'completed', progress: 100 } : f)
				);
			} catch (error) {
				console.error(`❌ Upload failed for ${fileData.name}:`, error);
				// Update status to error on failure
				setUploadedFiles(prev => 
					prev.map(f => f.id === fileData.id ? { ...f, status: 'error', progress: 0 } : f)
				);
			}
		}
		
		console.log('🎉 All file uploads completed!');
	};

	// Drag and drop handlers
	const handleDragEnter = (e: React.DragEvent) => {
		e.preventDefault();
		e.stopPropagation();
		setDragActive(true);
	};

	const handleDragLeave = (e: React.DragEvent) => {
		e.preventDefault();
		e.stopPropagation();
		setDragActive(false);
	};

	const handleDragOver = (e: React.DragEvent) => {
		e.preventDefault();
		e.stopPropagation();
	};

	const handleDrop = (e: React.DragEvent) => {
		e.preventDefault();
		e.stopPropagation();
		setDragActive(false);

		const files = e.dataTransfer.files;
		if (files && files.length > 0) {
			handleFileUpload(files);
		}
	};

	const formatFileSize = (bytes: number) => {
		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 removeFile = (fileId: string) => {
		setUploadedFiles(prev => prev.filter(f => f.id !== fileId));
	};

	// Function to determine model type based on selected model
	const getModelType = (modelId: string): 'classification' | 'segmentation' | 'generation' | 'bbox' => {
		const model = allModels.find(m => m.id === modelId);
		if (!model) return 'generation'; // default fallback
		
		// Map based on model tags and name
		if (model.tags.includes('bert') || model.tags.includes('classification') || model.name.toLowerCase().includes('clinical')) {
			return 'classification';
		}
		if (model.tags.includes('segmentation') || model.name.toLowerCase().includes('segment')) {
			return 'segmentation';
		}
		if (model.tags.includes('bbox') || model.name.toLowerCase().includes('detection')) {
			return 'bbox';
		}
		// Default to generation for GPT-like models and others
		return 'generation';
	};

	// Function to start training
	const handleStartTraining = async () => {
		if (!selectedModel) {
			console.error('No model selected');
			return;
		}

		try {
			console.log('Starting training process...');
			
			const modelType = getModelType(selectedModel);
			console.log(`Model type determined
[truncated — 12803 more characters]
```

[58 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]