Makelore 2.0 initial clean snapshot
This commit is contained in:
725
src/pages/Settings/index.tsx
Normal file
725
src/pages/Settings/index.tsx
Normal file
@@ -0,0 +1,725 @@
|
||||
/**
|
||||
* Settings Page
|
||||
* Makelore application configuration.
|
||||
*/
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
ChevronDown,
|
||||
ExternalLink,
|
||||
FileText,
|
||||
FolderPlus,
|
||||
Play,
|
||||
RefreshCw,
|
||||
RotateCw,
|
||||
Square,
|
||||
} from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { UpdateSettings } from '@/components/settings/UpdateSettings';
|
||||
import { SUPPORTED_LANGUAGES } from '@/i18n';
|
||||
import { invokeIpc, toUserMessage } from '@/lib/api-client';
|
||||
import {
|
||||
clearUiTelemetry,
|
||||
getUiTelemetrySnapshot,
|
||||
subscribeUiTelemetry,
|
||||
type UiTelemetryEntry,
|
||||
} from '@/lib/telemetry';
|
||||
import { hostApiFetch } from '@/lib/host-api';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useOpencodeStore } from '@/stores/opencode';
|
||||
import { useProjectConfigStore } from '@/stores/project-config';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
function getHealthCheckedLabel(value: number | null): string {
|
||||
if (!value) return 'Health not checked yet';
|
||||
return `Health checked ${new Date(value).toLocaleTimeString()}`;
|
||||
}
|
||||
|
||||
export function Settings() {
|
||||
const { t } = useTranslation('settings');
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
language,
|
||||
setLanguage,
|
||||
launchAtStartup,
|
||||
setLaunchAtStartup,
|
||||
proxyEnabled,
|
||||
proxyServer,
|
||||
proxyHttpServer,
|
||||
proxyHttpsServer,
|
||||
proxyAllServer,
|
||||
proxyBypassRules,
|
||||
setProxyEnabled,
|
||||
setProxyServer,
|
||||
setProxyHttpServer,
|
||||
setProxyHttpsServer,
|
||||
setProxyAllServer,
|
||||
setProxyBypassRules,
|
||||
devModeUnlocked,
|
||||
setDevModeUnlocked,
|
||||
telemetryEnabled,
|
||||
setTelemetryEnabled,
|
||||
} = useSettingsStore();
|
||||
|
||||
const opencodeStatus = useOpencodeStore((state) => state.status);
|
||||
const opencodeHealth = useOpencodeStore((state) => state.health);
|
||||
const opencodeHealthCheckedAt = useOpencodeStore((state) => state.healthCheckedAt);
|
||||
const opencodeRuntimeConfigSummary = useOpencodeStore((state) => state.runtimeConfigSummary);
|
||||
const opencodeProjects = useOpencodeStore((state) => state.projects);
|
||||
const activeOpencodeProject = useOpencodeStore((state) => state.activeProject);
|
||||
const opencodeLoading = useOpencodeStore((state) => state.loading);
|
||||
const opencodeError = useOpencodeStore((state) => state.error);
|
||||
const refreshOpencodeStatus = useOpencodeStore((state) => state.refreshStatus);
|
||||
const startOpencodeRuntime = useOpencodeStore((state) => state.start);
|
||||
const stopOpencodeRuntime = useOpencodeStore((state) => state.stop);
|
||||
const restartOpencodeRuntime = useOpencodeStore((state) => state.restart);
|
||||
const checkOpencodeHealth = useOpencodeStore((state) => state.checkHealth);
|
||||
const loadOpencodeRuntimeConfigSummary = useOpencodeStore(
|
||||
(state) => state.loadRuntimeConfigSummary
|
||||
);
|
||||
const loadOpencodeProjects = useOpencodeStore((state) => state.loadProjects);
|
||||
const pickAndOpenOpencodeProject = useOpencodeStore((state) => state.pickAndOpenProject);
|
||||
const setActiveOpencodeProject = useOpencodeStore((state) => state.setActiveProject);
|
||||
const loadProjectTemplate = useProjectConfigStore((state) => state.load);
|
||||
|
||||
const [proxyServerDraft, setProxyServerDraft] = useState('');
|
||||
const [proxyHttpServerDraft, setProxyHttpServerDraft] = useState('');
|
||||
const [proxyHttpsServerDraft, setProxyHttpsServerDraft] = useState('');
|
||||
const [proxyAllServerDraft, setProxyAllServerDraft] = useState('');
|
||||
const [proxyBypassRulesDraft, setProxyBypassRulesDraft] = useState('');
|
||||
const [proxyEnabledDraft, setProxyEnabledDraft] = useState(false);
|
||||
const [savingProxy, setSavingProxy] = useState(false);
|
||||
const [showLogs, setShowLogs] = useState(false);
|
||||
const [logContent, setLogContent] = useState('');
|
||||
const [showTelemetryViewer, setShowTelemetryViewer] = useState(false);
|
||||
const [telemetryEntries, setTelemetryEntries] = useState<UiTelemetryEntry[]>([]);
|
||||
const [showAdditionalSettings, setShowAdditionalSettings] = useState(false);
|
||||
const [workspaceProjectError, setWorkspaceProjectError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setProxyEnabledDraft(proxyEnabled);
|
||||
}, [proxyEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
setProxyServerDraft(proxyServer);
|
||||
}, [proxyServer]);
|
||||
|
||||
useEffect(() => {
|
||||
setProxyHttpServerDraft(proxyHttpServer);
|
||||
}, [proxyHttpServer]);
|
||||
|
||||
useEffect(() => {
|
||||
setProxyHttpsServerDraft(proxyHttpsServer);
|
||||
}, [proxyHttpsServer]);
|
||||
|
||||
useEffect(() => {
|
||||
setProxyAllServerDraft(proxyAllServer);
|
||||
}, [proxyAllServer]);
|
||||
|
||||
useEffect(() => {
|
||||
setProxyBypassRulesDraft(proxyBypassRules);
|
||||
}, [proxyBypassRules]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!devModeUnlocked) return;
|
||||
setTelemetryEntries(getUiTelemetrySnapshot(200));
|
||||
const unsubscribe = subscribeUiTelemetry((entry) => {
|
||||
setTelemetryEntries((prev) => {
|
||||
const next = [...prev, entry];
|
||||
if (next.length > 200) {
|
||||
next.splice(0, next.length - 200);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
});
|
||||
return unsubscribe;
|
||||
}, [devModeUnlocked]);
|
||||
|
||||
useEffect(() => {
|
||||
void Promise.allSettled([
|
||||
refreshOpencodeStatus(),
|
||||
loadOpencodeRuntimeConfigSummary(),
|
||||
loadOpencodeProjects(),
|
||||
]);
|
||||
}, [loadOpencodeProjects, loadOpencodeRuntimeConfigSummary, refreshOpencodeStatus]);
|
||||
|
||||
const proxySettingsDirty = useMemo(() => {
|
||||
return (
|
||||
proxyEnabledDraft !== proxyEnabled ||
|
||||
proxyServerDraft.trim() !== proxyServer ||
|
||||
proxyHttpServerDraft.trim() !== proxyHttpServer ||
|
||||
proxyHttpsServerDraft.trim() !== proxyHttpsServer ||
|
||||
proxyAllServerDraft.trim() !== proxyAllServer ||
|
||||
proxyBypassRulesDraft.trim() !== proxyBypassRules
|
||||
);
|
||||
}, [
|
||||
proxyAllServer,
|
||||
proxyAllServerDraft,
|
||||
proxyBypassRules,
|
||||
proxyBypassRulesDraft,
|
||||
proxyEnabled,
|
||||
proxyEnabledDraft,
|
||||
proxyHttpServer,
|
||||
proxyHttpServerDraft,
|
||||
proxyHttpsServer,
|
||||
proxyHttpsServerDraft,
|
||||
proxyServer,
|
||||
proxyServerDraft,
|
||||
]);
|
||||
|
||||
const handleSaveProxySettings = async () => {
|
||||
setSavingProxy(true);
|
||||
try {
|
||||
const normalizedProxyServer = proxyServerDraft.trim();
|
||||
const normalizedHttpServer = proxyHttpServerDraft.trim();
|
||||
const normalizedHttpsServer = proxyHttpsServerDraft.trim();
|
||||
const normalizedAllServer = proxyAllServerDraft.trim();
|
||||
const normalizedBypassRules = proxyBypassRulesDraft.trim();
|
||||
await invokeIpc('settings:setMany', {
|
||||
proxyEnabled: proxyEnabledDraft,
|
||||
proxyServer: normalizedProxyServer,
|
||||
proxyHttpServer: normalizedHttpServer,
|
||||
proxyHttpsServer: normalizedHttpsServer,
|
||||
proxyAllServer: normalizedAllServer,
|
||||
proxyBypassRules: normalizedBypassRules,
|
||||
});
|
||||
setProxyEnabled(proxyEnabledDraft);
|
||||
setProxyServer(normalizedProxyServer);
|
||||
setProxyHttpServer(normalizedHttpServer);
|
||||
setProxyHttpsServer(normalizedHttpsServer);
|
||||
setProxyAllServer(normalizedAllServer);
|
||||
setProxyBypassRules(normalizedBypassRules);
|
||||
toast.success('Proxy settings saved');
|
||||
} catch (error) {
|
||||
toast.error(`Failed to save proxy settings: ${toUserMessage(error)}`);
|
||||
} finally {
|
||||
setSavingProxy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefreshOpencodeSettings = () => {
|
||||
void Promise.allSettled([
|
||||
refreshOpencodeStatus(),
|
||||
loadOpencodeRuntimeConfigSummary(),
|
||||
loadOpencodeProjects(),
|
||||
]);
|
||||
};
|
||||
|
||||
const handleAddOpencodeProject = async () => {
|
||||
const project = await pickAndOpenOpencodeProject();
|
||||
if (!project) return;
|
||||
await activateVerifiedProject(project.id);
|
||||
};
|
||||
|
||||
const activateVerifiedProject = async (projectId: string) => {
|
||||
try {
|
||||
setWorkspaceProjectError(null);
|
||||
const result = await loadProjectTemplate(projectId);
|
||||
if (result.status === 'valid') {
|
||||
await setActiveOpencodeProject(projectId);
|
||||
return;
|
||||
}
|
||||
if (result.status === 'missing') {
|
||||
setWorkspaceProjectError('Project configuration is missing. Create the project from the sidebar first.');
|
||||
return;
|
||||
}
|
||||
setWorkspaceProjectError(result.error ?? 'Project configuration is invalid.');
|
||||
} catch (error) {
|
||||
setWorkspaceProjectError(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowLogs = async () => {
|
||||
try {
|
||||
const logs = await hostApiFetch<{ content: string }>('/api/logs?tailLines=100');
|
||||
setLogContent(logs.content);
|
||||
setShowLogs(true);
|
||||
} catch {
|
||||
setLogContent('(Failed to load logs)');
|
||||
setShowLogs(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenLogDir = async () => {
|
||||
try {
|
||||
const { dir: logDir } = await hostApiFetch<{ dir: string | null }>('/api/logs/dir');
|
||||
if (logDir) {
|
||||
await invokeIpc('shell:showItemInFolder', logDir);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-full bg-background">
|
||||
<div className="mx-auto flex max-w-4xl flex-col gap-8 px-8 py-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-normal">{t('title')}</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{t('subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<section className="space-y-5">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-lg font-medium">{t('appearance.title')}</h2>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 bg-transparent text-muted-foreground/70 hover:bg-accent/60 hover:text-foreground"
|
||||
aria-label={t('appearance.showMoreSettings')}
|
||||
aria-expanded={showAdditionalSettings}
|
||||
data-testid="settings-reveal-additional"
|
||||
onClick={() => setShowAdditionalSettings((value) => !value)}
|
||||
>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'h-4 w-4 transition-transform',
|
||||
showAdditionalSettings && 'rotate-180'
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{t('appearance.description')}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 rounded-lg border bg-card p-4">
|
||||
<Label htmlFor="language">{t('appearance.language')}</Label>
|
||||
<select
|
||||
id="language"
|
||||
value={language}
|
||||
onChange={(event) => setLanguage(event.target.value)}
|
||||
className="h-10 rounded-md border bg-background px-3 text-sm"
|
||||
>
|
||||
{SUPPORTED_LANGUAGES.map((item) => (
|
||||
<option key={item.code} value={item.code}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between rounded-lg border bg-card p-4">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">{t('appearance.launchAtStartup')}</Label>
|
||||
<p className="text-sm text-muted-foreground">{t('appearance.launchAtStartupDesc')}</p>
|
||||
</div>
|
||||
<Switch checked={launchAtStartup} onCheckedChange={setLaunchAtStartup} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{showAdditionalSettings && (
|
||||
<>
|
||||
<Separator />
|
||||
|
||||
<section className="space-y-5" data-testid="settings-opencode-workspace">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium">Workspace</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Configure the local runtime and project workspace.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRefreshOpencodeSettings}
|
||||
disabled={opencodeLoading}
|
||||
aria-label="Refresh runtime settings"
|
||||
>
|
||||
<RefreshCw className={cn('mr-2 h-4 w-4', opencodeLoading && 'animate-spin')} />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
|
||||
<div
|
||||
className="space-y-4 rounded-lg border bg-card p-4"
|
||||
data-testid="settings-opencode-runtime"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">Runtime</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Local runtime process used by Makelore sessions.
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={opencodeStatus.state === 'running' ? 'default' : 'secondary'}>
|
||||
{opencodeStatus.state}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{opencodeStatus.state === 'running' ? (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => void restartOpencodeRuntime()}
|
||||
disabled={opencodeLoading}
|
||||
aria-label="Restart runtime"
|
||||
>
|
||||
<RotateCw className="mr-2 h-4 w-4" />
|
||||
Restart runtime
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => void stopOpencodeRuntime()}
|
||||
disabled={opencodeLoading}
|
||||
aria-label="Stop runtime"
|
||||
>
|
||||
<Square className="mr-2 h-4 w-4" />
|
||||
Stop runtime
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => void startOpencodeRuntime()}
|
||||
disabled={opencodeLoading}
|
||||
aria-label="Start runtime"
|
||||
>
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
Start runtime
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
className={opencodeStatus.state === 'running' ? 'sm:col-span-2' : undefined}
|
||||
onClick={() => void checkOpencodeHealth()}
|
||||
disabled={opencodeLoading || opencodeStatus.state !== 'running'}
|
||||
aria-label="Check health"
|
||||
>
|
||||
Check health
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 text-sm">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-muted-foreground">Port</span>
|
||||
<span className="font-medium">{opencodeStatus.port}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-muted-foreground">Health</span>
|
||||
<span className="font-medium">
|
||||
{opencodeHealth ? (opencodeHealth.ok ? 'Healthy' : 'Unhealthy') : 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{getHealthCheckedLabel(opencodeHealthCheckedAt)}
|
||||
</div>
|
||||
{(opencodeStatus.error || opencodeError) && (
|
||||
<div className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
||||
{opencodeStatus.error ?? opencodeError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="space-y-4 rounded-lg border bg-card p-4"
|
||||
data-testid="settings-opencode-models"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">Models</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Resolved from the runtime configuration.
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={() => navigate('/models')}>
|
||||
Configure models
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">Default model</div>
|
||||
<div className="break-all font-medium">
|
||||
{opencodeRuntimeConfigSummary?.model ?? 'No configured model'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">Small model</div>
|
||||
<div className="break-all font-medium">
|
||||
{opencodeRuntimeConfigSummary?.smallModel ?? 'No configured small model'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-muted-foreground">Providers</span>
|
||||
<span className="font-medium">
|
||||
{`${opencodeRuntimeConfigSummary?.providerCount ?? 0} providers`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 rounded-lg border bg-card p-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">Projects</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Folders opened as project workspaces.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => void handleAddOpencodeProject()}
|
||||
disabled={opencodeLoading}
|
||||
aria-label="Add project"
|
||||
>
|
||||
<FolderPlus className="mr-2 h-4 w-4" />
|
||||
Add project
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border bg-background/60 p-3">
|
||||
<div className="text-xs text-muted-foreground">Active project</div>
|
||||
<div className="mt-1 truncate text-sm font-medium">
|
||||
{activeOpencodeProject?.name ?? 'No project selected'}
|
||||
</div>
|
||||
<div className="mt-1 truncate text-xs text-muted-foreground">
|
||||
{activeOpencodeProject?.path ?? 'Add a project folder to start a session.'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{workspaceProjectError && (
|
||||
<div className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
||||
{workspaceProjectError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{opencodeProjects.length > 0 && (
|
||||
<div className="max-h-56 space-y-2 overflow-auto pr-1">
|
||||
{opencodeProjects.map((project) => {
|
||||
const active = activeOpencodeProject?.id === project.id;
|
||||
return (
|
||||
<div
|
||||
key={project.id}
|
||||
className="flex items-center justify-between gap-3 rounded-md border bg-background/50 px-3 py-2"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-medium">{project.name}</div>
|
||||
<div className="truncate text-xs text-muted-foreground">
|
||||
{project.path}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant={active ? 'secondary' : 'outline'}
|
||||
size="sm"
|
||||
disabled={active || opencodeLoading}
|
||||
onClick={() => void activateVerifiedProject(project.id)}
|
||||
aria-label={
|
||||
active
|
||||
? `Active project ${project.name}`
|
||||
: `Use project ${project.name}`
|
||||
}
|
||||
>
|
||||
{active ? 'Active' : 'Use'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<section className="space-y-5">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium">Network</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Configure proxy settings used by Makelore runtime.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 rounded-lg border bg-card p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">Proxy</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Route local runtime traffic through a proxy.
|
||||
</p>
|
||||
</div>
|
||||
<Switch checked={proxyEnabledDraft} onCheckedChange={setProxyEnabledDraft} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="proxy-server">Proxy Server</Label>
|
||||
<Input
|
||||
id="proxy-server"
|
||||
value={proxyServerDraft}
|
||||
onChange={(event) => setProxyServerDraft(event.target.value)}
|
||||
placeholder="http://127.0.0.1:7890"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="proxy-http-server">HTTP Proxy</Label>
|
||||
<Input
|
||||
id="proxy-http-server"
|
||||
value={proxyHttpServerDraft}
|
||||
onChange={(event) => setProxyHttpServerDraft(event.target.value)}
|
||||
placeholder="http://127.0.0.1:7890"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="proxy-https-server">HTTPS Proxy</Label>
|
||||
<Input
|
||||
id="proxy-https-server"
|
||||
value={proxyHttpsServerDraft}
|
||||
onChange={(event) => setProxyHttpsServerDraft(event.target.value)}
|
||||
placeholder="http://127.0.0.1:7890"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="proxy-all-server">ALL_PROXY / SOCKS</Label>
|
||||
<Input
|
||||
id="proxy-all-server"
|
||||
value={proxyAllServerDraft}
|
||||
onChange={(event) => setProxyAllServerDraft(event.target.value)}
|
||||
placeholder="socks5://127.0.0.1:7890"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="proxy-bypass">Bypass Rules</Label>
|
||||
<Input
|
||||
id="proxy-bypass"
|
||||
value={proxyBypassRulesDraft}
|
||||
onChange={(event) => setProxyBypassRulesDraft(event.target.value)}
|
||||
placeholder="localhost,127.0.0.1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={handleSaveProxySettings}
|
||||
disabled={!proxySettingsDirty || savingProxy}
|
||||
>
|
||||
{savingProxy ? <RefreshCw className="mr-2 h-4 w-4 animate-spin" /> : null}
|
||||
Save Proxy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<section className="space-y-5">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium">{t('updates.title')}</h2>
|
||||
<p className="text-sm text-muted-foreground">{t('updates.description')}</p>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<UpdateSettings />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<section className="space-y-5">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium">{t('advanced.title')}</h2>
|
||||
<p className="text-sm text-muted-foreground">{t('advanced.description')}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between rounded-lg border bg-card p-4">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">{t('advanced.devMode')}</Label>
|
||||
<p className="text-sm text-muted-foreground">{t('advanced.devModeDesc')}</p>
|
||||
</div>
|
||||
<Switch checked={devModeUnlocked} onCheckedChange={setDevModeUnlocked} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between rounded-lg border bg-card p-4">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">{t('advanced.telemetry')}</Label>
|
||||
<p className="text-sm text-muted-foreground">{t('advanced.telemetryDesc')}</p>
|
||||
</div>
|
||||
<Switch checked={telemetryEnabled} onCheckedChange={setTelemetryEnabled} />
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">Application Logs</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Inspect recent local application logs.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" onClick={handleShowLogs}>
|
||||
<FileText className="mr-2 h-4 w-4" />
|
||||
View Logs
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleOpenLogDir}>
|
||||
<ExternalLink className="mr-2 h-4 w-4" />
|
||||
Open Folder
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{showLogs && (
|
||||
<pre className="mt-4 max-h-80 overflow-auto whitespace-pre-wrap rounded-md bg-muted p-3 text-xs">
|
||||
{logContent || '(No logs available yet)'}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{devModeUnlocked && (
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">Telemetry Viewer</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Local-only UI and performance events.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowTelemetryViewer((value) => !value)}
|
||||
>
|
||||
{showTelemetryViewer ? 'Hide' : 'Show'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
clearUiTelemetry();
|
||||
setTelemetryEntries([]);
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{showTelemetryViewer && (
|
||||
<pre className="mt-4 max-h-80 overflow-auto whitespace-pre-wrap rounded-md bg-muted p-3 text-xs">
|
||||
{telemetryEntries.length > 0
|
||||
? JSON.stringify(telemetryEntries, null, 2)
|
||||
: '(No telemetry entries)'}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
Reference in New Issue
Block a user