Files
Nat20-Notes/frontend/src/pages/Campaigns.tsx
KansaiGaijin bc7ac32de3 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
2026-07-10 21:41:29 +12:00

121 lines
4.0 KiB
TypeScript

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>
);
}