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

505 lines
18 KiB
TypeScript
Raw Normal View History

2025-06-25 09:36:07 +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, Key } from 'lucide-react';
2025-06-25 09:36:07 +01:00
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';
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}.wav`);
try {
await audio.play();
} catch (err) {
2025-06-25 09:36:07 +01:00
console.error('Failed to play sound:', err);
}
};
const handleSave = async () => {
if (!config) return;
2025-06-25 09:36:07 +01:00
setSaving(true);
setError(null);
setSuccess(false);
2025-06-25 09:36:07 +01:00
try {
// Save the main configuration
2025-06-21 22:33:33 +01:00
const success = await saveConfig();
2025-06-25 09:36:07 +01:00
2025-06-21 22:33:33 +01:00
if (success) {
setSuccess(true);
// Update theme provider to reflect the saved theme
setTheme(config.theme);
2025-06-25 09:36:07 +01:00
setTimeout(() => setSuccess(false), 3000);
} else {
2025-06-25 09:36:07 +01:00
setError('Failed to save configuration');
}
} catch (err) {
2025-06-25 09:36:07 +01:00
setError('Failed to save configuration');
console.error('Error saving config:', err);
} finally {
setSaving(false);
}
};
const resetDisclaimer = async () => {
if (!config) return;
2025-06-25 09:36:07 +01:00
updateConfig({ disclaimer_acknowledged: false });
};
const resetOnboarding = async () => {
if (!config) return;
2025-06-25 09:36:07 +01:00
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">
2025-06-25 09:36:07 +01:00
<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 className="border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-200">
2025-06-25 09:36:07 +01:00
<AlertDescription className="font-medium">
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 });
setTheme(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>
<SelectItem value="purple">Purple</SelectItem>
<SelectItem value="green">Green</SelectItem>
<SelectItem value="blue">Blue</SelectItem>
<SelectItem value="orange">Orange</SelectItem>
<SelectItem value="red">Red</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}
2025-06-25 09:36:07 +01:00
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}
2025-06-25 09:36:07 +01:00
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>
2025-06-25 09:36:07 +01:00
{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"
2025-06-25 09:36:07 +01:00
value={config.editor.custom_command || ''}
onChange={(e) =>
updateConfig({
editor: {
...config.editor,
custom_command: e.target.value || null,
},
})
}
/>
<p className="text-sm text-muted-foreground">
2025-06-25 09:36:07 +01:00
Enter the command to run your custom editor. Use spaces for
arguments (e.g., "code --wait").
</p>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Key className="h-5 w-5" />
GitHub Integration
</CardTitle>
<CardDescription>
Configure GitHub settings for creating pull requests from task
attempts.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="github-token">Personal Access Token</Label>
<Input
id="github-token"
type="password"
placeholder="ghp_xxxxxxxxxxxxxxxxxxxx"
value={config.github.token || ''}
onChange={(e) =>
updateConfig({
github: {
...config.github,
token: e.target.value || null,
},
})
}
/>
<p className="text-sm text-muted-foreground">
GitHub Personal Access Token with 'repo' permissions. Required
for creating pull requests.{' '}
<a
href="https://github.com/settings/tokens"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
Create token here
</a>
</p>
</div>
<div className="space-y-2">
<Label htmlFor="default-pr-base">Default PR Base Branch</Label>
<Input
id="default-pr-base"
placeholder="main"
value={config.github.default_pr_base || ''}
onChange={(e) =>
updateConfig({
github: {
...config.github,
default_pr_base: e.target.value || null,
},
})
}
/>
<p className="text-sm text-muted-foreground">
Default base branch for pull requests. Defaults to 'main' if
not specified.
</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}
2025-06-25 09:36:07 +01:00
onCheckedChange={(checked: boolean) =>
updateConfig({ sound_alerts: checked })
}
/>
<div className="space-y-0.5">
2025-06-25 09:36:07 +01:00
<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-25 09:36:07 +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}
2025-06-25 09:36:07 +01:00
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">
2025-06-25 09:36:07 +01:00
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}
2025-06-25 09:36:07 +01:00
onCheckedChange={(checked: boolean) =>
updateConfig({ push_notifications: checked })
}
/>
<div className="space-y-0.5">
2025-06-25 09:36:07 +01:00
<Label
htmlFor="push-notifications"
className="cursor-pointer"
>
Push Notifications
2025-06-25 09:36:07 +01:00
</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">
2025-06-25 09:36:07 +01:00
{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">
2025-06-25 09:36:07 +01:00
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">
2025-06-25 09:36:07 +01:00
{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">
2025-06-25 09:36:07 +01:00
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">
2025-06-25 09:36:07 +01:00
<Button
onClick={handleSave}
disabled={saving || success}
className={success ? 'bg-green-600 hover:bg-green-700' : ''}
>
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{success && <span className="mr-2"></span>}
2025-06-25 09:36:07 +01:00
{success ? 'Settings Saved!' : 'Save Settings'}
</Button>
</div>
</div>
2025-06-25 09:36:07 +01:00
{/* Spacer to prevent content from being hidden behind sticky button */}
<div className="h-20"></div>
</div>
</div>
);
}