/ components / SettingsDialog.tsx
SettingsDialog.tsx
1 import { useState, useEffect } from 'react'; 2 import { FolderOpen } from 'lucide-react'; 3 import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; 4 import { Button } from '@/components/ui/button'; 5 import { CODE_THEMES, CODE_FONTS } from '@/lib/constants'; 6 import type { CodeTheme, CodeFont } from '@/lib/constants'; 7 8 interface Props { 9 open: boolean; 10 onOpenChange: (open: boolean) => void; 11 onThemeChange?: (theme: string) => void; 12 } 13 14 export function applyCodeFont(fontId: string) { 15 const font = CODE_FONTS.find((f) => f.id === fontId); 16 if (font) { 17 document.documentElement.style.setProperty('--font-mono', `${font.family}, ui-monospace, monospace`); 18 } 19 } 20 21 export function SettingsDialog({ open, onOpenChange, onThemeChange }: Props) { 22 const [codeTheme, setCodeTheme] = useState<CodeTheme>('aurora-x'); 23 const [codeFont, setCodeFont] = useState<CodeFont>('jetbrains-mono'); 24 const [enableTools, setEnableTools] = useState(false); 25 const [autoReviewOnRequest, setAutoReviewOnRequest] = useState(false); 26 const [notifications, setNotifications] = useState(true); 27 const [claudePath, setClaudePath] = useState(''); 28 const [geminiPath, setGeminiPath] = useState(''); 29 const [claudeDetected, setClaudeDetected] = useState(''); 30 const [geminiDetected, setGeminiDetected] = useState(''); 31 32 useEffect(() => { 33 if (!open) return; 34 void window.electronAPI.loadPreferences().then((prefs) => { 35 if (prefs.codeTheme) setCodeTheme(prefs.codeTheme as CodeTheme); 36 if (prefs.codeFont) setCodeFont(prefs.codeFont as CodeFont); 37 setEnableTools(prefs.enableTools); 38 setAutoReviewOnRequest(prefs.autoReviewOnRequest ?? false); 39 setNotifications(prefs.notifications); 40 setClaudePath(prefs.claudePath || ''); 41 setGeminiPath(prefs.geminiPath || ''); 42 }); 43 void window.electronAPI.detectBinaryPath('claude').then(setClaudeDetected); 44 void window.electronAPI.detectBinaryPath('gemini').then(setGeminiDetected); 45 }, [open]); 46 47 function saveField(overrides: Partial<Record<string, string | boolean>>) { 48 void window.electronAPI.loadPreferences().then((prefs) => { 49 void window.electronAPI.savePreferences({ ...prefs, ...overrides }); 50 }); 51 } 52 53 function handleSelectTheme(theme: CodeTheme) { 54 setCodeTheme(theme); 55 saveField({ codeTheme: theme }); 56 onThemeChange?.(theme); 57 } 58 59 function handleSelectFont(font: CodeFont) { 60 setCodeFont(font); 61 saveField({ codeFont: font }); 62 applyCodeFont(font); 63 } 64 65 return ( 66 <Dialog open={open} onOpenChange={onOpenChange}> 67 <DialogContent className="bg-card sm:max-w-md"> 68 <DialogHeader> 69 <DialogTitle>Settings</DialogTitle> 70 <DialogDescription>Configure your preferences</DialogDescription> 71 </DialogHeader> 72 73 <div className="flex flex-col gap-4"> 74 <div className="flex flex-col gap-1.5"> 75 <label className="text-sm font-medium">Code font</label> 76 <div className="flex flex-wrap gap-2"> 77 {CODE_FONTS.map((f) => ( 78 <button 79 key={f.id} 80 type="button" 81 onClick={() => handleSelectFont(f.id)} 82 className={`rounded-md border px-3 py-1.5 text-sm transition-colors ${ 83 codeFont === f.id 84 ? 'border-primary bg-primary text-primary-foreground' 85 : 'border-input bg-transparent text-muted-foreground hover:text-foreground hover:border-foreground/30' 86 }`} 87 style={{ fontFamily: `${f.family}, monospace` }} 88 > 89 {f.label} 90 </button> 91 ))} 92 </div> 93 </div> 94 95 <div className="flex flex-col gap-1.5"> 96 <label className="text-sm font-medium">Code theme</label> 97 <div className="flex flex-wrap gap-2"> 98 {CODE_THEMES.map((t) => ( 99 <button 100 key={t.id} 101 type="button" 102 onClick={() => handleSelectTheme(t.id)} 103 className={`rounded-md border px-3 py-1.5 text-sm transition-colors ${ 104 codeTheme === t.id 105 ? 'border-primary bg-primary text-primary-foreground' 106 : 'border-input bg-transparent text-muted-foreground hover:text-foreground hover:border-foreground/30' 107 }`} 108 > 109 {t.label} 110 </button> 111 ))} 112 </div> 113 </div> 114 115 <div className="flex items-center justify-between gap-2"> 116 <div className="flex flex-col gap-0.5"> 117 <label className="text-sm font-medium">Enable AI tools</label> 118 <p className="text-xs text-muted-foreground"> 119 Allow the AI to search the web and fetch GitHub context (slower but more thorough) 120 </p> 121 </div> 122 <button 123 type="button" 124 role="switch" 125 aria-checked={enableTools} 126 onClick={() => { 127 const next = !enableTools; 128 setEnableTools(next); 129 saveField({ enableTools: next }); 130 }} 131 className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors ${ 132 enableTools ? 'bg-primary' : 'bg-muted' 133 }`} 134 > 135 <span 136 className={`pointer-events-none block h-4 w-4 rounded-full bg-white shadow-sm transition-transform ${ 137 enableTools ? 'translate-x-4' : 'translate-x-0' 138 }`} 139 /> 140 </button> 141 </div> 142 143 <div className="flex items-center justify-between gap-2"> 144 <div className="flex flex-col gap-0.5"> 145 <label className="text-sm font-medium">Auto-review when assigned</label> 146 <p className="text-xs text-muted-foreground"> 147 Automatically run an AI review when you are added as a reviewer on a PR. You will get a notification when it's ready. 148 </p> 149 </div> 150 <button 151 type="button" 152 role="switch" 153 aria-checked={autoReviewOnRequest} 154 onClick={() => { 155 const next = !autoReviewOnRequest; 156 setAutoReviewOnRequest(next); 157 saveField({ autoReviewOnRequest: next }); 158 }} 159 className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors ${ 160 autoReviewOnRequest ? 'bg-primary' : 'bg-muted' 161 }`} 162 > 163 <span 164 className={`pointer-events-none block h-4 w-4 rounded-full bg-white shadow-sm transition-transform ${ 165 autoReviewOnRequest ? 'translate-x-4' : 'translate-x-0' 166 }`} 167 /> 168 </button> 169 </div> 170 171 <div className="flex items-center justify-between gap-2"> 172 <div className="flex flex-col gap-0.5"> 173 <label className="text-sm font-medium">Desktop notifications</label> 174 <p className="text-xs text-muted-foreground">Notify when a review completes</p> 175 </div> 176 <button 177 type="button" 178 role="switch" 179 aria-checked={notifications} 180 onClick={() => { 181 const next = !notifications; 182 setNotifications(next); 183 saveField({ notifications: next }); 184 }} 185 className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors ${ 186 notifications ? 'bg-primary' : 'bg-muted' 187 }`} 188 > 189 <span 190 className={`pointer-events-none block h-4 w-4 rounded-full bg-white shadow-sm transition-transform ${ 191 notifications ? 'translate-x-4' : 'translate-x-0' 192 }`} 193 /> 194 </button> 195 </div> 196 197 <div className="flex flex-col gap-1.5"> 198 <label className="text-sm font-medium">Claude CLI path</label> 199 <input 200 type="text" 201 value={claudePath} 202 placeholder={claudeDetected || 'auto-detect'} 203 onChange={(e) => setClaudePath(e.target.value)} 204 onBlur={() => saveField({ claudePath })} 205 className="rounded-md border border-input bg-transparent px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring" 206 /> 207 <p className="text-xs text-muted-foreground">Leave empty to auto-detect</p> 208 </div> 209 210 <div className="flex flex-col gap-1.5"> 211 <label className="text-sm font-medium">Gemini CLI path</label> 212 <input 213 type="text" 214 value={geminiPath} 215 placeholder={geminiDetected || 'auto-detect'} 216 onChange={(e) => setGeminiPath(e.target.value)} 217 onBlur={() => saveField({ geminiPath })} 218 className="rounded-md border border-input bg-transparent px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring" 219 /> 220 <p className="text-xs text-muted-foreground">Leave empty to auto-detect</p> 221 </div> 222 223 <div className="border-t border-border pt-4"> 224 <Button 225 variant="outline" 226 size="sm" 227 className="gap-1.5" 228 onClick={() => void window.electronAPI.openLogsDirectory()} 229 > 230 <FolderOpen className="h-3.5 w-3.5" /> 231 Open logs 232 </Button> 233 </div> 234 </div> 235 </DialogContent> 236 </Dialog> 237 ); 238 }