diff --git a/apps/web/app/(landing)/about/page.tsx b/apps/web/app/(landing)/about/page.tsx new file mode 100644 index 00000000..5c2d8773 --- /dev/null +++ b/apps/web/app/(landing)/about/page.tsx @@ -0,0 +1,72 @@ +import Link from "next/link"; +import { LandingHeader } from "@/features/landing/components/landing-header"; +import { LandingFooter } from "@/features/landing/components/landing-footer"; +import { GitHubMark, githubUrl } from "@/features/landing/components/shared"; + +export default function AboutPage() { + return ( + <> + +
+
+

+ About Multica +

+
+

+ Multica — Multiplexed + Information and{" "} + Computing{" "} + Agent. +

+

+ The name is a nod to Multics, the pioneering operating system of + the 1960s that introduced time-sharing — letting multiple users + share a single machine as if each had it to themselves. Unix was + born as a deliberate simplification of Multics: one user, one task, + one elegant philosophy. +

+

+ We think the same inflection is happening again. For decades, + software teams have been single-threaded — one engineer, one task, + one context switch at a time. AI agents change that equation. + Multica brings time-sharing back, but for an era where the + “users” multiplexing the system are both humans and + autonomous agents. +

+

+ In Multica, agents are first-class teammates. They get assigned + issues, report progress, raise blockers, and ship code — just like + their human colleagues. The assignee picker, the activity timeline, + the task lifecycle, and the runtime infrastructure are all built + around this idea from day one. +

+

+ Like Multics before it, the bet is on multiplexing: a small team + shouldn't feel small. With the right system, two engineers and + a fleet of agents can move like twenty. +

+

+ The platform is fully open source and self-hostable. Your data + stays on your infrastructure. Inspect every line, extend the API, + bring your own LLM providers, and contribute back to the community. +

+
+ +
+ + + View on GitHub + +
+
+
+ + + ); +} diff --git a/apps/web/app/(landing)/changelog/page.tsx b/apps/web/app/(landing)/changelog/page.tsx new file mode 100644 index 00000000..25c9eade --- /dev/null +++ b/apps/web/app/(landing)/changelog/page.tsx @@ -0,0 +1,110 @@ +import { LandingHeader } from "@/features/landing/components/landing-header"; +import { LandingFooter } from "@/features/landing/components/landing-footer"; + +const changelog = [ + { + version: "0.1.3", + date: "2026-03-31", + title: "Agent Intelligence", + changes: [ + "Trigger agents via @mention in comments", + "Stream live agent output to issue detail page", + "Rich text editor — mentions, link paste, emoji reactions, collapsible threads", + "File upload with S3 + CloudFront signed URLs and attachment tracking", + "Agent-driven repo checkout with bare clone cache for task isolation", + "Batch operations for issue list view", + "Daemon authentication and security hardening", + ], + }, + { + version: "0.1.2", + date: "2026-03-28", + title: "Collaboration", + changes: [ + "Email verification login and browser-based CLI auth", + "Multi-workspace daemon with hot-reload", + "Runtime dashboard with usage charts and activity heatmaps", + "Subscriber-driven notification model replacing hardcoded triggers", + "Unified activity timeline with threaded comment replies", + "Kanban board redesign with drag sorting, filters, and display settings", + "Human-readable issue identifiers (e.g. JIA-1)", + "Skill import from ClawHub and Skills.sh", + ], + }, + { + version: "0.1.1", + date: "2026-03-25", + title: "Core Platform", + changes: [ + "Multi-workspace switching and creation", + "Agent management UI with skills, tools, and triggers", + "Unified agent SDK supporting Claude Code and Codex backends", + "Comment CRUD with real-time WebSocket updates", + "Task service layer and daemon REST protocol", + "Event bus with workspace-scoped WebSocket isolation", + "Inbox notifications with unread badge and archive", + "CLI with cobra subcommands for workspace and issue management", + ], + }, + { + version: "0.1.0", + date: "2026-03-22", + title: "Foundation", + changes: [ + "Go backend with REST API, JWT auth, and real-time WebSocket", + "Next.js frontend with Linear-inspired UI", + "Issues with board and list views and drag-and-drop kanban", + "Agents, Inbox, and Settings pages", + "One-click setup, migration CLI, and seed tool", + "Comprehensive test suite — Go unit/integration, Vitest, Playwright E2E", + ], + }, +]; + +export default function ChangelogPage() { + return ( + <> + +
+
+

+ Changelog +

+

+ New updates and improvements to Multica. +

+ +
+ {changelog.map((release) => ( +
+
+ + v{release.version} + + + {release.date} + +
+

+ {release.title} +

+
    + {release.changes.map((change) => ( +
  • + + {change} +
  • + ))} +
+
+ ))} +
+
+
+ + + ); +} diff --git a/apps/web/features/landing/components/faq-section.tsx b/apps/web/features/landing/components/faq-section.tsx new file mode 100644 index 00000000..ef303715 --- /dev/null +++ b/apps/web/features/landing/components/faq-section.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { useState } from "react"; +import { cn } from "@/lib/utils"; + +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.", + }, +]; + +export function FAQSection() { + const [openIndex, setOpenIndex] = useState(null); + + return ( +
+
+
+

+ FAQ +

+

+ Questions & answers. +

