2025-06-21 22:33:33 +01:00
|
|
|
import { useState } from "react";
|
2025-06-19 12:53:41 -04:00
|
|
|
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";
|
2025-06-19 21:18:18 -04:00
|
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
2025-06-20 19:25:18 +01:00
|
|
|
import { Input } from "@/components/ui/input";
|
2025-06-24 10:50:20 +01:00
|
|
|
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";
|
2025-06-19 12:53:41 -04:00
|
|
|
import { useTheme } from "@/components/theme-provider";
|
2025-06-21 22:33:33 +01:00
|
|
|
import { useConfig } from "@/components/config-provider";
|
2025-06-19 12:53:41 -04:00
|
|
|
|
|
|
|
|
export function Settings() {
|
2025-06-21 22:33:33 +01:00
|
|
|
const { config, updateConfig, saveConfig, loading } = useConfig();
|
2025-06-19 12:53:41 -04:00
|
|
|
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();
|
2025-06-19 12:53:41 -04:00
|
|
|
|
2025-06-24 10:50:20 +01:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-19 12:53:41 -04:00
|
|
|
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-19 12:53:41 -04:00
|
|
|
|
2025-06-21 22:33:33 +01:00
|
|
|
if (success) {
|
2025-06-19 12:53:41 -04:00
|
|
|
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");
|
2025-06-19 12:53:41 -04:00
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setError("Failed to save configuration");
|
|
|
|
|
console.error("Error saving config:", err);
|
|
|
|
|
} finally {
|
|
|
|
|
setSaving(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-19 19:11:48 -04:00
|
|
|
const resetDisclaimer = async () => {
|
|
|
|
|
if (!config) return;
|
|
|
|
|
|
|
|
|
|
updateConfig({ disclaimer_acknowledged: false });
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-22 22:58:01 +01:00
|
|
|
const resetOnboarding = async () => {
|
|
|
|
|
if (!config) return;
|
|
|
|
|
|
|
|
|
|
updateConfig({ onboarding_acknowledged: false });
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-19 12:53:41 -04:00
|
|
|
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>
|
2025-06-22 22:58:01 +01:00
|
|
|
{EXECUTOR_TYPES.map((type) => (
|
|
|
|
|
<SelectItem key={type} value={type}>
|
|
|
|
|
{EXECUTOR_LABELS[type]}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
2025-06-19 12:53:41 -04:00
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
Choose the default executor for running tasks.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
2025-06-19 19:11:48 -04:00
|
|
|
|
2025-06-20 19:25:18 +01:00
|
|
|
<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>
|
2025-06-22 22:58:01 +01:00
|
|
|
{EDITOR_TYPES.map((type) => (
|
|
|
|
|
<SelectItem key={type} value={type}>
|
|
|
|
|
{EDITOR_LABELS[type]}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
2025-06-20 19:25:18 +01:00
|
|
|
</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>
|
|
|
|
|
|
2025-06-19 21:18:18 -04:00
|
|
|
<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>
|
2025-06-24 10:50:20 +01:00
|
|
|
|
|
|
|
|
{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>
|
|
|
|
|
)}
|
2025-06-21 23:28:24 +01:00
|
|
|
<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>
|
2025-06-19 21:18:18 -04:00
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
2025-06-19 19:11:48 -04:00
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle>Safety & Disclaimers</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
Manage safety warnings and acknowledgments.
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
2025-06-22 22:58:01 +01:00
|
|
|
<CardContent className="space-y-6">
|
2025-06-19 19:11:48 -04:00
|
|
|
<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>
|
2025-06-22 22:58:01 +01:00
|
|
|
<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>
|
2025-06-19 19:11:48 -04:00
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
2025-06-19 12:53:41 -04:00
|
|
|
</div>
|
|
|
|
|
|
2025-06-20 17:47:36 +01:00
|
|
|
{/* 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>
|
2025-06-19 12:53:41 -04:00
|
|
|
</div>
|
2025-06-20 17:47:36 +01:00
|
|
|
|
|
|
|
|
{/* Spacer to prevent content from being hidden behind sticky button */}
|
|
|
|
|
<div className="h-20"></div>
|
2025-06-19 12:53:41 -04:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|