mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-12 20:57:42 +02:00
[eric] onboarding: first-run proactive welcome chat (seeded greeting + in-chat chips, zero run) + cursor pop-nudge at Continue
This commit is contained in:
@@ -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?',
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user