feat: add company builder wizard with AI-powered org generation (#73)
* feat: add company builder wizard with AI-powered org generation Adds a new "Build Your Company" step to the onboarding wizard that lets users describe their business and generates a full agent org structure using OpenClaw's AI. Includes company plan generation, role deduplication, agent bootstrap with main-agent reuse, org chart preview, confetti on success, CSS voxel running-avatar loader, amber theme unification, and best-effort SSH workspace cleanup. Made-with: Cursor * fix: resolve lint errors in CompanyBuilderModal Replace setState-in-effect pattern with a direct callback, escape apostrophes in JSX text, and derive org chart hover state without side effects. Made-with: Cursor --------- Co-authored-by: iamlukethedev <lucas.guilherme@smartwayslfl.com>
This commit is contained in:
@@ -50,8 +50,8 @@ export const AgentsStep = ({ agentCount, connected }: AgentsStepProps) => {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3 rounded-lg border border-emerald-500/20 bg-emerald-500/5 px-4 py-3">
|
||||
<Users className="h-5 w-5 text-emerald-400" />
|
||||
<div className="flex items-center gap-3 rounded-lg border border-amber-500/20 bg-amber-500/10 px-4 py-3">
|
||||
<Users className="h-5 w-5 text-amber-300" />
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white">
|
||||
{agentCount} agent{agentCount !== 1 ? "s" : ""} discovered
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Building2, Sparkles, Users, Wand2 } from "lucide-react";
|
||||
|
||||
export type CompanyStepProps = {
|
||||
connected: boolean;
|
||||
agentCount: number;
|
||||
onOpenCompanyBuilder: () => void;
|
||||
};
|
||||
|
||||
export const CompanyStep = ({
|
||||
connected,
|
||||
agentCount,
|
||||
onOpenCompanyBuilder,
|
||||
}: CompanyStepProps) => {
|
||||
const canOpenBuilder = connected && agentCount > 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border border-amber-500/20 bg-amber-500/10 px-4 py-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-amber-500/15">
|
||||
<Building2 className="h-5 w-5 text-amber-300" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-semibold text-white">Bootstrap your company with AI</p>
|
||||
<p className="text-xs leading-5 text-white/60">
|
||||
Describe what your company does and Claw3D can turn that into a full org structure
|
||||
with specialized agents, working files, and role instructions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 sm:grid-cols-3">
|
||||
{[
|
||||
{
|
||||
icon: Sparkles,
|
||||
title: "Improve the brief",
|
||||
description: "Use your connected OpenClaw runtime to sharpen the company prompt.",
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: "Generate the team",
|
||||
description: "Get a practical org chart with roles, responsibilities, and handoffs.",
|
||||
},
|
||||
{
|
||||
icon: Wand2,
|
||||
title: "Create everything",
|
||||
description: "Write agent files and create the team directly in OpenClaw.",
|
||||
},
|
||||
].map(({ icon: Icon, title, description }) => (
|
||||
<div
|
||||
key={title}
|
||||
className="rounded-md border border-white/8 bg-white/[0.02] px-3 py-3"
|
||||
>
|
||||
<Icon className="h-4 w-4 text-white/70" />
|
||||
<p className="mt-2 text-[11px] font-semibold text-white">{title}</p>
|
||||
<p className="mt-1 text-[10px] leading-4 text-white/45">{description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
{canOpenBuilder ? (
|
||||
<div className="flex justify-center">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2 rounded-md bg-amber-500 px-4 py-2 text-xs font-semibold text-[#1a1206] transition-colors hover:bg-amber-400"
|
||||
onClick={onOpenCompanyBuilder}
|
||||
>
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
Open Company Builder
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md border border-amber-500/20 bg-amber-500/10 px-4 py-3 text-xs text-amber-100/80">
|
||||
Connect to OpenClaw and keep at least one planning agent available to generate the
|
||||
company with AI.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,38 +1,91 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* CompleteStep — Final wizard screen before entering the office.
|
||||
*/
|
||||
import { useEffect, useRef } from "react";
|
||||
import confetti from "canvas-confetti";
|
||||
import { Building2, Rocket } from "lucide-react";
|
||||
|
||||
export const CompleteStep = () => (
|
||||
<div className="flex flex-col items-center justify-center gap-5 py-4">
|
||||
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-emerald-500/15">
|
||||
<Rocket className="h-7 w-7 text-emerald-400" />
|
||||
</div>
|
||||
export const CompleteStep = ({
|
||||
companyCreated = false,
|
||||
companyName = null,
|
||||
}: {
|
||||
companyCreated?: boolean;
|
||||
companyName?: string | null;
|
||||
}) => {
|
||||
const hasFiredConfettiRef = useRef(false);
|
||||
|
||||
<div className="space-y-2 text-center">
|
||||
<p className="text-base font-semibold text-white">
|
||||
Welcome to your AI office
|
||||
</p>
|
||||
<p className="max-w-sm text-sm text-white/60">
|
||||
Your gateway is connected and your agents are ready. Step inside and
|
||||
explore the 3D workspace where your AI team operates.
|
||||
</p>
|
||||
</div>
|
||||
useEffect(() => {
|
||||
if (!companyCreated || hasFiredConfettiRef.current) return;
|
||||
hasFiredConfettiRef.current = true;
|
||||
const defaults = {
|
||||
spread: 68,
|
||||
startVelocity: 32,
|
||||
ticks: 220,
|
||||
gravity: 1.05,
|
||||
zIndex: 100130,
|
||||
colors: ["#67e8f9", "#fbbf24", "#fde047", "#f472b6", "#c4b5fd"],
|
||||
};
|
||||
void confetti({
|
||||
...defaults,
|
||||
particleCount: 90,
|
||||
origin: { x: 0.5, y: 0.35 },
|
||||
});
|
||||
window.setTimeout(() => {
|
||||
void confetti({
|
||||
...defaults,
|
||||
particleCount: 70,
|
||||
angle: 60,
|
||||
origin: { x: 0.15, y: 0.45 },
|
||||
});
|
||||
void confetti({
|
||||
...defaults,
|
||||
particleCount: 70,
|
||||
angle: 120,
|
||||
origin: { x: 0.85, y: 0.45 },
|
||||
});
|
||||
}, 180);
|
||||
}, [companyCreated]);
|
||||
|
||||
<div className="w-full max-w-xs space-y-2">
|
||||
<div className="flex items-center gap-2.5 rounded-lg border border-white/8 bg-white/[0.03] px-3.5 py-2.5">
|
||||
<Building2 className="h-4 w-4 shrink-0 text-emerald-400" />
|
||||
<div>
|
||||
<p className="text-xs font-medium text-white">Explore the Office</p>
|
||||
<p className="text-[10px] text-white/45">
|
||||
Navigate rooms, watch agents, and interact
|
||||
</p>
|
||||
return (
|
||||
<div className="relative flex flex-col items-center justify-center gap-5 py-4">
|
||||
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-amber-400/15">
|
||||
<Rocket className="h-7 w-7 text-amber-300" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-center">
|
||||
<p className="text-base font-semibold text-white">
|
||||
{companyCreated
|
||||
? `${companyName?.trim() || "Your company"} created successfully`
|
||||
: "Welcome to your AI office"}
|
||||
</p>
|
||||
<p className="max-w-sm text-sm text-white/60">
|
||||
{companyCreated
|
||||
? `${companyName?.trim() || "Your company"} is ready. Your new team has been created in OpenClaw and placed into the office.`
|
||||
: "Your gateway is connected and your agents are ready. Step inside and explore the 3D workspace where your AI team operates."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-xs space-y-2">
|
||||
<div className="flex items-center gap-2.5 rounded-lg border border-white/8 bg-white/[0.03] px-3.5 py-2.5">
|
||||
<Building2 className="h-4 w-4 shrink-0 text-amber-300" />
|
||||
<div>
|
||||
<p className="text-xs font-medium text-white">
|
||||
{companyCreated ? "Meet Your New Team" : "Explore the Office"}
|
||||
</p>
|
||||
<p className="text-[10px] text-white/45">
|
||||
{companyCreated
|
||||
? "Walk the office, inspect the new roles, and start delegating work."
|
||||
: "Navigate rooms, watch agents, and interact"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-white/35">
|
||||
You can always re-run onboarding from Studio settings.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
<p className="text-[11px] text-white/35">
|
||||
You can always re-run onboarding from Studio settings.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
* layout suited for the wizard modal.
|
||||
*/
|
||||
import { useState } from "react";
|
||||
import { CheckCircle2, Eye, EyeOff, Loader2, Wifi, WifiOff } from "lucide-react";
|
||||
import { CheckCircle2, Eye, EyeOff, Wifi, WifiOff } from "lucide-react";
|
||||
import { RunningAvatarLoader } from "@/features/agents/components/RunningAvatarLoader";
|
||||
|
||||
export type ConnectStepProps = {
|
||||
gatewayUrl: string;
|
||||
@@ -33,8 +34,8 @@ export const ConnectStep = ({
|
||||
if (connected) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-3 py-8">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-emerald-500/20">
|
||||
<CheckCircle2 className="h-6 w-6 text-emerald-400" />
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-amber-500/20">
|
||||
<CheckCircle2 className="h-6 w-6 text-amber-300" />
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-white">Connected!</p>
|
||||
<p className="text-xs text-white/60">
|
||||
@@ -57,7 +58,7 @@ export const ConnectStep = ({
|
||||
Gateway URL
|
||||
</span>
|
||||
<input
|
||||
className="h-9 rounded-md border border-white/10 bg-white/5 px-3 font-mono text-sm text-white outline-none placeholder:text-white/30 focus:border-emerald-500/50"
|
||||
className="h-9 rounded-md border border-white/10 bg-white/5 px-3 font-mono text-sm text-white outline-none placeholder:text-white/30 focus:border-amber-400/50"
|
||||
type="text"
|
||||
value={gatewayUrl}
|
||||
onChange={(e) => onGatewayUrlChange(e.target.value)}
|
||||
@@ -72,7 +73,7 @@ export const ConnectStep = ({
|
||||
</span>
|
||||
<div className="relative">
|
||||
<input
|
||||
className="h-9 w-full rounded-md border border-white/10 bg-white/5 px-3 pr-9 font-mono text-sm text-white outline-none placeholder:text-white/30 focus:border-emerald-500/50"
|
||||
className="h-9 w-full rounded-md border border-white/10 bg-white/5 px-3 pr-9 font-mono text-sm text-white outline-none placeholder:text-white/30 focus:border-amber-400/50"
|
||||
type={showToken ? "text" : "password"}
|
||||
value={token}
|
||||
onChange={(e) => onTokenChange(e.target.value)}
|
||||
@@ -96,13 +97,13 @@ export const ConnectStep = ({
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-md bg-emerald-600 px-4 text-xs font-semibold text-white transition-colors hover:bg-emerald-500 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-md bg-amber-500 px-4 text-xs font-semibold text-[#1a1206] transition-colors hover:bg-amber-400 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
onClick={onConnect}
|
||||
disabled={connecting || !gatewayUrl.trim()}
|
||||
>
|
||||
{connecting ? (
|
||||
<>
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
<RunningAvatarLoader size={16} trackWidth={32} inline />
|
||||
Connecting…
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -19,6 +19,7 @@ import { WelcomeStep } from "@/features/onboarding/components/WelcomeStep";
|
||||
import { PrerequisitesStep } from "@/features/onboarding/components/PrerequisitesStep";
|
||||
import { ConnectStep } from "@/features/onboarding/components/ConnectStep";
|
||||
import { AgentsStep } from "@/features/onboarding/components/AgentsStep";
|
||||
import { CompanyStep } from "@/features/onboarding/components/CompanyStep";
|
||||
import { CompleteStep } from "@/features/onboarding/components/CompleteStep";
|
||||
|
||||
export type OnboardingWizardProps = {
|
||||
@@ -36,6 +37,12 @@ export type OnboardingWizardProps = {
|
||||
onConnect: () => void;
|
||||
/** Called when the user finishes or dismisses the wizard. */
|
||||
onComplete: () => void;
|
||||
/** Opens the reusable company builder. */
|
||||
onOpenCompanyBuilder: () => void;
|
||||
initialStep?: OnboardingStepId;
|
||||
initialCompletedSteps?: OnboardingStepId[];
|
||||
createdCompanyName?: string | null;
|
||||
companyCreated?: boolean;
|
||||
/** Connection error message, if any. */
|
||||
connectionError: string | null;
|
||||
/** Whether we're currently connecting. */
|
||||
@@ -51,12 +58,17 @@ export const OnboardingWizard = ({
|
||||
onTokenChange,
|
||||
onConnect,
|
||||
onComplete,
|
||||
onOpenCompanyBuilder,
|
||||
initialStep = "welcome",
|
||||
initialCompletedSteps,
|
||||
createdCompanyName = null,
|
||||
companyCreated = false,
|
||||
connectionError,
|
||||
connecting,
|
||||
}: OnboardingWizardProps) => {
|
||||
const [currentStep, setCurrentStep] = useState<OnboardingStepId>("welcome");
|
||||
const [currentStep, setCurrentStep] = useState<OnboardingStepId>(initialStep);
|
||||
const [completedSteps, setCompletedSteps] = useState<Set<OnboardingStepId>>(
|
||||
new Set(),
|
||||
() => new Set(initialCompletedSteps ?? []),
|
||||
);
|
||||
|
||||
const stepIndex = useMemo(() => getStepIndex(currentStep), [currentStep]);
|
||||
@@ -116,8 +128,21 @@ export const OnboardingWizard = ({
|
||||
);
|
||||
case "agents":
|
||||
return <AgentsStep agentCount={agentCount} connected={gatewayConnected} />;
|
||||
case "company":
|
||||
return (
|
||||
<CompanyStep
|
||||
connected={gatewayConnected}
|
||||
agentCount={agentCount}
|
||||
onOpenCompanyBuilder={onOpenCompanyBuilder}
|
||||
/>
|
||||
);
|
||||
case "complete":
|
||||
return <CompleteStep />;
|
||||
return (
|
||||
<CompleteStep
|
||||
companyCreated={companyCreated}
|
||||
companyName={createdCompanyName}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -125,7 +150,7 @@ export const OnboardingWizard = ({
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100000] flex items-center justify-center bg-black/70 backdrop-blur-sm">
|
||||
<div className="relative mx-4 flex w-full max-w-[560px] flex-col overflow-hidden rounded-xl border border-white/10 bg-[#0d1117] shadow-2xl">
|
||||
<div className="relative mx-4 flex h-[min(92vh,640px)] w-full max-w-[560px] flex-col overflow-hidden rounded-xl border border-white/10 bg-[#0d1117] shadow-2xl">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-white/10 px-6 py-4">
|
||||
<div>
|
||||
@@ -154,9 +179,9 @@ export const OnboardingWizard = ({
|
||||
key={step.id}
|
||||
className={`h-1 flex-1 rounded-full transition-colors ${
|
||||
idx <= stepIndex
|
||||
? "bg-emerald-500"
|
||||
? "bg-amber-400"
|
||||
: completedSteps.has(step.id)
|
||||
? "bg-emerald-500/40"
|
||||
? "bg-amber-400/40"
|
||||
: "bg-white/10"
|
||||
}`}
|
||||
/>
|
||||
@@ -164,7 +189,7 @@ export const OnboardingWizard = ({
|
||||
</div>
|
||||
|
||||
{/* Step content */}
|
||||
<div className="min-h-[280px] px-6 py-5">{renderStepContent()}</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-6 py-4">{renderStepContent()}</div>
|
||||
|
||||
{/* Footer navigation */}
|
||||
<div className="flex items-center justify-between border-t border-white/10 px-6 py-4">
|
||||
@@ -187,7 +212,7 @@ export const OnboardingWizard = ({
|
||||
{currentStep === "complete" ? (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1.5 rounded-md bg-emerald-600 px-4 py-2 text-xs font-semibold text-white transition-colors hover:bg-emerald-500"
|
||||
className="inline-flex items-center gap-1.5 rounded-md bg-amber-500 px-4 py-2 text-xs font-semibold text-[#1a1206] transition-colors hover:bg-amber-400"
|
||||
onClick={onComplete}
|
||||
>
|
||||
Enter Office
|
||||
|
||||
@@ -28,24 +28,24 @@ const prerequisites = [
|
||||
] as const;
|
||||
|
||||
export const PrerequisitesStep = () => (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-white/70">
|
||||
<div className="space-y-2.5">
|
||||
<p className="text-[13px] leading-5 text-white/70">
|
||||
Make sure you have these ready before connecting. If you already have
|
||||
OpenClaw running, you can skip this step.
|
||||
</p>
|
||||
|
||||
<div className="space-y-2.5">
|
||||
<div className="space-y-1.5">
|
||||
{prerequisites.map(({ label, detail, ...rest }) => (
|
||||
<div
|
||||
key={label}
|
||||
className="flex gap-3 rounded-lg border border-white/8 bg-white/[0.02] px-3.5 py-3"
|
||||
className="flex gap-2.5 rounded-lg border border-white/8 bg-white/[0.02] px-3 py-2"
|
||||
>
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-white/30" />
|
||||
<CheckCircle2 className="mt-0.5 h-3.5 w-3.5 shrink-0 text-white/30" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs font-semibold text-white">{label}</p>
|
||||
<p className="mt-0.5 text-[11px] text-white/55">{detail}</p>
|
||||
<p className="text-[11px] font-semibold text-white">{label}</p>
|
||||
<p className="mt-0.5 text-[10px] leading-4 text-white/55">{detail}</p>
|
||||
{"command" in rest ? (
|
||||
<code className="mt-1.5 block rounded bg-black/40 px-2 py-1 font-mono text-[11px] text-emerald-400">
|
||||
<code className="mt-1 block rounded bg-black/40 px-2 py-0.5 font-mono text-[10px] text-amber-300">
|
||||
{rest.command}
|
||||
</code>
|
||||
) : null}
|
||||
@@ -54,7 +54,7 @@ export const PrerequisitesStep = () => (
|
||||
href={rest.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-1.5 inline-flex items-center gap-1 text-[11px] text-emerald-400 hover:text-emerald-300"
|
||||
className="mt-1 inline-flex items-center gap-1 text-[10px] leading-4 text-amber-300 hover:text-amber-200"
|
||||
>
|
||||
{rest.linkLabel ?? "Learn more"}
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
@@ -65,13 +65,13 @@ export const PrerequisitesStep = () => (
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-white/40">
|
||||
<p className="text-[10px] leading-4 text-white/40">
|
||||
Need help? Check{" "}
|
||||
<a
|
||||
href="https://docs.openclaw.ai"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-emerald-400/70 hover:text-emerald-300"
|
||||
className="text-amber-300/70 hover:text-amber-200"
|
||||
>
|
||||
docs.openclaw.ai
|
||||
</a>{" "}
|
||||
@@ -80,7 +80,7 @@ export const PrerequisitesStep = () => (
|
||||
href="https://discord.com/invite/clawd"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-emerald-400/70 hover:text-emerald-300"
|
||||
className="text-amber-300/70 hover:text-amber-200"
|
||||
>
|
||||
join Discord
|
||||
</a>
|
||||
|
||||
@@ -48,7 +48,7 @@ export const WelcomeStep = () => (
|
||||
className="rounded-lg border border-white/8 bg-white/[0.03] px-3.5 py-3"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="h-4 w-4 shrink-0 text-emerald-400" />
|
||||
<Icon className="h-4 w-4 shrink-0 text-amber-300" />
|
||||
<span className="text-xs font-semibold text-white">{title}</span>
|
||||
</div>
|
||||
<p className="mt-1.5 text-[11px] leading-snug text-white/55">
|
||||
|
||||
Reference in New Issue
Block a user