experiment: add hiring plan review (step 5) and hire tasks (step 6)
Step 5 - Review Hiring Plan: - Parses CEO's markdown plan into structured role cards - Each role has a checkbox (include/exclude), edit button, delete button - Inline editing for role name and description - "Add role" button to create new roles manually - "Revise with CEO" button returns to chat (step 4) - Collapsible raw plan view for reference - "Approve" button only enabled when at least one role is selected Step 6 - Make Your First Hires: - Summary showing company, CEO, and all approved roles - "Make your first hires" creates one task per approved role assigned to the CEO (e.g., "Hire: Content Strategist") - Navigates to the task list after completion Plan parsing handles "- **Role Name**: description" markdown format. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b4ef0618e5
commit
b60fcd8d06
1 changed files with 307 additions and 43 deletions
|
|
@ -51,7 +51,11 @@ import {
|
||||||
Loader2,
|
Loader2,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
X
|
X,
|
||||||
|
Plus,
|
||||||
|
Pencil,
|
||||||
|
Trash2,
|
||||||
|
MessageSquare
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
type Step = 1 | 2 | 3 | 4 | 5 | 6;
|
type Step = 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
|
|
@ -81,6 +85,45 @@ function buildMissionFromQuestionnaire(q1: string, q2: string, q3: string, q4: s
|
||||||
return parts.join(" ");
|
return parts.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface HiringRole {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
enabled: boolean;
|
||||||
|
editing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let roleIdCounter = 0;
|
||||||
|
function nextRoleId(): string {
|
||||||
|
return `role-${++roleIdCounter}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a markdown hiring plan into structured roles.
|
||||||
|
* Looks for bullet points with bold role names: "- **Role Name**: description"
|
||||||
|
* Falls back to any bold text in bullet points.
|
||||||
|
*/
|
||||||
|
function parseHiringPlan(markdown: string): HiringRole[] {
|
||||||
|
const roles: HiringRole[] = [];
|
||||||
|
const lines = markdown.split("\n");
|
||||||
|
for (const line of lines) {
|
||||||
|
// Match "- **Role Name**: description" or "- **Role Name** - description"
|
||||||
|
const match = line.match(
|
||||||
|
/^\s*[-*]\s+\*\*([^*]+)\*\*[:\s-]*(.*)$/
|
||||||
|
);
|
||||||
|
if (match) {
|
||||||
|
roles.push({
|
||||||
|
id: nextRoleId(),
|
||||||
|
name: match[1].trim(),
|
||||||
|
description: match[2].trim(),
|
||||||
|
enabled: true,
|
||||||
|
editing: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_TASK_DESCRIPTION = `Setup yourself as the CEO. Use the ceo persona found here:
|
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
|
||||||
|
|
@ -150,6 +193,8 @@ export function OnboardingWizard() {
|
||||||
// Planning task + hiring plan
|
// Planning task + hiring plan
|
||||||
const [planningTaskId, setPlanningTaskId] = useState<string | null>(null);
|
const [planningTaskId, setPlanningTaskId] = useState<string | null>(null);
|
||||||
const [planContent, setPlanContent] = useState<string | null>(null);
|
const [planContent, setPlanContent] = useState<string | null>(null);
|
||||||
|
const [hiringRoles, setHiringRoles] = useState<HiringRole[]>([]);
|
||||||
|
const [showRawPlan, setShowRawPlan] = useState(false);
|
||||||
|
|
||||||
// Created entity IDs — pre-populate from existing company when skipping step 1
|
// Created entity IDs — pre-populate from existing company when skipping step 1
|
||||||
const [createdCompanyId, setCreatedCompanyId] = useState<string | null>(
|
const [createdCompanyId, setCreatedCompanyId] = useState<string | null>(
|
||||||
|
|
@ -284,6 +329,8 @@ export function OnboardingWizard() {
|
||||||
setQ4("");
|
setQ4("");
|
||||||
setPlanningTaskId(null);
|
setPlanningTaskId(null);
|
||||||
setPlanContent(null);
|
setPlanContent(null);
|
||||||
|
setHiringRoles([]);
|
||||||
|
setShowRawPlan(false);
|
||||||
setAgentName("CEO");
|
setAgentName("CEO");
|
||||||
setAdapterType("claude_local");
|
setAdapterType("claude_local");
|
||||||
setCwd("");
|
setCwd("");
|
||||||
|
|
@ -551,33 +598,35 @@ export function OnboardingWizard() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
let issueRef = createdIssueRef;
|
// Create a hire task for each approved role
|
||||||
if (!issueRef) {
|
const approvedRoles = hiringRoles.filter(
|
||||||
const issue = await issuesApi.create(createdCompanyId, {
|
(r) => r.enabled && r.name.trim()
|
||||||
title: taskTitle.trim(),
|
);
|
||||||
...(taskDescription.trim()
|
for (const role of approvedRoles) {
|
||||||
? { description: taskDescription.trim() }
|
await issuesApi.create(createdCompanyId, {
|
||||||
: {}),
|
title: `Hire: ${role.name}`,
|
||||||
|
description: role.description
|
||||||
|
? `Hire a ${role.name} for the company.\n\n${role.description}`
|
||||||
|
: `Hire a ${role.name} for the company.`,
|
||||||
assigneeAgentId: createdAgentId,
|
assigneeAgentId: createdAgentId,
|
||||||
status: "todo"
|
status: "todo"
|
||||||
});
|
});
|
||||||
issueRef = issue.identifier ?? issue.id;
|
|
||||||
setCreatedIssueRef(issueRef);
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: queryKeys.issues.list(createdCompanyId)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: queryKeys.issues.list(createdCompanyId)
|
||||||
|
});
|
||||||
|
|
||||||
setSelectedCompanyId(createdCompanyId);
|
setSelectedCompanyId(createdCompanyId);
|
||||||
reset();
|
reset();
|
||||||
closeOnboarding();
|
closeOnboarding();
|
||||||
navigate(
|
navigate(
|
||||||
createdCompanyPrefix
|
createdCompanyPrefix
|
||||||
? `/${createdCompanyPrefix}/issues/${issueRef}`
|
? `/${createdCompanyPrefix}/issues`
|
||||||
: `/issues/${issueRef}`
|
: `/issues`
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Failed to create task");
|
setError(err instanceof Error ? err.message : "Failed to create hire tasks");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -1394,9 +1443,9 @@ export function OnboardingWizard() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Step 5: Review hiring plan — placeholder */}
|
{/* Step 5: Review hiring plan */}
|
||||||
{step === 5 && (
|
{step === 5 && (
|
||||||
<div className="space-y-5">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-3 mb-1">
|
<div className="flex items-center gap-3 mb-1">
|
||||||
<div className="bg-muted/50 p-2">
|
<div className="bg-muted/50 p-2">
|
||||||
<ListTodo className="h-5 w-5 text-muted-foreground" />
|
<ListTodo className="h-5 w-5 text-muted-foreground" />
|
||||||
|
|
@ -1404,16 +1453,209 @@ export function OnboardingWizard() {
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-medium">Review your hiring plan</h3>
|
<h3 className="font-medium">Review your hiring plan</h3>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Select which roles to hire. You can edit, add, or remove
|
Select which roles to hire. Edit, add, or remove roles
|
||||||
roles before approving.
|
before approving.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border border-border p-4 min-h-[200px] flex items-center justify-center">
|
|
||||||
<p className="text-sm text-muted-foreground">
|
{hiringRoles.length === 0 ? (
|
||||||
Hiring plan review component coming soon.
|
<div className="rounded-md border border-dashed border-border p-4 text-center">
|
||||||
</p>
|
<p className="text-sm text-muted-foreground mb-2">
|
||||||
</div>
|
No roles parsed from the hiring plan yet.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
setHiringRoles([
|
||||||
|
{
|
||||||
|
id: nextRoleId(),
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
enabled: true,
|
||||||
|
editing: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Plus className="h-3.5 w-3.5 mr-1" />
|
||||||
|
Add a role manually
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{hiringRoles.map((role) => (
|
||||||
|
<div
|
||||||
|
key={role.id}
|
||||||
|
className={cn(
|
||||||
|
"rounded-md border px-3 py-2.5 transition-colors",
|
||||||
|
role.enabled
|
||||||
|
? "border-border bg-background"
|
||||||
|
: "border-border/50 bg-muted/30 opacity-60"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{role.editing ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<input
|
||||||
|
className="w-full rounded border border-border bg-transparent px-2 py-1 text-sm font-medium outline-none focus:ring-1 focus:ring-ring"
|
||||||
|
placeholder="Role name"
|
||||||
|
value={role.name}
|
||||||
|
onChange={(e) =>
|
||||||
|
setHiringRoles((prev) =>
|
||||||
|
prev.map((r) =>
|
||||||
|
r.id === role.id
|
||||||
|
? { ...r, name: e.target.value }
|
||||||
|
: r
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
className="w-full rounded border border-border bg-transparent px-2 py-1 text-sm outline-none focus:ring-1 focus:ring-ring resize-none min-h-[40px]"
|
||||||
|
placeholder="Role description"
|
||||||
|
value={role.description}
|
||||||
|
onChange={(e) =>
|
||||||
|
setHiringRoles((prev) =>
|
||||||
|
prev.map((r) =>
|
||||||
|
r.id === role.id
|
||||||
|
? { ...r, description: e.target.value }
|
||||||
|
: r
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() =>
|
||||||
|
setHiringRoles((prev) =>
|
||||||
|
prev.map((r) =>
|
||||||
|
r.id === role.id
|
||||||
|
? { ...r, editing: false }
|
||||||
|
: r
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Check className="h-3 w-3 mr-1" />
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-start gap-2.5">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={role.enabled}
|
||||||
|
onChange={(e) =>
|
||||||
|
setHiringRoles((prev) =>
|
||||||
|
prev.map((r) =>
|
||||||
|
r.id === role.id
|
||||||
|
? { ...r, enabled: e.target.checked }
|
||||||
|
: r
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="mt-1 shrink-0"
|
||||||
|
/>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
{role.name || "Untitled role"}
|
||||||
|
</p>
|
||||||
|
{role.description && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
{role.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
|
<button
|
||||||
|
className="p-1 text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
onClick={() =>
|
||||||
|
setHiringRoles((prev) =>
|
||||||
|
prev.map((r) =>
|
||||||
|
r.id === role.id
|
||||||
|
? { ...r, editing: true }
|
||||||
|
: r
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Pencil className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="p-1 text-muted-foreground hover:text-destructive transition-colors"
|
||||||
|
onClick={() =>
|
||||||
|
setHiringRoles((prev) =>
|
||||||
|
prev.filter((r) => r.id !== role.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Add role button */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
setHiringRoles((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
id: nextRoleId(),
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
enabled: true,
|
||||||
|
editing: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Plus className="h-3.5 w-3.5 mr-1" />
|
||||||
|
Add role
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Revise with CEO */}
|
||||||
|
{planningTaskId && (
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
onClick={() => setStep(4)}
|
||||||
|
>
|
||||||
|
<MessageSquare className="h-3 w-3" />
|
||||||
|
Revise with CEO
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Collapsible raw plan */}
|
||||||
|
{planContent && (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
onClick={() => setShowRawPlan((v) => !v)}
|
||||||
|
>
|
||||||
|
<ChevronDown
|
||||||
|
className={cn(
|
||||||
|
"h-3 w-3 transition-transform",
|
||||||
|
showRawPlan ? "rotate-0" : "-rotate-90"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
View raw plan
|
||||||
|
</button>
|
||||||
|
{showRawPlan && (
|
||||||
|
<div className="mt-2 rounded-md border border-border p-3 text-xs bg-muted/30 max-h-[200px] overflow-y-auto">
|
||||||
|
<pre className="whitespace-pre-wrap">{planContent}</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -1427,8 +1669,8 @@ export function OnboardingWizard() {
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-medium">Make your first hires</h3>
|
<h3 className="font-medium">Make your first hires</h3>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Everything is set up. Approving will create hire tasks
|
Your CEO will create these roles for{" "}
|
||||||
for your CEO to act on.
|
<span className="font-medium text-foreground">{companyName}</span>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1439,7 +1681,9 @@ export function OnboardingWizard() {
|
||||||
<p className="text-sm font-medium truncate">
|
<p className="text-sm font-medium truncate">
|
||||||
{companyName}
|
{companyName}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">Company</p>
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{companyGoal}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Check className="h-4 w-4 text-green-500 shrink-0" />
|
<Check className="h-4 w-4 text-green-500 shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1455,18 +1699,27 @@ export function OnboardingWizard() {
|
||||||
</div>
|
</div>
|
||||||
<Check className="h-4 w-4 text-green-500 shrink-0" />
|
<Check className="h-4 w-4 text-green-500 shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 px-3 py-2.5">
|
{hiringRoles
|
||||||
<ListTodo className="h-4 w-4 text-muted-foreground shrink-0" />
|
.filter((r) => r.enabled && r.name.trim())
|
||||||
<div className="flex-1 min-w-0">
|
.map((role) => (
|
||||||
<p className="text-sm font-medium truncate">
|
<div
|
||||||
Hiring plan approved
|
key={role.id}
|
||||||
</p>
|
className="flex items-center gap-3 px-3 py-2.5"
|
||||||
<p className="text-xs text-muted-foreground">
|
>
|
||||||
{companyGoal}
|
<Bot className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||||
</p>
|
<div className="flex-1 min-w-0">
|
||||||
</div>
|
<p className="text-sm font-medium truncate">
|
||||||
<Check className="h-4 w-4 text-green-500 shrink-0" />
|
{role.name}
|
||||||
</div>
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground truncate">
|
||||||
|
{role.description || "New hire"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] text-amber-500 font-medium">
|
||||||
|
To hire
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1538,14 +1791,24 @@ export function OnboardingWizard() {
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setStep(5)}
|
onClick={() => {
|
||||||
|
if (planContent) {
|
||||||
|
setHiringRoles(parseHiringPlan(planContent));
|
||||||
|
}
|
||||||
|
setStep(5);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Skip chat
|
Skip chat
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
onClick={() => setStep(5)}
|
onClick={() => {
|
||||||
|
if (planContent) {
|
||||||
|
setHiringRoles(parseHiringPlan(planContent));
|
||||||
|
}
|
||||||
|
setStep(5);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ArrowRight className="h-3.5 w-3.5 mr-1" />
|
<ArrowRight className="h-3.5 w-3.5 mr-1" />
|
||||||
Next
|
Next
|
||||||
|
|
@ -1555,9 +1818,10 @@ export function OnboardingWizard() {
|
||||||
{step === 5 && (
|
{step === 5 && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
disabled={!hiringRoles.some((r) => r.enabled && r.name.trim())}
|
||||||
onClick={() => setStep(6)}
|
onClick={() => setStep(6)}
|
||||||
>
|
>
|
||||||
<ArrowRight className="h-3.5 w-3.5 mr-1" />
|
<Check className="h-3.5 w-3.5 mr-1" />
|
||||||
Approve hiring plan
|
Approve hiring plan
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue