feat(web): add how-it-works, open-source, FAQ, and footer sections to landing page

Add four new sections: How it Works (4-step onboarding guide), Open Source
(self-host / no lock-in highlights), FAQ (centered accordion), and Footer
(link columns + giant logo with Game of Life animation).
This commit is contained in:
Jiayuan 2026-04-01 00:18:25 +08:00
parent 11041052d0
commit e9ce376b96

View file

@ -74,7 +74,21 @@ export function MulticaLanding() {
</div>
<div id="preview" className="mt-14 sm:mt-16">
<div className="mt-10 flex items-center justify-center gap-8">
<span className="text-[15px] text-white/50">Works with</span>
<div className="flex items-center gap-6">
<div className="flex items-center gap-2.5 text-white/80">
<ClaudeCodeLogo className="size-5" />
<span className="text-[15px] font-medium">Claude Code</span>
</div>
<div className="flex items-center gap-2.5 text-white/80">
<CodexLogo className="size-5" />
<span className="text-[15px] font-medium">Codex</span>
</div>
</div>
</div>
<div id="preview" className="mt-10 sm:mt-12">
<ProductImage />
</div>
</section>
@ -82,6 +96,10 @@ export function MulticaLanding() {
</div>
<FeaturesSection />
<HowItWorksSection />
<OpenSourceSection />
<FAQSection />
<LandingFooter />
</>
);
}
@ -310,6 +328,511 @@ function FeaturesSection() {
);
}
/* -------------------------------------------------------------------------- */
/* How it Works Section */
/* -------------------------------------------------------------------------- */
const steps = [
{
number: "01",
title: "Sign up & create your workspace",
description:
"Enter your email, verify with a code, and you're in. Your workspace is created automatically — no setup wizard, no configuration forms.",
},
{
number: "02",
title: "Install the CLI & connect your machine",
description:
"Run multica login to authenticate, then multica daemon start. The daemon auto-detects Claude Code and Codex on your machine — plug in and go.",
},
{
number: "03",
title: "Create your first agent",
description:
"Give it a name, write instructions, attach skills, and set triggers. Choose when it activates: on assignment, on comment, or on mention.",
},
{
number: "04",
title: "Assign an issue and watch it work",
description:
"Pick your agent from the assignee dropdown — just like assigning to a teammate. The task is queued, claimed, and executed automatically. Watch progress in real time.",
},
];
function HowItWorksSection() {
return (
<section id="how-it-works" className="bg-[#05070b] text-white">
<div className="mx-auto max-w-[1320px] px-4 py-24 sm:px-6 sm:py-32 lg:px-8 lg:py-40">
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-white/40">
Get started
</p>
<h2 className="mt-4 font-[family-name:var(--font-serif)] text-[2.6rem] leading-[1.05] tracking-[-0.03em] sm:text-[3.4rem] lg:text-[4.2rem]">
Hire your first AI employee
<br />
<span className="text-white/40">in the next hour.</span>
</h2>
<div className="mt-20 grid gap-px overflow-hidden rounded-2xl border border-white/10 bg-white/10 sm:grid-cols-2 lg:grid-cols-4">
{steps.map((step) => (
<div
key={step.number}
className="flex flex-col bg-[#05070b] p-8 lg:p-10"
>
<span className="text-[13px] font-semibold tabular-nums text-white/28">
{step.number}
</span>
<h3 className="mt-4 text-[17px] font-semibold leading-snug text-white sm:text-[18px]">
{step.title}
</h3>
<p className="mt-3 text-[14px] leading-[1.7] text-white/50 sm:text-[15px]">
{step.description}
</p>
</div>
))}
</div>
<div className="mt-14 flex flex-wrap items-center gap-4">
<Link href="/login" className={heroButtonClassName("solid")}>
Get started
</Link>
<Link
href={githubUrl}
target="_blank"
rel="noreferrer"
className={heroButtonClassName("ghost")}
>
<GitHubMark className="size-4" />
View on GitHub
</Link>
</div>
</div>
</section>
);
}
/* -------------------------------------------------------------------------- */
/* Open Source Section */
/* -------------------------------------------------------------------------- */
const openSourceHighlights = [
{
title: "Self-host anywhere",
description:
"Run Multica on your own infrastructure. Docker Compose, single binary, or Kubernetes — your data never leaves your network.",
},
{
title: "No vendor lock-in",
description:
"Bring your own LLM provider, swap agent backends, extend the API. You own the stack, top to bottom.",
},
{
title: "Transparent by default",
description:
"Every line of code is auditable. See exactly how your agents make decisions, how tasks are routed, and where your data flows.",
},
{
title: "Community-driven",
description:
"Built with the community, not just for it. Contribute skills, integrations, and agent backends that benefit everyone.",
},
];
function OpenSourceSection() {
return (
<section id="open-source" className="bg-white text-[#0a0d12]">
<div className="mx-auto max-w-[1320px] px-4 py-24 sm:px-6 sm:py-32 lg:px-8 lg:py-40">
<div className="flex flex-col gap-16 lg:flex-row lg:items-start lg:gap-24">
{/* Left column — heading + CTA */}
<div className="lg:w-[480px] lg:shrink-0">
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-[#0a0d12]/40">
Open source
</p>
<h2 className="mt-4 font-[family-name:var(--font-serif)] text-[2.6rem] leading-[1.05] tracking-[-0.03em] sm:text-[3.4rem] lg:text-[4.2rem]">
Open source
<br />
for all.
</h2>
<p className="mt-6 max-w-[420px] text-[15px] leading-7 text-[#0a0d12]/60 sm:text-[16px]">
Multica is fully open source under the MIT license. Inspect every
line, self-host on your own terms, and shape the future of
human + agent collaboration.
</p>
<div className="mt-8 flex flex-wrap items-center gap-3">
<Link
href={githubUrl}
target="_blank"
rel="noreferrer"
className="inline-flex items-center justify-center gap-2.5 rounded-[12px] bg-[#0a0d12] px-5 py-3 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0d12]/88"
>
<GitHubMark className="size-4" />
Star on GitHub
</Link>
</div>
</div>
{/* Right column — highlight grid */}
<div className="flex-1">
<div className="grid gap-px overflow-hidden rounded-2xl border border-[#0a0d12]/8 bg-[#0a0d12]/8 sm:grid-cols-2">
{openSourceHighlights.map((item) => (
<div key={item.title} className="bg-white p-8 lg:p-10">
<h3 className="text-[17px] font-semibold leading-snug text-[#0a0d12] sm:text-[18px]">
{item.title}
</h3>
<p className="mt-3 text-[14px] leading-[1.7] text-[#0a0d12]/56 sm:text-[15px]">
{item.description}
</p>
</div>
))}
</div>
</div>
</div>
</div>
</section>
);
}
/* -------------------------------------------------------------------------- */
/* FAQ Section */
/* -------------------------------------------------------------------------- */
const faqs = [
{
question: "What coding agents does Multica support?",
answer:
"Multica currently supports Claude Code and OpenAI Codex out of the box. The daemon auto-detects whichever CLIs you have installed. More backends are on the roadmap — and since it's open source, you can add your own.",
},
{
question: "Do I need to self-host, or is there a cloud version?",
answer:
"Both. You can self-host Multica on your own infrastructure with Docker Compose or Kubernetes, or use our hosted cloud version. Your data, your choice.",
},
{
question: "How is this different from just using Claude Code or Codex directly?",
answer:
"Coding agents are great at executing. Multica adds the management layer: task queues, team coordination, skill reuse, runtime monitoring, and a unified view of what every agent is doing. Think of it as the project manager for your agents.",
},
{
question: "Can agents work on long-running tasks autonomously?",
answer:
"Yes. Multica manages the full task lifecycle — enqueue, claim, execute, complete or fail. Agents report blockers proactively and stream progress in real time. You can check in whenever you want or let them run overnight.",
},
{
question: "Is my code safe? Where does agent execution happen?",
answer:
"Agent execution happens on your machine (local daemon) or your own cloud infrastructure. Code never passes through Multica servers. The platform only coordinates task state and broadcasts events.",
},
{
question: "How many agents can I run?",
answer:
"As many as your hardware supports. Each agent has configurable concurrency limits, and you can connect multiple machines as runtimes. There are no artificial caps in the open source version.",
},
];
function FAQSection() {
const [openIndex, setOpenIndex] = useState<number | null>(null);
return (
<section id="faq" className="bg-[#f8f8f8] text-[#0a0d12]">
<div className="mx-auto max-w-[860px] px-4 py-24 sm:px-6 sm:py-32 lg:py-40">
<div className="text-center">
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-[#0a0d12]/40">
FAQ
</p>
<h2 className="mt-4 font-[family-name:var(--font-serif)] text-[2.6rem] leading-[1.05] tracking-[-0.03em] sm:text-[3.4rem] lg:text-[4.2rem]">
Questions & answers.
</h2>
</div>
<div className="mt-14 divide-y divide-[#0a0d12]/10 sm:mt-16">
{faqs.map((faq, i) => (
<div key={faq.question}>
<button
onClick={() =>
setOpenIndex(openIndex === i ? null : i)
}
className="flex w-full items-start justify-between gap-4 py-6 text-left"
>
<span className="text-[16px] font-semibold leading-snug text-[#0a0d12] sm:text-[17px]">
{faq.question}
</span>
<span
className={cn(
"mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full border border-[#0a0d12]/12 text-[#0a0d12]/40 transition-transform",
openIndex === i && "rotate-45",
)}
>
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
>
<path d="M6 1v10M1 6h10" />
</svg>
</span>
</button>
<div
className={cn(
"grid transition-[grid-template-rows] duration-200 ease-out",
openIndex === i
? "grid-rows-[1fr]"
: "grid-rows-[0fr]",
)}
>
<div className="overflow-hidden">
<p className="pb-6 pr-12 text-[14px] leading-[1.7] text-[#0a0d12]/56 sm:text-[15px]">
{faq.answer}
</p>
</div>
</div>
</div>
))}
</div>
</div>
</section>
);
}
/* -------------------------------------------------------------------------- */
/* Footer */
/* -------------------------------------------------------------------------- */
const footerLinks = {
Product: [
{ label: "Features", href: "#features" },
{ label: "How it Works", href: "#how-it-works" },
{ label: "Pricing", href: "#" },
{ label: "Changelog", href: "#" },
],
Resources: [
{ label: "Documentation", href: "#" },
{ label: "API Reference", href: "#" },
{ label: "Blog", href: "#" },
{ label: "Community", href: "#" },
],
Company: [
{ label: "About", href: "#" },
{ label: "Open Source", href: "#open-source" },
{ label: "GitHub", href: githubUrl },
{ label: "Contact", href: "#" },
],
Legal: [
{ label: "Privacy Policy", href: "#" },
{ label: "Terms of Service", href: "#" },
{ label: "MIT License", href: `${githubUrl}/blob/main/LICENSE` },
],
};
function LandingFooter() {
return (
<footer className="bg-white text-[#0a0d12]">
<div className="mx-auto max-w-[1320px] px-4 sm:px-6 lg:px-8">
{/* Top: CTA + link columns */}
<div className="flex flex-col gap-12 border-b border-[#0a0d12]/8 py-16 sm:py-20 lg:flex-row lg:gap-20">
{/* Left — newsletter / CTA */}
<div className="lg:w-[340px] lg:shrink-0">
<Link href="#product" className="flex items-center gap-3">
<MulticaIcon className="size-5 text-[#0a0d12]" noSpin />
<span className="text-[18px] font-semibold tracking-[0.04em] lowercase">
multica
</span>
</Link>
<p className="mt-4 max-w-[300px] text-[14px] leading-[1.7] text-[#0a0d12]/56 sm:text-[15px]">
Project management for human + agent teams. Open source, self-hostable, built for the future of work.
</p>
<div className="mt-6">
<Link
href="/login"
className="inline-flex items-center justify-center rounded-[11px] bg-[#0a0d12] px-5 py-2.5 text-[13px] font-semibold text-white transition-colors hover:bg-[#0a0d12]/88"
>
Get started
</Link>
</div>
</div>
{/* Right — link columns */}
<div className="grid flex-1 grid-cols-2 gap-8 sm:grid-cols-4">
{Object.entries(footerLinks).map(([group, links]) => (
<div key={group}>
<h4 className="text-[12px] font-semibold uppercase tracking-[0.1em] text-[#0a0d12]/40">
{group}
</h4>
<ul className="mt-4 flex flex-col gap-2.5">
{links.map((link) => (
<li key={link.label}>
<Link
href={link.href}
{...(link.href.startsWith("http")
? { target: "_blank", rel: "noreferrer" }
: {})}
className="text-[14px] text-[#0a0d12]/60 transition-colors hover:text-[#0a0d12]"
>
{link.label}
</Link>
</li>
))}
</ul>
</div>
))}
</div>
</div>
{/* Bottom: copyright + legal */}
<div className="flex items-center justify-between py-6">
<p className="text-[13px] text-[#0a0d12]/36">
&copy; {new Date().getFullYear()} Multica. All rights reserved.
</p>
</div>
{/* Giant logo + Game of Life */}
<div className="relative overflow-hidden pb-4">
<div className="flex items-end gap-6 sm:gap-8">
<MulticaIcon
className="size-[clamp(4rem,12vw,10rem)] shrink-0 text-[#0a0d12]"
noSpin
/>
<span className="font-[family-name:var(--font-serif)] text-[clamp(6rem,22vw,16rem)] font-normal leading-[0.82] tracking-[-0.04em] text-[#0a0d12] lowercase">
multica
</span>
</div>
<div className="pointer-events-none absolute bottom-0 right-0 top-0 w-[40%]">
<GameOfLife />
</div>
</div>
</div>
</footer>
);
}
/* -------------------------------------------------------------------------- */
/* Game of Life */
/* -------------------------------------------------------------------------- */
function GameOfLife() {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const cellSize = 10;
let cols = 0;
let rows = 0;
let grid: boolean[][] = [];
function resize() {
const dpr = window.devicePixelRatio || 1;
const rect = canvas!.getBoundingClientRect();
canvas!.width = rect.width * dpr;
canvas!.height = rect.height * dpr;
ctx!.setTransform(dpr, 0, 0, dpr, 0, 0);
const newCols = Math.ceil(rect.width / cellSize);
const newRows = Math.ceil(rect.height / cellSize);
if (newCols !== cols || newRows !== rows) {
cols = newCols;
rows = newRows;
grid = initGrid(cols, rows);
}
}
function initGrid(c: number, r: number): boolean[][] {
return Array.from({ length: c }, () =>
Array.from({ length: r }, () => Math.random() < 0.3),
);
}
function step() {
const next: boolean[][] = Array.from({ length: cols }, () =>
Array(rows).fill(false),
);
for (let x = 0; x < cols; x++) {
for (let y = 0; y < rows; y++) {
let neighbors = 0;
for (let dx = -1; dx <= 1; dx++) {
for (let dy = -1; dy <= 1; dy++) {
if (dx === 0 && dy === 0) continue;
const nx = x + dx;
const ny = y + dy;
if (nx >= 0 && nx < cols && ny >= 0 && ny < rows && grid[nx]?.[ny]) {
neighbors++;
}
}
}
if (grid[x]?.[y]) {
next[x]![y] = neighbors === 2 || neighbors === 3;
} else {
next[x]![y] = neighbors === 3;
}
}
}
grid = next;
}
function draw() {
const rect = canvas!.getBoundingClientRect();
ctx!.clearRect(0, 0, rect.width, rect.height);
for (let x = 0; x < cols; x++) {
for (let y = 0; y < rows; y++) {
if (grid[x]?.[y]) {
ctx!.fillStyle = "rgba(10, 13, 18, 0.12)";
ctx!.beginPath();
ctx!.arc(
x * cellSize + cellSize / 2,
y * cellSize + cellSize / 2,
3,
0,
Math.PI * 2,
);
ctx!.fill();
}
}
}
}
resize();
let frame: number;
let lastStep = 0;
const interval = 300;
function loop(time: number) {
if (time - lastStep > interval) {
step();
draw();
lastStep = time;
}
frame = requestAnimationFrame(loop);
}
draw();
frame = requestAnimationFrame(loop);
const onResize = () => resize();
window.addEventListener("resize", onResize);
return () => {
cancelAnimationFrame(frame);
window.removeEventListener("resize", onResize);
};
}, []);
return (
<canvas
ref={canvasRef}
className="size-full"
style={{ maskImage: "linear-gradient(to right, transparent, black 30%)" }}
/>
);
}
/* -------------------------------------------------------------------------- */
/* Shared components */
/* -------------------------------------------------------------------------- */
@ -362,6 +885,32 @@ function ImageIcon({ className }: { className?: string }) {
}
function ClaudeCodeLogo({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
aria-hidden="true"
className={className}
fill="currentColor"
>
<path d="M15.31 3.99A11.2 11.2 0 0 0 12 3.6c-1.14 0-2.24.14-3.31.39C5.45 4.93 3 7.98 3 12c0 4.02 2.45 7.07 5.69 8.01A11.2 11.2 0 0 0 12 20.4c1.14 0 2.24-.14 3.31-.39C18.55 19.07 21 16.02 21 12c0-4.02-2.45-7.07-5.69-8.01ZM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm2.75 13.5a.75.75 0 0 1-1.06.06l-1.5-1.33-1.5 1.33a.75.75 0 1 1-1-1.12l1.75-1.56V9.5a.75.75 0 0 1 1.5 0v3.38l1.75 1.56a.75.75 0 0 1 .06 1.06Z" />
</svg>
);
}
function CodexLogo({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
aria-hidden="true"
className={className}
fill="currentColor"
>
<path d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073ZM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494ZM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646ZM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872v.024Zm16.597 3.855-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667Zm2.01-3.023-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66v.018ZM8.318 12.898l-2.024-1.168a.074.074 0 0 1-.038-.052V6.095a4.494 4.494 0 0 1 7.37-3.456l-.14.081-4.78 2.758a.795.795 0 0 0-.392.681l-.014 6.739h.018Zm1.1-2.36 2.602-1.5 2.595 1.5v2.999l-2.595 1.5-2.602-1.5v-3Z" />
</svg>
);
}
function ProductImage() {
return (
<div>