import { useState, useRef, useCallback, useEffect } from 'react' import { X, Eye, EyeSlash, GithubLogo, SignOut } from '@phosphor-icons/react' import { GitHubDeviceFlow } from './GitHubDeviceFlow' import type { Settings } from '../types' interface SettingsPanelProps { open: boolean settings: Settings onSave: (settings: Settings) => void onClose: () => void } interface KeyFieldProps { label: string placeholder: string value: string onChange: (value: string) => void onClear: () => void } function KeyField({ label, placeholder, value, onChange, onClear }: KeyFieldProps) { const [revealed, setRevealed] = useState(false) const inputRef = useRef(null) return (
onChange(e.target.value)} placeholder={placeholder} className="w-full border border-border bg-transparent text-foreground rounded" style={{ fontSize: 13, padding: '8px 60px 8px 10px', outline: 'none', fontFamily: 'inherit' }} autoComplete="off" data-testid={`settings-key-${label.toLowerCase().replace(/\s+/g, '-')}`} />
{value && ( <> )}
) } // --- GitHub OAuth Section --- interface GitHubSectionProps { githubUsername: string | null githubToken: string | null onConnected: (token: string, username: string) => void onDisconnect: () => void } function GitHubSection({ githubUsername, githubToken, onConnected, onDisconnect }: GitHubSectionProps) { const isConnected = !!githubToken && !!githubUsername if (isConnected) { return } return } function GitHubConnectedRow({ username, onDisconnect }: { username: string; onDisconnect: () => void }) { return (
{username} Connected
) } // --- Settings Panel --- export function SettingsPanel({ open, settings, onSave, onClose }: SettingsPanelProps) { if (!open) return null return } function SettingsPanelInner({ settings, onSave, onClose }: Omit) { const [openaiKey, setOpenaiKey] = useState(settings.openai_key ?? '') const [googleKey, setGoogleKey] = useState(settings.google_key ?? '') const [githubToken, setGithubToken] = useState(settings.github_token) const [githubUsername, setGithubUsername] = useState(settings.github_username) const [pullInterval, setPullInterval] = useState(settings.auto_pull_interval_minutes ?? 5) const [updateChannel, setUpdateChannel] = useState(settings.update_channel ?? 'stable') const [crashReporting, setCrashReporting] = useState(settings.crash_reporting_enabled ?? false) const [analytics, setAnalytics] = useState(settings.analytics_enabled ?? false) const panelRef = useRef(null) // Auto-focus first input when settings panel opens useEffect(() => { const timer = setTimeout(() => { const input = panelRef.current?.querySelector('input') input?.focus() }, 50) return () => clearTimeout(timer) }, []) const buildSettings = useCallback((ghOverride?: { token: string | null; username: string | null }): Settings => ({ openai_key: openaiKey.trim() || null, google_key: googleKey.trim() || null, github_token: ghOverride ? ghOverride.token : (githubToken ?? null), github_username: ghOverride ? ghOverride.username : (githubUsername ?? null), auto_pull_interval_minutes: pullInterval, telemetry_consent: (crashReporting || analytics) ? true : (settings.telemetry_consent === null ? null : false), crash_reporting_enabled: crashReporting, analytics_enabled: analytics, anonymous_id: (crashReporting || analytics) ? (settings.anonymous_id ?? crypto.randomUUID()) : settings.anonymous_id, update_channel: updateChannel === 'stable' ? null : updateChannel, }), [openaiKey, googleKey, githubToken, githubUsername, pullInterval, updateChannel, crashReporting, analytics, settings.telemetry_consent, settings.anonymous_id]) const handleSave = () => { onSave(buildSettings()) onClose() } const handleGitHubConnected = useCallback((token: string, username: string) => { setGithubToken(token) setGithubUsername(username) onSave(buildSettings({ token, username })) }, [onSave, buildSettings]) const handleGitHubDisconnect = useCallback(() => { setGithubToken(null) setGithubUsername(null) onSave(buildSettings({ token: null, username: null })) }, [onSave, buildSettings]) const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Escape') { e.stopPropagation() onClose() } if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault() handleSave() } } return (
{ if (e.target === e.currentTarget) onClose() }} onKeyDown={handleKeyDown} data-testid="settings-panel" >
) } function SettingsHeader({ onClose }: { onClose: () => void }) { return (
Settings
) } interface SettingsBodyProps { openaiKey: string; setOpenaiKey: (v: string) => void googleKey: string; setGoogleKey: (v: string) => void githubToken: string | null; githubUsername: string | null onGitHubConnected: (token: string, username: string) => void onGitHubDisconnect: () => void pullInterval: number; setPullInterval: (v: number) => void updateChannel: string; setUpdateChannel: (v: string) => void crashReporting: boolean; setCrashReporting: (v: boolean) => void analytics: boolean; setAnalytics: (v: boolean) => void } function SettingsBody(props: SettingsBodyProps) { return (
AI Provider Keys
API keys are stored locally on your device. Never sent to our servers.
props.setOpenaiKey('')} /> props.setGoogleKey('')} />
GitHub
Connect your GitHub account to clone and sync vaults.
Sync
Automatically pull vault changes from Git in the background.
Updates
Canary builds include the latest features but may be less stable. Restart required after changing.
Privacy & Telemetry
Anonymous data helps us fix bugs and improve Laputa. No vault content, note titles, or file paths are ever sent.
) } function TelemetryToggle({ label, description, checked, onChange, testId }: { label: string; description: string; checked: boolean; onChange: (v: boolean) => void; testId: string }) { return ( ) } function SettingsFooter({ onClose, onSave }: { onClose: () => void; onSave: () => void }) { return (
{'\u2318'}, to open settings
) }