mirror of
https://github.com/Anil-matcha/Open-Generative-AI.git
synced 2026-05-07 01:17:18 +00:00
Slice 3 of the batch feature. Marketing team can now upload the
Rasika-style CSV, watch each row auto-map to the trainer/studio
they uploaded in slice 2, get a real MuAPI cost estimate, and
persist the batch as a draft.
Backend
-------
- POST /api/batches: creates a batch in status='draft' along with one
job per CSV row (status='queued', not yet picked up since the
worker arrives in slice 4).
- GET /api/batches: list view payload.
- GET /api/batches/[id]: detail with jobs joined to trainer/studio
rows. Used by the slice-5 progress UI.
- POST /api/batches/[id]/estimate-cost: forwards the batch's model +
payload to MuAPI's /api/v1/app/calculate_dynamic_cost, multiplies
by row count, returns {perJob, total, currency}.
CSV
---
- lib/csvParser.js: PapaParse-based parser that validates required
columns, normalises duration ("15 sec" -> 15) snapped to Seedance
2.0's [5,10,15], maps quality ("1080P" -> "high"), and composes a
prompt from description + start position + camera angle.
UI
--
- components/batch/NewBatchWizard.jsx: full-screen 3-step wizard.
- Step 1 — name, CSV upload, model/duration/quality/aspect/
concurrency settings.
- Step 2 — review every row in a table. Auto-maps Character ->
Trainer.csvLabel and Studio -> Studio.csvLabel, shows per-row
override dropdowns and bulk-assign helpers, hard-blocks Next
until every active row has both a trainer and studio.
- Step 3 — saves the batch and triggers the cost estimate. On
success, surfaces the batch id and points the user to the
upcoming worker slice.
- components/batch/BatchesTab.jsx: replaces the placeholder with a
real list view (status pill, progress counts, model, created-at)
and the "+ New batch" button that opens the wizard.
- components/batch/BatchShell.jsx: pass apiKey to BatchesTab.
Verification
------------
- POST /api/batches with one row -> 201, batch + job persisted in
Postgres (verified with psql).
- GET /api/batches lists the row.
- Cost estimate endpoint returns 401 without a key, calls MuAPI
correctly with one (live test pending real credits).
110 lines
3.7 KiB
JavaScript
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 apiKey={apiKey} />}
|
|
{activeTab === 'trainers' && <TrainersTab apiKey={apiKey} />}
|
|
{activeTab === 'studios' && <StudiosTab apiKey={apiKey} />}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|