Compare commits

...

1 Commits

Author SHA1 Message Date
Jiayuan Zhang
67581c47f3 feat(runtimes): simplify "Add a computer" dialog (MUL-2408)
- Align Runtimes connect flow with Onboarding CLI install: install.sh + multica setup
- Drop manual "I've started the daemon" step; subscribe to daemon:register WS and auto-advance
- Rename Connect remote machine -> Add a computer, remove EC2-specific copy
- Rework UI per web design guidelines (focus rings, aria labels, live status, footer alignment)
- Fix DialogFooter negative-margin overflow with p-0 content; use outline Cancel button

Co-authored-by: multica-agent <github@multica.ai>
2026-05-19 14:33:06 +08:00
3 changed files with 220 additions and 295 deletions

View File

@@ -3,7 +3,7 @@
"title": "Runtimes",
"tagline": "Machines and cloud workers running CLI sessions for your agents.",
"learn_more": "Learn more →",
"connect_remote": "Connect remote machine",
"connect_remote": "Add a computer",
"search_placeholder": "Search runtimes…",
"scope_mine": "Mine",
"scope_all": "All",
@@ -13,7 +13,7 @@
"filter_all_description": "All runtimes in this view",
"empty": {
"title": "No runtimes yet",
"hint": "Desktop auto-scans your local machine. For AWS EC2 or other remote machines, connect them using the setup wizard."
"hint": "Desktop auto-scans this computer. To use another machine — a server, dev box, or any computer with a terminal — add it here."
},
"no_matches": {
"title": "No matches",
@@ -139,33 +139,24 @@
"queued_one": "{{count}} queued",
"queued_other": "{{count}} queued",
"connect": {
"title": "Connect a remote machine",
"description": "Run these commands on your remote machine (e.g. AWS EC2) to install the Multica CLI and register it as a runtime.",
"step1": "1. Install the CLI",
"step2": "2. Configure",
"step3": "3. Login with a personal access token",
"step3_hint_prefix": "Create one in ",
"step3_hint_destination": "Settings → Tokens",
"step3_hint_suffix": ".",
"step4": "4. Start the daemon",
"security_label": "Security: ",
"security_body": "Use an EC2 IAM role or least-privilege credentials. Never put root keys into agent ",
"security_body_suffix": ". The daemon uses outbound connections only — no inbound ports needed.",
"troubleshooting": "Troubleshooting",
"trouble_check_status": "Check status: ",
"trouble_view_logs": "View logs: ",
"trouble_verify_provider": "Verify provider: ",
"trouble_remote_note_prefix": "Desktop auto-scans only your local machine. Remote machines must run ",
"trouble_remote_note_suffix": " separately.",
"title": "Add a computer",
"description": "Run these two commands on the computer you want to add. We'll detect it the moment the daemon comes online.",
"step1_label": "Install the Multica CLI",
"step2_label": "Start the daemon",
"step2_hint": "Opens a browser to sign in, then keeps the daemon running in the background.",
"live_listening": "Waiting for your computer",
"live_listening_hint": "We'll detect it as soon as the daemon starts — usually under a minute.",
"troubleshooting": "Can't open a browser on that computer?",
"trouble_intro": "Use a token instead of the browser sign-in. This replaces step 2 and works anywhere you have a terminal.",
"trouble_token_hint_prefix": "Create a token in ",
"trouble_token_hint_destination": "Settings → Tokens",
"trouble_token_hint_suffix": ". The daemon only makes outbound connections — no inbound ports required.",
"trouble_check_status": "Check status",
"trouble_view_logs": "View logs",
"copy_aria": "Copy",
"cancel": "Cancel",
"started_daemon": "I've started the daemon",
"waiting_title": "Waiting for runtime…",
"waiting_description": "Listening for your remote daemon to register. This page updates automatically — no need to refresh.",
"waiting_hint_prefix": "Run ",
"waiting_hint_suffix": " on the remote machine to verify it's running.",
"back": "Back",
"success_title": "Runtime connected!",
"success_description": "Your remote machine has registered as a runtime. You can now create an agent that dispatches tasks to it.",
"success_title": "Computer connected",
"success_description": "Your computer is online and ready. Create an agent and it can start picking up tasks here.",
"view_runtime": "View runtime",
"create_agent": "Create an agent"
},

View File

@@ -3,7 +3,7 @@
"title": "运行时",
"tagline": "为智能体跑 CLI 会话的机器和云端 worker。",
"learn_more": "了解更多 →",
"connect_remote": "连接远程机器",
"connect_remote": "添加电脑",
"search_placeholder": "搜索运行时...",
"scope_mine": "我的",
"scope_all": "全部",
@@ -13,7 +13,7 @@
"filter_all_description": "当前视图下的所有运行时",
"empty": {
"title": "还没有运行时",
"hint": "桌面端会自动扫描本地机器。对于 AWS EC2 或其他远程机器,可使用连接向导。"
"hint": "桌面端会自动扫描这台电脑。想把另一台电脑加进来 —— 服务器、远程开发机,或任何能开终端的电脑 —— 在这里添加。"
},
"no_matches": {
"title": "无匹配",
@@ -130,33 +130,24 @@
"running_other": "{{count}} 个运行中",
"queued_other": "{{count}} 个排队中",
"connect": {
"title": "连接远程机器",
"description": "在远程机器(例如 AWS EC2上运行这些命令安装 Multica CLI 并注册为运行时。",
"step1": "1. 安装 CLI",
"step2": "2. 配置",
"step3": "3. 用个人访问令牌登录",
"step3_hint_prefix": "可在 ",
"step3_hint_destination": "设置 → Tokens",
"step3_hint_suffix": " 中创建。",
"step4": "4. 启动守护进程",
"security_label": "安全提示: ",
"security_body": "使用 EC2 IAM 角色或最小权限凭证。切勿把 root 凭证写入智能体的 ",
"security_body_suffix": "。守护进程只使用出站连接 —— 无需开放入站端口。",
"troubleshooting": "故障排查",
"trouble_check_status": "检查状态: ",
"trouble_view_logs": "查看日志: ",
"trouble_verify_provider": "验证 provider ",
"trouble_remote_note_prefix": "桌面端只会自动扫描本地机器。远程机器需要单独运行 ",
"trouble_remote_note_suffix": "。",
"title": "添加电脑",
"description": "在要添加的电脑上运行这两条命令。守护进程一上线,这里就会自动识别。",
"step1_label": "安装 Multica CLI",
"step2_label": "启动守护进程",
"step2_hint": "会打开浏览器登录,然后在后台保持守护进程运行。",
"live_listening": "等待你的电脑上线",
"live_listening_hint": "守护进程一启动就会被识别,通常不到一分钟。",
"troubleshooting": "那台电脑打不开浏览器?",
"trouble_intro": "改用 token 登录替代第 2 步,适用于任何有终端的环境。",
"trouble_token_hint_prefix": " ",
"trouble_token_hint_destination": "设置 → Tokens",
"trouble_token_hint_suffix": " 中创建 token。守护进程只使用出站连接 —— 无需开放入站端口。",
"trouble_check_status": "检查状态",
"trouble_view_logs": "查看日志",
"copy_aria": "复制",
"cancel": "取消",
"started_daemon": "我已启动守护进程",
"waiting_title": "正在等待运行时...",
"waiting_description": "正在监听远程守护进程的注册。此页面会自动更新,无需刷新。",
"waiting_hint_prefix": "在远程机器上运行 ",
"waiting_hint_suffix": " 来确认它在运行。",
"back": "返回",
"success_title": "运行时已连接!",
"success_description": "你的远程机器已注册为运行时。现在可以创建一个智能体,把 task 派发到它上。",
"success_title": "电脑已连接",
"success_description": "电脑已经上线就绪。创建一个智能体task 就可以派发到这里运行。",
"view_runtime": "查看运行时",
"create_agent": "创建智能体"
},

View File

@@ -1,16 +1,7 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import {
Check,
ChevronRight,
Copy,
Loader2,
Server,
ShieldAlert,
Terminal,
Wrench,
} from "lucide-react";
import { Check, ChevronRight, Copy, Terminal } from "lucide-react";
import { useQueryClient } from "@tanstack/react-query";
import { useWorkspaceId } from "@multica/core/hooks";
import { runtimeKeys } from "@multica/core/runtimes/queries";
@@ -28,47 +19,41 @@ import { Button } from "@multica/ui/components/ui/button";
import { useNavigation } from "../../navigation";
import { useT } from "../../i18n";
type Step = "instructions" | "waiting" | "success";
type Step = "instructions" | "success";
const INSTALL_CMD =
"curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash";
const SETUP_CMD = "multica setup";
const TOKEN_CMD = `multica config set server_url https://api.multica.ai
multica config set app_url https://multica.ai
multica login --token <YOUR_TOKEN>
multica daemon start`;
export function ConnectRemoteDialog({ onClose }: { onClose: () => void }) {
const [step, setStep] = useState<Step>("instructions");
const [copied, setCopied] = useState<string | null>(null);
const wsId = useWorkspaceId();
const slug = useWorkspaceSlug();
const qc = useQueryClient();
const navigation = useNavigation();
const newRuntimeIdRef = useRef<string | null>(null);
// Listen for a new runtime registration while the dialog is open
// `multica setup` is one blocking command that handles config + login
// + daemon start; the dialog passively listens for the resulting
// `daemon:register` WS event and auto-advances to success.
const handleDaemonRegister = useCallback(
(payload: unknown) => {
if (step === "waiting" || step === "instructions") {
qc.invalidateQueries({ queryKey: runtimeKeys.all(wsId) });
const p = payload as Record<string, unknown> | null;
if (p?.runtime_id && typeof p.runtime_id === "string") {
newRuntimeIdRef.current = p.runtime_id;
}
setStep("success");
if (step !== "instructions") return;
qc.invalidateQueries({ queryKey: runtimeKeys.all(wsId) });
const p = payload as Record<string, unknown> | null;
if (p?.runtime_id && typeof p.runtime_id === "string") {
newRuntimeIdRef.current = p.runtime_id;
}
setStep("success");
},
[step, qc, wsId],
);
useWSEvent("daemon:register", handleDaemonRegister);
const copyToClipboard = useCallback(
(text: string, key: string) => {
navigator.clipboard.writeText(text);
setCopied(key);
},
[],
);
useEffect(() => {
if (!copied) return;
const t = setTimeout(() => setCopied(null), 2000);
return () => clearTimeout(t);
}, [copied]);
const handleGoToAgents = () => {
onClose();
if (slug) {
@@ -87,18 +72,8 @@ export function ConnectRemoteDialog({ onClose }: { onClose: () => void }) {
return (
<Dialog open onOpenChange={(v) => !v && onClose()}>
<DialogContent className="flex max-h-[85vh] flex-col sm:max-w-xl">
{step === "instructions" && (
<InstructionsStep
copied={copied}
onCopy={copyToClipboard}
onNext={() => setStep("waiting")}
onClose={onClose}
/>
)}
{step === "waiting" && (
<WaitingStep onBack={() => setStep("instructions")} />
)}
<DialogContent className="flex max-h-[85vh] flex-col gap-0 p-0 sm:max-w-lg">
{step === "instructions" && <InstructionsStep onClose={onClose} />}
{step === "success" && (
<SuccessStep
onGoToAgents={handleGoToAgents}
@@ -113,232 +88,195 @@ export function ConnectRemoteDialog({ onClose }: { onClose: () => void }) {
}
// ---------------------------------------------------------------------------
// Step 1: Installation instructions
// Copy button + code row — mirrors onboarding/CliInstallInstructions
// ---------------------------------------------------------------------------
const INSTALL_CMD = "curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash";
function CopyButton({ text, ariaLabel }: { text: string; ariaLabel: string }) {
const [copied, setCopied] = useState(false);
const CONFIGURE_CMD = `multica config set server_url https://api.multica.ai
multica config set app_url https://multica.ai`;
useEffect(() => {
if (!copied) return;
const t = setTimeout(() => setCopied(false), 2000);
return () => clearTimeout(t);
}, [copied]);
const LOGIN_CMD = "multica login --token <YOUR_TOKEN>";
const handleCopy = () => {
navigator.clipboard.writeText(text);
setCopied(true);
};
const START_CMD = `multica daemon start --device-name "my-ec2-instance"
multica daemon status`;
function CodeBlock({
code,
copyKey,
copied,
onCopy,
}: {
code: string;
copyKey: string;
copied: string | null;
onCopy: (text: string, key: string) => void;
}) {
const isCopied = copied === copyKey;
return (
<div className="relative rounded-md border bg-muted/50">
<pre className="overflow-x-auto p-2.5 pr-10 font-mono text-xs leading-relaxed text-foreground">
{code}
</pre>
<button
type="button"
onClick={() => onCopy(code, copyKey)}
className="absolute top-1.5 right-1.5 flex h-6 w-6 items-center justify-center rounded border bg-background text-muted-foreground transition-colors hover:text-foreground"
>
{isCopied ? (
<Check className="h-3 w-3 text-success" />
) : (
<Copy className="h-3 w-3" />
)}
</button>
<button
type="button"
onClick={handleCopy}
aria-label={ariaLabel}
className="shrink-0 rounded p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{copied ? (
<Check className="h-3.5 w-3.5 text-success" aria-hidden />
) : (
<Copy className="h-3.5 w-3.5" aria-hidden />
)}
</button>
);
}
function CommandStep({
n,
label,
cmd,
copyAria,
}: {
n: number;
label: string;
cmd: string;
copyAria: string;
}) {
return (
<div>
<p className="mb-1.5 text-xs font-medium text-foreground">
{n}. {label}
</p>
<div className="flex items-start gap-2 rounded-lg bg-muted px-3 py-2.5 font-mono text-sm">
<Terminal
className="mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground"
aria-hidden
/>
<code className="min-w-0 flex-1 break-all whitespace-pre-wrap tabular-nums">
{cmd}
</code>
<CopyButton text={cmd} ariaLabel={copyAria} />
</div>
</div>
);
}
function InstructionsStep({
copied,
onCopy,
onNext,
onClose,
}: {
copied: string | null;
onCopy: (text: string, key: string) => void;
onNext: () => void;
onClose: () => void;
}) {
// ---------------------------------------------------------------------------
// Step 1: Instructions
// ---------------------------------------------------------------------------
function InstructionsStep({ onClose }: { onClose: () => void }) {
const { t } = useT("runtimes");
return (
<>
<DialogHeader>
<DialogTitle>{t(($) => $.connect.title)}</DialogTitle>
<DialogDescription>
<DialogHeader className="px-6 pt-6 pb-2">
<DialogTitle className="text-base text-balance">
{t(($) => $.connect.title)}
</DialogTitle>
<DialogDescription className="text-xs text-balance">
{t(($) => $.connect.description)}
</DialogDescription>
</DialogHeader>
<div className="-mx-4 min-h-0 flex-1 overflow-y-auto px-4">
<div className="space-y-3">
<div>
<div className="mb-1 flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
<Terminal className="h-3.5 w-3.5" />
{t(($) => $.connect.step1)}
</div>
<CodeBlock
code={INSTALL_CMD}
copyKey="install"
copied={copied}
onCopy={onCopy}
/>
</div>
<div className="min-h-0 flex-1 overflow-y-auto px-6 py-4">
<div className="space-y-4">
<CommandStep
n={1}
label={t(($) => $.connect.step1_label)}
cmd={INSTALL_CMD}
copyAria={t(($) => $.connect.copy_aria)}
/>
<div>
<div className="mb-1 flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
<Server className="h-3.5 w-3.5" />
{t(($) => $.connect.step2)}
</div>
<CodeBlock
code={CONFIGURE_CMD}
copyKey="config"
copied={copied}
onCopy={onCopy}
<CommandStep
n={2}
label={t(($) => $.connect.step2_label)}
cmd={SETUP_CMD}
copyAria={t(($) => $.connect.copy_aria)}
/>
</div>
<div>
<div className="mb-1 flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
{t(($) => $.connect.step3)}
</div>
<CodeBlock
code={LOGIN_CMD}
copyKey="login"
copied={copied}
onCopy={onCopy}
/>
<p className="mt-1 text-[11px] text-muted-foreground">
{t(($) => $.connect.step3_hint_prefix)}
<span className="font-medium text-foreground">
{t(($) => $.connect.step3_hint_destination)}
</span>
{t(($) => $.connect.step3_hint_suffix)}
<p className="mt-1.5 text-[11px] leading-[1.55] text-muted-foreground">
{t(($) => $.connect.step2_hint)}
</p>
</div>
<div>
<div className="mb-1 flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
{t(($) => $.connect.step4)}
</div>
<CodeBlock
code={START_CMD}
copyKey="start"
copied={copied}
onCopy={onCopy}
/>
</div>
<LiveListening />
<div className="rounded-md border border-warning/30 bg-warning/5 p-2.5">
<div className="flex items-start gap-2">
<ShieldAlert className="mt-0.5 h-3.5 w-3.5 shrink-0 text-warning" />
<div className="text-[11px] leading-relaxed text-muted-foreground">
<span className="font-medium text-foreground">{t(($) => $.connect.security_label)}</span>
{t(($) => $.connect.security_body)}
<code className="rounded bg-muted px-1 py-0.5 font-mono text-[10px]">
{"custom_env"}
</code>
{t(($) => $.connect.security_body_suffix)}
</div>
</div>
</div>
<details className="group pb-1">
<summary className="flex cursor-pointer items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground">
<Wrench className="h-3.5 w-3.5" />
{t(($) => $.connect.troubleshooting)}
<ChevronRight className="h-3 w-3 transition-transform group-open:rotate-90" />
</summary>
<ul className="mt-1.5 list-disc space-y-0.5 pl-8 text-[11px] text-muted-foreground">
<li>
{t(($) => $.connect.trouble_check_status)}
<code className="rounded bg-muted px-1 py-0.5 font-mono text-[10px]">
{"multica daemon status"}
</code>
</li>
<li>
{t(($) => $.connect.trouble_view_logs)}
<code className="rounded bg-muted px-1 py-0.5 font-mono text-[10px]">
{"multica daemon logs -f"}
</code>
</li>
<li>
{t(($) => $.connect.trouble_verify_provider)}
<code className="rounded bg-muted px-1 py-0.5 font-mono text-[10px]">
{"claude --version"}
</code>
</li>
<li>
{t(($) => $.connect.trouble_remote_note_prefix)}
<code className="rounded bg-muted px-1 py-0.5 font-mono text-[10px]">
{"multica daemon"}
</code>
{t(($) => $.connect.trouble_remote_note_suffix)}
</li>
</ul>
</details>
<TroubleshootingDetails />
</div>
</div>
<DialogFooter>
<Button variant="ghost" onClick={onClose}>
<DialogFooter className="m-0 rounded-b-xl border-t bg-muted/30 px-6 py-3">
<Button variant="outline" size="sm" onClick={onClose}>
{t(($) => $.connect.cancel)}
</Button>
<Button onClick={onNext}>
{t(($) => $.connect.started_daemon)}
<ChevronRight className="h-3.5 w-3.5" />
</Button>
</DialogFooter>
</>
);
}
// ---------------------------------------------------------------------------
// Step 2: Waiting for registration
// ---------------------------------------------------------------------------
function WaitingStep({ onBack }: { onBack: () => void }) {
function TroubleshootingDetails() {
const { t } = useT("runtimes");
return (
<>
<DialogHeader>
<DialogTitle>{t(($) => $.connect.waiting_title)}</DialogTitle>
<DialogDescription>
{t(($) => $.connect.waiting_description)}
</DialogDescription>
</DialogHeader>
<div className="flex flex-col items-center gap-3 py-8">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
<p className="text-sm text-muted-foreground">
{t(($) => $.connect.waiting_hint_prefix)}
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">
{"multica daemon status"}
</code>
{t(($) => $.connect.waiting_hint_suffix)}
<details className="group rounded-lg border border-dashed">
<summary className="flex cursor-pointer list-none items-center gap-1.5 px-3 py-2 text-xs font-medium text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
<ChevronRight
className="h-3 w-3 transition-transform group-open:rotate-90"
aria-hidden
/>
{t(($) => $.connect.troubleshooting)}
</summary>
<div className="space-y-2 border-t px-3 pt-2.5 pb-3 text-[11px] leading-[1.55] text-muted-foreground">
<p>{t(($) => $.connect.trouble_intro)}</p>
<CommandStep
n={2}
label={t(($) => $.connect.step2_label)}
cmd={TOKEN_CMD}
copyAria={t(($) => $.connect.copy_aria)}
/>
<p>
{t(($) => $.connect.trouble_token_hint_prefix)}
<span className="font-medium text-foreground">
{t(($) => $.connect.trouble_token_hint_destination)}
</span>
{t(($) => $.connect.trouble_token_hint_suffix)}
</p>
<ul className="space-y-1">
<li className="flex items-center gap-1.5">
<span>{t(($) => $.connect.trouble_check_status)}</span>
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-[10px] text-foreground">
multica daemon status
</code>
</li>
<li className="flex items-center gap-1.5">
<span>{t(($) => $.connect.trouble_view_logs)}</span>
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-[10px] text-foreground">
multica daemon logs -f
</code>
</li>
</ul>
</div>
<DialogFooter>
<Button variant="ghost" onClick={onBack}>
{t(($) => $.connect.back)}
</Button>
</DialogFooter>
</>
</details>
);
}
// ---------------------------------------------------------------------------
// Step 3: Success
// Live-listening indicator
// ---------------------------------------------------------------------------
function LiveListening() {
const { t } = useT("runtimes");
return (
<div
className="flex items-center gap-2.5 rounded-lg border bg-muted/40 px-3 py-2.5 text-xs"
role="status"
aria-live="polite"
>
<span className="relative inline-flex shrink-0" aria-hidden>
<span className="absolute inline-flex h-2 w-2 animate-ping rounded-full bg-success opacity-60 motion-reduce:hidden" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-success" />
</span>
<span className="font-medium text-foreground">
{t(($) => $.connect.live_listening)}
</span>
<span className="text-muted-foreground">
{t(($) => $.connect.live_listening_hint)}
</span>
</div>
);
}
// ---------------------------------------------------------------------------
// Step 2: Success
// ---------------------------------------------------------------------------
function SuccessStep({
@@ -351,28 +289,33 @@ function SuccessStep({
const { t } = useT("runtimes");
return (
<>
<DialogHeader>
<DialogTitle>{t(($) => $.connect.success_title)}</DialogTitle>
<DialogDescription>
<DialogHeader className="px-6 pt-6 pb-2">
<DialogTitle className="text-base text-balance">
{t(($) => $.connect.success_title)}
</DialogTitle>
<DialogDescription className="text-xs text-balance">
{t(($) => $.connect.success_description)}
</DialogDescription>
</DialogHeader>
<div className="flex flex-col items-center gap-3 py-6">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-success/10">
<div className="flex flex-col items-center gap-3 px-6 py-8">
<div
className="flex h-12 w-12 items-center justify-center rounded-full bg-success/10"
aria-hidden
>
<Check className="h-6 w-6 text-success" />
</div>
</div>
<DialogFooter>
<DialogFooter className="m-0 rounded-b-xl border-t bg-muted/30 px-6 py-3">
{onGoToRuntime && (
<Button variant="ghost" onClick={onGoToRuntime}>
<Button variant="ghost" size="sm" onClick={onGoToRuntime}>
{t(($) => $.connect.view_runtime)}
</Button>
)}
<Button onClick={onGoToAgents}>
<Button size="sm" onClick={onGoToAgents}>
{t(($) => $.connect.create_agent)}
<ChevronRight className="h-3.5 w-3.5" />
<ChevronRight className="h-3.5 w-3.5" aria-hidden />
</Button>
</DialogFooter>
</>