experiment: CEO welcome flow, merged steps, orientation screen, UX polish
CEO welcome & user agency:
- Removed auto-posted user message — CEO greets the board first
- "CEO is waking up..." → "CEO is composing..." → welcome message fades in
- Response chips ("Yes, get started!" / "Let's discuss first") fade in after
- Planning task created unassigned — CEO only wakes when user initiates
- "Yes" chip sends directly; "Discuss" pre-fills input for editing
Merged steps 5+6:
- "Approve & hire" on the Plan step creates hire tasks directly
- No redundant confirmation step
- Step 6 is now a welcome/orientation screen (hidden from nav tabs)
Orientation screen:
- Shows what to expect: Tasks, Agents, Approvals, Dashboard
- "Go to dashboard" button closes wizard and navigates
Nav tabs cleaned up:
- Mission → Launch → CEO → Plan → Review (5 visible tabs)
- Orientation step exists but not in nav (no redundant rockets)
Chat UX polish:
- Single-line input (like a URL bar) instead of multi-line textarea
- CEO welcome message always visible in conversation history
- Structured task description tells CEO exact format for role specs
- "Confirm mission" button hidden until user has chosen a path
- Switching paths preserves previously entered text
Parser improvements:
- Handles N. **Role Name** with indented bullets (fallback format)
- Summary field populated from first expertise line when no explicit summary
- Better skip patterns for non-role sections
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0c1582ef47
commit
05a2848b02
2 changed files with 321 additions and 119 deletions
|
|
@ -13,6 +13,8 @@ interface OnboardingChatProps {
|
|||
taskId: string;
|
||||
agentId: string;
|
||||
agentName: string;
|
||||
companyName: string;
|
||||
companyGoal: string;
|
||||
onPlanDetected?: (planMarkdown: string) => void;
|
||||
onReviewPlan?: () => void;
|
||||
}
|
||||
|
|
@ -81,6 +83,8 @@ export function OnboardingChat({
|
|||
taskId,
|
||||
agentId,
|
||||
agentName,
|
||||
companyName,
|
||||
companyGoal,
|
||||
onPlanDetected,
|
||||
onReviewPlan,
|
||||
}: OnboardingChatProps) {
|
||||
|
|
@ -96,7 +100,7 @@ export function OnboardingChat({
|
|||
string | null
|
||||
>(null);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const {
|
||||
data: rawComments,
|
||||
|
|
@ -165,14 +169,12 @@ export function OnboardingChat({
|
|||
}
|
||||
}, [comments, onPlanDetected, detectedPlanCommentId, ignoreBeforeCommentId, taskId]);
|
||||
|
||||
const handleSend = useCallback(async () => {
|
||||
const body = input.trim();
|
||||
if (!body || sending) return;
|
||||
const sendMessage = useCallback(async (body: string) => {
|
||||
const trimmed = body.trim();
|
||||
if (!trimmed || sending) return;
|
||||
setSending(true);
|
||||
try {
|
||||
// Ensure the task is assigned to the CEO and in_progress before commenting.
|
||||
// The CEO tends to unassign itself and set status to in_review after responding,
|
||||
// which prevents the comment wakeup from working.
|
||||
try {
|
||||
await issuesApi.update(taskId, { assigneeUserId: null });
|
||||
} catch { /* may already be null */ }
|
||||
|
|
@ -183,11 +185,9 @@ export function OnboardingChat({
|
|||
});
|
||||
} catch { /* may already be assigned */ }
|
||||
|
||||
await issuesApi.addComment(taskId, body, true, true);
|
||||
await issuesApi.addComment(taskId, trimmed, true, true);
|
||||
setInput("");
|
||||
// Clear detected plan — user is asking for revisions, so the old plan
|
||||
// is stale. A new plan will be detected when the CEO responds again.
|
||||
// Mark the last known comment so the detector ignores older plans.
|
||||
// Clear detected plan — user is asking for revisions
|
||||
const latestId = comments?.[comments.length - 1]?.id ?? null;
|
||||
setIgnoreBeforeCommentId(latestId);
|
||||
setDetectedPlanCommentId(null);
|
||||
|
|
@ -198,7 +198,11 @@ export function OnboardingChat({
|
|||
setSending(false);
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}, [input, sending, taskId, queryClient]);
|
||||
}, [sending, taskId, agentId, queryClient, comments]);
|
||||
|
||||
const handleSend = useCallback(() => {
|
||||
sendMessage(input);
|
||||
}, [input, sendMessage]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
|
|
@ -257,11 +261,18 @@ export function OnboardingChat({
|
|||
ref={scrollRef}
|
||||
className="flex-1 overflow-y-auto space-y-3 mb-3 min-h-[180px] max-h-[320px] pr-1"
|
||||
>
|
||||
{(!comments || comments.length === 0) && (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">
|
||||
Starting conversation with {agentName}...
|
||||
</p>
|
||||
)}
|
||||
{/* CEO welcome message + chips — delayed reveal */}
|
||||
<WelcomeMessage
|
||||
agentName={agentName}
|
||||
companyName={companyName}
|
||||
companyGoal={companyGoal}
|
||||
hasComments={Boolean(comments?.length)}
|
||||
onDiscuss={() => {
|
||||
setInput("I want to discuss the plan before you get started.");
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
onStart={() => sendMessage("Yes, get started on the hiring plan!")}
|
||||
/>
|
||||
{comments?.map((comment) => {
|
||||
const isAgent = Boolean(comment.authorAgentId);
|
||||
const isPlan =
|
||||
|
|
@ -355,15 +366,15 @@ export function OnboardingChat({
|
|||
)}
|
||||
|
||||
{/* Input area */}
|
||||
<div className="flex items-end gap-2 border-t border-border pt-3">
|
||||
<textarea
|
||||
<div className="flex items-center gap-2 border-t border-border pt-3">
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="flex-1 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-[40px] max-h-[100px]"
|
||||
placeholder={detectedPlanCommentId ? "Ask your CEO to revise the plan, or review it above..." : "Message your CEO..."}
|
||||
type="text"
|
||||
className="flex-1 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={detectedPlanCommentId ? "Ask your CEO to revise the plan..." : "Message your CEO..."}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
rows={1}
|
||||
autoFocus={!detectedPlanCommentId}
|
||||
/>
|
||||
<Button
|
||||
|
|
@ -382,3 +393,89 @@ export function OnboardingChat({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WelcomeMessage({
|
||||
agentName,
|
||||
companyName,
|
||||
companyGoal,
|
||||
hasComments,
|
||||
onDiscuss,
|
||||
onStart,
|
||||
}: {
|
||||
agentName: string;
|
||||
companyName: string;
|
||||
companyGoal: string;
|
||||
hasComments: boolean;
|
||||
onDiscuss: () => void;
|
||||
onStart: () => void;
|
||||
}) {
|
||||
const [phase, setPhase] = useState<"waking" | "composing" | "message" | "chips">("waking");
|
||||
|
||||
useEffect(() => {
|
||||
const t1 = setTimeout(() => setPhase("composing"), 2500);
|
||||
const t2 = setTimeout(() => setPhase("message"), 5500);
|
||||
const t3 = setTimeout(() => setPhase("chips"), 6500);
|
||||
return () => { clearTimeout(t1); clearTimeout(t2); clearTimeout(t3); };
|
||||
}, []);
|
||||
|
||||
const showMessage = phase === "message" || phase === "chips";
|
||||
const showChips = phase === "chips" && !hasComments;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Message — appears after typing indicator */}
|
||||
{showMessage && (
|
||||
<div className="rounded-md px-3 py-2 text-sm bg-muted/50 border border-border mr-8 animate-in fade-in duration-300">
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<span className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{agentName}
|
||||
</span>
|
||||
</div>
|
||||
<p>
|
||||
Hello board! Thank you for appointing me CEO of <strong>{companyName}</strong>.
|
||||
</p>
|
||||
<p className="mt-1">
|
||||
Our mission is: <em>{companyGoal}</em>
|
||||
</p>
|
||||
<p className="mt-1">
|
||||
I'm ready to build a hiring plan. Shall I get started?
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Chips — fade in after message */}
|
||||
{showChips && (
|
||||
<div className="flex gap-2 ml-auto justify-end animate-in fade-in duration-500">
|
||||
<button
|
||||
className="rounded-full border border-border px-3 py-1 text-xs hover:bg-accent/50 transition-colors text-muted-foreground hover:text-foreground"
|
||||
onClick={onDiscuss}
|
||||
>
|
||||
Let's discuss first
|
||||
</button>
|
||||
<button
|
||||
className="rounded-full border border-foreground bg-foreground text-background px-3 py-1 text-xs hover:opacity-90 transition-opacity"
|
||||
onClick={onStart}
|
||||
>
|
||||
Yes, get started!
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Typing indicator — anchored at bottom of scroll area, before real status messages */}
|
||||
{!showMessage && (
|
||||
<div className="flex-1" />
|
||||
)}
|
||||
{!showMessage && (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground px-3 py-2">
|
||||
<span className="relative flex h-2.5 w-2.5 shrink-0">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-cyan-400 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-cyan-500" />
|
||||
</span>
|
||||
{phase === "waking"
|
||||
? `${agentName} is waking up...`
|
||||
: `${agentName} is composing a message...`}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,31 +144,55 @@ function parseHiringPlan(markdown: string): HiringRole[] {
|
|||
const roles: HiringRole[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
// Split into ## sections (each role is a ## heading)
|
||||
const roleSections = markdown.split(/^##\s+/m).slice(1).filter(Boolean);
|
||||
// Find role headings: any ## or ### heading with a numbered prefix
|
||||
// like "### 1. Content Marketing Officer" or "## Role 2: CTO"
|
||||
const rolePattern = /^(?:role\s*\d+[:.]\s*|\d+[.)]\s*)/i;
|
||||
const roleHeadingRegex = /^#{2,3}\s+(.+)$/gm;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
for (const section of roleSections) {
|
||||
const lines = section.split("\n");
|
||||
const titleLine = lines[0]?.trim() ?? "";
|
||||
// First pass: find all role heading positions (start of line, end of heading)
|
||||
const rolePositions: Array<{ title: string; lineStart: number; contentStart: number }> = [];
|
||||
while ((match = roleHeadingRegex.exec(markdown)) !== null) {
|
||||
if (rolePattern.test(match[1].trim())) {
|
||||
rolePositions.push({
|
||||
title: match[1].trim(),
|
||||
lineStart: match.index,
|
||||
contentStart: match.index + match[0].length,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Extract role name — match "Role N: Name" or just "N. Name" or plain name
|
||||
let name = titleLine
|
||||
// Extract body for each role (from heading end to the next role heading start)
|
||||
const sections: Array<{ title: string; body: string }> = [];
|
||||
for (let i = 0; i < rolePositions.length; i++) {
|
||||
const end = i + 1 < rolePositions.length
|
||||
? rolePositions[i + 1].lineStart
|
||||
: markdown.length;
|
||||
sections.push({
|
||||
title: rolePositions[i].title,
|
||||
body: markdown.slice(rolePositions[i].contentStart, end),
|
||||
});
|
||||
}
|
||||
|
||||
for (const section of sections) {
|
||||
if (!rolePattern.test(section.title)) continue;
|
||||
|
||||
let name = section.title
|
||||
.replace(/^role\s*\d*[:.]\s*/i, "")
|
||||
.replace(/^\d+[.)]\s*/, "")
|
||||
.replace(/\*\*/g, "")
|
||||
.trim();
|
||||
|
||||
// Skip non-role sections
|
||||
const skipPatterns = /^(mission|hiring approach|hiring|roles|approach|open|phase|deferred|timeline|budget|summary|next steps|overview|notes|questions|appendix|---)/i;
|
||||
if (skipPatterns.test(name) || name.length < 3) continue;
|
||||
if (name.length < 3) continue;
|
||||
if (seen.has(name.toLowerCase())) continue;
|
||||
|
||||
// Parse content: **Label:** bullets and ### sub-sections
|
||||
const fields: Record<string, string[]> = {};
|
||||
let currentField: string | null = null;
|
||||
const bodyLines = section.body.split("\n");
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const raw = lines[i];
|
||||
for (let i = 0; i < bodyLines.length; i++) {
|
||||
const raw = bodyLines[i];
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) continue;
|
||||
|
||||
|
|
@ -210,12 +234,21 @@ function parseHiringPlan(markdown: string): HiringRole[] {
|
|||
|
||||
const join = (arr?: string[]) => (arr ?? []).join("\n");
|
||||
|
||||
// If no summary, use first line of expertise
|
||||
let summary = join(fields.summary);
|
||||
let expertise = join(fields.expertise);
|
||||
if (!summary && expertise) {
|
||||
const lines = expertise.split("\n");
|
||||
summary = lines[0];
|
||||
expertise = lines.slice(1).join("\n");
|
||||
}
|
||||
|
||||
seen.add(name.toLowerCase());
|
||||
roles.push({
|
||||
id: nextRoleId(),
|
||||
name,
|
||||
summary: join(fields.summary),
|
||||
expertise: join(fields.expertise),
|
||||
summary,
|
||||
expertise,
|
||||
priorities: join(fields.priorities),
|
||||
boundaries: join(fields.boundaries),
|
||||
tools: join(fields.tools),
|
||||
|
|
@ -226,27 +259,64 @@ function parseHiringPlan(markdown: string): HiringRole[] {
|
|||
});
|
||||
}
|
||||
|
||||
// Fallback: simple bullet parsing from comment text
|
||||
// Fallback: parse "N. **Role Name**" with indented bullets
|
||||
if (roles.length === 0) {
|
||||
const lines = markdown.split("\n");
|
||||
for (const line of lines) {
|
||||
const bulletMatch = line.match(
|
||||
/^\s*(?:[-*]|\d+[.)]\s*)\s*\*\*([^*]+)\*\*[:\s—–-]*(.*)$/
|
||||
);
|
||||
if (!bulletMatch) continue;
|
||||
const name = bulletMatch[1].trim();
|
||||
const summary = cleanMd(bulletMatch[2]);
|
||||
if (seen.has(name.toLowerCase())) continue;
|
||||
const skip = /^(phase|month|step|update|note|question|summary|timeline|priority|plan|total|budget|immediate|hire|\d+ immediate)/i;
|
||||
if (skip.test(name) || name.length < 3) continue;
|
||||
let currentRole: HiringRole | null = null;
|
||||
|
||||
seen.add(name.toLowerCase());
|
||||
roles.push({
|
||||
id: nextRoleId(), name, summary,
|
||||
expertise: "", priorities: "", boundaries: "",
|
||||
tools: "", communication: "", collaboration: "",
|
||||
enabled: true, editing: false,
|
||||
});
|
||||
for (const line of lines) {
|
||||
// Match numbered bold role: "1. **Content Strategist / CMO**"
|
||||
const roleMatch = line.match(/^\s*(\d+)[.)]\s+\*\*([^*]+)\*\*/);
|
||||
if (roleMatch) {
|
||||
const name = roleMatch[2].trim();
|
||||
const skip = /^(phase|month|step|update|note|question|summary|timeline|priority|plan|total|budget|immediate|hire)/i;
|
||||
if (skip.test(name) || name.length < 3) continue;
|
||||
if (seen.has(name.toLowerCase())) continue;
|
||||
|
||||
if (currentRole) roles.push(currentRole);
|
||||
seen.add(name.toLowerCase());
|
||||
currentRole = {
|
||||
id: nextRoleId(), name, summary: "", expertise: "",
|
||||
priorities: "", boundaries: "", tools: "",
|
||||
communication: "", collaboration: "",
|
||||
enabled: true, editing: false,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
// Indented bullets under the current role
|
||||
if (currentRole && /^\s{2,}[-*]/.test(line)) {
|
||||
const cleaned = cleanMd(line);
|
||||
if (!cleaned) continue;
|
||||
|
||||
// Check for labeled bullet: "*Why first:*", "**Tools:**", etc.
|
||||
const labelMatch = cleaned.match(/^\*?([^:*]+)\*?:\s*(.*)/);
|
||||
if (labelMatch) {
|
||||
const field = classifyBullet(labelMatch[1].trim());
|
||||
if (field && typeof currentRole[field] === "string") {
|
||||
const val = labelMatch[2].trim();
|
||||
const prev = currentRole[field] as string;
|
||||
(currentRole as unknown as Record<string, unknown>)[field] = prev
|
||||
? `${prev}\n${val}` : val;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Default: add to expertise
|
||||
currentRole.expertise = currentRole.expertise
|
||||
? `${currentRole.expertise}\n${cleaned}` : cleaned;
|
||||
}
|
||||
}
|
||||
if (currentRole) roles.push(currentRole);
|
||||
|
||||
// If summary is empty, use the first line of expertise as the summary
|
||||
for (const role of roles) {
|
||||
if (!role.summary && role.expertise) {
|
||||
const firstLine = role.expertise.split("\n")[0];
|
||||
if (firstLine) {
|
||||
role.summary = firstLine;
|
||||
role.expertise = role.expertise.split("\n").slice(1).join("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -681,18 +751,36 @@ export function OnboardingWizard() {
|
|||
queryKey: queryKeys.agents.list(createdCompanyId)
|
||||
});
|
||||
|
||||
// Create the planning task and kick off the conversation
|
||||
// Create the planning task unassigned — the CEO only gets assigned
|
||||
// when the user sends their first message (user controls initiation)
|
||||
const planningIssue = await issuesApi.create(createdCompanyId, {
|
||||
title: "Build hiring plan with CEO",
|
||||
description: `Company mission: ${companyGoal}\n\nCollaborate with the board to create a hiring plan for the company.`,
|
||||
assigneeAgentId: agent.id,
|
||||
status: "in_progress"
|
||||
description: `Company mission: ${companyGoal}
|
||||
|
||||
Collaborate with the board to create a hiring plan for the company.
|
||||
|
||||
IMPORTANT: When writing the hiring plan document, use this exact format for EACH role. Use ## headings for each role (e.g. "## 1. Role Name") and ### sub-headings for each section within the role:
|
||||
|
||||
## 1. Role Name
|
||||
### Summary
|
||||
One-line description of this role.
|
||||
### Expertise & Responsibilities
|
||||
What this agent does, detailed responsibilities.
|
||||
### Priorities
|
||||
Ordered list of what matters most.
|
||||
### Boundaries
|
||||
What this role should NOT do.
|
||||
### Tools & Permissions
|
||||
What tools and access this role needs.
|
||||
### Communication
|
||||
Tone, style, and interaction guidelines.
|
||||
### Collaboration & Escalation
|
||||
Who this role works with, escalation paths.
|
||||
|
||||
Follow this structure for every role in the plan.`,
|
||||
status: "backlog"
|
||||
});
|
||||
setPlanningTaskId(planningIssue.id);
|
||||
await issuesApi.addComment(
|
||||
planningIssue.id,
|
||||
`Our company mission is: ${companyGoal}\n\nLet's build a hiring plan together. What roles do you think we need to accomplish this mission?`
|
||||
);
|
||||
|
||||
setStep(4);
|
||||
} catch (err) {
|
||||
|
|
@ -789,13 +877,7 @@ export function OnboardingWizard() {
|
|||
});
|
||||
|
||||
setSelectedCompanyId(createdCompanyId);
|
||||
reset();
|
||||
closeOnboarding();
|
||||
navigate(
|
||||
createdCompanyPrefix
|
||||
? `/${createdCompanyPrefix}/issues`
|
||||
: `/issues`
|
||||
);
|
||||
setStep(6); // → orientation screen
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to create hire tasks");
|
||||
} finally {
|
||||
|
|
@ -803,6 +885,13 @@ export function OnboardingWizard() {
|
|||
}
|
||||
}
|
||||
|
||||
function handleFinishOnboarding() {
|
||||
const prefix = createdCompanyPrefix;
|
||||
reset(); // clears localStorage
|
||||
closeOnboarding();
|
||||
navigate(prefix ? `/${prefix}/dashboard` : `/dashboard`);
|
||||
}
|
||||
|
||||
function handleKeyDown(e: React.KeyboardEvent) {
|
||||
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
|
|
@ -854,9 +943,8 @@ export function OnboardingWizard() {
|
|||
{ step: 1 as Step, label: "Mission", icon: Building2 },
|
||||
{ step: 2 as Step, label: "Launch", icon: Rocket },
|
||||
{ step: 3 as Step, label: "CEO", icon: Bot },
|
||||
{ step: 4 as Step, label: "Chat", icon: Sparkles },
|
||||
{ step: 5 as Step, label: "Plan", icon: ListTodo },
|
||||
{ step: 6 as Step, label: "Hire", icon: Bot }
|
||||
{ step: 4 as Step, label: "Plan", icon: Sparkles },
|
||||
{ step: 5 as Step, label: "Review", icon: ListTodo },
|
||||
] as const
|
||||
).map(({ step: s, label, icon: Icon }) => (
|
||||
<button
|
||||
|
|
@ -983,7 +1071,7 @@ export function OnboardingWizard() {
|
|||
</div>
|
||||
<button
|
||||
className="text-[11px] text-muted-foreground hover:text-foreground transition-colors"
|
||||
onClick={() => { setMissionPath(null); setCompanyGoal(""); }}
|
||||
onClick={() => setMissionPath(null)}
|
||||
>
|
||||
← Choose a different path
|
||||
</button>
|
||||
|
|
@ -1052,7 +1140,7 @@ export function OnboardingWizard() {
|
|||
)}
|
||||
<button
|
||||
className="text-[11px] text-muted-foreground hover:text-foreground transition-colors block"
|
||||
onClick={() => { setMissionPath(null); setQ1(""); setQ2(""); setQ3(""); setQ4(""); }}
|
||||
onClick={() => setMissionPath(null)}
|
||||
>
|
||||
← Choose a different path
|
||||
</button>
|
||||
|
|
@ -1603,6 +1691,8 @@ export function OnboardingWizard() {
|
|||
taskId={planningTaskId}
|
||||
agentId={createdAgentId!}
|
||||
agentName={agentName}
|
||||
companyName={companyName}
|
||||
companyGoal={companyGoal}
|
||||
onPlanDetected={(md) => setPlanContent(md)}
|
||||
onReviewPlan={async () => {
|
||||
// Always fetch the latest plan document for the richest content
|
||||
|
|
@ -1736,43 +1826,58 @@ export function OnboardingWizard() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 6: Make your first hires */}
|
||||
{/* Step 6: Welcome & orientation */}
|
||||
{step === 6 && (
|
||||
<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">Make your first hires</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Your CEO will create these roles for{" "}
|
||||
<span className="font-medium text-foreground">{companyName}</span>.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-6 py-2">
|
||||
<div className="text-center">
|
||||
<div className="text-4xl mb-3">🎉</div>
|
||||
<h3 className="text-lg font-semibold">
|
||||
{companyName} is ready to go!
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Your CEO is now hiring{" "}
|
||||
{hiringRoles.filter((r) => r.enabled && r.name.trim()).length} roles.
|
||||
Here's what to expect on your dashboard:
|
||||
</p>
|
||||
</div>
|
||||
<div className="border border-border divide-y divide-border rounded-md">
|
||||
{hiringRoles
|
||||
.filter((r) => r.enabled && r.name.trim())
|
||||
.map((role) => (
|
||||
<div
|
||||
key={role.id}
|
||||
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">
|
||||
{role.name}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
{role.summary || "New hire"}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-[10px] text-amber-500 font-medium">
|
||||
To hire
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-3 rounded-md border border-border px-3 py-2.5">
|
||||
<ListTodo className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">Tasks</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Your CEO has hire tasks queued up. Watch them progress from todo → in progress → done.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 rounded-md border border-border px-3 py-2.5">
|
||||
<Bot className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">Agents</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
New agents will appear here as your CEO completes each hire. You may need to approve them.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 rounded-md border border-border px-3 py-2.5">
|
||||
<Check className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">Approvals</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Check your inbox for pending approvals. Your CEO may need your sign-off before agents can start working.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 rounded-md border border-border px-3 py-2.5">
|
||||
<Building2 className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">Dashboard</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Your command center — see agent activity, costs, and overall company health at a glance.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -1800,7 +1905,7 @@ export function OnboardingWizard() {
|
|||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{step === 1 && (
|
||||
{step === 1 && missionPath && (missionPath !== "questionnaire" || missionConfirmed) && (
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={!companyName.trim() || !companyGoal.trim() || loading}
|
||||
|
|
@ -1851,21 +1956,21 @@ export function OnboardingWizard() {
|
|||
{step === 5 && (
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={!hiringRoles.some((r) => r.enabled && r.name.trim())}
|
||||
onClick={() => setStep(6)}
|
||||
disabled={!hiringRoles.some((r) => r.enabled && r.name.trim()) || loading}
|
||||
onClick={handleLaunch}
|
||||
>
|
||||
<Check className="h-3.5 w-3.5 mr-1" />
|
||||
Approve hiring plan
|
||||
</Button>
|
||||
)}
|
||||
{step === 6 && (
|
||||
<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" />
|
||||
<Check className="h-3.5 w-3.5 mr-1" />
|
||||
)}
|
||||
{loading ? "Creating tasks..." : "Make your first hires"}
|
||||
{loading ? "Creating hires..." : "Approve & hire"}
|
||||
</Button>
|
||||
)}
|
||||
{step === 6 && (
|
||||
<Button size="sm" onClick={handleFinishOnboarding}>
|
||||
<Rocket className="h-3.5 w-3.5 mr-1" />
|
||||
Go to dashboard
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue