per-campaign settings, default campaign folders, custom prompt fix
All checks were successful
Build and Push / build (push) Successful in 12m10s

This commit is contained in:
KansaiGaijin
2026-07-21 17:46:37 +12:00
parent 0daca2f208
commit ca13989ca0
14 changed files with 181 additions and 99 deletions

View File

@@ -1,11 +1,12 @@
const BASE_URL = '/api';
// Flat settings schema - must mirror backend/app/config.py's DEFAULT_SETTINGS
// exactly. This is the single source of truth; don't reintroduce a nested
// shape here without updating config.py, routers/settings.py, Setup.tsx and
// Settings.tsx together.
export interface AppSettings {
// Global settings (just onboarding tracking).
export interface GlobalSettings {
onboarding_completed: boolean;
}
// Per-campaign settings — mirrors backend CAMPAIGN_SETTINGS.
export interface CampaignSettings {
whisper_model: string;
whisper_compute_type: string;
hf_token: string;
@@ -100,11 +101,11 @@ export const campaignApi = {
export const api = {
// Settings
getSettings: async (): Promise<AppSettings> => {
getSettings: async (): Promise<GlobalSettings> => {
const res = await fetch(`${BASE_URL}/settings`);
return res.json();
},
updateSettings: async (settings: Partial<AppSettings>): Promise<AppSettings> => {
updateSettings: async (settings: Partial<GlobalSettings>): Promise<GlobalSettings> => {
const res = await fetch(`${BASE_URL}/settings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -112,9 +113,21 @@ export const api = {
});
return res.json();
},
// kept as an alias since some pages call it by this name
saveSettings: async (settings: Partial<AppSettings>): Promise<AppSettings> => {
return api.updateSettings(settings);
// Campaign settings
getCampaignSettings: async (campaignId: string): Promise<CampaignSettings> => {
const res = await fetch(`${BASE_URL}/campaigns/${encodeURIComponent(campaignId)}/settings`);
if (!res.ok) throw new Error('Failed to get campaign settings');
return res.json();
},
updateCampaignSettings: async (campaignId: string, settings: Partial<CampaignSettings>): Promise<CampaignSettings> => {
const res = await fetch(`${BASE_URL}/campaigns/${encodeURIComponent(campaignId)}/settings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings),
});
if (!res.ok) throw new Error('Failed to update campaign settings');
return res.json();
},
// Sessions

View File

@@ -1,27 +1,35 @@
import { useEffect, useState } from "react";
import { api } from "../api";
import { useCampaign } from "../contexts/CampaignContext";
export default function Settings() {
const { currentCampaign } = useCampaign();
const [settings, setSettings] = useState<any>(null);
const [saved, setSaved] = useState(false);
useEffect(() => {
api.getSettings().then(setSettings);
}, []);
const campaignId = currentCampaign?.id;
if (!settings) return null;
useEffect(() => {
if (!campaignId) return;
api.getCampaignSettings(campaignId).then(setSettings);
}, [campaignId]);
if (!settings || !campaignId) return null;
const set = (k: string, v: any) => setSettings({ ...settings, [k]: v });
const save = async () => {
await api.updateSettings(settings);
await api.updateCampaignSettings(campaignId, settings);
setSaved(true);
setTimeout(() => setSaved(false), 2000);
};
return (
<div className="max-w-4xl mx-auto py-16 px-4">
<h1 className="font-display text-3xl mt-4 mb-8">Settings</h1>
<div className="flex items-center gap-3 mb-8">
<h1 className="font-display text-3xl mt-4">Settings</h1>
<span className="text-sm text-ink/40 font-mono mt-4">for {currentCampaign?.name}</span>
</div>
<div className="grid grid-cols-2 gap-6 mb-6">
<div className="card p-6 space-y-4">

View File

@@ -1,8 +1,7 @@
import { useState, useEffect } from 'react';
import { api, AppSettings } from '../api';
import { api } from '../api';
const DEFAULT_SETTINGS: AppSettings = {
onboarding_completed: false,
const DEFAULT_SETTINGS: Record<string, string> = {
whisper_model: 'medium',
whisper_compute_type: 'int8',
hf_token: '',
@@ -21,23 +20,24 @@ const DEFAULT_SETTINGS: AppSettings = {
export default function Setup({ onComplete }: { onComplete: () => void }) {
const [step, setStep] = useState(1);
const [settings, setSettings] = useState<AppSettings>(DEFAULT_SETTINGS);
const [settings, setSettings] = useState<Record<string, string>>(DEFAULT_SETTINGS);
// On first load, try to fetch existing settings from the "default" campaign
// (which is always created by init_db).
useEffect(() => {
api.getSettings().then(data => {
if (data) {
setSettings({ ...DEFAULT_SETTINGS, ...data });
}
}).catch(err => {
console.error('Failed to connect to settings endpoint:', err);
});
api.getCampaignSettings('default')
.then(data => {
if (data) setSettings(prev => ({ ...prev, ...data }));
})
.catch(() => {});
}, []);
const handleNext = () => setStep(prev => prev + 1);
const handleBack = () => setStep(prev => prev - 1);
const handleSave = async () => {
await api.saveSettings({ ...settings, onboarding_completed: true });
await api.updateCampaignSettings('default', settings);
await api.updateSettings({ onboarding_completed: true });
onComplete();
};
@@ -195,7 +195,7 @@ export default function Setup({ onComplete }: { onComplete: () => void }) {
<h3 className="text-sm font-semibold text-slate-300">Player recap style</h3>
<select
value={settings.player_recap_style}
onChange={e => setSettings({ ...settings, player_recap_style: e.target.value as AppSettings['player_recap_style'] })}
onChange={e => setSettings({ ...settings, player_recap_style: e.target.value })}
className="w-full bg-slate-900 border border-slate-700 rounded px-3 py-2"
>
<option value="story">Story Recap (previously on)</option>