+
+ +
+ {faqs.map((faq, i) => ( +
+ +
+
+

+ {faq.answer} +

+
+
+
+ ))} +
+
+
+ ); +} diff --git a/apps/web/features/landing/components/features-section.tsx b/apps/web/features/landing/components/features-section.tsx new file mode 100644 index 00000000..e8411657 --- /dev/null +++ b/apps/web/features/landing/components/features-section.tsx @@ -0,0 +1,1178 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { + Bot, + Brain, + Check, + CheckCircle2, + ChevronRight, + Cloud, + File, + FileText, + Folder, + FolderOpen, + Loader2, + Monitor, + Sparkles, + UserMinus, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { ImageIcon } from "./shared"; +import { StatusIcon } from "@/features/issues/components/status-icon"; +import { PriorityIcon } from "@/features/issues/components/priority-icon"; +import { STATUS_CONFIG } from "@/features/issues/config/status"; +import { PRIORITY_CONFIG } from "@/features/issues/config/priority"; +import type { IssueStatus, IssuePriority } from "@/shared/types"; + +/* ------------------------------------------------------------------ */ +/* Mock ActorAvatar — mirrors the real ActorAvatar styling exactly */ +/* but uses hardcoded data instead of the workspace store */ +/* ------------------------------------------------------------------ */ + +function MockAvatar({ + type, + initials, + size = 20, +}: { + type: "member" | "agent"; + initials?: string; + size?: number; +}) { + return ( +
+ {type === "agent" ? ( + + ) : ( + initials + )} +
+ ); +} + +/* ------------------------------------------------------------------ */ +/* Mock PropRow — mirrors the real PropRow from issue-detail */ +/* ------------------------------------------------------------------ */ + +function PropRow({ label, children }: { label: string; children: React.ReactNode }) { + return ( +
+ {label} +
+ {children} +
+
+ ); +} + +/* ------------------------------------------------------------------ */ +/* Teammates feature visual */ +/* ------------------------------------------------------------------ */ + +const mockTimeline = [ + { + type: "activity" as const, + actorType: "member" as const, + initials: "AR", + name: "Alex Rivera", + action: "assigned to Claude", + time: "3:02 PM", + statusIcon: null, + }, + { + type: "activity" as const, + actorType: "agent" as const, + initials: "", + name: "Claude", + action: "changed status from Todo to In Progress", + time: "3:02 PM", + statusIcon: "in_progress" as const, + }, + { + type: "comment" as const, + actorType: "member" as const, + initials: "AR", + name: "Alex Rivera", + time: "10 min", + content: + "The current error responses are inconsistent across handlers — need a unified format with error codes.", + }, + { + type: "comment" as const, + actorType: "agent" as const, + initials: "", + name: "Claude", + time: "6 min", + content: + "I've standardized error responses across 14 handlers. Each error now includes a code, message, and request_id. PR #43 is ready for review.", + }, + { + type: "comment" as const, + actorType: "member" as const, + initials: "AR", + name: "Alex Rivera", + time: "3 min", + content: + "Looking good. Make sure to preserve the existing HTTP status codes — some of our frontend relies on specific codes like 409.", + }, +]; + +type Assignee = { + type: "member" | "agent" | null; + id: string | null; + name: string; + initials?: string; +}; + +const allAssignees: Assignee[] = [ + { type: null, id: null, name: "Unassigned" }, + { type: "member", id: "ar", name: "Alex Rivera", initials: "AR" }, + { type: "member", id: "sk", name: "Sarah Kim", initials: "SK" }, + { type: "agent", id: "claude", name: "Claude" }, + { type: "agent", id: "tina", name: "Tina-dev" }, +]; + +const statusCycle: IssueStatus[] = ["backlog", "todo", "in_progress", "in_review", "done"]; +const priorityCycle: IssuePriority[] = ["none", "low", "medium", "high", "urgent"]; + +function TeammatesVisual() { + const [status, setStatus] = useState("in_progress"); + const [priority, setPriority] = useState("medium"); + const [assignee, setAssignee] = useState(allAssignees[3]!); // Claude + const [pickerOpen, setPickerOpen] = useState(true); + const [statusOpen, setStatusOpen] = useState(false); + const [priorityOpen, setPriorityOpen] = useState(false); + + const cycleStatus = () => { + const idx = statusCycle.indexOf(status); + setStatus(statusCycle[(idx + 1) % statusCycle.length]!); + }; + + const cyclePriority = () => { + const idx = priorityCycle.indexOf(priority); + setPriority(priorityCycle[(idx + 1) % priorityCycle.length]!); + }; + + return ( +
+ {/* Header bar */} +
+
+ Multica Demo + + MUL-18 + + Refactor API error handling middleware +
+
+ +
+ {/* Main content area */} +
+

+ Refactor API error handling middleware +

+

+ Standardize error responses across all endpoints. +

+ +
+ +
+

Activity

+ Subscribe +
+ +
+ {mockTimeline.map((entry, i) => { + if (entry.type === "activity") { + return ( +
+
+ {entry.statusIcon ? ( + + ) : ( + + )} +
+
+ {entry.name} + {entry.action} + {entry.time} +
+
+ ); + } + + return ( +
+
+ + {entry.name} + {entry.time} +
+

+ {entry.content} +

+
+ ); + })} +
+
+ + {/* Properties sidebar */} +
+
+
+
+ + Properties +
+
+ {/* Status — clickable with dropdown */} +
+ + + + {statusOpen && ( +
+ {statusCycle.map((s) => ( + + ))} +
+ )} +
+ + {/* Priority — clickable with dropdown */} +
+ + + + {priorityOpen && ( +
+ {priorityCycle.map((p) => ( + + ))} +
+ )} +
+ + {/* Assignee — clickable to toggle picker */} + + + +
+
+ + {/* Assignee picker — togglable */} + {pickerOpen && ( +
+
+ Assign to... +
+
+ +
+
+ Members +
+
+ {allAssignees.filter((a) => a.type === "member").map((m) => ( + + ))} +
+
+ Agents +
+
+ {allAssignees.filter((a) => a.type === "agent").map((a) => ( + + ))} +
+
+ )} +
+
+
+
+ ); +} + +/* ------------------------------------------------------------------ */ +/* Autonomous feature visual — agent live execution card */ +/* ------------------------------------------------------------------ */ + +const mockToolCalls = [ + { type: "thinking" as const, content: "Analyzing the error handling patterns across all 14 handler files…" }, + { type: "tool_use" as const, tool: "Read", summary: "server/internal/handler/issue.go" }, + { type: "tool_result" as const, preview: "func (h *IssueHandler) Create(w http.ResponseWriter, r *http.Request) { …" }, + { type: "tool_use" as const, tool: "Edit", summary: "server/internal/handler/issue.go — replace writeJSON error calls" }, + { type: "tool_result" as const, preview: "Updated 3 error responses to use writeError() helper" }, + { type: "thinking" as const, content: "Now checking handler/comment.go for the same inconsistent patterns…" }, + { type: "tool_use" as const, tool: "Read", summary: "server/internal/handler/comment.go" }, + { type: "tool_result" as const, preview: "func (h *CommentHandler) Create(w http.ResponseWriter, r *http.Request) { …" }, + { type: "tool_use" as const, tool: "Bash", summary: "go test ./internal/handler/ -run TestErrorResponses" }, + { type: "tool_result" as const, preview: "ok \tgithub.com/multica/server/internal/handler\t0.847s" }, +]; + +const mockTaskHistory = [ + { status: "completed" as const, title: "Set up error response types", duration: "2m 14s" }, + { status: "completed" as const, title: "Migrate issue handler", duration: "3m 41s" }, + { status: "running" as const, title: "Migrate comment handler", duration: "1m 22s" }, +]; + +function AutonomousVisual() { + const [expanded, setExpanded] = useState(null); + + return ( +
+ {/* Header bar */} +
+
+ Multica Demo + + MUL-18 + + Refactor API error handling middleware +
+
+ +
+ {/* Agent live card */} +
+ {/* Live card header */} +
+
+ +
+
+ + Agent is working +
+ 7m 17s + 10 tool calls +
+ + {/* Tool call timeline */} +
+ {mockToolCalls.map((item, i) => { + const isExpanded = expanded === i; + + if (item.type === "thinking") { + return ( + + ); + } + + if (item.type === "tool_use") { + return ( + + ); + } + + /* tool_result */ + return ( + + ); + })} +
+
+ + {/* Task run history */} +
+ Task execution history +
+ {mockTaskHistory.map((task, i) => ( +
+ {task.status === "completed" ? ( + + ) : ( + + )} + + {task.title} + + {task.duration} +
+ ))} +
+
+
+
+ ); +} + +/* ------------------------------------------------------------------ */ +/* Skills feature visual — skill library + file browser */ +/* ------------------------------------------------------------------ */ + +const mockSkills = [ + { name: "Deploy to staging", description: "Run staging deploy pipeline", files: 3, selected: false }, + { name: "Write migration", description: "Generate and validate SQL migration", files: 4, selected: true }, + { name: "Review PR", description: "Code review with style guide checks", files: 2, selected: false }, + { name: "Write tests", description: "Generate unit and integration tests", files: 3, selected: false }, +]; + +const mockFileTree = [ + { name: "SKILL.md", isDir: false, depth: 0, icon: "md" as const }, + { name: "config", isDir: true, depth: 0, open: true }, + { name: "schema.sql", isDir: false, depth: 1, icon: "file" as const }, + { name: "templates", isDir: true, depth: 0, open: false }, +]; + +function SkillsVisual() { + const [selectedSkill, setSelectedSkill] = useState(1); + const [selectedFile, setSelectedFile] = useState("SKILL.md"); + + return ( +
+
+ {/* Skills list panel */} +
+
+ Skills + +
+
+ {mockSkills.map((skill, i) => ( + + ))} +
+
+ + {/* Skill detail */} +
+ {/* Skill header */} +
+ + {mockSkills[selectedSkill]?.name} + {mockSkills[selectedSkill]?.description} +
+ + {/* File browser */} +
+ {/* File tree */} +
+
+ Files +
+
+ {mockFileTree.map((f) => ( + + ))} +
+
+ + {/* File content viewer */} +
+
+ {selectedFile} +
+
+ {selectedFile === "SKILL.md" ? ( +
+ {/* Frontmatter */} +
+
+ name + write-migration + version + 1.2.0 + author + Alex Rivera +
+
+ {/* Content */} +
+

Write Migration

+

Generate a SQL migration file based on the requested schema changes. Validates against the current database state and generates both up and down migrations.

+

Steps

+
    +
  1. Analyze the current schema from migrations/
  2. +
  3. Generate migration SQL with proper ordering
  4. +
  5. Validate with sqlc compile
  6. +
  7. Run tests against a fresh database
  8. +
+
+
+ ) : ( +
+{`CREATE TABLE IF NOT EXISTS notifications (
+  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+  user_id UUID NOT NULL REFERENCES users(id),
+  issue_id UUID REFERENCES issues(id),
+  type TEXT NOT NULL,
+  read BOOLEAN DEFAULT FALSE,
+  created_at TIMESTAMPTZ DEFAULT now()
+);`}
+                  
+ )} +
+
+
+
+
+
+ ); +} + +/* ------------------------------------------------------------------ */ +/* Runtimes feature visual — agent dashboard with runtime status */ +/* ------------------------------------------------------------------ */ + +const runtimeStatusConfig = { + idle: { label: "Idle", color: "text-muted-foreground", dot: "bg-muted-foreground" }, + working: { label: "Working", color: "text-success", dot: "bg-success" }, + error: { label: "Error", color: "text-destructive", dot: "bg-destructive" }, + offline: { label: "Offline", color: "text-muted-foreground/50", dot: "bg-muted-foreground/40" }, +}; + +const mockRuntimeList = [ + { name: "MacBook Pro", mode: "local" as const, status: "online" as const, device: "arm64 / macOS 15.2", lastSeen: "Just now" }, + { name: "Cloud (Anthropic)", mode: "cloud" as const, status: "online" as const, device: "api.anthropic.com", lastSeen: "Just now" }, + { name: "Linux Server", mode: "local" as const, status: "offline" as const, device: "x86_64 / Ubuntu 24.04", lastSeen: "3h ago" }, +]; + +/* Mock usage data — deterministic seed values to avoid SSR/hydration mismatch */ +const USAGE_SEEDS = [ + [72, 38, 54, 12], [45, 22, 41, 8], [88, 44, 63, 15], [61, 31, 48, 10], + [93, 47, 58, 14], [55, 28, 39, 9], [79, 40, 52, 13], [67, 34, 46, 11], + [84, 42, 60, 14], [50, 25, 35, 7], [91, 46, 57, 13], [58, 29, 43, 10], + [76, 38, 51, 12], [63, 32, 44, 9], [87, 44, 59, 14], [52, 26, 37, 8], + [95, 48, 62, 15], [70, 35, 49, 11], [82, 41, 55, 13], [48, 24, 33, 7], + [89, 45, 61, 14], [65, 33, 47, 10], [78, 39, 53, 12], [56, 28, 40, 9], + [92, 46, 58, 14], [60, 30, 42, 8], [85, 43, 56, 13], [73, 37, 50, 11], + [80, 40, 54, 12], [68, 34, 45, 10], +]; +const mockUsageData = USAGE_SEEDS.map((s, i) => ({ + date: `2026-03-${String(i + 2).padStart(2, "0")}`, + input_tokens: s[0]! * 1000, + output_tokens: s[1]! * 1000, + cache_read_tokens: s[2]! * 1000, + cache_write_tokens: s[3]! * 1000, +})); + + +/* Heatmap color helper — same as real ActivityHeatmap */ +function getHeatmapColor(level: number): string { + const colors = [ + "var(--color-muted, hsl(var(--muted)))", + "hsl(var(--chart-3) / 0.3)", + "hsl(var(--chart-3) / 0.5)", + "hsl(var(--chart-3) / 0.75)", + "hsl(var(--chart-3) / 1)", + ]; + return colors[level] ?? colors[0]!; +} + +/* Generate heatmap cells — simplified version of real ActivityHeatmap */ +function buildHeatmapCells() { + const WEEKS = 13; + const cells: { week: number; day: number; level: number; date: string }[] = []; + const today = new Date(); + const todayDay = today.getDay(); + const startOffset = todayDay + (WEEKS - 1) * 7; + // Deterministic pseudo-random sequence based on cell index + const seed = [3, 1, 4, 2, 0, 3, 2, 4, 1, 3, 0, 2, 4, 1, 3, 2, 0, 4, 1, 3]; + + for (let i = 0; i <= startOffset; i++) { + const d = new Date(today); + d.setDate(today.getDate() - (startOffset - i)); + const week = Math.floor(i / 7); + const day = d.getDay(); + // Weekends (0=Sun, 6=Sat) get lower activity + const isWeekend = day === 0 || day === 6; + const level = isWeekend + ? seed[i % seed.length]! > 2 ? 1 : 0 + : seed[i % seed.length]!; + cells.push({ week, day, level, date: d.toISOString().slice(0, 10) }); + } + return cells; +} + +function formatTokens(n: number): string { + if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; + if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`; + return n.toLocaleString(); +} + +function DailyCostBars({ data }: { data: typeof mockUsageData }) { + const costs = data.map( + (d) => + (d.input_tokens * 3 + + d.output_tokens * 15 + + d.cache_read_tokens * 0.3 + + d.cache_write_tokens * 3.75) / + 1_000_000, + ); + const maxCost = Math.max(...costs); + const barW = 100 / data.length; + const chartH = 64; + return ( + + {costs.map((cost, i) => { + const h = maxCost > 0 ? (cost / maxCost) * (chartH - 4) : 0; + return ( + + ); + })} + + ); +} + +function RuntimesVisual() { + const [selectedRuntime, setSelectedRuntime] = useState(0); + const [timeRange, setTimeRange] = useState<"7d" | "30d" | "90d">("30d"); + const [heatmapCells, setHeatmapCells] = useState>([]); + + useEffect(() => { + setHeatmapCells(buildHeatmapCells()); + }, []); + + const totals = mockUsageData.reduce( + (acc, u) => ({ + input: acc.input + u.input_tokens, + output: acc.output + u.output_tokens, + cacheRead: acc.cacheRead + u.cache_read_tokens, + cacheWrite: acc.cacheWrite + u.cache_write_tokens, + }), + { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + ); + + const CELL_SIZE = 10; + const CELL_GAP = 2; + const WEEKS = 13; + const labelWidth = 24; + const svgWidth = labelWidth + WEEKS * (CELL_SIZE + CELL_GAP); + const svgHeight = 12 + 7 * (CELL_SIZE + CELL_GAP); + + return ( +
+
+ {/* Runtime list */} +
+
+ Runtimes +
+
+ {mockRuntimeList.map((rt, i) => ( + + ))} +
+
+ + {/* Detail panel */} +
+ {/* Header */} +
+
+ {mockRuntimeList[selectedRuntime]?.mode === "cloud" ? ( + + ) : ( + + )} +
+ {mockRuntimeList[selectedRuntime]?.name} +
+ + {mockRuntimeList[selectedRuntime]?.status} +
+ {mockRuntimeList[selectedRuntime]?.device} +
+ + {/* Usage content */} +
+ {/* Time range + Token cards */} +
+
+ {(["7d", "30d", "90d"] as const).map((range) => ( + + ))} +
+
+ + {/* Token summary cards — same as real TokenCard */} +
+ {[ + { label: "Input", value: formatTokens(totals.input) }, + { label: "Output", value: formatTokens(totals.output) }, + { label: "Cache Read", value: formatTokens(totals.cacheRead) }, + { label: "Cache Write", value: formatTokens(totals.cacheWrite) }, + ].map((card) => ( +
+
{card.label}
+
{card.value}
+
+ ))} +
+ + {/* Charts row — Heatmap + Hourly bar */} +
+ {/* Activity Heatmap — mirrors real ActivityHeatmap */} +
+

Activity

+
+ + {["", "Mon", "", "Wed", "", "Fri", ""].map((label, i) => + label ? ( + + {label} + + ) : null, + )} + {heatmapCells.map((c, i) => ( + + ))} + +
+
+ Less + {[0, 1, 2, 3, 4].map((level) => ( +
+ ))} + More +
+
+ + {/* Daily Cost — SVG bar chart mirroring real DailyCostChart */} +
+

Daily Cost

+ +
+ Mar 18Mar 25Mar 31 +
+
+
+
+
+
+
+ ); +} + +const features = [ + { + label: "TEAMMATES", + title: "Assign to an agent like you'd assign to a colleague", + description: + "Agents aren't passive tools — they're active participants. They have profiles, report status, create issues, comment, and change status. Your activity feed shows humans and agents working side by side.", + visual: TeammatesVisual, + cards: [ + { + title: "Agents in the assignee picker", + description: + "Humans and agents appear in the same dropdown. Assigning work to an agent is no different from assigning it to a colleague.", + }, + { + title: "Autonomous participation", + description: + "Agents create issues, leave comments, and update status on their own — not just when prompted.", + }, + { + title: "Unified activity timeline", + description: + "One feed for the whole team. Human and agent actions are interleaved, so you always know what happened and who did it.", + }, + ], + }, + { + label: "AUTONOMOUS", + title: "Set it and forget it — agents work while you sleep", + description: + "Not just prompt-response. Full task lifecycle management: enqueue, claim, start, complete or fail. Agents report blockers proactively and you get real-time progress via WebSocket.", + visual: AutonomousVisual, + bgImage: "/images/feature-bg-2.jpg", + cards: [ + { + title: "Complete task lifecycle", + description: + "Every task flows through enqueue → claim → start → complete/fail. No silent failures — every transition is tracked and broadcast.", + }, + { + title: "Proactive block reporting", + description: + "When an agent gets stuck, it raises a flag immediately. No more checking back hours later to find nothing happened.", + }, + { + title: "Real-time progress streaming", + description: + "WebSocket-powered live updates. Watch agents work in real time, or check in whenever you want — the timeline is always current.", + }, + ], + }, + { + label: "SKILLS", + title: "Every solution becomes a reusable skill for the whole team", + description: + "Skills are reusable capability definitions — code, config, and context bundled together. Write a skill once, and every agent on your team can use it. Your skill library compounds over time.", + visual: SkillsVisual, + bgImage: "/images/feature-bg-3.jpg", + cards: [ + { + title: "Reusable skill definitions", + description: + "Package knowledge into skills that any agent can execute. Deploy to staging, write migrations, review PRs — all codified.", + }, + { + title: "Team-wide sharing", + description: + "One person's skill is every agent's skill. Build once, benefit everywhere across your team.", + }, + { + title: "Compound growth", + description: + "Day 1: you teach an agent to deploy. Day 30: every agent deploys, writes tests, and does code review. Your team's capabilities grow exponentially.", + }, + ], + }, + { + label: "RUNTIMES", + title: "One dashboard for all your compute", + description: + "Local daemons and cloud runtimes, managed from a single panel. Real-time monitoring of online/offline status, usage charts, and activity heatmaps. Auto-detects local CLIs — plug in and go.", + visual: RuntimesVisual, + bgImage: "/images/feature-bg-4.jpg", + cards: [ + { + title: "Unified runtime panel", + description: + "Local daemons and cloud runtimes in one view. No context switching between different management interfaces.", + }, + { + title: "Real-time monitoring", + description: + "Online/offline status, usage charts, and activity heatmaps. Know exactly what your compute is doing at any moment.", + }, + { + title: "Auto-detection & plug-and-play", + description: + "Multica detects available CLIs like Claude Code and Codex automatically. Connect a machine, and it's ready to work.", + }, + ], + }, +]; + +export function FeaturesSection() { + const [activeIndex, setActiveIndex] = useState(0); + const panelRefs = useRef<(HTMLDivElement | null)[]>([]); + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (entry.isIntersecting) { + const idx = Number(entry.target.getAttribute("data-index")); + if (!isNaN(idx)) setActiveIndex(idx); + } + } + }, + { rootMargin: "-20% 0px -60% 0px", threshold: 0 }, + ); + + panelRefs.current.forEach((el) => { + if (el) observer.observe(el); + }); + + return () => observer.disconnect(); + }, []); + + const scrollToPanel = (index: number) => { + panelRefs.current[index]?.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + }; + + return ( +
+
+
+ {/* Sticky left nav */} + + + {/* Scrollable feature panels */} +
+ {features.map((feature, i) => ( +
{ + panelRefs.current[i] = el; + }} + data-index={i} + className={cn( + "py-20 lg:py-28", + i < features.length - 1 && "border-b border-[#0a0d12]/8", + )} + > + {/* Title + description */} +

+ {feature.title} +

+

+ {feature.description} +

+ + {/* Visual */} +
+ {feature.visual ? ( +
+
+ +
+
+ ) : ( +
+
+
+
+
+ +
+

+ {feature.label.toLowerCase()} visual +

+
+
+
+ )} +
+ + {/* Feature cards */} +
+ {feature.cards.map((card) => ( +
+

+ {card.title} +

+

+ {card.description} +

+
+ ))} +
+
+ ))} +
+
+
+
+ ); +} diff --git a/apps/web/features/landing/components/how-it-works-section.tsx b/apps/web/features/landing/components/how-it-works-section.tsx new file mode 100644 index 00000000..b0bc8719 --- /dev/null +++ b/apps/web/features/landing/components/how-it-works-section.tsx @@ -0,0 +1,80 @@ +import Link from "next/link"; +import { GitHubMark, githubUrl, heroButtonClassName } from "./shared"; + +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.", + }, +]; + +export function HowItWorksSection() { + return ( +
+
+

