- Third agentsApi.create() call with role: "general", name: "Generalist"
- metadata: { pendingSkillGroups: ["Creative"] } records Phase 11 intent
- Updated description text: mentions all 3 agents (PM, engineer, generalist)
- Placed BEFORE queryClient.invalidateQueries for clean ordering
228 lines
7.5 KiB
TypeScript
228 lines
7.5 KiB
TypeScript
// [nexus] Replacement onboarding wizard — single-step root directory flow
|
|
// Exports `OnboardingWizard` to match the named import in App.tsx.
|
|
// Wired via Vite alias: all imports of ./components/OnboardingWizard are
|
|
// redirected here at build time; the original file is preserved for upstream rebase.
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { createPortal } from "react-dom"; // [nexus] use raw portal, not radix DialogPortal
|
|
import { useLocation, useNavigate, useParams } from "@/lib/router";
|
|
import { VOCAB } from "@paperclipai/branding";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
import { useCompany } from "../context/CompanyContext";
|
|
import { useDialog } from "../context/DialogContext";
|
|
import { companiesApi } from "../api/companies";
|
|
import { agentsApi } from "../api/agents";
|
|
import { queryKeys } from "../lib/queryKeys";
|
|
import { resolveRouteOnboardingOptions } from "../lib/onboarding-route";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { cn } from "../lib/utils";
|
|
|
|
// [nexus] Single-step onboarding wizard: root directory → workspace + PM + Engineer
|
|
export function OnboardingWizard() {
|
|
const { onboardingOpen, onboardingOptions, closeOnboarding } = useDialog();
|
|
const { companies, setSelectedCompanyId, loading: companiesLoading } = useCompany();
|
|
const queryClient = useQueryClient();
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const { companyPrefix } = useParams<{ companyPrefix?: string }>();
|
|
const [routeDismissed, setRouteDismissed] = useState(false);
|
|
|
|
// Preserve wizard-show detection logic from the original OnboardingWizard
|
|
const routeOnboardingOptions =
|
|
companyPrefix && companiesLoading
|
|
? null
|
|
: resolveRouteOnboardingOptions({
|
|
pathname: location.pathname,
|
|
companyPrefix,
|
|
companies,
|
|
});
|
|
|
|
const effectiveOnboardingOpen =
|
|
onboardingOpen || (routeOnboardingOptions !== null && !routeDismissed);
|
|
|
|
useEffect(() => {
|
|
setRouteDismissed(false);
|
|
}, [location.pathname]);
|
|
|
|
// Form state
|
|
const [rootDir, setRootDir] = useState("");
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Reset form when wizard closes
|
|
useEffect(() => {
|
|
if (!effectiveOnboardingOpen) {
|
|
setRootDir("");
|
|
setError(null);
|
|
setLoading(false);
|
|
}
|
|
}, [effectiveOnboardingOpen]);
|
|
|
|
function handleClose() {
|
|
setRouteDismissed(true);
|
|
closeOnboarding();
|
|
}
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
if (!rootDir.trim()) return;
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
// Step 1: Create workspace (company) named after VOCAB.appName
|
|
const company = await companiesApi.create({ name: VOCAB.appName });
|
|
setSelectedCompanyId(company.id);
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.companies.all });
|
|
|
|
const adapterConfig = { cwd: rootDir.trim() };
|
|
const runtimeConfig = {
|
|
heartbeat: {
|
|
enabled: true,
|
|
intervalSec: 3600,
|
|
wakeOnDemand: true,
|
|
cooldownSec: 10,
|
|
maxConcurrentRuns: 1,
|
|
},
|
|
};
|
|
|
|
// Step 2: Create PM agent with role "ceo" for elevated permissions
|
|
// (display label is "Project Manager" via AGENT_ROLE_LABELS; ceo/ bundle
|
|
// has been rewritten with PM content in 04-01)
|
|
await agentsApi.create(company.id, {
|
|
name: "Project Manager",
|
|
role: "ceo",
|
|
adapterType: "claude_local",
|
|
adapterConfig,
|
|
runtimeConfig,
|
|
});
|
|
|
|
// Step 3: Create Engineer agent
|
|
await agentsApi.create(company.id, {
|
|
name: "Engineer",
|
|
role: "engineer",
|
|
adapterType: "claude_local",
|
|
adapterConfig,
|
|
runtimeConfig,
|
|
});
|
|
|
|
// Step 4: Create Generalist agent (non-code work: copy, research, docs)
|
|
await agentsApi.create(company.id, {
|
|
name: "Generalist",
|
|
role: "general",
|
|
adapterType: "claude_local",
|
|
adapterConfig,
|
|
runtimeConfig,
|
|
metadata: { pendingSkillGroups: ["Creative"] },
|
|
});
|
|
|
|
queryClient.invalidateQueries({
|
|
queryKey: queryKeys.agents.list(company.id),
|
|
});
|
|
|
|
// Navigate to dashboard — not an issue detail page
|
|
closeOnboarding();
|
|
navigate(`/${company.issuePrefix}/dashboard`);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Setup failed. Please try again.");
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
if (!effectiveOnboardingOpen) return null;
|
|
|
|
return createPortal(
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
{/* Backdrop */}
|
|
<div
|
|
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
|
onClick={handleClose}
|
|
/>
|
|
|
|
{/* Card */}
|
|
<div
|
|
className={cn(
|
|
"relative z-10 w-full max-w-md mx-4 rounded-xl border bg-card text-card-foreground shadow-2xl",
|
|
"p-8 flex flex-col gap-6"
|
|
)}
|
|
>
|
|
{/* Header */}
|
|
<div className="flex flex-col gap-2 text-center">
|
|
<h1 className="text-2xl font-semibold tracking-tight">
|
|
Welcome to {VOCAB.appName}
|
|
</h1>
|
|
<p className="text-sm text-muted-foreground">
|
|
Choose a project root directory. {VOCAB.appName} will set up a{" "}
|
|
{VOCAB.ceo.toLowerCase()}, engineer, and generalist to start working.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Form */}
|
|
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
|
<div className="flex flex-col gap-2">
|
|
<label
|
|
htmlFor="nexus-root-dir"
|
|
className="text-sm font-medium leading-none"
|
|
>
|
|
Project root directory
|
|
</label>
|
|
<Input
|
|
id="nexus-root-dir"
|
|
type="text"
|
|
placeholder="~/projects/my-project"
|
|
value={rootDir}
|
|
onChange={(e) => setRootDir(e.target.value)}
|
|
disabled={loading}
|
|
autoFocus
|
|
autoComplete="off"
|
|
className="font-mono text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{error && (
|
|
<p className="text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2">
|
|
{error}
|
|
</p>
|
|
)}
|
|
|
|
<Button
|
|
type="submit"
|
|
disabled={loading || !rootDir.trim()}
|
|
className="w-full"
|
|
>
|
|
{loading ? (
|
|
<span className="flex items-center gap-2">
|
|
<svg
|
|
className="h-4 w-4 animate-spin"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
aria-hidden="true"
|
|
>
|
|
<circle
|
|
className="opacity-25"
|
|
cx="12"
|
|
cy="12"
|
|
r="10"
|
|
stroke="currentColor"
|
|
strokeWidth="4"
|
|
/>
|
|
<path
|
|
className="opacity-75"
|
|
fill="currentColor"
|
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
/>
|
|
</svg>
|
|
Setting up…
|
|
</span>
|
|
) : (
|
|
"Get Started"
|
|
)}
|
|
</Button>
|
|
</form>
|
|
</div>
|
|
</div>,
|
|
document.body // [nexus] portal to body, not radix DialogPortal
|
|
);
|
|
}
|