[eric] onboarding: first-run proactive welcome chat (seeded greeting + in-chat chips, zero run) + cursor pop-nudge at Continue

This commit is contained in:
ciregenz
2026-06-14 00:56:48 -07:00
parent 1e7604961d
commit 665dec63e2
15 changed files with 374 additions and 121 deletions
+53
View File
@@ -0,0 +1,53 @@
import { Search, Hammer, PenLine, GraduationCap } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
// Two-level starters shared by the empty-state and the first-run welcome chat: pick a
// category, then its concrete prompts. Every prompt is one-click-runnable (no [placeholders])
// and free-trial-safe, it touches the web or the App Builder sandbox, never the user's files.
// target 'app-builder' opens the App Builder (live preview); the rest run as a normal agent.
export type StarterCategory = {
id: string;
label: string;
Icon: LucideIcon;
prompts: string[];
target?: 'app-builder';
};
export const STARTER_CATEGORIES: StarterCategory[] = [
{
id: 'research', label: 'Research', Icon: Search,
prompts: [
'Find today\'s top news and summarize it for me',
'Compare the 3 best standing desks and recommend one',
'Plan a weekend trip to Tokyo with a day-by-day itinerary',
'Find the strangest world record I could actually break',
],
},
{
id: 'build', label: 'Build', Icon: Hammer, target: 'app-builder',
prompts: [
'Build a focus timer that dings when the break starts',
'Make a tip calculator that splits the bill',
'Create a Snake game I can play right now',
'Build a tiny Minecraft-style block world I can walk around in',
],
},
{
id: 'write', label: 'Write', Icon: PenLine,
prompts: [
'Write a friendly email introducing myself to a new client',
'Turn my rough notes into a polished update',
'Write a product description for a coffee mug',
'Write my morning routine as an epic fantasy quest',
],
},
{
id: 'learn', label: 'Learn', Icon: GraduationCap,
prompts: [
'Explain how AI chatbots actually work, in plain English',
'Teach me the basics of investing in 5 minutes',
'Explain the stock market like I\'m five',
'What would happen if the moon disappeared tomorrow?',
],
},
];
+14 -4
View File
@@ -101,6 +101,9 @@ export interface AgentSession {
connection_state?: 'live' | 'reconnecting';
/** Aux-LLM verb-phrase for the current turn; ThinkingBubble swaps in then back when turn ends. */
turn_label?: { label: string; turn_id: string } | null;
/** Frontend-only: this draft is the first-run welcome (seeded greeting + quick-reply chips).
* Dropped on the server swap in launchAndSendFirstMessage.fulfilled, so it never persists. */
is_welcome_draft?: boolean;
}
export interface AgentConfig {
@@ -508,8 +511,8 @@ const agentsSlice = createSlice({
initialState,
reducers: {
createDraftSession: {
reducer(state, action: PayloadAction<{ draftId: string; mode: string; setActive: boolean; targetDirectory?: string; model?: string; provider?: string; thinkingLevel?: 'off' | 'low' | 'medium' | 'high' | 'auto' }>) {
const { draftId, mode, setActive, targetDirectory, model, provider, thinkingLevel } = action.payload;
reducer(state, action: PayloadAction<{ draftId: string; mode: string; setActive: boolean; targetDirectory?: string; model?: string; provider?: string; thinkingLevel?: 'off' | 'low' | 'medium' | 'high' | 'auto'; seededMessages?: AgentMessage[]; welcome?: boolean; dashboardId?: string }>) {
const { draftId, mode, setActive, targetDirectory, model, provider, thinkingLevel, seededMessages, welcome, dashboardId } = action.payload;
state.sessions[draftId] = {
id: draftId,
name: 'New chat',
@@ -526,13 +529,17 @@ const agentsSlice = createSlice({
created_at: new Date().toISOString(),
cost_usd: 0,
tokens: { input: 0, output: 0 },
messages: [],
// A seeded greeting is purely cosmetic: launchAndSendFirstMessage.fulfilled deletes this
// draft and swaps in the raw server session, so seeded messages never reach the backend.
messages: seededMessages ?? [],
pending_approvals: [],
branches: { main: { id: 'main', parent_branch_id: null, fork_point_message_id: null, created_at: new Date().toISOString() } },
active_branch_id: 'main',
target_directory: targetDirectory || null,
tool_group_meta: {},
thinking_level: thinkingLevel,
dashboard_id: dashboardId,
is_welcome_draft: welcome === true,
};
if (setActive) {
state.activeSessionId = draftId;
@@ -541,7 +548,7 @@ const agentsSlice = createSlice({
}
}
},
prepare(opts?: { mode?: string; setActive?: boolean; targetDirectory?: string; model?: string; provider?: string; thinkingLevel?: 'off' | 'low' | 'medium' | 'high' | 'auto' }) {
prepare(opts?: { mode?: string; setActive?: boolean; targetDirectory?: string; model?: string; provider?: string; thinkingLevel?: 'off' | 'low' | 'medium' | 'high' | 'auto'; seededMessages?: AgentMessage[]; welcome?: boolean; dashboardId?: string }) {
return {
payload: {
draftId: `draft-${Date.now().toString(36)}`,
@@ -551,6 +558,9 @@ const agentsSlice = createSlice({
model: opts?.model,
provider: opts?.provider,
thinkingLevel: opts?.thinkingLevel,
seededMessages: opts?.seededMessages,
welcome: opts?.welcome,
dashboardId: opts?.dashboardId,
},
};
},
@@ -32,6 +32,8 @@ export interface OnboardingProgressState {
disableSkipIf: boolean;
/** True once we've gently auto-opened the panel after the first agent win (once, ever). */
revealedAfterWin?: boolean;
/** True once the first-run welcome chat has been created (once ever, survives reload). */
welcomeShown?: boolean;
}
export function loadFromStorage(): OnboardingProgressState | null {
@@ -53,6 +55,8 @@ export function loadFromStorage(): OnboardingProgressState | null {
initialized: true,
justCompletedStepId: null,
disableSkipIf: Boolean((parsed as any).disableSkipIf),
revealedAfterWin: Boolean((parsed as any).revealedAfterWin),
welcomeShown: Boolean((parsed as any).welcomeShown),
};
} catch {
return null;
@@ -101,6 +105,7 @@ const initialState: OnboardingProgressState = {
justCompletedStepId: null,
disableSkipIf: false,
revealedAfterWin: false,
welcomeShown: false,
};
const slice = createSlice({
@@ -129,6 +134,7 @@ const slice = createSlice({
state.initialized = true;
state.disableSkipIf = Boolean(action.payload.disableSkipIf);
state.revealedAfterWin = false;
state.welcomeShown = false;
},
hydrate(state, action: PayloadAction<OnboardingProgressState>) {
Object.assign(state, action.payload, { running: false, initialized: true });
@@ -162,6 +168,9 @@ const slice = createSlice({
markRevealedAfterWin(state) {
state.revealedAfterWin = true;
},
markWelcomeShown(state) {
state.welcomeShown = true;
},
unmarkStepCompleted(state, action: PayloadAction<string>) {
state.completedSteps = state.completedSteps.filter((id) => id !== action.payload);
},
@@ -186,6 +195,7 @@ const slice = createSlice({
state.running = false;
state.startedAt = Date.now();
state.revealedAfterWin = false;
state.welcomeShown = false;
// Explicit restart: suppress skipIf so residual prior-tour data can't auto-mark.
state.disableSkipIf = true;
},
@@ -200,6 +210,7 @@ export const {
markStepCompleted,
clearJustCompleted,
markRevealedAfterWin,
markWelcomeShown,
unmarkStepCompleted,
setRunning,
recordMultiChoice,