mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-12 17:57:50 +02:00
459 lines
16 KiB
TypeScript
459 lines
16 KiB
TypeScript
"use client";
|
|
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { Eye, EyeOff } from "lucide-react";
|
|
import { useCallback, useEffect, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { useWayfernTerms } from "@/hooks/use-wayfern-terms";
|
|
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
|
|
import { CopyToClipboard } from "./ui/copy-to-clipboard";
|
|
|
|
interface AppSettings {
|
|
api_enabled: boolean;
|
|
api_port: number;
|
|
api_token?: string;
|
|
mcp_enabled: boolean;
|
|
mcp_port?: number;
|
|
mcp_token?: string;
|
|
}
|
|
|
|
interface McpConfig {
|
|
port: number;
|
|
token: string;
|
|
}
|
|
|
|
interface IntegrationsDialogProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function IntegrationsDialog({
|
|
isOpen,
|
|
onClose,
|
|
}: IntegrationsDialogProps) {
|
|
const { t } = useTranslation();
|
|
const [settings, setSettings] = useState<AppSettings>({
|
|
api_enabled: false,
|
|
api_port: 10108,
|
|
api_token: undefined,
|
|
mcp_enabled: false,
|
|
mcp_port: undefined,
|
|
mcp_token: undefined,
|
|
});
|
|
const [apiServerPort, setApiServerPort] = useState<number | null>(null);
|
|
const [mcpConfig, setMcpConfig] = useState<McpConfig | null>(null);
|
|
const [, setMcpRunning] = useState(false);
|
|
const [showApiToken, setShowApiToken] = useState(false);
|
|
const [showMcpToken, setShowMcpToken] = useState(false);
|
|
const [isApiStarting, setIsApiStarting] = useState(false);
|
|
const [isMcpStarting, setIsMcpStarting] = useState(false);
|
|
const [mcpInClaudeDesktop, setMcpInClaudeDesktop] = useState(false);
|
|
const [mcpInClaudeCode, setMcpInClaudeCode] = useState(false);
|
|
|
|
const { termsAccepted } = useWayfernTerms();
|
|
|
|
const loadSettings = useCallback(async () => {
|
|
try {
|
|
const loaded = await invoke<AppSettings>("get_app_settings");
|
|
setSettings(loaded);
|
|
} catch (e) {
|
|
console.error("Failed to load settings:", e);
|
|
}
|
|
}, []);
|
|
|
|
const loadMcpConfig = useCallback(async () => {
|
|
try {
|
|
const config = await invoke<McpConfig | null>("get_mcp_config");
|
|
setMcpConfig(config);
|
|
} catch (e) {
|
|
console.error("Failed to get MCP config:", e);
|
|
}
|
|
}, []);
|
|
|
|
const loadMcpServerStatus = useCallback(async () => {
|
|
try {
|
|
const isRunning = await invoke<boolean>("get_mcp_server_status");
|
|
setMcpRunning(isRunning);
|
|
} catch (e) {
|
|
console.error("Failed to get MCP server status:", e);
|
|
}
|
|
}, []);
|
|
|
|
const loadApiServerStatus = useCallback(async () => {
|
|
try {
|
|
const port = await invoke<number | null>("get_api_server_status");
|
|
setApiServerPort(port);
|
|
} catch (e) {
|
|
console.error("Failed to get API server status:", e);
|
|
}
|
|
}, []);
|
|
|
|
const loadClaudeDesktopStatus = useCallback(async () => {
|
|
try {
|
|
const exists = await invoke<boolean>("is_mcp_in_claude_desktop");
|
|
setMcpInClaudeDesktop(exists);
|
|
} catch {
|
|
// Not critical
|
|
}
|
|
}, []);
|
|
|
|
const loadClaudeCodeStatus = useCallback(async () => {
|
|
try {
|
|
const exists = await invoke<boolean>("is_mcp_in_claude_code");
|
|
setMcpInClaudeCode(exists);
|
|
} catch {
|
|
// Claude CLI may not be installed
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
void loadSettings();
|
|
void loadApiServerStatus();
|
|
void loadMcpConfig();
|
|
void loadMcpServerStatus();
|
|
void loadClaudeDesktopStatus();
|
|
void loadClaudeCodeStatus();
|
|
}
|
|
}, [
|
|
isOpen,
|
|
loadSettings,
|
|
loadApiServerStatus,
|
|
loadMcpConfig,
|
|
loadMcpServerStatus,
|
|
loadClaudeDesktopStatus,
|
|
loadClaudeCodeStatus,
|
|
]);
|
|
|
|
const handleApiToggle = async (enabled: boolean) => {
|
|
setIsApiStarting(true);
|
|
try {
|
|
if (enabled) {
|
|
const port = await invoke<number>("start_api_server", {
|
|
port: settings.api_port,
|
|
});
|
|
setApiServerPort(port);
|
|
const next = await invoke<AppSettings>("save_app_settings", {
|
|
settings: { ...settings, api_enabled: true },
|
|
});
|
|
setSettings(next);
|
|
showSuccessToast(`API server started on port ${port}`);
|
|
} else {
|
|
await invoke("stop_api_server");
|
|
setApiServerPort(null);
|
|
const next = await invoke<AppSettings>("save_app_settings", {
|
|
settings: { ...settings, api_enabled: false, api_token: null },
|
|
});
|
|
setSettings(next);
|
|
showSuccessToast("API server stopped");
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to toggle API:", e);
|
|
showErrorToast("Failed to toggle API server", {
|
|
description: e instanceof Error ? e.message : "Unknown error",
|
|
});
|
|
} finally {
|
|
setIsApiStarting(false);
|
|
}
|
|
};
|
|
|
|
const handleMcpToggle = async (enabled: boolean) => {
|
|
setIsMcpStarting(true);
|
|
try {
|
|
if (enabled) {
|
|
const port = await invoke<number>("start_mcp_server");
|
|
const next = await invoke<AppSettings>("save_app_settings", {
|
|
settings: { ...settings, mcp_enabled: true, mcp_port: port },
|
|
});
|
|
setSettings(next);
|
|
void loadMcpConfig();
|
|
showSuccessToast(`MCP server started on port ${port}`);
|
|
} else {
|
|
await invoke("stop_mcp_server");
|
|
const next = await invoke<AppSettings>("save_app_settings", {
|
|
settings: { ...settings, mcp_enabled: false },
|
|
});
|
|
setSettings(next);
|
|
setMcpConfig(null);
|
|
showSuccessToast("MCP server stopped");
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to toggle MCP server:", e);
|
|
showErrorToast("Failed to toggle MCP server", {
|
|
description: e instanceof Error ? e.message : "Unknown error",
|
|
});
|
|
} finally {
|
|
setIsMcpStarting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog
|
|
open={isOpen}
|
|
onOpenChange={(open) => {
|
|
if (!open) onClose();
|
|
}}
|
|
>
|
|
<DialogContent className="max-w-xl max-h-[80vh] my-8 flex flex-col">
|
|
<DialogHeader className="shrink-0">
|
|
<DialogTitle>Integrations</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="overflow-y-auto flex-1 min-h-0">
|
|
<Tabs defaultValue="api" className="w-full">
|
|
<TabsList className="grid w-full grid-cols-2">
|
|
<TabsTrigger value="api">Local API</TabsTrigger>
|
|
<TabsTrigger value="mcp">MCP (AI Assistants)</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="api" className="space-y-4 mt-4">
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="api-enabled"
|
|
checked={apiServerPort !== null}
|
|
disabled={isApiStarting}
|
|
onCheckedChange={(checked) => void handleApiToggle(!!checked)}
|
|
/>
|
|
<div className="grid gap-1.5 leading-none">
|
|
<Label
|
|
htmlFor="api-enabled"
|
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
>
|
|
Enable Local API Server
|
|
</Label>
|
|
<p className="text-xs text-muted-foreground">
|
|
Allow managing profiles, groups, and proxies via REST API.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{settings.api_enabled && (
|
|
<div className="space-y-4 p-4 rounded-md border bg-muted/40">
|
|
<div className="space-y-2">
|
|
<Label className="text-sm font-medium">Port</Label>
|
|
<div className="flex items-center space-x-2">
|
|
<Input
|
|
value={apiServerPort ?? settings.api_port}
|
|
readOnly
|
|
className="w-24 font-mono"
|
|
/>
|
|
<span className="text-xs text-muted-foreground">
|
|
Server is running
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-sm font-medium">
|
|
Authentication Token
|
|
</Label>
|
|
<div className="flex items-center space-x-2">
|
|
<div className="relative flex-1">
|
|
<Input
|
|
type={showApiToken ? "text" : "password"}
|
|
value={settings.api_token ?? ""}
|
|
readOnly
|
|
className="font-mono pr-10"
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
className="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
|
|
onClick={() => {
|
|
setShowApiToken(!showApiToken);
|
|
}}
|
|
>
|
|
{showApiToken ? (
|
|
<EyeOff className="h-4 w-4" />
|
|
) : (
|
|
<Eye className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
<CopyToClipboard
|
|
text={settings.api_token ?? ""}
|
|
successMessage="Token copied"
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
Include in Authorization header: Bearer {"<token>"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</TabsContent>
|
|
|
|
<TabsContent value="mcp" className="space-y-4 mt-4">
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="mcp-enabled"
|
|
checked={settings.mcp_enabled && mcpConfig !== null}
|
|
disabled={!termsAccepted || isMcpStarting}
|
|
onCheckedChange={(checked) => void handleMcpToggle(!!checked)}
|
|
/>
|
|
<div className="grid gap-1.5 leading-none">
|
|
<Label
|
|
htmlFor="mcp-enabled"
|
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
>
|
|
Enable MCP Server (Model Context Protocol)
|
|
</Label>
|
|
<p className="text-xs text-muted-foreground">
|
|
Allow AI assistants like Claude Desktop to control browsers.
|
|
{!termsAccepted && (
|
|
<span className="ml-1 text-warning">
|
|
(Accept Wayfern terms in Settings first)
|
|
</span>
|
|
)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{mcpConfig && (
|
|
<div className="space-y-4 p-4 rounded-md border bg-muted/40">
|
|
<div className="space-y-2">
|
|
<Label className="text-sm font-medium">
|
|
{t("integrations.mcp.url")}
|
|
</Label>
|
|
<div className="flex items-center space-x-2">
|
|
<div className="relative flex-1">
|
|
<Input
|
|
type={showMcpToken ? "text" : "password"}
|
|
value={`http://127.0.0.1:${mcpConfig.port}/mcp/${mcpConfig.token}`}
|
|
readOnly
|
|
className="font-mono text-xs pr-10"
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
className="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
|
|
onClick={() => {
|
|
setShowMcpToken(!showMcpToken);
|
|
}}
|
|
>
|
|
{showMcpToken ? (
|
|
<EyeOff className="h-4 w-4" />
|
|
) : (
|
|
<Eye className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
<CopyToClipboard
|
|
text={`http://127.0.0.1:${mcpConfig.port}/mcp/${mcpConfig.token}`}
|
|
successMessage={t("integrations.mcp.urlCopied")}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2 pt-1 border-t">
|
|
<p className="text-xs font-medium text-muted-foreground">
|
|
{t("integrations.mcp.claudeDesktopTitle")}
|
|
</p>
|
|
{mcpInClaudeDesktop ? (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="w-full"
|
|
onClick={async () => {
|
|
try {
|
|
await invoke("remove_mcp_from_claude_desktop");
|
|
setMcpInClaudeDesktop(false);
|
|
showSuccessToast(
|
|
t("integrations.mcp.removedFromClaudeDesktop"),
|
|
);
|
|
} catch (e) {
|
|
showErrorToast(String(e));
|
|
}
|
|
}}
|
|
>
|
|
{t("integrations.mcp.removeFromClaudeDesktop")}
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="w-full"
|
|
onClick={async () => {
|
|
try {
|
|
await invoke("add_mcp_to_claude_desktop");
|
|
setMcpInClaudeDesktop(true);
|
|
showSuccessToast(
|
|
t("integrations.mcp.addedToClaudeDesktop"),
|
|
);
|
|
} catch (e) {
|
|
showErrorToast(String(e));
|
|
}
|
|
}}
|
|
>
|
|
{t("integrations.mcp.addToClaudeDesktop")}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2 pt-1 border-t">
|
|
<p className="text-xs font-medium text-muted-foreground">
|
|
{t("integrations.mcp.claudeCodeTitle")}
|
|
</p>
|
|
{mcpInClaudeCode ? (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="w-full"
|
|
onClick={async () => {
|
|
try {
|
|
await invoke("remove_mcp_from_claude_code");
|
|
setMcpInClaudeCode(false);
|
|
showSuccessToast(
|
|
t("integrations.mcp.removedFromClaudeCode"),
|
|
);
|
|
} catch (e) {
|
|
showErrorToast(String(e));
|
|
}
|
|
}}
|
|
>
|
|
{t("integrations.mcp.removeFromClaudeCode")}
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="w-full"
|
|
onClick={async () => {
|
|
try {
|
|
await invoke("add_mcp_to_claude_code");
|
|
setMcpInClaudeCode(true);
|
|
showSuccessToast(
|
|
t("integrations.mcp.addedToClaudeCode"),
|
|
);
|
|
} catch (e) {
|
|
showErrorToast(String(e));
|
|
}
|
|
}}
|
|
>
|
|
{t("integrations.mcp.addToClaudeCode")}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|