mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-24 02:39:42 +02:00
navigator.clipboard is only exposed in a secure context (https or localhost). On self-hosted instances served over plain http:// it is undefined, so every copy / "copy all" / export button silently failed and left the clipboard empty (GitHub #3781). Add a shared copyText(text): Promise<boolean> helper in @multica/ui/lib/clipboard that prefers the async Clipboard API and falls back to a hidden <textarea> + document.execCommand('copy') for non-secure contexts. Migrate all direct navigator.clipboard.writeText call sites (code blocks, agent transcript copy-all, token / webhook / issue-link copy, etc.) to it, gating success side-effects on the returned boolean, and remove the now-redundant copyMarkdown wrapper. Secure-context users keep the native path unchanged. MUL-3068 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
107 lines
3.0 KiB
TypeScript
107 lines
3.0 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { Check, Copy, Terminal } from "lucide-react";
|
|
import { copyText } from "@multica/ui/lib/clipboard";
|
|
import { useLocale } from "../../i18n";
|
|
|
|
const INSTALL_CMD =
|
|
"curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash";
|
|
const SETUP_CMD = "multica setup";
|
|
|
|
/**
|
|
* Scenario-first CLI section. Copy leans into servers / remote dev
|
|
* boxes / headless setups rather than positioning CLI as a
|
|
* lightweight Desktop. Two copy-and-paste command blocks.
|
|
*/
|
|
export function CliSection() {
|
|
const { t } = useLocale();
|
|
const d = t.download.cli;
|
|
|
|
return (
|
|
<section id="cli" className="bg-[#f7f7f5] py-20 text-[#0a0d12] sm:py-24">
|
|
<div className="mx-auto max-w-[820px] px-4 sm:px-6 lg:px-8">
|
|
<h2 className="font-[family-name:var(--font-serif)] text-[2.2rem] leading-[1.1] tracking-[-0.03em] sm:text-[2.6rem]">
|
|
{d.title}
|
|
</h2>
|
|
<p className="mt-4 max-w-[620px] text-[15px] leading-7 text-[#0a0d12]/72">
|
|
{d.sub}
|
|
</p>
|
|
|
|
<div className="mt-10 flex flex-col gap-5">
|
|
<CommandBlock
|
|
label={d.installLabel}
|
|
cmd={INSTALL_CMD}
|
|
copyLabel={d.copyLabel}
|
|
copiedLabel={d.copiedLabel}
|
|
/>
|
|
<CommandBlock
|
|
label={d.startLabel}
|
|
cmd={SETUP_CMD}
|
|
copyLabel={d.copyLabel}
|
|
copiedLabel={d.copiedLabel}
|
|
/>
|
|
</div>
|
|
|
|
<p className="mt-6 text-[13px] text-[#0a0d12]/60">{d.sshNote}</p>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function CommandBlock({
|
|
label,
|
|
cmd,
|
|
copyLabel,
|
|
copiedLabel,
|
|
}: {
|
|
label: string;
|
|
cmd: string;
|
|
copyLabel: string;
|
|
copiedLabel: string;
|
|
}) {
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
const onCopy = async () => {
|
|
if (await copyText(cmd)) {
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 1800);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<p className="mb-2 text-[12px] font-medium uppercase tracking-[0.08em] text-[#0a0d12]/55">
|
|
{label}
|
|
</p>
|
|
<div className="flex items-start gap-3 rounded-xl border border-[#0a0d12]/10 bg-white px-4 py-3 font-mono text-[13.5px]">
|
|
<Terminal
|
|
className="mt-0.5 size-4 shrink-0 text-[#0a0d12]/55"
|
|
aria-hidden
|
|
/>
|
|
<code className="min-w-0 flex-1 whitespace-pre-wrap break-all">
|
|
{cmd}
|
|
</code>
|
|
<button
|
|
type="button"
|
|
onClick={onCopy}
|
|
aria-label={copied ? copiedLabel : copyLabel}
|
|
className="inline-flex shrink-0 items-center gap-1.5 rounded-md px-2 py-1 text-[12px] font-medium text-[#0a0d12]/70 transition-colors hover:bg-[#0a0d12]/5 hover:text-[#0a0d12]"
|
|
>
|
|
{copied ? (
|
|
<>
|
|
<Check className="size-3.5" />
|
|
{copiedLabel}
|
|
</>
|
|
) : (
|
|
<>
|
|
<Copy className="size-3.5" />
|
|
{copyLabel}
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|