Add campaign layer, GPU model caching, file browser cleanup
- Campaigns: new table, CRUD API, React context + provider
- Sessions: scoped to campaigns, paths under campaigns/{id}/
- File browser: scoped per campaign, removed copy/paste/autoPlay
- Sidebar: campaign selector dropdown at top
- Transcribe: GPU model cached/released via job counter
- Jobs: status text updates dynamically in real-time
- Auto-redirect: blocked when summarize job is active
This commit is contained in:
120
frontend/src/pages/Campaigns.tsx
Normal file
120
frontend/src/pages/Campaigns.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { campaignApi, type Campaign } from '../api';
|
||||
import { useCampaign } from '../contexts/CampaignContext';
|
||||
|
||||
export default function Campaigns() {
|
||||
const [campaigns, setCampaigns] = useState<Campaign[]>([]);
|
||||
const [name, setName] = useState('');
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [editName, setEditName] = useState('');
|
||||
const nav = useNavigate();
|
||||
const { setCampaign, refreshCampaigns } = useCampaign();
|
||||
|
||||
const load = async () => {
|
||||
const list = await campaignApi.list();
|
||||
setCampaigns(list);
|
||||
};
|
||||
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
const create = async () => {
|
||||
if (!name.trim()) return;
|
||||
const c = await campaignApi.create(name.trim());
|
||||
setName('');
|
||||
await load();
|
||||
await refreshCampaigns();
|
||||
setCampaign(c);
|
||||
nav('/');
|
||||
};
|
||||
|
||||
const startEdit = (c: Campaign) => {
|
||||
setEditingId(c.id);
|
||||
setEditName(c.name);
|
||||
};
|
||||
|
||||
const saveEdit = async (id: string) => {
|
||||
if (!editName.trim()) return;
|
||||
await campaignApi.update(id, { name: editName.trim() });
|
||||
setEditingId(null);
|
||||
await load();
|
||||
await refreshCampaigns();
|
||||
};
|
||||
|
||||
const deleteCampaign = async (id: string) => {
|
||||
if (!confirm('Delete this campaign? Sessions will be unlinked (notes/history preserved).')) return;
|
||||
await campaignApi.delete(id);
|
||||
await load();
|
||||
await refreshCampaigns();
|
||||
};
|
||||
|
||||
const select = (c: Campaign) => {
|
||||
setCampaign(c);
|
||||
nav('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto py-16 px-4">
|
||||
<div className="eyebrow mb-2">Nat20 Notes</div>
|
||||
<h1 className="font-display text-4xl mb-8">Campaigns</h1>
|
||||
|
||||
<div className="card p-6 mb-8">
|
||||
<h2 className="font-display text-xl mb-4">New campaign</h2>
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
className="input flex-1"
|
||||
placeholder="Campaign name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && create()}
|
||||
/>
|
||||
<button className="btn-primary" disabled={!name.trim()} onClick={create}>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{campaigns.length === 0 && (
|
||||
<p className="text-ink/40">No campaigns yet — create one above.</p>
|
||||
)}
|
||||
{campaigns.map((c) => (
|
||||
<div key={c.id} className="card p-4 flex items-center justify-between hover:border-brass/40 border border-transparent">
|
||||
<div className="flex-1 min-w-0 cursor-pointer" onClick={() => select(c)}>
|
||||
{editingId === c.id ? (
|
||||
<input
|
||||
className="input w-full"
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') saveEdit(c.id);
|
||||
if (e.key === 'Escape') setEditingId(null);
|
||||
}}
|
||||
onBlur={() => saveEdit(c.id)}
|
||||
autoFocus
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="font-medium">{c.name}</div>
|
||||
<div className="text-xs text-ink/40 mt-0.5">
|
||||
{c.session_count} session{c.session_count !== 1 ? 's' : ''}
|
||||
{c.description && ` \u00B7 ${c.description}`}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2 ml-4 shrink-0">
|
||||
<button className="btn-secondary text-xs px-2 py-1" onClick={(e) => { e.stopPropagation(); startEdit(c); }}>
|
||||
Rename
|
||||
</button>
|
||||
<button className="btn-secondary text-xs px-2 py-1 text-rose-400" onClick={(e) => { e.stopPropagation(); deleteCampaign(c.id); }}>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user