737 lines
30 KiB
TypeScript
737 lines
30 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { useDialog } from "../context/DialogContext";
|
|
import { useCompany } from "../context/CompanyContext";
|
|
import { companiesApi } from "../api/companies";
|
|
import { goalsApi } from "../api/goals";
|
|
import { agentsApi } from "../api/agents";
|
|
import { issuesApi } from "../api/issues";
|
|
import { queryKeys } from "../lib/queryKeys";
|
|
import { Dialog, DialogOverlay, DialogPortal } from "@/components/ui/dialog";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover";
|
|
import { Button } from "@/components/ui/button";
|
|
import { cn } from "../lib/utils";
|
|
import { getUIAdapter } from "../adapters";
|
|
import { defaultCreateValues } from "./agent-config-defaults";
|
|
import { AsciiArtAnimation } from "./AsciiArtAnimation";
|
|
import { ChoosePathButton } from "./PathInstructionsModal";
|
|
import { HintIcon } from "./agent-config-primitives";
|
|
import {
|
|
Building2,
|
|
Bot,
|
|
Code,
|
|
ListTodo,
|
|
Rocket,
|
|
ArrowLeft,
|
|
ArrowRight,
|
|
Terminal,
|
|
Globe,
|
|
Sparkles,
|
|
MousePointer2,
|
|
Check,
|
|
Loader2,
|
|
FolderOpen,
|
|
ChevronDown,
|
|
X,
|
|
} from "lucide-react";
|
|
|
|
type Step = 1 | 2 | 3 | 4;
|
|
type AdapterType = "claude_local" | "codex_local" | "process" | "http" | "openclaw";
|
|
|
|
const DEFAULT_TASK_DESCRIPTION = `Setup yourself as the CEO. Use the ceo persona found here: [https://github.com/paperclipai/companies/blob/main/default/ceo/AGENTS.md](https://github.com/paperclipai/companies/blob/main/default/ceo/AGENTS.md)
|
|
|
|
Ensure you have a folder agents/ceo and then download this AGENTS.md as well as the sibling HEARTBEAT.md, SOUL.md, and TOOLS.md. and set that AGENTS.md as the path to your agents instruction file`;
|
|
|
|
export function OnboardingWizard() {
|
|
const { onboardingOpen, onboardingOptions, closeOnboarding } = useDialog();
|
|
const { selectedCompanyId, companies, setSelectedCompanyId } = useCompany();
|
|
const queryClient = useQueryClient();
|
|
const navigate = useNavigate();
|
|
|
|
const initialStep = onboardingOptions.initialStep ?? 1;
|
|
const existingCompanyId = onboardingOptions.companyId;
|
|
|
|
const [step, setStep] = useState<Step>(initialStep);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [modelOpen, setModelOpen] = useState(false);
|
|
|
|
// Step 1
|
|
const [companyName, setCompanyName] = useState("");
|
|
const [companyGoal, setCompanyGoal] = useState("");
|
|
|
|
// Step 2
|
|
const [agentName, setAgentName] = useState("CEO");
|
|
const [adapterType, setAdapterType] = useState<AdapterType>("claude_local");
|
|
const [cwd, setCwd] = useState("");
|
|
const [model, setModel] = useState("");
|
|
const [command, setCommand] = useState("");
|
|
const [args, setArgs] = useState("");
|
|
const [url, setUrl] = useState("");
|
|
|
|
// Step 3
|
|
const [taskTitle, setTaskTitle] = useState("Create your CEO HEARTBEAT.md");
|
|
const [taskDescription, setTaskDescription] = useState(DEFAULT_TASK_DESCRIPTION);
|
|
|
|
// Created entity IDs — pre-populate from existing company when skipping step 1
|
|
const [createdCompanyId, setCreatedCompanyId] = useState<string | null>(existingCompanyId ?? null);
|
|
const [createdCompanyPrefix, setCreatedCompanyPrefix] = useState<string | null>(null);
|
|
const [createdAgentId, setCreatedAgentId] = useState<string | null>(null);
|
|
const [createdIssueRef, setCreatedIssueRef] = useState<string | null>(null);
|
|
|
|
// Sync step and company when onboarding opens with options.
|
|
// Keep this independent from company-list refreshes so Step 1 completion
|
|
// doesn't get reset after creating a company.
|
|
useEffect(() => {
|
|
if (!onboardingOpen) return;
|
|
const cId = onboardingOptions.companyId ?? null;
|
|
setStep(onboardingOptions.initialStep ?? 1);
|
|
setCreatedCompanyId(cId);
|
|
setCreatedCompanyPrefix(null);
|
|
}, [onboardingOpen, onboardingOptions.companyId, onboardingOptions.initialStep]);
|
|
|
|
// Backfill issue prefix for an existing company once companies are loaded.
|
|
useEffect(() => {
|
|
if (!onboardingOpen || !createdCompanyId || createdCompanyPrefix) return;
|
|
const company = companies.find((c) => c.id === createdCompanyId);
|
|
if (company) setCreatedCompanyPrefix(company.issuePrefix);
|
|
}, [onboardingOpen, createdCompanyId, createdCompanyPrefix, companies]);
|
|
|
|
const { data: adapterModels } = useQuery({
|
|
queryKey: ["adapter-models", adapterType],
|
|
queryFn: () => agentsApi.adapterModels(adapterType),
|
|
enabled: onboardingOpen && step === 2,
|
|
});
|
|
|
|
const selectedModel = (adapterModels ?? []).find((m) => m.id === model);
|
|
|
|
function reset() {
|
|
setStep(1);
|
|
setLoading(false);
|
|
setError(null);
|
|
setCompanyName("");
|
|
setCompanyGoal("");
|
|
setAgentName("CEO");
|
|
setAdapterType("claude_local");
|
|
setCwd("");
|
|
setModel("");
|
|
setCommand("");
|
|
setArgs("");
|
|
setUrl("");
|
|
setTaskTitle("Create your CEO HEARTBEAT.md");
|
|
setTaskDescription(DEFAULT_TASK_DESCRIPTION);
|
|
setCreatedCompanyId(null);
|
|
setCreatedCompanyPrefix(null);
|
|
setCreatedAgentId(null);
|
|
setCreatedIssueRef(null);
|
|
}
|
|
|
|
function handleClose() {
|
|
reset();
|
|
closeOnboarding();
|
|
}
|
|
|
|
function buildAdapterConfig(): Record<string, unknown> {
|
|
const adapter = getUIAdapter(adapterType);
|
|
return adapter.buildAdapterConfig({
|
|
...defaultCreateValues,
|
|
adapterType,
|
|
cwd,
|
|
model,
|
|
command,
|
|
args,
|
|
url,
|
|
dangerouslySkipPermissions: adapterType === "claude_local",
|
|
});
|
|
}
|
|
|
|
async function handleStep1Next() {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const company = await companiesApi.create({ name: companyName.trim() });
|
|
setCreatedCompanyId(company.id);
|
|
setCreatedCompanyPrefix(company.issuePrefix);
|
|
setSelectedCompanyId(company.id);
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.companies.all });
|
|
|
|
if (companyGoal.trim()) {
|
|
await goalsApi.create(company.id, {
|
|
title: companyGoal.trim(),
|
|
level: "company",
|
|
status: "active",
|
|
});
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.goals.list(company.id) });
|
|
}
|
|
|
|
setStep(2);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Failed to create company");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function handleStep2Next() {
|
|
if (!createdCompanyId) return;
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const agent = await agentsApi.create(createdCompanyId, {
|
|
name: agentName.trim(),
|
|
role: "ceo",
|
|
adapterType,
|
|
adapterConfig: buildAdapterConfig(),
|
|
runtimeConfig: {
|
|
heartbeat: {
|
|
enabled: true,
|
|
intervalSec: 300,
|
|
wakeOnDemand: true,
|
|
cooldownSec: 10,
|
|
maxConcurrentRuns: 1,
|
|
},
|
|
},
|
|
});
|
|
setCreatedAgentId(agent.id);
|
|
queryClient.invalidateQueries({
|
|
queryKey: queryKeys.agents.list(createdCompanyId),
|
|
});
|
|
setStep(3);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Failed to create agent");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function handleStep3Next() {
|
|
if (!createdCompanyId || !createdAgentId) return;
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const issue = await issuesApi.create(createdCompanyId, {
|
|
title: taskTitle.trim(),
|
|
...(taskDescription.trim() ? { description: taskDescription.trim() } : {}),
|
|
assigneeAgentId: createdAgentId,
|
|
status: "todo",
|
|
});
|
|
setCreatedIssueRef(issue.identifier ?? issue.id);
|
|
queryClient.invalidateQueries({
|
|
queryKey: queryKeys.issues.list(createdCompanyId),
|
|
});
|
|
setStep(4);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Failed to create task");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function handleLaunch() {
|
|
if (!createdAgentId) return;
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
await agentsApi.invoke(createdAgentId);
|
|
} catch {
|
|
// Agent may already be running from auto-wake — that's fine
|
|
}
|
|
setLoading(false);
|
|
reset();
|
|
closeOnboarding();
|
|
if (createdCompanyPrefix && createdIssueRef) {
|
|
navigate(`/${createdCompanyPrefix}/issues/${createdIssueRef}`);
|
|
return;
|
|
}
|
|
if (createdCompanyPrefix) {
|
|
navigate(`/${createdCompanyPrefix}/dashboard`);
|
|
return;
|
|
}
|
|
navigate("/dashboard");
|
|
}
|
|
|
|
function handleKeyDown(e: React.KeyboardEvent) {
|
|
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
e.preventDefault();
|
|
if (step === 1 && companyName.trim()) handleStep1Next();
|
|
else if (step === 2 && agentName.trim()) handleStep2Next();
|
|
else if (step === 3 && taskTitle.trim()) handleStep3Next();
|
|
else if (step === 4) handleLaunch();
|
|
}
|
|
}
|
|
|
|
if (!onboardingOpen) return null;
|
|
|
|
return (
|
|
<Dialog
|
|
open={onboardingOpen}
|
|
onOpenChange={(open) => {
|
|
if (!open) handleClose();
|
|
}}
|
|
>
|
|
<DialogPortal>
|
|
<DialogOverlay className="bg-background" />
|
|
<div
|
|
className="fixed inset-0 z-50 flex"
|
|
onKeyDown={handleKeyDown}
|
|
>
|
|
{/* Close button */}
|
|
<button
|
|
onClick={handleClose}
|
|
className="absolute top-4 left-4 z-10 rounded-sm p-1.5 text-muted-foreground/60 hover:text-foreground transition-colors"
|
|
>
|
|
<X className="h-5 w-5" />
|
|
<span className="sr-only">Close</span>
|
|
</button>
|
|
|
|
{/* Left half — form */}
|
|
<div className="w-full md:w-1/2 flex flex-col overflow-y-auto">
|
|
<div className="w-full max-w-md mx-auto my-auto px-8 py-12">
|
|
{/* Progress indicators */}
|
|
<div className="flex items-center gap-2 mb-8">
|
|
<Sparkles className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-sm font-medium">Get Started</span>
|
|
<span className="text-sm text-muted-foreground/60">
|
|
Step {step} of 4
|
|
</span>
|
|
<div className="flex items-center gap-1.5 ml-auto">
|
|
{[1, 2, 3, 4].map((s) => (
|
|
<div
|
|
key={s}
|
|
className={cn(
|
|
"h-1.5 w-6 rounded-full transition-colors",
|
|
s < step
|
|
? "bg-green-500"
|
|
: s === step
|
|
? "bg-foreground"
|
|
: "bg-muted"
|
|
)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Step content */}
|
|
{step === 1 && (
|
|
<div className="space-y-5">
|
|
<div className="flex items-center gap-3 mb-1">
|
|
<div className="bg-muted/50 p-2">
|
|
<Building2 className="h-5 w-5 text-muted-foreground" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-medium">Name your company</h3>
|
|
<p className="text-xs text-muted-foreground">
|
|
This is the organization your agents will work for.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs text-muted-foreground mb-1 block">
|
|
Company name
|
|
</label>
|
|
<input
|
|
className="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50"
|
|
placeholder="Acme Corp"
|
|
value={companyName}
|
|
onChange={(e) => setCompanyName(e.target.value)}
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs text-muted-foreground mb-1 block">
|
|
Mission / goal (optional)
|
|
</label>
|
|
<textarea
|
|
className="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50 resize-none min-h-[60px]"
|
|
placeholder="What is this company trying to achieve?"
|
|
value={companyGoal}
|
|
onChange={(e) => setCompanyGoal(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{step === 2 && (
|
|
<div className="space-y-5">
|
|
<div className="flex items-center gap-3 mb-1">
|
|
<div className="bg-muted/50 p-2">
|
|
<Bot className="h-5 w-5 text-muted-foreground" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-medium">Create your first agent</h3>
|
|
<p className="text-xs text-muted-foreground">
|
|
Choose how this agent will run tasks.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs text-muted-foreground mb-1 block">
|
|
Agent name
|
|
</label>
|
|
<input
|
|
className="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50"
|
|
placeholder="CEO"
|
|
value={agentName}
|
|
onChange={(e) => setAgentName(e.target.value)}
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
|
|
{/* Adapter type radio cards */}
|
|
<div>
|
|
<label className="text-xs text-muted-foreground mb-2 block">
|
|
Adapter type
|
|
</label>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{([
|
|
{
|
|
value: "claude_local" as const,
|
|
label: "Claude Code",
|
|
icon: Sparkles,
|
|
desc: "Local Claude agent",
|
|
},
|
|
{
|
|
value: "codex_local" as const,
|
|
label: "Codex",
|
|
icon: Code,
|
|
desc: "Local Codex agent",
|
|
},
|
|
{
|
|
value: "openclaw" as const,
|
|
label: "OpenClaw",
|
|
icon: Bot,
|
|
desc: "Notify OpenClaw webhook",
|
|
comingSoon: true,
|
|
},
|
|
{
|
|
value: "cursor" as const,
|
|
label: "Cursor",
|
|
icon: MousePointer2,
|
|
desc: "Cursor AI agent",
|
|
comingSoon: true,
|
|
},
|
|
{
|
|
value: "process" as const,
|
|
label: "Shell Command",
|
|
icon: Terminal,
|
|
desc: "Run a process",
|
|
comingSoon: true,
|
|
},
|
|
{
|
|
value: "http" as const,
|
|
label: "HTTP Webhook",
|
|
icon: Globe,
|
|
desc: "Call an endpoint",
|
|
comingSoon: true,
|
|
},
|
|
]).map((opt) => (
|
|
<button
|
|
key={opt.value}
|
|
disabled={!!opt.comingSoon}
|
|
className={cn(
|
|
"flex flex-col items-center gap-1.5 rounded-md border p-3 text-xs transition-colors relative",
|
|
opt.comingSoon
|
|
? "border-border opacity-40 cursor-not-allowed"
|
|
: adapterType === opt.value
|
|
? "border-foreground bg-accent"
|
|
: "border-border hover:bg-accent/50"
|
|
)}
|
|
onClick={() => {
|
|
if (!opt.comingSoon) setAdapterType(opt.value as AdapterType);
|
|
}}
|
|
>
|
|
<opt.icon className="h-4 w-4" />
|
|
<span className="font-medium">{opt.label}</span>
|
|
<span className="text-muted-foreground text-[10px]">
|
|
{opt.comingSoon ? "Coming soon" : opt.desc}
|
|
</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Conditional adapter fields */}
|
|
{(adapterType === "claude_local" || adapterType === "codex_local") && (
|
|
<div className="space-y-3">
|
|
<div>
|
|
<div className="flex items-center gap-1.5 mb-1">
|
|
<label className="text-xs text-muted-foreground">
|
|
Working directory
|
|
</label>
|
|
<HintIcon text="Paperclip works best if you create a new folder for your agents to keep their memories and stay organized. Create a new folder and put the path here." />
|
|
</div>
|
|
<div className="flex items-center gap-2 rounded-md border border-border px-2.5 py-1.5">
|
|
<FolderOpen className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
<input
|
|
className="w-full bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/50"
|
|
placeholder="/path/to/project"
|
|
value={cwd}
|
|
onChange={(e) => setCwd(e.target.value)}
|
|
/>
|
|
<ChoosePathButton />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs text-muted-foreground mb-1 block">
|
|
Model
|
|
</label>
|
|
<Popover open={modelOpen} onOpenChange={setModelOpen}>
|
|
<PopoverTrigger asChild>
|
|
<button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2.5 py-1.5 text-sm hover:bg-accent/50 transition-colors w-full justify-between">
|
|
<span className={cn(!model && "text-muted-foreground")}>
|
|
{selectedModel ? selectedModel.label : model || "Default"}
|
|
</span>
|
|
<ChevronDown className="h-3 w-3 text-muted-foreground" />
|
|
</button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-1" align="start">
|
|
<button
|
|
className={cn(
|
|
"flex items-center gap-2 w-full px-2 py-1.5 text-sm rounded hover:bg-accent/50",
|
|
!model && "bg-accent"
|
|
)}
|
|
onClick={() => { setModel(""); setModelOpen(false); }}
|
|
>
|
|
Default
|
|
</button>
|
|
{(adapterModels ?? []).map((m) => (
|
|
<button
|
|
key={m.id}
|
|
className={cn(
|
|
"flex items-center justify-between w-full px-2 py-1.5 text-sm rounded hover:bg-accent/50",
|
|
m.id === model && "bg-accent"
|
|
)}
|
|
onClick={() => { setModel(m.id); setModelOpen(false); }}
|
|
>
|
|
<span>{m.label}</span>
|
|
<span className="text-xs text-muted-foreground font-mono">{m.id}</span>
|
|
</button>
|
|
))}
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{adapterType === "process" && (
|
|
<div className="space-y-3">
|
|
<div>
|
|
<label className="text-xs text-muted-foreground mb-1 block">
|
|
Command
|
|
</label>
|
|
<input
|
|
className="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm font-mono outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50"
|
|
placeholder="e.g. node, python"
|
|
value={command}
|
|
onChange={(e) => setCommand(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs text-muted-foreground mb-1 block">
|
|
Args (comma-separated)
|
|
</label>
|
|
<input
|
|
className="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm font-mono outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50"
|
|
placeholder="e.g. script.js, --flag"
|
|
value={args}
|
|
onChange={(e) => setArgs(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{(adapterType === "http" || adapterType === "openclaw") && (
|
|
<div>
|
|
<label className="text-xs text-muted-foreground mb-1 block">
|
|
Webhook URL
|
|
</label>
|
|
<input
|
|
className="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm font-mono outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50"
|
|
placeholder="https://..."
|
|
value={url}
|
|
onChange={(e) => setUrl(e.target.value)}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{step === 3 && (
|
|
<div className="space-y-5">
|
|
<div className="flex items-center gap-3 mb-1">
|
|
<div className="bg-muted/50 p-2">
|
|
<ListTodo className="h-5 w-5 text-muted-foreground" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-medium">Give it something to do</h3>
|
|
<p className="text-xs text-muted-foreground">
|
|
Give your agent a small task to start with — a bug fix, a
|
|
research question, writing a script.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs text-muted-foreground mb-1 block">
|
|
Task title
|
|
</label>
|
|
<input
|
|
className="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50"
|
|
placeholder="e.g. Research competitor pricing"
|
|
value={taskTitle}
|
|
onChange={(e) => setTaskTitle(e.target.value)}
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs text-muted-foreground mb-1 block">
|
|
Description (optional)
|
|
</label>
|
|
<textarea
|
|
className="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50 resize-none min-h-[80px]"
|
|
placeholder="Add more detail about what the agent should do..."
|
|
value={taskDescription}
|
|
onChange={(e) => setTaskDescription(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{step === 4 && (
|
|
<div className="space-y-5">
|
|
<div className="flex items-center gap-3 mb-1">
|
|
<div className="bg-muted/50 p-2">
|
|
<Rocket className="h-5 w-5 text-muted-foreground" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-medium">Ready to launch</h3>
|
|
<p className="text-xs text-muted-foreground">
|
|
Everything is set up. Launch your agent and watch it work.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="border border-border divide-y divide-border">
|
|
<div className="flex items-center gap-3 px-3 py-2.5">
|
|
<Building2 className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium truncate">{companyName}</p>
|
|
<p className="text-xs text-muted-foreground">Company</p>
|
|
</div>
|
|
<Check className="h-4 w-4 text-green-500 shrink-0" />
|
|
</div>
|
|
<div className="flex items-center gap-3 px-3 py-2.5">
|
|
<Bot className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium truncate">{agentName}</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{getUIAdapter(adapterType).label}
|
|
</p>
|
|
</div>
|
|
<Check className="h-4 w-4 text-green-500 shrink-0" />
|
|
</div>
|
|
<div className="flex items-center gap-3 px-3 py-2.5">
|
|
<ListTodo className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium truncate">{taskTitle}</p>
|
|
<p className="text-xs text-muted-foreground">Task</p>
|
|
</div>
|
|
<Check className="h-4 w-4 text-green-500 shrink-0" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Error */}
|
|
{error && (
|
|
<div className="mt-3">
|
|
<p className="text-xs text-destructive">{error}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Footer navigation */}
|
|
<div className="flex items-center justify-between mt-8">
|
|
<div>
|
|
{step > 1 && step > (onboardingOptions.initialStep ?? 1) && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setStep((step - 1) as Step)}
|
|
disabled={loading}
|
|
>
|
|
<ArrowLeft className="h-3.5 w-3.5 mr-1" />
|
|
Back
|
|
</Button>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{step === 1 && (
|
|
<Button
|
|
size="sm"
|
|
disabled={!companyName.trim() || loading}
|
|
onClick={handleStep1Next}
|
|
>
|
|
{loading ? (
|
|
<Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" />
|
|
) : (
|
|
<ArrowRight className="h-3.5 w-3.5 mr-1" />
|
|
)}
|
|
{loading ? "Creating..." : "Next"}
|
|
</Button>
|
|
)}
|
|
{step === 2 && (
|
|
<Button
|
|
size="sm"
|
|
disabled={!agentName.trim() || loading}
|
|
onClick={handleStep2Next}
|
|
>
|
|
{loading ? (
|
|
<Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" />
|
|
) : (
|
|
<ArrowRight className="h-3.5 w-3.5 mr-1" />
|
|
)}
|
|
{loading ? "Creating..." : "Next"}
|
|
</Button>
|
|
)}
|
|
{step === 3 && (
|
|
<Button
|
|
size="sm"
|
|
disabled={!taskTitle.trim() || loading}
|
|
onClick={handleStep3Next}
|
|
>
|
|
{loading ? (
|
|
<Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" />
|
|
) : (
|
|
<ArrowRight className="h-3.5 w-3.5 mr-1" />
|
|
)}
|
|
{loading ? "Creating..." : "Next"}
|
|
</Button>
|
|
)}
|
|
{step === 4 && (
|
|
<Button size="sm" disabled={loading} onClick={handleLaunch}>
|
|
{loading ? (
|
|
<Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" />
|
|
) : (
|
|
<Rocket className="h-3.5 w-3.5 mr-1" />
|
|
)}
|
|
{loading ? "Launching..." : "Launch Agent"}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right half — ASCII art (hidden on mobile) */}
|
|
<div className="hidden md:block w-1/2 overflow-hidden">
|
|
<AsciiArtAnimation />
|
|
</div>
|
|
</div>
|
|
</DialogPortal>
|
|
</Dialog>
|
|
);
|
|
}
|