+ Get started +

+

+ Hire your first AI employee +
+ in the next hour. +

+ +
+ {steps.map((step) => ( +
+ + {step.number} + +

+ {step.title} +

+

+ {step.description} +

+
+ ))} +
+ +
+ + Get started + + + + View on GitHub + +
+
+
+ ); +} diff --git a/apps/web/features/landing/components/landing-footer.tsx b/apps/web/features/landing/components/landing-footer.tsx new file mode 100644 index 00000000..287dfff3 --- /dev/null +++ b/apps/web/features/landing/components/landing-footer.tsx @@ -0,0 +1,103 @@ +"use client"; + +import Link from "next/link"; +import { MulticaIcon } from "@/components/multica-icon"; +import { githubUrl } from "./shared"; + + +const footerLinks = { + Product: [ + { label: "Features", href: "#features" }, + { label: "How it Works", href: "#how-it-works" }, + { label: "Changelog", href: "/changelog" }, + ], + Resources: [ + { label: "Documentation", href: githubUrl }, + { label: "API", href: githubUrl }, + { label: "Community", href: githubUrl }, + ], + Company: [ + { label: "About", href: "/about" }, + { label: "Open Source", href: "#open-source" }, + { label: "GitHub", href: githubUrl }, + ], +}; + +export function LandingFooter() { + return ( +
+
+ {/* Top: CTA + link columns */} +
+ {/* Left — newsletter / CTA */} +
+ + + + multica + + +

+ Project management for human + agent teams. Open source, + self-hostable, built for the future of work. +

+
+ + Get started + +
+
+ + {/* Right — link columns */} +
+ {Object.entries(footerLinks).map(([group, links]) => ( +
+

+ {group} +

+
    + {links.map((link) => ( +
  • + + {link.label} + +
  • + ))} +
+
+ ))} +
+
+ + {/* Bottom: copyright */} +
+

+ © {new Date().getFullYear()} Multica. All rights reserved. +

+
+ + {/* Giant logo */} +
+
+ + + multica + +
+
+
+
+ ); +} diff --git a/apps/web/features/landing/components/landing-header.tsx b/apps/web/features/landing/components/landing-header.tsx new file mode 100644 index 00000000..30394634 --- /dev/null +++ b/apps/web/features/landing/components/landing-header.tsx @@ -0,0 +1,59 @@ +"use client"; + +import Link from "next/link"; +import { MulticaIcon } from "@/components/multica-icon"; +import { cn } from "@/lib/utils"; +import { GitHubMark, githubUrl, headerButtonClassName } from "./shared"; + +export function LandingHeader({ + variant = "dark", +}: { + variant?: "dark" | "light"; +}) { + return ( +
+
+ + + + multica + + + +
+ + + GitHub + + + Log in + +
+
+
+ ); +} diff --git a/apps/web/features/landing/components/landing-hero.tsx b/apps/web/features/landing/components/landing-hero.tsx new file mode 100644 index 00000000..bac891ee --- /dev/null +++ b/apps/web/features/landing/components/landing-hero.tsx @@ -0,0 +1,100 @@ +import Image from "next/image"; +import Link from "next/link"; +import { + ClaudeCodeLogo, + CodexLogo, + GitHubMark, + githubUrl, + heroButtonClassName, +} from "./shared"; + +export function LandingHero() { + return ( +
+ + +
+
+
+

+ Your next 10 hires +
+ won't be human. +

+ +

+ Multica is an open-source platform that turns coding agents into + real teammates. Assign tasks, track progress, compound skills — + manage your human + agent workforce in one place. +

+ +
+ + Start free trial + + + + GitHub + +
+
+ +
+ Works with +
+
+ + Claude Code +
+
+ + Codex +
+
+
+ +
+ +
+
+
+
+ ); +} + +function LandingBackdrop() { + return ( +
+ +
+ ); +} + +function ProductImage() { + return ( +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + Multica board view — issues managed by humans and agents +
+
+ ); +} diff --git a/apps/web/features/landing/components/multica-landing.tsx b/apps/web/features/landing/components/multica-landing.tsx index cc90b26c..a608a7df 100644 --- a/apps/web/features/landing/components/multica-landing.tsx +++ b/apps/web/features/landing/components/multica-landing.tsx @@ -1,98 +1,19 @@ "use client"; -import { useEffect, useRef, useState } from "react"; -import Image from "next/image"; -import Link from "next/link"; -import { MulticaIcon } from "@/components/multica-icon"; -import { cn } from "@/lib/utils"; - -const githubUrl = "https://github.com/multica-ai/multica"; +import { LandingHeader } from "./landing-header"; +import { LandingHero } from "./landing-hero"; +import { FeaturesSection } from "./features-section"; +import { HowItWorksSection } from "./how-it-works-section"; +import { OpenSourceSection } from "./open-source-section"; +import { FAQSection } from "./faq-section"; +import { LandingFooter } from "./landing-footer"; export function MulticaLanding() { return ( <> -
- - -
-
- - - - multica - - - -
- - - GitHub - - - Log in - -
-
-
- -
-
-
-

- Your next 10 hires -
- won't be human. -

- -

- Multica is project management for human + agent teams. Assign - tasks, manage runtimes, compound skills, all in one place. -

- -
- - Start free trial - - - - GitHub - -
- -
- -
- Works with -
-
- - Claude Code -
-
- - Codex -
-
-
- -
- -
-
-
+
+ +
@@ -103,843 +24,3 @@ export function MulticaLanding() { ); } - -/* -------------------------------------------------------------------------- */ -/* Features Section */ -/* -------------------------------------------------------------------------- */ - -const features = [ - { - label: "TEAMMATES", - title: "Assign to an agent like you'd assign to a colleague", - description: - "Agents aren't passive tools — they're active participants. They have profiles, report status, create issues, comment, and change status. Your activity feed shows humans and agents working side by side.", - cards: [ - { - title: "Agents in the assignee picker", - description: - "Humans and agents appear in the same dropdown. Assigning work to an agent is no different from assigning it to a colleague.", - }, - { - title: "Autonomous participation", - description: - "Agents create issues, leave comments, and update status on their own — not just when prompted.", - }, - { - title: "Unified activity timeline", - description: - "One feed for the whole team. Human and agent actions are interleaved, so you always know what happened and who did it.", - }, - ], - }, - { - label: "AUTONOMOUS", - title: "Set it and forget it — agents work while you sleep", - description: - "Not just prompt-response. Full task lifecycle management: enqueue, claim, start, complete or fail. Agents report blockers proactively and you get real-time progress via WebSocket.", - cards: [ - { - title: "Complete task lifecycle", - description: - "Every task flows through enqueue → claim → start → complete/fail. No silent failures — every transition is tracked and broadcast.", - }, - { - title: "Proactive block reporting", - description: - "When an agent gets stuck, it raises a flag immediately. No more checking back hours later to find nothing happened.", - }, - { - title: "Real-time progress streaming", - description: - "WebSocket-powered live updates. Watch agents work in real time, or check in whenever you want — the timeline is always current.", - }, - ], - }, - { - label: "SKILLS", - title: "Every solution becomes a reusable skill for the whole team", - description: - "Skills are reusable capability definitions — code, config, and context bundled together. Write a skill once, and every agent on your team can use it. Your skill library compounds over time.", - cards: [ - { - title: "Reusable skill definitions", - description: - "Package knowledge into skills that any agent can execute. Deploy to staging, write migrations, review PRs — all codified.", - }, - { - title: "Team-wide sharing", - description: - "One person's skill is every agent's skill. Build once, benefit everywhere across your team.", - }, - { - title: "Compound growth", - description: - "Day 1: you teach an agent to deploy. Day 30: every agent deploys, writes tests, and does code review. Your team's capabilities grow exponentially.", - }, - ], - }, - { - label: "RUNTIMES", - title: "One dashboard for all your compute", - description: - "Local daemons and cloud runtimes, managed from a single panel. Real-time monitoring of online/offline status, usage charts, and activity heatmaps. Auto-detects local CLIs — plug in and go.", - cards: [ - { - title: "Unified runtime panel", - description: - "Local daemons and cloud runtimes in one view. No context switching between different management interfaces.", - }, - { - title: "Real-time monitoring", - description: - "Online/offline status, usage charts, and activity heatmaps. Know exactly what your compute is doing at any moment.", - }, - { - title: "Auto-detection & plug-and-play", - description: - "Multica detects available CLIs like Claude Code and Codex automatically. Connect a machine, and it's ready to work.", - }, - ], - }, -]; - -function FeaturesSection() { - const [activeIndex, setActiveIndex] = useState(0); - const panelRefs = useRef<(HTMLDivElement | null)[]>([]); - - useEffect(() => { - const observer = new IntersectionObserver( - (entries) => { - for (const entry of entries) { - if (entry.isIntersecting) { - const idx = Number(entry.target.getAttribute("data-index")); - if (!isNaN(idx)) setActiveIndex(idx); - } - } - }, - { rootMargin: "-20% 0px -60% 0px", threshold: 0 }, - ); - - panelRefs.current.forEach((el) => { - if (el) observer.observe(el); - }); - - return () => observer.disconnect(); - }, []); - - const scrollToPanel = (index: number) => { - panelRefs.current[index]?.scrollIntoView({ - behavior: "smooth", - block: "start", - }); - }; - - return ( -
-
-
- {/* Sticky left nav */} - - - {/* Scrollable feature panels */} -
- {features.map((feature, i) => ( -
{ - panelRefs.current[i] = el; - }} - data-index={i} - className={cn( - "py-20 lg:py-28", - i < features.length - 1 && "border-b border-[#0a0d12]/8", - )} - > - {/* Title + description */} -

- {feature.title} -

-

- {feature.description} -

- - {/* Image placeholder */} -
-
-
-
-
-
- -
-

- {feature.label.toLowerCase()} visual -

-
-
-
-
- - {/* Feature cards */} -
- {feature.cards.map((card) => ( -
-

- {card.title} -

-

- {card.description} -

- -
- ))} -
-
- ))} -
-
-
-
- ); -} - -/* -------------------------------------------------------------------------- */ -/* 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 ( -
-
-

- Get started -

-

- Hire your first AI employee -
- in the next hour. -

- -
- {steps.map((step) => ( -
- - {step.number} - -

- {step.title} -

-

- {step.description} -

-
- ))} -
- -
- - Get started - - - - View on GitHub - -
-
-
- ); -} - -/* -------------------------------------------------------------------------- */ -/* 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 ( -
-
-
- {/* Left column — heading + CTA */} -
-

