Files
vibe-kanban/frontend/src/pages/Settings.tsx

381 lines
14 KiB
TypeScript
Raw Normal View History

2025-06-21 22:33:33 +01:00
import { useState } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Label } from "@/components/ui/label";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Loader2, Volume2 } from "lucide-react";
import type { ThemeMode, EditorType, SoundFile } from "shared/types";
import { EXECUTOR_TYPES, EDITOR_TYPES, EXECUTOR_LABELS, EDITOR_LABELS, SOUND_FILES, SOUND_LABELS } from "shared/types";
import { useTheme } from "@/components/theme-provider";
2025-06-21 22:33:33 +01:00
import { useConfig } from "@/components/config-provider";
export function Settings() {
2025-06-21 22:33:33 +01:00
const { config, updateConfig, saveConfig, loading } = useConfig();
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
2025-06-19 12:56:37 -04:00
const { setTheme } = useTheme();
const playSound = async (soundFile: SoundFile) => {
const audio = new Audio(`/api/sounds/${soundFile}.mp3`);
try {
await audio.play();
} catch (err) {
console.error("Failed to play sound:", err);
}
};
const handleSave = async () => {
if (!config) return;
setSaving(true);
setError(null);
setSuccess(false);
try {
2025-06-21 22:33:33 +01:00
const success = await saveConfig();
2025-06-21 22:33:33 +01:00
if (success) {
setSuccess(true);
// Update theme provider to reflect the saved theme
setTheme(config.theme);
setTimeout(() => setSuccess(false), 3000);
} else {
2025-06-21 22:33:33 +01:00
setError("Failed to save configuration");
}
} catch (err) {
setError("Failed to save configuration");
console.error("Error saving config:", err);
} finally {
setSaving(false);
}
};
const resetDisclaimer = async () => {
if (!config) return;
updateConfig({ disclaimer_acknowledged: false });
};
const resetOnboarding = async () => {
if (!config) return;
updateConfig({ onboarding_acknowledged: false });
};
if (loading) {
return (
<div className="container mx-auto px-4 py-8">
<div className="flex items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin" />
<span className="ml-2">Loading settings...</span>
</div>
</div>
);
}
if (!config) {
return (
<div className="container mx-auto px-4 py-8">
<Alert variant="destructive">
<AlertDescription>
Failed to load settings. {error}
</AlertDescription>
</Alert>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8 max-w-4xl">
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold">Settings</h1>
<p className="text-muted-foreground">
Configure your preferences and application settings.
</p>
</div>
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{success && (
<Alert>
<AlertDescription>Settings saved successfully!</AlertDescription>
</Alert>
)}
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>Appearance</CardTitle>
<CardDescription>
Customize how the application looks and feels.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="theme">Theme</Label>
<Select
value={config.theme}
onValueChange={(value: ThemeMode) => updateConfig({ theme: value })}
>
<SelectTrigger id="theme">
<SelectValue placeholder="Select theme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Choose your preferred color scheme.
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Task Execution</CardTitle>
<CardDescription>
Configure how tasks are executed and processed.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="executor">Default Executor</Label>
<Select
value={config.executor.type}
onValueChange={(value: "echo" | "claude" | "amp") => updateConfig({ executor: { type: value } })}
>
<SelectTrigger id="executor">
<SelectValue placeholder="Select executor" />
</SelectTrigger>
<SelectContent>
{EXECUTOR_TYPES.map((type) => (
<SelectItem key={type} value={type}>
{EXECUTOR_LABELS[type]}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Choose the default executor for running tasks.
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Editor</CardTitle>
<CardDescription>
Configure which editor to open when viewing task attempts.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="editor">Preferred Editor</Label>
<Select
value={config.editor.editor_type}
onValueChange={(value: EditorType) => updateConfig({
editor: {
...config.editor,
editor_type: value,
custom_command: value === "custom" ? config.editor.custom_command : null
}
})}
>
<SelectTrigger id="editor">
<SelectValue placeholder="Select editor" />
</SelectTrigger>
<SelectContent>
{EDITOR_TYPES.map((type) => (
<SelectItem key={type} value={type}>
{EDITOR_LABELS[type]}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Choose your preferred code editor for opening task attempts.
</p>
</div>
{config.editor.editor_type === "custom" && (
<div className="space-y-2">
<Label htmlFor="custom-command">Custom Command</Label>
<Input
id="custom-command"
placeholder="e.g., code, subl, vim"
value={config.editor.custom_command || ""}
onChange={(e) => updateConfig({
editor: {
...config.editor,
custom_command: e.target.value || null
}
})}
/>
<p className="text-sm text-muted-foreground">
Enter the command to run your custom editor. Use spaces for arguments (e.g., "code --wait").
</p>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Notifications</CardTitle>
<CardDescription>
Configure how you receive notifications about task completion.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="sound-alerts"
checked={config.sound_alerts}
onCheckedChange={(checked: boolean) => updateConfig({ sound_alerts: checked })}
/>
<div className="space-y-0.5">
<Label htmlFor="sound-alerts" className="cursor-pointer">Sound Alerts</Label>
<p className="text-sm text-muted-foreground">
Play a sound when task attempts finish running.
</p>
</div>
</div>
{config.sound_alerts && (
<div className="space-y-2 ml-6">
<Label htmlFor="sound-file">Sound</Label>
<div className="flex items-center gap-2">
<Select
value={config.sound_file}
onValueChange={(value: SoundFile) => updateConfig({ sound_file: value })}
>
<SelectTrigger id="sound-file" className="flex-1">
<SelectValue placeholder="Select sound" />
</SelectTrigger>
<SelectContent>
{SOUND_FILES.map((soundFile) => (
<SelectItem key={soundFile} value={soundFile}>
{SOUND_LABELS[soundFile]}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="outline"
size="sm"
onClick={() => playSound(config.sound_file)}
className="px-3"
>
<Volume2 className="h-4 w-4" />
</Button>
</div>
<p className="text-sm text-muted-foreground">
Choose the sound to play when tasks complete. Click the volume button to preview.
</p>
</div>
)}
<div className="flex items-center space-x-2">
<Checkbox
id="push-notifications"
checked={config.push_notifications}
onCheckedChange={(checked: boolean) => updateConfig({ push_notifications: checked })}
/>
<div className="space-y-0.5">
<Label htmlFor="push-notifications" className="cursor-pointer">Push Notifications (macOS)</Label>
<p className="text-sm text-muted-foreground">
Show system notifications when task attempts finish running.
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Safety & Disclaimers</CardTitle>
<CardDescription>
Manage safety warnings and acknowledgments.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<div className="flex items-center justify-between">
<div>
<Label>Disclaimer Status</Label>
<p className="text-sm text-muted-foreground">
{config.disclaimer_acknowledged
? "You have acknowledged the safety disclaimer."
: "The safety disclaimer has not been acknowledged."}
</p>
</div>
<Button
onClick={resetDisclaimer}
variant="outline"
size="sm"
disabled={!config.disclaimer_acknowledged}
>
Reset Disclaimer
</Button>
</div>
<p className="text-xs text-muted-foreground">
Resetting the disclaimer will require you to acknowledge the safety warning again on next app start.
</p>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<div>
<Label>Onboarding Status</Label>
<p className="text-sm text-muted-foreground">
{config.onboarding_acknowledged
? "You have completed the onboarding process."
: "The onboarding process has not been completed."}
</p>
</div>
<Button
onClick={resetOnboarding}
variant="outline"
size="sm"
disabled={!config.onboarding_acknowledged}
>
Reset Onboarding
</Button>
</div>
<p className="text-xs text-muted-foreground">
Resetting the onboarding will show the setup screen again on next app start.
</p>
</div>
</CardContent>
</Card>
</div>
{/* Sticky save button */}
<div className="fixed bottom-0 left-0 right-0 bg-background/80 backdrop-blur-sm border-t p-4 z-10">
<div className="container mx-auto max-w-4xl flex justify-end">
<Button onClick={handleSave} disabled={saving}>
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Save Settings
</Button>
</div>
</div>
{/* Spacer to prevent content from being hidden behind sticky button */}
<div className="h-20"></div>
</div>
</div>
);
}