mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-07-24 15:24:06 +08:00
feat(agent): enhance local Agent connection handling with silent mode and update UI instructions
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { App, Button, Input, Segmented, Tooltip } from "antd";
|
||||
import copyToClipboard from "copy-to-clipboard";
|
||||
@@ -103,7 +103,7 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
||||
source.addEventListener("hello", () => {
|
||||
errorLoggedRef.current = false;
|
||||
connectedRef.current = true;
|
||||
setAgentState({ connected: true, activity: "已连接", connectError: "", messages: useAgentStore.getState().messages.filter((item) => !isConnectionErrorMessage(item)) });
|
||||
setAgentState({ connected: true, activity: "已连接", connectError: "", silentConnect: false, messages: useAgentStore.getState().messages.filter((item) => !isConnectionErrorMessage(item)) });
|
||||
if (!headless) message.success("本地 Agent 已连接");
|
||||
void postState(endpoint, token, clientId, canvasContextRef.current?.snapshot || null);
|
||||
});
|
||||
@@ -131,14 +131,15 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
||||
});
|
||||
source.onerror = () => {
|
||||
const wasConnected = connectedRef.current;
|
||||
const silent = useAgentStore.getState().silentConnect && !wasConnected;
|
||||
const text = wasConnected ? "本地 Agent 连接失败或已断开" : "连接失败,请检查地址和 token";
|
||||
if (!errorLoggedRef.current || wasConnected) {
|
||||
addEventLog(wasConnected ? "连接断开" : "连接失败", { endpoint, error: text });
|
||||
if (!headless) message.error(text);
|
||||
if (!headless && !silent) message.error(text);
|
||||
}
|
||||
errorLoggedRef.current = true;
|
||||
connectedRef.current = false;
|
||||
clearAgentSession({ activity: wasConnected ? "连接断开" : "连接失败", connected: false, connectError: text });
|
||||
clearAgentSession({ activity: wasConnected ? "连接断开" : "连接失败", connected: false, connectError: silent ? "" : text, silentConnect: false });
|
||||
if (!wasConnected) {
|
||||
source.close();
|
||||
setAgentState({ enabled: false });
|
||||
@@ -327,7 +328,7 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
||||
if (connected) void postState(endpoint, token, clientIdRef.current, restored);
|
||||
};
|
||||
|
||||
const toggleAgentConnection = async () => {
|
||||
const toggleAgentConnection = async ({ silent = false }: { silent?: boolean } = {}) => {
|
||||
if (enabled) {
|
||||
clearAgentSession({ enabled: false, connected: false, activity: "离线", connectError: "" });
|
||||
return;
|
||||
@@ -339,14 +340,18 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
||||
const nextToken = (urlToken || token.trim() || discovered?.token || "").trim();
|
||||
if (!nextEndpoint) {
|
||||
const text = "请填写本地 Agent 地址";
|
||||
setAgentState({ connectError: text });
|
||||
if (!headless) message.warning(text);
|
||||
if (!silent) {
|
||||
setAgentState({ connectError: text });
|
||||
if (!headless) message.warning(text);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!nextToken) {
|
||||
const text = "没有发现本地 Agent,请先在 Codex 使用插件或手动启动 Canvas Agent";
|
||||
setAgentState({ connectError: text });
|
||||
if (!headless) message.warning(text);
|
||||
if (!silent) {
|
||||
setAgentState({ connectError: text });
|
||||
if (!headless) message.warning(text);
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -354,12 +359,14 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("invalid protocol");
|
||||
} catch {
|
||||
const text = "本地 Agent 地址格式不正确";
|
||||
setAgentState({ connectError: text });
|
||||
if (!headless) message.warning(text);
|
||||
if (!silent) {
|
||||
setAgentState({ connectError: text });
|
||||
if (!headless) message.warning(text);
|
||||
}
|
||||
return;
|
||||
}
|
||||
errorLoggedRef.current = false;
|
||||
setAgentState({ url: nextEndpoint, token: nextToken, enabled: true, connected: false, activity: "连接中", connectError: "", activeTab: "setup" });
|
||||
setAgentState({ url: nextEndpoint, token: nextToken, enabled: true, connected: false, silentConnect: silent, activity: "连接中", connectError: "", activeTab: "setup" });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -369,7 +376,7 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
||||
useEffect(() => {
|
||||
if (!autoConnect || autoConnectRef.current || enabled || connected) return;
|
||||
autoConnectRef.current = true;
|
||||
void toggleAgentConnection();
|
||||
void toggleAgentConnection({ silent: true });
|
||||
}, [autoConnect, connected, enabled]);
|
||||
|
||||
function clearAgentSession(patch: Parameters<typeof setAgentState>[0] = {}) {
|
||||
@@ -630,6 +637,26 @@ function AgentConnectView({ theme, url, token, enabled, connected, activity, con
|
||||
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">
|
||||
@@ -640,43 +667,27 @@ function AgentConnectView({ theme, url, token, enabled, connected, activity, con
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{AGENT_CONNECT_STEPS.map((step) => {
|
||||
{AGENT_CONNECT_STEPS.map((step, index) => {
|
||||
const command = "command" in step ? step.command : "";
|
||||
return (
|
||||
<div key={step.title} 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>
|
||||
<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 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>
|
||||
<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">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { App, Button, Form, Input, Modal, Progress, Select, Switch, Tabs } from "antd";
|
||||
import { CircleAlert, Cloud, KeyRound, Link2, Plus, RefreshCw, ShieldCheck, Trash2, Wifi } from "lucide-react";
|
||||
import { App, Button, Form, Input, Modal, Progress, Select, Tabs } from "antd";
|
||||
import { CircleAlert, Cloud, Plus, RefreshCw, Trash2, Wifi } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { ModelPicker } from "@/components/model-picker";
|
||||
@@ -7,7 +7,6 @@ import { fetchChannelModels } from "@/services/api/image";
|
||||
import { syncAppDataToWebdav, type AppSyncDomainKey, type AppSyncProgressEvent } from "@/services/app-sync";
|
||||
import { testWebdavConnection, WEBDAV_MANIFEST_FILE_NAME } from "@/services/webdav-sync";
|
||||
import { audioFormatOptions, audioVoiceOptions, normalizeAudioSpeedValue } from "@/lib/audio-generation";
|
||||
import { useAgentStore } from "@/stores/use-agent-store";
|
||||
import { createModelChannel, defaultBaseUrlForApiFormat, filterModelsByCapability, modelOptionLabel, modelOptionsFromChannels, normalizeModelOptionValue, useConfigStore, type AiConfig, type ApiCallFormat, type ConfigTabKey, type ModelCapability, type ModelChannel } from "@/stores/use-config-store";
|
||||
|
||||
type ModelGroup = {
|
||||
@@ -45,12 +44,6 @@ const webdavDomainLabels: Record<AppSyncDomainKey, string> = {
|
||||
"image-workbench": "生图工作台",
|
||||
"video-workbench": "视频创作台",
|
||||
};
|
||||
const codexSetupSteps = [
|
||||
{ title: "方式一:在 Codex 中使用插件", text: "先在 Codex App 安装 Infinite Canvas 插件,再通过插件启动画布,插件会自动启动本地 Canvas Agent 并带上连接信息。" },
|
||||
{ title: "方式二:直接运行 Agent", text: "不使用 Codex 插件时,在终端运行下面命令,再回到网页里连接或手动填入 Local URL 和 Connect token。", command: "npx -y @basketikun/canvas-agent" },
|
||||
];
|
||||
const codexPluginRemoveCommand = "codex plugin remove infinite-canvas";
|
||||
const codexMcpRemoveCommand = "codex mcp remove infinite-canvas";
|
||||
|
||||
function createWebdavDomainProgress(): Record<AppSyncDomainKey, WebdavDomainProgress> {
|
||||
return webdavDomainKeys.reduce(
|
||||
@@ -77,16 +70,6 @@ export function AppConfigPanel({ showDoneButton = false, initialTab = "channels"
|
||||
const shouldPromptContinue = useConfigStore((state) => state.shouldPromptContinue);
|
||||
const setConfigDialogOpen = useConfigStore((state) => state.setConfigDialogOpen);
|
||||
const clearPromptContinue = useConfigStore((state) => state.clearPromptContinue);
|
||||
const agentUrl = useAgentStore((state) => state.url);
|
||||
const agentToken = useAgentStore((state) => state.token);
|
||||
const agentConnected = useAgentStore((state) => state.connected);
|
||||
const agentEnabled = useAgentStore((state) => state.enabled);
|
||||
const agentActivity = useAgentStore((state) => state.activity);
|
||||
const agentConnectError = useAgentStore((state) => state.connectError);
|
||||
const agentConfirmTools = useAgentStore((state) => state.confirmTools);
|
||||
const setAgentState = useAgentStore((state) => state.setAgentState);
|
||||
const connectAgent = useAgentStore((state) => state.connectAgent);
|
||||
const disconnectAgent = useAgentStore((state) => state.disconnectAgent);
|
||||
const modelOptions = config.models.map((model) => ({ label: modelOptionLabel(config, model), value: model }));
|
||||
const webdavReady = Boolean(webdav.url.trim());
|
||||
useEffect(() => setActiveTab(initialTab), [initialTab]);
|
||||
@@ -222,14 +205,6 @@ export function AppConfigPanel({ showDoneButton = false, initialTab = "channels"
|
||||
}
|
||||
};
|
||||
|
||||
const updateAgentConfig = (patch: { url?: string; token?: string }) => {
|
||||
setAgentState({ ...patch, connectError: "" });
|
||||
if (patch.url !== undefined) localStorage.setItem("canvas-agent-url", patch.url.trim().replace(/\/$/, ""));
|
||||
if (patch.token !== undefined) localStorage.setItem("canvas-agent-token", patch.token);
|
||||
};
|
||||
|
||||
const toggleAgentConnection = () => (agentEnabled ? disconnectAgent({ connectError: "" }) : connectAgent());
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs
|
||||
@@ -423,67 +398,6 @@ export function AppConfigPanel({ showDoneButton = false, initialTab = "channels"
|
||||
</Form>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "codex",
|
||||
label: "Codex",
|
||||
children: (
|
||||
<Form layout="vertical" requiredMark={false}>
|
||||
<section className="rounded-lg border border-stone-200 p-3 dark:border-stone-800">
|
||||
<div className="mb-3 flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-sm font-semibold">
|
||||
<Link2 className="size-4" />
|
||||
连接本地 Codex
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-stone-500">用于画布 Agent 连接本机 Codex 插件启动的 Canvas Agent。</div>
|
||||
</div>
|
||||
<div className={agentConnectError ? "text-xs text-red-600" : "text-xs text-stone-500"}>{agentConnectError ? "连接失败" : agentConnected ? agentActivity || "已连接" : agentEnabled ? "连接中" : "未连接"}</div>
|
||||
</div>
|
||||
<div className="mb-4 grid gap-2 md:grid-cols-2">
|
||||
{codexSetupSteps.map((step, index) => (
|
||||
<div key={step.title} className="rounded-md border border-stone-200 p-3 dark:border-stone-800">
|
||||
<div className="text-xs font-semibold text-stone-500">连接方式 {index + 1}</div>
|
||||
<div className="mt-1 text-sm font-medium">{step.title}</div>
|
||||
<div className="mt-1 text-xs leading-5 text-stone-500">{step.text}</div>
|
||||
{step.command ? <code className="mt-2 block overflow-x-auto rounded bg-stone-100 px-2 py-1.5 text-[11px] text-stone-700 dark:bg-stone-900 dark:text-stone-200">{step.command}</code> : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mb-4 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs leading-5 text-amber-800 dark:border-amber-900/60 dark:bg-amber-950/30 dark:text-amber-200">
|
||||
<div className="font-semibold">Codex 插件提醒</div>
|
||||
<div className="mt-1">只有安装 Codex 插件或手动添加 MCP 后,工具列表才会进入 Codex 上下文并增加 token 消耗;仅运行 `npx -y @basketikun/canvas-agent` 启动本地 Agent 不会安装 MCP。</div>
|
||||
<code className="mt-2 block overflow-x-auto rounded bg-white/70 px-2 py-1.5 text-[11px] text-amber-900 dark:bg-black/20 dark:text-amber-100">移除插件:{codexPluginRemoveCommand}</code>
|
||||
<code className="mt-1 block overflow-x-auto rounded bg-white/70 px-2 py-1.5 text-[11px] text-amber-900 dark:bg-black/20 dark:text-amber-100">移除手动 MCP:{codexMcpRemoveCommand}</code>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Form.Item label="Local URL" className="mb-4">
|
||||
<Input prefix={<Link2 className="mr-1 size-4 text-stone-400" />} value={agentUrl} placeholder="http://127.0.0.1:17371" onChange={(event) => updateAgentConfig({ url: event.target.value })} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Connect token" className="mb-4">
|
||||
<Input.Password prefix={<KeyRound className="mr-1 size-4 text-stone-400" />} value={agentToken} placeholder="自动发现,或手动填入 Connect token" onChange={(event) => updateAgentConfig({ token: event.target.value })} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
{agentConnectError ? <div className="mb-3 rounded-md border border-red-200 px-3 py-2 text-xs text-red-600 dark:border-red-900/60">{agentConnectError}</div> : null}
|
||||
<div className="mb-3 flex justify-end">
|
||||
<Button type={agentEnabled ? "default" : "primary"} icon={<Wifi className="size-4" />} onClick={toggleAgentConnection}>
|
||||
{agentConnected ? "断开" : agentEnabled ? "取消连接" : "连接"}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 rounded-md border border-stone-200 px-3 py-2 dark:border-stone-800">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<ShieldCheck className="size-4 text-stone-500" />
|
||||
<div>
|
||||
<div className="text-sm font-medium">执行画布操作前确认</div>
|
||||
<div className="mt-0.5 text-xs text-stone-500">关闭后,本地 Codex 可直接执行画布工具调用。不再需要人工确认</div>
|
||||
</div>
|
||||
</div>
|
||||
<Switch checked={agentConfirmTools} onChange={(confirmTools) => setAgentState({ confirmTools })} />
|
||||
</div>
|
||||
</section>
|
||||
</Form>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{showDoneButton ? (
|
||||
|
||||
@@ -9,7 +9,6 @@ import { UserStatusActions } from "@/components/layout/user-status-actions";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useAgentStore } from "@/stores/use-agent-store";
|
||||
import { useConfigStore } from "@/stores/use-config-store";
|
||||
|
||||
export function AppTopNav() {
|
||||
const { pathname } = useLocation();
|
||||
@@ -28,7 +27,7 @@ export function AppTopNav() {
|
||||
useEffect(() => {
|
||||
if (autoConnectRef.current || agentEnabled || agentConnected || !agentToken.trim()) return;
|
||||
autoConnectRef.current = true;
|
||||
connectAgent();
|
||||
connectAgent({ silent: true });
|
||||
}, [agentConnected, agentEnabled, agentToken, connectAgent]);
|
||||
|
||||
return (
|
||||
@@ -82,7 +81,6 @@ export function AppTopNav() {
|
||||
</div>
|
||||
|
||||
<div className="my-auto flex h-9 min-w-0 items-center justify-end gap-2 justify-self-end whitespace-nowrap">
|
||||
<CodexStatusButton />
|
||||
<Tooltip title={panelOpen ? "收起 Agent" : "打开 Agent"}>
|
||||
<Button type="text" shape="circle" className="!h-8 !w-8 !min-w-8" icon={<Bot className="size-4" />} onClick={togglePanel} aria-label="打开 Agent" />
|
||||
</Tooltip>
|
||||
@@ -97,21 +95,3 @@ export function AppTopNav() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CodexStatusButton() {
|
||||
const connected = useAgentStore((state) => state.connected);
|
||||
const enabled = useAgentStore((state) => state.enabled);
|
||||
const activity = useAgentStore((state) => state.activity);
|
||||
const connectError = useAgentStore((state) => state.connectError);
|
||||
const openConfigDialog = useConfigStore((state) => state.openConfigDialog);
|
||||
const color = connectError ? "#dc2626" : connected ? "#16a34a" : enabled ? "#d97706" : "currentColor";
|
||||
const title = connectError || (connected ? activity || "Codex 已连接" : enabled ? "Codex 连接中" : "Codex 未连接");
|
||||
return (
|
||||
<Tooltip title={title}>
|
||||
<Button type="text" shape="circle" className="relative !h-8 !w-8 !min-w-8" onClick={() => openConfigDialog(false, "codex")} aria-label="Codex 连接状态">
|
||||
<span className="mx-auto block size-4" style={{ background: color, WebkitMask: "url(/icons/openai.svg) center / contain no-repeat", mask: "url(/icons/openai.svg) center / contain no-repeat" }} />
|
||||
<span className="absolute right-1 top-1 size-2 rounded-full border border-background" style={{ background: color }} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user