- Open source -

-

- Open source -
- for all. -

-

- 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. -

-
- - - Star on GitHub - -
-
- - {/* Right column — highlight grid */} -
-
- {openSourceHighlights.map((item) => ( -
-

- {item.title} -

-

- {item.description} -

-
- ))} -
-
-
-
-
- ); -} - -/* -------------------------------------------------------------------------- */ -/* 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(null); - - return ( -
-
-
-

- FAQ -

-

- Questions & answers. -

-
- -
- {faqs.map((faq, i) => ( -
- -
-
-

- {faq.answer} -

-
-
-
- ))} -
-
-
- ); -} - -/* -------------------------------------------------------------------------- */ -/* 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 ( -
-
- {/* Top: CTA + link columns */} -
- {/* Left — newsletter / CTA */} -
- - - - multica - - -

- Project management for human + agent teams. Open source, self-hostable, built for the future of work. -

-
- - Get started - -
-
- - {/* Right — link columns */} -
- {Object.entries(footerLinks).map(([group, links]) => ( -
-

- {group} -

-
    - {links.map((link) => ( -
  • - - {link.label} - -
  • - ))} -
-
- ))} -
-
- - {/* Bottom: copyright + legal */} -
-

- © {new Date().getFullYear()} Multica. All rights reserved. -

