mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-05 17:04:27 +08:00
158 lines
9.0 KiB
TypeScript
158 lines
9.0 KiB
TypeScript
import { Fragment } from "react";
|
||
import { App, Button, Input, Tooltip } from "antd";
|
||
import copyToClipboard from "copy-to-clipboard";
|
||
import { Copy, KeyRound, Link2, PlugZap } from "lucide-react";
|
||
|
||
import { canvasThemes } from "@/lib/canvas-theme";
|
||
|
||
const AGENT_CONNECT_STEPS = [
|
||
{ title: "方式一:在 Codex 中使用插件", text: "在 Codex app 安装 Infinite Canvas 插件后,通过插件启动画布,插件会自动启动本地 Agent 并带上连接信息。" },
|
||
{ title: "方式二:直接运行 Agent", text: "不使用 Codex 插件时,在终端运行下面命令,再回到网页里连接或手动填入 Local URL 和 Connect token。", command: "npx -y @basketikun/canvas-agent" },
|
||
];
|
||
const AGENT_PLUGIN_REMOVE_COMMAND = "codex plugin remove infinite-canvas";
|
||
const AGENT_MCP_REMOVE_COMMAND = "codex mcp remove infinite-canvas";
|
||
|
||
export function AgentConnectView({
|
||
theme,
|
||
url,
|
||
token,
|
||
enabled,
|
||
connected,
|
||
activity,
|
||
connectError,
|
||
onUrlChange,
|
||
onTokenChange,
|
||
onToggleEnabled,
|
||
}: {
|
||
theme: (typeof canvasThemes)[keyof typeof canvasThemes];
|
||
url: string;
|
||
token: string;
|
||
enabled: boolean;
|
||
connected: boolean;
|
||
activity: string;
|
||
connectError: string;
|
||
onUrlChange: (value: string) => void;
|
||
onTokenChange: (value: string) => void;
|
||
onToggleEnabled: () => void;
|
||
}) {
|
||
const { message } = App.useApp();
|
||
const statusText = connectError ? "连接失败" : connected ? activity : enabled ? "连接中" : "未连接";
|
||
const statusColor = connectError ? "#dc2626" : connected ? "#16a34a" : enabled ? "#d97706" : theme.node.muted;
|
||
const copyCommand = (command: string) => {
|
||
copyToClipboard(command);
|
||
message.success("命令已复制");
|
||
};
|
||
const codexPluginReminder = (
|
||
<div className="rounded-lg border px-3 py-2.5 text-xs leading-5" style={{ borderColor: theme.node.stroke, color: theme.node.muted }}>
|
||
<div className="font-medium" style={{ color: theme.node.text }}>
|
||
Codex 插件提醒
|
||
</div>
|
||
<div className="mt-1">只有安装 Codex 插件或手动添加 MCP 后,工具列表才会进入 Codex 上下文并增加 token 消耗;仅运行 `npx -y @basketikun/canvas-agent` 启动本地 Agent 不会安装 MCP。</div>
|
||
<div className="mt-2 grid gap-1.5">
|
||
{[
|
||
["移除插件", AGENT_PLUGIN_REMOVE_COMMAND],
|
||
["移除手动 MCP", AGENT_MCP_REMOVE_COMMAND],
|
||
].map(([label, command]) => (
|
||
<div key={command} className="flex items-center gap-2 rounded-md border bg-transparent px-2 py-1.5" style={{ borderColor: theme.node.stroke, color: theme.node.text }}>
|
||
<span className="shrink-0 text-[11px]" style={{ color: theme.node.muted }}>
|
||
{label}
|
||
</span>
|
||
<code className="min-w-0 flex-1 overflow-x-auto whitespace-nowrap text-[11px] leading-5">{command}</code>
|
||
<Tooltip title="复制命令">
|
||
<Button size="small" type="text" className="!h-6 !w-6 !min-w-6" icon={<Copy className="size-3.5" />} onClick={() => copyCommand(command)} />
|
||
</Tooltip>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
return (
|
||
<div className="thin-scrollbar min-h-0 flex-1 overflow-y-auto p-4">
|
||
<div className="space-y-4">
|
||
<div>
|
||
<div className="text-base font-semibold leading-6">连接本地 Agent</div>
|
||
<div className="mt-1 text-xs leading-5" style={{ color: theme.node.muted }}>
|
||
按使用场景选择一种连接方式。
|
||
</div>
|
||
</div>
|
||
<div className="space-y-2">
|
||
{AGENT_CONNECT_STEPS.map((step, index) => {
|
||
const command = "command" in step ? step.command : "";
|
||
return (
|
||
<Fragment key={step.title}>
|
||
<div className="rounded-lg px-3 py-2.5">
|
||
<div className="text-sm font-medium leading-5">{step.title}</div>
|
||
<div className="mt-1 text-xs leading-5" style={{ color: theme.node.muted }}>
|
||
{step.text}
|
||
</div>
|
||
{command ? (
|
||
<div className="mt-2 flex items-center gap-2 rounded-md border bg-transparent px-2 py-1.5" style={{ borderColor: theme.node.stroke, color: theme.node.text }}>
|
||
<code className="min-w-0 flex-1 overflow-x-auto whitespace-nowrap text-[11px] leading-5">{command}</code>
|
||
<Tooltip title="复制命令">
|
||
<Button size="small" type="text" className="!h-6 !w-6 !min-w-6" icon={<Copy className="size-3.5" />} onClick={() => copyCommand(command)} />
|
||
</Tooltip>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
{index === 0 ? codexPluginReminder : null}
|
||
</Fragment>
|
||
);
|
||
})}
|
||
</div>
|
||
<div className="rounded-lg border p-3" style={{ borderColor: theme.node.stroke }}>
|
||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||
<div className="min-w-0 flex-1">
|
||
<div className="flex min-w-0 items-center gap-2">
|
||
<span className="shrink-0 text-sm font-medium leading-5">网页连接</span>
|
||
<span
|
||
className="inline-flex min-w-0 items-center gap-1.5 rounded-full border px-2 py-0.5 text-[11px] leading-4"
|
||
style={{ borderColor: connected || enabled || connectError ? statusColor : theme.node.stroke, color: statusColor }}
|
||
>
|
||
<span className="size-1.5 shrink-0 rounded-full" style={{ background: statusColor }} />
|
||
<span className="truncate">{statusText}</span>
|
||
</span>
|
||
</div>
|
||
<div className="mt-1 text-xs leading-5" style={{ color: theme.node.muted }}>
|
||
默认自动读取 Local URL 和 Connect token,失败时再手动填写。
|
||
</div>
|
||
</div>
|
||
<Button className="!h-8 !px-3" type={enabled ? "default" : "primary"} icon={<PlugZap className="size-4" />} onClick={onToggleEnabled}>
|
||
{enabled ? "断开" : "连接"}
|
||
</Button>
|
||
</div>
|
||
<div className="mt-3 grid gap-2.5">
|
||
<label className="grid gap-1.5">
|
||
<span className="flex items-center gap-1.5 text-xs font-medium" style={{ color: theme.node.muted }}>
|
||
<Link2 className="size-3.5" />
|
||
本地地址
|
||
<span className="font-normal opacity-70">Local URL</span>
|
||
</span>
|
||
<Input size="large" prefix={<Link2 className="mr-1 size-4" style={{ color: theme.node.faint }} />} value={url} onChange={(event) => onUrlChange(event.target.value)} placeholder="例如 http://127.0.0.1:17371" />
|
||
</label>
|
||
<label className="grid gap-1.5">
|
||
<span className="flex items-center gap-1.5 text-xs font-medium" style={{ color: theme.node.muted }}>
|
||
<KeyRound className="size-3.5" />
|
||
连接 Token
|
||
<span className="font-normal opacity-70">Connect token</span>
|
||
</span>
|
||
<Input.Password
|
||
size="large"
|
||
prefix={<KeyRound className="mr-1 size-4" style={{ color: theme.node.faint }} />}
|
||
value={token}
|
||
onChange={(event) => onTokenChange(event.target.value)}
|
||
placeholder="自动发现,或手动填入 Connect token"
|
||
/>
|
||
</label>
|
||
{connectError ? (
|
||
<div className="rounded-md border px-2.5 py-2 text-xs leading-5" style={{ borderColor: "rgba(220,38,38,.35)", color: "#dc2626" }}>
|
||
{connectError}
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|