Open-Generative-AI/components/batch/AddAssetModal.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

131 lines
5 KiB
JavaScript

'use client';
import { useState } from 'react';
export default function AddAssetModal({ apiKey, endpoint, label, onClose, onCreated }) {
const [name, setName] = useState('');
const [csvLabel, setCsvLabel] = useState('');
const [file, setFile] = useState(null);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null);
const previewUrl = file ? URL.createObjectURL(file) : null;
const handleSubmit = async (e) => {
e.preventDefault();
if (!name.trim() || !file) {
setError('Name and image are required.');
return;
}
setError(null);
setSubmitting(true);
try {
const form = new FormData();
form.append('name', name.trim());
if (csvLabel.trim()) form.append('csvLabel', csvLabel.trim());
form.append('image', file);
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'x-api-key': apiKey },
body: form,
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || `Upload failed: ${res.status}`);
}
await onCreated();
} catch (err) {
setError(err.message);
} finally {
setSubmitting(false);
}
};
return (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 animate-fade-in-up">
<div className="bg-[#0a0a0a] border border-white/10 rounded-xl p-8 w-full max-w-md shadow-2xl">
<h2 className="text-white font-bold text-lg mb-1">Add {label.toLowerCase()}</h2>
<p className="text-white/40 text-[13px] mb-6">
Uploads to MuAPI once and saves a local backup. Reused across every batch.
</p>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-[11px] font-bold text-white/40 uppercase tracking-wide mb-1.5">
Name
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={label === 'Trainer' ? 'e.g. Raj' : 'e.g. Ahoum studio'}
className="w-full bg-white/5 border border-white/[0.03] rounded-md px-4 py-2 text-white placeholder-white/20 focus:outline-none focus:ring-1 focus:ring-[#d9ff00]/30"
required
/>
</div>
<div>
<label className="block text-[11px] font-bold text-white/40 uppercase tracking-wide mb-1.5">
CSV label (optional)
</label>
<input
type="text"
value={csvLabel}
onChange={(e) => setCsvLabel(e.target.value)}
placeholder={label === 'Trainer' ? 'e.g. Trainer 1' : 'e.g. Studio 1'}
className="w-full bg-white/5 border border-white/[0.03] rounded-md px-4 py-2 text-white placeholder-white/20 focus:outline-none focus:ring-1 focus:ring-[#d9ff00]/30"
/>
<p className="text-[11px] text-white/30 mt-1">
Used to auto-match CSV rows. Set once; CSV column value "{label} 1" maps to this {label.toLowerCase()}.
</p>
</div>
<div>
<label className="block text-[11px] font-bold text-white/40 uppercase tracking-wide mb-1.5">
Image
</label>
<input
type="file"
accept="image/*"
onChange={(e) => setFile(e.target.files?.[0] || null)}
className="w-full text-[13px] text-white/70 file:mr-3 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:bg-white/10 file:text-white/80 file:text-[12px] file:cursor-pointer hover:file:bg-white/20"
required
/>
{previewUrl && (
<div className="mt-3 rounded-md overflow-hidden bg-black/40 aspect-square max-w-[160px]">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={previewUrl} alt="preview" className="w-full h-full object-cover" />
</div>
)}
</div>
{error && (
<div className="bg-red-500/10 border border-red-500/30 text-red-300 text-[13px] rounded-md px-3 py-2">
{error}
</div>
)}
<div className="flex gap-3 pt-2">
<button
type="button"
onClick={onClose}
disabled={submitting}
className="flex-1 h-10 rounded-md bg-white/5 text-white/80 hover:bg-white/10 text-xs font-semibold border border-white/5 transition-all disabled:opacity-50"
>
Cancel
</button>
<button
type="submit"
disabled={submitting}
className="flex-1 h-10 rounded-md bg-[#d9ff00] text-black hover:bg-[#e5ff33] text-xs font-semibold transition-all disabled:opacity-50"
>
{submitting ? 'Uploading…' : 'Upload'}
</button>
</div>
</form>
</div>
</div>
);
}