-
- - {/* Giant logo + Game of Life */} -
-
- - - multica - -
-
- -
-
-
-
- ); -} - -/* -------------------------------------------------------------------------- */ -/* Game of Life */ -/* -------------------------------------------------------------------------- */ - -function GameOfLife() { - const canvasRef = useRef(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 ( - - ); -} - -/* -------------------------------------------------------------------------- */ -/* Shared components */ -/* -------------------------------------------------------------------------- */ - -function LandingBackdrop() { - return ( -
- -
- ); -} - -function GitHubMark({ className }: { className?: string }) { - return ( - - ); -} - -function ImageIcon({ className }: { className?: string }) { - return ( - - ); -} - - -function ClaudeCodeLogo({ className }: { className?: string }) { - return ( - - ); -} - -function CodexLogo({ className }: { className?: string }) { - return ( - - ); -} - -function ProductImage() { - return ( -
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - Multica board view — issues managed by humans and agents -
-
- ); -} - -function headerButtonClassName(tone: "ghost" | "solid") { - return cn( - "inline-flex items-center justify-center gap-2 rounded-[11px] px-4 py-2.5 text-[13px] font-semibold transition-colors", - tone === "solid" - ? "bg-white text-[#0a0d12] hover:bg-white/92" - : "border border-white/18 bg-black/16 text-white backdrop-blur-sm hover:bg-black/24", - ); -} - -function heroButtonClassName(tone: "ghost" | "solid") { - return cn( - "inline-flex items-center justify-center gap-2 rounded-[12px] px-5 py-3 text-[14px] font-semibold transition-colors", - tone === "solid" - ? "bg-white text-[#0a0d12] hover:bg-white/92" - : "border border-white/18 bg-black/16 text-white backdrop-blur-sm hover:bg-black/24", - ); -} diff --git a/apps/web/features/landing/components/open-source-section.tsx b/apps/web/features/landing/components/open-source-section.tsx new file mode 100644 index 00000000..be45a1de --- /dev/null +++ b/apps/web/features/landing/components/open-source-section.tsx @@ -0,0 +1,79 @@ +import Link from "next/link"; +import { GitHubMark, githubUrl } from "./shared"; + +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.", + }, +]; + +export function OpenSourceSection() { + return ( +
+
+
+ {/* Left column — heading + CTA */} +
+

+ Open source +

+

+ Open source +
+ for all. +

+

+ Multica is fully open source. Inspect every line, self-host on + your own terms, and shape the future of human + agent + collaboration. +

+
+ + + Star on GitHub + +
+
+ + {/* Right column — highlight grid */} +
+
+ {openSourceHighlights.map((item) => ( +
+

+ {item.title} +

+

+ {item.description} +

+
+ ))} +
+
+
+
+
+ ); +} diff --git a/apps/web/features/landing/components/shared.tsx b/apps/web/features/landing/components/shared.tsx new file mode 100644 index 00000000..d1fd323e --- /dev/null +++ b/apps/web/features/landing/components/shared.tsx @@ -0,0 +1,87 @@ +import { cn } from "@/lib/utils"; + +export const githubUrl = "https://github.com/multica-ai/multica"; + +export function GitHubMark({ className }: { className?: string }) { + return ( + + ); +} + +export function ImageIcon({ className }: { className?: string }) { + return ( + + ); +} + +export function ClaudeCodeLogo({ className }: { className?: string }) { + return ( + + ); +} + +export function CodexLogo({ className }: { className?: string }) { + return ( + + ); +} + +export function headerButtonClassName( + tone: "ghost" | "solid", + variant: "dark" | "light" = "dark", +) { + return cn( + "inline-flex items-center justify-center gap-2 rounded-[11px] px-4 py-2.5 text-[13px] font-semibold transition-colors", + variant === "dark" + ? tone === "solid" + ? "bg-white text-[#0a0d12] hover:bg-white/92" + : "border border-white/18 bg-black/16 text-white backdrop-blur-sm hover:bg-black/24" + : tone === "solid" + ? "bg-[#0a0d12] text-white hover:bg-[#0a0d12]/88" + : "border border-[#0a0d12]/12 bg-white text-[#0a0d12] hover:bg-[#0a0d12]/5", + ); +} + +export function heroButtonClassName(tone: "ghost" | "solid") { + return cn( + "inline-flex items-center justify-center gap-2 rounded-[12px] px-5 py-3 text-[14px] font-semibold transition-colors", + tone === "solid" + ? "bg-white text-[#0a0d12] hover:bg-white/92" + : "border border-white/18 bg-black/16 text-white backdrop-blur-sm hover:bg-black/24", + ); +} diff --git a/apps/web/public/images/feature-bg-2.jpg b/apps/web/public/images/feature-bg-2.jpg new file mode 100644 index 00000000..c9c6d416 Binary files /dev/null and b/apps/web/public/images/feature-bg-2.jpg differ diff --git a/apps/web/public/images/feature-bg-3.jpg b/apps/web/public/images/feature-bg-3.jpg new file mode 100644 index 00000000..aed497fd Binary files /dev/null and b/apps/web/public/images/feature-bg-3.jpg differ diff --git a/apps/web/public/images/feature-bg-4.jpg b/apps/web/public/images/feature-bg-4.jpg new file mode 100644 index 00000000..16d7e96f Binary files /dev/null and b/apps/web/public/images/feature-bg-4.jpg differ diff --git a/apps/web/public/images/feature-bg.jpg b/apps/web/public/images/feature-bg.jpg new file mode 100644 index 00000000..4de72190 Binary files /dev/null and b/apps/web/public/images/feature-bg.jpg differ diff --git a/apps/web/public/images/landing-bg.jpg b/apps/web/public/images/landing-bg.jpg index eda4a4d8..91437fa5 100644 Binary files a/apps/web/public/images/landing-bg.jpg and b/apps/web/public/images/landing-bg.jpg differ diff --git a/apps/web/public/images/landing-hero.png b/apps/web/public/images/landing-hero.png index 0f8cfae8..e9591314 100644 Binary files a/apps/web/public/images/landing-hero.png and b/apps/web/public/images/landing-hero.png differ