This commit is contained in:
ZacharyZhang-NY committed 2026-03-16 00:22:21 -04:00
1 parent d067589276
commit 41eafc7c44
6 files changed
+555 -85

No files matched your search

+313 -60
View File
@@ -1,9 +1,21 @@
import { useState } from "react";
import { motion } from "framer-motion";
import { Buildings, CheckCircle, FolderOpen, WarningCircle, XCircle } from "@phosphor-icons/react";
import { useCallback, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Buildings,
CheckCircle,
FolderOpen,
Lightning,
Robot,
Rocket,
Target,
WarningCircle,
XCircle,
} from "@phosphor-icons/react";
import type { ProfileSnapshot } from "@shared/types";
import { unwrap } from "../lib/desktop";
import { ActionButton, Input } from "./ui";
import { ActionButton, Input, TextArea } from "./ui";
type Step = "company" | "goal" | "launch";
export function CompanyOnboarding({
connectors,
@@ -12,12 +24,19 @@ export function CompanyOnboarding({
connectors: ProfileSnapshot["connectors"];
onDone: () => Promise<void>;
}) {
const [step, setStep] = useState<Step>("company");
const [companyName, setCompanyName] = useState("");
const [workspacePath, setWorkspacePath] = useState("");
const [autoApproveHires, setAutoApproveHires] = useState(true);
const [goalTitle, setGoalTitle] = useState("");
const [goalDescription, setGoalDescription] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [createdCompanyId, setCreatedCompanyId] = useState<string | null>(null);
const hasReadyConnector = connectors.some((c) => c.status === "ready" || c.status === "degraded");
const readyConnectors = connectors.filter((c) => c.status === "ready" || c.status === "degraded");
const bestConnector = readyConnectors[0] ?? null;
async function pickDirectory() {
setError(null);
@@ -31,7 +50,7 @@ export function CompanyOnboarding({
}
}
async function submit() {
const createCompany = useCallback(async () => {
if (loading) return;
if (!companyName.trim()) {
setError("Enter a company name.");
@@ -49,6 +68,7 @@ export function CompanyOnboarding({
name: companyName.trim(),
description: "",
status: "active",
autoApproveHires,
}),
);
if (workspacePath) {
@@ -64,32 +84,133 @@ export function CompanyOnboarding({
);
}
unwrap(await window.agentCompany.setCurrentCompany(companyId));
await onDone();
setCreatedCompanyId(companyId);
setStep("goal");
} catch (submitError) {
setError(submitError instanceof Error ? submitError.message : "Unable to create the company.");
} finally {
setLoading(false);
}
}
}, [loading, companyName, hasReadyConnector, autoApproveHires, workspacePath]);
const createGoalAndLaunch = useCallback(async () => {
if (loading || !createdCompanyId) return;
setLoading(true);
setError(null);
try {
// Create goal
if (goalTitle.trim()) {
unwrap(
await window.agentCompany.saveGoal({
companyId: createdCompanyId,
title: goalTitle.trim(),
description: goalDescription.trim() || `Achieve: ${goalTitle.trim()}`,
status: "active",
parentId: null,
ownerAgentId: null,
}),
);
}
// Create CEO agent automatically
if (bestConnector) {
const ceoId = unwrap(
await window.agentCompany.saveAgent({
companyId: createdCompanyId,
name: "CEO",
role: "CEO",
title: "Chief Executive Officer",
reportsTo: null,
connectorId: bestConnector.id,
workspaceId: null,
model: null,
capabilities: "Executive leadership, strategic planning, team building, goal decomposition, delegation, and organizational management. Responsible for hiring the leadership team, setting strategy, and coordinating all company operations.",
budgetMonthlyUsd: 250,
status: "idle",
}),
);
// Enable heartbeat on CEO for autonomous operation
unwrap(await window.agentCompany.setHeartbeat({ agentId: ceoId, companyId: createdCompanyId, enabled: true, intervalSec: 120 }));
setStep("launch");
// Auto-trigger the CEO's first run after a brief delay for UI
setTimeout(async () => {
try {
await window.agentCompany.triggerHeartbeat({ agentId: ceoId, companyId: createdCompanyId, trigger: "assignment" });
} catch {
// Non-fatal — CEO will wake on next heartbeat cycle
}
}, 1500);
}
await onDone();
} catch (submitError) {
setError(submitError instanceof Error ? submitError.message : "Unable to set up the company.");
setLoading(false);
}
}, [loading, createdCompanyId, goalTitle, goalDescription, bestConnector, onDone]);
const skipGoalAndFinish = useCallback(async () => {
if (loading) return;
setLoading(true);
setError(null);
try {
await onDone();
} catch (submitError) {
setError(submitError instanceof Error ? submitError.message : "Unable to continue.");
} finally {
setLoading(false);
}
}, [loading, onDone]);
const stepIndex = step === "company" ? 0 : step === "goal" ? 1 : 2;
return (
<div className="grid min-h-[100dvh] grid-cols-1 bg-[color:var(--bg)] md:grid-cols-[minmax(0,1fr)_440px]">
<div className="grid min-h-[100dvh] grid-cols-1 bg-[color:var(--bg)] md:grid-cols-[minmax(0,1fr)_480px]">
{/* Left panel — branding */}
<div className="flex items-center px-8 py-12 md:px-16">
<motion.div
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.45, ease: [0.16, 1, 0.3, 1] }}
className="max-w-[420px]"
className="max-w-[440px]"
>
<div className="mb-3 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">AgentCompany Desktop</div>
<h1 className="text-[38px] leading-[0.98] tracking-[-0.03em] text-[color:var(--text)] md:text-[48px]">
Welcome.
Build your<br />AI company.
</h1>
<p className="mt-4 text-[14px] leading-relaxed text-[color:var(--muted)]">
We detected the following coding agents on your system. You'll configure your organization structure, goals, and agents from the dashboard after setup.
<p className="mt-4 max-w-[380px] text-[14px] leading-relaxed text-[color:var(--muted)]">
Set up a fully autonomous AI-powered organization. Your agents will hire, delegate, execute, review, and coordinate — just like a real company.
</p>
<div className="mt-8">
<div className="mb-3 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Detected connectors</div>
{/* Step indicators */}
<div className="mt-10 flex items-center gap-3">
{[
{ label: "Create Company", icon: Buildings },
{ label: "Set Mission", icon: Target },
{ label: "Launch", icon: Rocket },
].map((s, i) => (
<div key={s.label} className="flex items-center gap-2">
{i > 0 ? <div className={`h-px w-6 ${i <= stepIndex ? "bg-[color:var(--accent)]" : "bg-[color:var(--line)]"}`} /> : null}
<div className={`flex items-center gap-2 rounded-full px-3 py-1.5 text-[11px] font-medium ${
i < stepIndex
? "bg-[color:var(--success-soft)] text-[color:var(--success)]"
: i === stepIndex
? "bg-[color:var(--accent-soft)] text-[color:var(--accent)]"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted)]"
}`}>
{i < stepIndex ? <CheckCircle size={14} weight="fill" /> : <s.icon size={14} />}
{s.label}
</div>
</div>
))}
</div>
{/* Detected connectors */}
<div className="mt-10">
<div className="mb-3 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Detected agents</div>
<div className="space-y-2.5">
{connectors.map((c) => {
const isOk = c.status === "ready" || c.status === "degraded";
@@ -122,56 +243,188 @@ export function CompanyOnboarding({
</motion.div>
</div>
{/* Right panel — form steps */}
<div className="flex items-center border-l border-[color:var(--line)] bg-[color:var(--bg-subtle)] px-6 py-10 md:px-10">
<motion.div
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.08, duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
className="surface w-full rounded-[12px] p-7"
>
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Getting started</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">Create your company</div>
<div className="mb-6 h-px bg-[color:var(--line)]" />
<div className="space-y-4">
<Input label="Company name" value={companyName} onChange={setCompanyName} placeholder="e.g. Acme Engineering" />
<div>
<label
htmlFor="onboarding-workspace-path"
className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]"
>
Workspace folder <span className="font-normal text-[color:var(--muted)]">(optional)</span>
</label>
<div className="flex gap-3">
<Input
id="onboarding-workspace-path"
ariaLabel="Workspace folder"
value={workspacePath}
onChange={setWorkspacePath}
placeholder="/Users/you/project"
<AnimatePresence mode="wait">
{step === "company" ? (
<motion.div
key="company"
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -18 }}
transition={{ duration: 0.35, ease: [0.16, 1, 0.3, 1] }}
className="surface w-full rounded-[12px] p-7"
>
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Step 1 of 3</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">Create your company</div>
<div className="mb-6 h-px bg-[color:var(--line)]" />
<div className="space-y-4">
<Input label="Company name" value={companyName} onChange={setCompanyName} placeholder="e.g. Acme AI Labs" />
<div>
<label
htmlFor="onboarding-workspace-path"
className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]"
>
Workspace folder <span className="font-normal text-[color:var(--muted)]">(optional)</span>
</label>
<div className="flex gap-3">
<Input
id="onboarding-workspace-path"
ariaLabel="Workspace folder"
value={workspacePath}
onChange={setWorkspacePath}
placeholder="/Users/you/project"
/>
<ActionButton label="Choose" onClick={() => void pickDirectory()} icon={FolderOpen} />
</div>
<div className="mt-1.5 text-[11px] text-[color:var(--muted)]">
The directory where your agents will work. You can add more later.
</div>
</div>
{/* Auto-approve toggle */}
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<label className="flex items-center gap-3">
<input
type="checkbox"
checked={autoApproveHires}
onChange={(e) => setAutoApproveHires(e.target.checked)}
className="h-4 w-4 rounded border-[color:var(--line)] accent-[color:var(--accent)]"
/>
<div>
<div className="text-[13px] font-medium text-[color:var(--text)]">Full Autonomy Mode</div>
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">
Auto-approve agent hires and org strategies. Agents will build the team without waiting for Board approval.
</div>
</div>
</label>
</div>
</div>
{error ? (
<div className="mt-5 rounded-[10px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]" role="alert">
{error}
</div>
) : null}
<div className="mt-8 flex items-center justify-end gap-4">
<ActionButton
label={loading ? "Creating..." : "Continue"}
onClick={() => void createCompany()}
icon={Buildings}
tone="accent"
loading={loading}
disabled={!hasReadyConnector || !companyName.trim()}
/>
<ActionButton label="Choose" onClick={() => void pickDirectory()} icon={FolderOpen} />
</div>
<div className="mt-1.5 text-[11px] text-[color:var(--muted)]">
You can add more workspaces later from the dashboard.
</motion.div>
) : step === "goal" ? (
<motion.div
key="goal"
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -18 }}
transition={{ duration: 0.35, ease: [0.16, 1, 0.3, 1] }}
className="surface w-full rounded-[12px] p-7"
>
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Step 2 of 3</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">Set your company's mission</div>
<div className="mb-2 text-[13px] text-[color:var(--muted)]">
What should your AI company accomplish? The CEO agent will use this to build a team and start working.
</div>
</div>
</div>
{error ? (
<div className="mt-5 rounded-[10px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]" role="alert">
{error}
</div>
) : null}
<div className="mt-8 flex items-center justify-end gap-4">
<ActionButton
label={loading ? "Creating..." : "Continue to dashboard"}
onClick={() => void submit()}
icon={Buildings}
tone="accent"
loading={loading}
disabled={!hasReadyConnector || !companyName.trim()}
/>
</div>
</motion.div>
<div className="mb-6 h-px bg-[color:var(--line)]" />
<div className="space-y-4">
<Input
label="Goal"
value={goalTitle}
onChange={setGoalTitle}
placeholder="e.g. Build a SaaS landing page with marketing copy"
/>
<TextArea
label="Description (acceptance criteria)"
value={goalDescription}
onChange={setGoalDescription}
placeholder="Describe what 'done' looks like. The more specific, the better."
rows={4}
/>
</div>
{/* Preview: what will happen */}
<div className="mt-5 rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">What happens next</div>
<div className="space-y-2 text-[12px] text-[color:var(--muted)]">
<div className="flex items-start gap-2">
<Robot size={14} className="mt-0.5 shrink-0 text-[color:var(--accent)]" />
<span>A <strong className="text-[color:var(--text)]">CEO agent</strong> will be created using {bestConnector?.label ?? "your best connector"}</span>
</div>
<div className="flex items-start gap-2">
<Target size={14} className="mt-0.5 shrink-0 text-[color:var(--accent)]" />
<span>The CEO will read your goal and <strong className="text-[color:var(--text)]">design an organization structure</strong></span>
</div>
<div className="flex items-start gap-2">
<Lightning size={14} className="mt-0.5 shrink-0 text-[color:var(--accent)]" />
<span>Agents will be hired, tasks delegated, and work will begin <strong className="text-[color:var(--text)]">autonomously</strong></span>
</div>
</div>
</div>
{error ? (
<div className="mt-5 rounded-[10px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]" role="alert">
{error}
</div>
) : null}
<div className="mt-8 flex items-center justify-between">
<button
type="button"
onClick={() => void skipGoalAndFinish()}
className="text-[12px] font-medium text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
>
Skip for now
</button>
<ActionButton
label={loading ? "Launching..." : "Launch your company"}
onClick={() => void createGoalAndLaunch()}
icon={Rocket}
tone="accent"
loading={loading}
disabled={!goalTitle.trim()}
/>
</div>
</motion.div>
) : (
<motion.div
key="launch"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
className="w-full text-center"
>
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, duration: 0.6, type: "spring", bounce: 0.4 }}
className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-[color:var(--success-soft)]"
>
<Rocket size={36} weight="fill" className="text-[color:var(--success)]" />
</motion.div>
<h2 className="text-[24px] font-bold tracking-[-0.02em] text-[color:var(--text)]">
{companyName} is live!
</h2>
<p className="mx-auto mt-3 max-w-[340px] text-[14px] leading-relaxed text-[color:var(--muted)]">
Your CEO agent is starting up. Watch the dashboard as your company comes to life.
</p>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.5 }}
className="mt-8"
>
<div className="flex items-center justify-center gap-2 text-[12px] text-[color:var(--muted)]">
<span className="h-2 w-2 animate-pulse rounded-full bg-[color:var(--success)]" />
CEO agent initializing...
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);