mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-19 07:12:03 +08:00
186 lines
7.0 KiB
TypeScript
186 lines
7.0 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react';
|
|
import { Download, Loader2 } from 'lucide-react';
|
|
|
|
import { Button } from '../../../../../shared/view/ui';
|
|
import { authenticatedFetch } from '../../../../../utils/api';
|
|
import SettingsCard from '../../SettingsCard';
|
|
import SettingsRow from '../../SettingsRow';
|
|
import SettingsSection from '../../SettingsSection';
|
|
import SettingsToggle from '../../SettingsToggle';
|
|
|
|
type BrowserUseSettings = {
|
|
enabled: boolean;
|
|
};
|
|
|
|
type BrowserUseStatus = {
|
|
enabled: boolean;
|
|
available: boolean;
|
|
playwrightInstalled: boolean;
|
|
chromiumInstalled: boolean;
|
|
installInProgress: boolean;
|
|
message: string;
|
|
};
|
|
|
|
async function readJson<T>(response: Response): Promise<T> {
|
|
const data = await response.json();
|
|
if (!response.ok || data.success === false) {
|
|
throw new Error(data.error || data.details || `Request failed (${response.status})`);
|
|
}
|
|
return data as T;
|
|
}
|
|
|
|
export default function BrowserUseSettingsTab() {
|
|
const [settings, setSettings] = useState<BrowserUseSettings | null>(null);
|
|
const [status, setStatus] = useState<BrowserUseStatus | null>(null);
|
|
const [isSettingsLoading, setIsSettingsLoading] = useState(true);
|
|
const [isStatusLoading, setIsStatusLoading] = useState(true);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [isInstalling, setIsInstalling] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const loadSettings = useCallback(async () => {
|
|
const settingsResponse = await authenticatedFetch('/api/browser-use/settings');
|
|
const settingsData = await readJson<{ data: { settings: BrowserUseSettings } }>(settingsResponse);
|
|
setSettings(settingsData.data.settings);
|
|
}, []);
|
|
|
|
const loadStatus = useCallback(async () => {
|
|
const statusResponse = await authenticatedFetch('/api/browser-use/status');
|
|
const statusData = await readJson<{ data: BrowserUseStatus }>(statusResponse);
|
|
setStatus(statusData.data);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
setError(null);
|
|
setIsSettingsLoading(true);
|
|
setIsStatusLoading(true);
|
|
|
|
void loadSettings()
|
|
.catch((err) => setError(err instanceof Error ? err.message : 'Failed to load Browser settings'))
|
|
.finally(() => setIsSettingsLoading(false));
|
|
|
|
void loadStatus()
|
|
.catch((err) => setError(err instanceof Error ? err.message : 'Failed to load Browser status'))
|
|
.finally(() => setIsStatusLoading(false));
|
|
}, [loadSettings, loadStatus]);
|
|
|
|
const updateSettings = async (nextSettings: Partial<BrowserUseSettings>) => {
|
|
setIsSaving(true);
|
|
setError(null);
|
|
try {
|
|
const response = await authenticatedFetch('/api/browser-use/settings', {
|
|
method: 'PUT',
|
|
body: JSON.stringify(nextSettings),
|
|
});
|
|
const data = await readJson<{ data: { settings: BrowserUseSettings } }>(response);
|
|
setSettings(data.data.settings);
|
|
window.dispatchEvent(new Event('browserUseSettingsChanged'));
|
|
setIsStatusLoading(true);
|
|
await loadStatus();
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to save Browser settings');
|
|
} finally {
|
|
setIsStatusLoading(false);
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
const installBrowserBinaries = async () => {
|
|
setIsInstalling(true);
|
|
setError(null);
|
|
try {
|
|
const response = await authenticatedFetch('/api/browser-use/runtime/install', { method: 'POST' });
|
|
await readJson(response);
|
|
setIsStatusLoading(true);
|
|
await loadStatus();
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to install browser runtime');
|
|
} finally {
|
|
setIsStatusLoading(false);
|
|
setIsInstalling(false);
|
|
}
|
|
};
|
|
|
|
const browserEnabled = settings?.enabled === true;
|
|
const needsBrowserBinaries = Boolean(browserEnabled && status && (!status.playwrightInstalled || !status.chromiumInstalled));
|
|
const runtimeLabel = (installed?: boolean) => {
|
|
if (isStatusLoading && !status) {
|
|
return 'checking...';
|
|
}
|
|
return installed ? 'installed' : 'missing';
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<SettingsSection
|
|
title="Browser"
|
|
description="Allow agents to create guarded Playwright browser sessions that you can monitor from the Browser tab."
|
|
>
|
|
<SettingsCard divided>
|
|
<SettingsRow
|
|
label="Enable Browser"
|
|
description="Registers Browser for supported agents. Agents can create browser sessions; you can watch, stop, and delete them."
|
|
>
|
|
{isSettingsLoading && !settings ? (
|
|
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
) : (
|
|
<SettingsToggle
|
|
checked={browserEnabled}
|
|
onChange={(value) => void updateSettings({ enabled: value })}
|
|
ariaLabel="Enable Browser"
|
|
disabled={isSaving}
|
|
/>
|
|
)}
|
|
</SettingsRow>
|
|
|
|
<div className="space-y-4 px-4 py-4">
|
|
<div className="flex flex-wrap gap-2 text-xs text-muted-foreground">
|
|
<span className="rounded-md border border-border px-2 py-1">
|
|
Playwright: {runtimeLabel(status?.playwrightInstalled)}
|
|
</span>
|
|
<span className="rounded-md border border-border px-2 py-1">
|
|
Chromium: {runtimeLabel(status?.chromiumInstalled)}
|
|
</span>
|
|
<span className="rounded-md border border-border px-2 py-1">
|
|
Status: {isStatusLoading && !status ? 'checking...' : status?.available ? 'ready' : browserEnabled ? 'setup required' : 'disabled'}
|
|
</span>
|
|
</div>
|
|
|
|
{needsBrowserBinaries && (
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
<div className="min-w-0 space-y-1">
|
|
<div className="text-sm font-medium text-foreground">Browser runtime required</div>
|
|
<p className="text-sm text-muted-foreground">
|
|
{status?.message || 'Install the browser runtime before agents can create Browser sessions.'}
|
|
</p>
|
|
</div>
|
|
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
onClick={() => void installBrowserBinaries()}
|
|
disabled={isInstalling || status?.installInProgress}
|
|
className="flex-shrink-0"
|
|
>
|
|
{isInstalling || status?.installInProgress ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<Download className="h-4 w-4" />
|
|
)}
|
|
{isInstalling || status?.installInProgress ? 'Installing...' : 'Install Runtime'}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-200">
|
|
{error}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</SettingsCard>
|
|
</SettingsSection>
|
|
</div>
|
|
);
|
|
}
|