per-campaign settings, default campaign folders, custom prompt fix
All checks were successful
Build and Push / build (push) Successful in 12m10s
All checks were successful
Build and Push / build (push) Successful in 12m10s
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user