Open-Generative-AI/components/batch/BatchShell.jsx
Anuragp22 1b12df90ef feat(batch): trainers + studios CRUD with /batch shell UI
Slice 2 of the batch feature. Marketing team can now upload the six
trainer images and the studio image once and reuse them across every
future batch.

Backend
-------
- lib/muapiUpload.js: Node-side wrapper around POST
  /api/v1/upload_file with FormData + x-api-key. Mirrors
  packages/studio/src/muapi.js:131-178 but uses native fetch +
  FormData (no XHR).
- lib/batchAuth.js: shared getApiKey() — header > cookie > env fallback.
- lib/localUploadStore.js: writes a local backup of every uploaded
  asset to /data/uploads/{trainers,studios}/<id>.<ext> on the
  uploads_data volume, so we can re-upload to MuAPI if their CDN
  ever expires the URL.
- app/api/trainers/route.js + [id]/route.js: GET list, POST multipart
  (uploads to MuAPI then persists), DELETE (refuses if any active job
  references the row).
- app/api/studios/route.js + [id]/route.js: identical shape.

UI
--
- app/batch/page.js: leaf route mounting BatchShell.
- components/batch/BatchShell.jsx: 3-tab dark-theme shell
  (Batches / Trainers / Studios) with the same MuAPI key gate as
  StandaloneShell (reuses ApiKeyModal + the muapi_key cookie).
- components/batch/AssetLibrary.jsx + AddAssetModal.jsx: shared grid +
  upload modal driving both tabs from one component.
- components/batch/TrainersTab.jsx, StudiosTab.jsx: thin wrappers.
- components/batch/BatchesTab.jsx: placeholder for slice 3.

Stub packages (upstream submodules unavailable)
-----------------------------------------------
The ai-agent and workflow-ui submodules referenced in .gitmodules
return 404 (Anil-matcha/workflow-ui and jaiprasad04/ai-agent are
deleted/private). next build couldn't resolve their imports.

- Removed the dead .gitmodules entries.
- Added local stubs at packages/ai-agent and packages/workflow-ui
  that export no-op components rendering "feature unavailable" so
  the build succeeds. The /agents/* and Workflows tab show that
  notice; the studios our marketing team actually uses
  (Image / Video / Lip Sync / Cinema) keep working since they
  live in the studio package which we have.

Docker / dev workflow
---------------------
- Dockerfile: run prisma generate during the builder stage, copy
  prisma/ and lib/ into the runner stage so migrations and shared
  helpers are present at runtime.
- docker-compose.override.yml (new): dev-mode overrides — mounts
  source, runs as root to dodge node_modules permission issues with
  the prod nextjs user, runs prisma generate + migrate deploy +
  next dev. Worker container is a placeholder until slice 4 lands.
- The full stack (postgres + web + worker) now boots with
  `docker compose up -d` and serves /batch on http://localhost:3000.
2026-04-23 07:32:49 +05:30

110 lines
3.7 KiB
JavaScript

'use client';
import { useEffect, useState } from 'react';
import ApiKeyModal from '@/components/ApiKeyModal';
import TrainersTab from './TrainersTab';
import StudiosTab from './StudiosTab';
import BatchesTab from './BatchesTab';
const STORAGE_KEY = 'muapi_key';
const TABS = [
{ id: 'batches', label: 'Batches' },
{ id: 'trainers', label: 'Trainers' },
{ id: 'studios', label: 'Studios' },
];
export default function BatchShell() {
const [apiKey, setApiKey] = useState(null);
const [hasMounted, setHasMounted] = useState(false);
const [activeTab, setActiveTab] = useState('batches');
useEffect(() => {
setHasMounted(true);
const stored = typeof window !== 'undefined' ? localStorage.getItem(STORAGE_KEY) : null;
if (stored) {
setApiKey(stored);
document.cookie = `muapi_key=${stored}; path=/; max-age=31536000; SameSite=Lax`;
}
}, []);
const handleKeySave = (key) => {
localStorage.setItem(STORAGE_KEY, key);
document.cookie = `muapi_key=${key}; path=/; max-age=31536000; SameSite=Lax`;
setApiKey(key);
};
const handleKeyChange = () => {
localStorage.removeItem(STORAGE_KEY);
document.cookie = 'muapi_key=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
setApiKey(null);
};
if (!hasMounted) {
return (
<div className="min-h-screen bg-[#050505] flex items-center justify-center">
<div className="animate-spin text-[#d9ff00] text-3xl"></div>
</div>
);
}
if (!apiKey) {
return <ApiKeyModal onSave={handleKeySave} />;
}
return (
<div className="min-h-screen bg-[#030303] text-white flex flex-col">
<header className="flex-shrink-0 h-14 border-b border-white/[0.03] flex items-center justify-between px-6 bg-black/20 backdrop-blur-md">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-white rounded-lg flex items-center justify-center">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="black" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
</div>
<span className="text-sm font-bold tracking-tight">Batch</span>
<span className="text-white/30 text-sm">/ Open Generative AI</span>
</div>
<nav className="flex items-center gap-6">
{TABS.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`relative py-4 text-[13px] font-medium transition-all whitespace-nowrap px-1 ${
activeTab === tab.id ? 'text-[#d9ff00]' : 'text-white/50 hover:text-white'
}`}
>
{tab.label}
{activeTab === tab.id && (
<div className="absolute bottom-0 left-0 right-0 h-[2px] bg-[#d9ff00] rounded-full" />
)}
</button>
))}
</nav>
<div className="flex items-center gap-3">
<a
href="/studio"
className="text-[12px] text-white/50 hover:text-white/80 transition-colors"
>
Studio
</a>
<button
onClick={handleKeyChange}
className="text-[11px] text-white/40 hover:text-red-400 transition-colors"
>
Change key
</button>
</div>
</header>
<main className="flex-1 overflow-y-auto px-6 py-8">
<div className="max-w-6xl mx-auto">
{activeTab === 'batches' && <BatchesTab />}
{activeTab === 'trainers' && <TrainersTab apiKey={apiKey} />}
{activeTab === 'studios' && <StudiosTab apiKey={apiKey} />}
</div>
</main>
</div>
);
}