[eric] onboarding: two-level starter prompts, pick a category (research/build/write/learn) then its prompts spawn, one-click launch

This commit is contained in:
ciregenz
2026-06-11 00:08:33 -07:00
parent 1fcaeb0134
commit d2505bba12
@@ -1,6 +1,9 @@
import React from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import { motion, AnimatePresence } from 'framer-motion';
import { Search, Hammer, PenLine, GraduationCap, ArrowLeft } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import type { ClaudeTokens } from '@/shared/styles/claudeTokens';
import { useDashboardActive } from '@/shared/hooks/useDashboardActive';
import { useAppSelector } from '@/shared/hooks';
@@ -10,13 +13,47 @@ import {
} from '@/app/components/Onboarding/steps/skipPredicates';
import ChatBubbleTeardrop from '../ChatBubbleTeardrop';
// Broad, one-click-complete prompts so a brand-new user gets the "it works"
// moment without thinking up a task. Each runs to a useful result on its own.
const STARTER_PROMPTS = [
'Find the latest AI news and give me a short summary',
'Research the top 3 standing desks and compare them',
'Explain how RAG works like I\'m five',
'Write a short poem about the sea',
// Two-level starters: pick a category, then its concrete prompts spawn. 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.
type StarterCategory = { id: string; label: string; Icon: LucideIcon; prompts: string[] };
const STARTER_CATEGORIES: StarterCategory[] = [
{
id: 'research', label: 'Research', Icon: Search,
prompts: [
'Find today\'s top AI news and summarize it',
'Compare the 3 best standing desks and recommend one',
'Plan a weekend trip to Tokyo with a day-by-day itinerary',
'Find the highest-rated wireless earbuds under $100',
],
},
{
id: 'build', label: 'Build', Icon: Hammer,
prompts: [
'Build a Pomodoro timer that dings when it ends',
'Make a tip calculator that splits the bill',
'Create a Snake game I can play right now',
'Build a habit tracker with a streak counter',
],
},
{
id: 'write', label: 'Write', Icon: PenLine,
prompts: [
'Draft a cold outreach email to a potential client',
'Turn rough notes into a polished status update',
'Write a product description for a coffee mug',
'Write a short poem about the sea',
],
},
{
id: 'learn', label: 'Learn', Icon: GraduationCap,
prompts: [
'Explain how RAG works like I\'m five',
'Break down what an LLM actually is',
'Compare REST and GraphQL and when to use each',
'Teach me the basics of investing in 5 minutes',
],
},
];
const DashboardEmptyState: React.FC<{
@@ -30,6 +67,8 @@ const DashboardEmptyState: React.FC<{
const mode = useAppSelector((s) => s.settings.data.default_mode);
const canRun = useAppSelector((s) => hasFreeTrialActive(s) || hasModelConnected(s));
const [launching, setLaunching] = React.useState(false);
const [expanded, setExpanded] = React.useState<string | null>(null);
const currentPrompts = STARTER_CATEGORIES.find((cat) => cat.id === expanded)?.prompts ?? [];
// Only offer chips once a run can actually succeed (free trial armed or a real
// model connected); otherwise fall back to the plain hint.
@@ -81,44 +120,97 @@ const DashboardEmptyState: React.FC<{
</Typography>
{showChips && (
<Box
sx={{
mt: 3,
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
gap: 1,
maxWidth: 520,
pointerEvents: 'auto',
}}
>
<Typography sx={{ width: '100%', textAlign: 'center', color: c.text.ghost, fontSize: '0.8rem', mb: 0.5 }}>
or try one of these
</Typography>
{STARTER_PROMPTS.map((prompt) => (
<Box
component="button"
key={prompt}
onClick={() => launch(prompt)}
disabled={launching}
sx={{
px: 1.4,
py: 0.8,
borderRadius: 2,
border: `1px solid ${c.border.medium}`,
background: c.bg.surface,
color: c.text.secondary,
fontSize: '0.82rem',
cursor: launching ? 'default' : 'pointer',
opacity: launching ? 0.5 : 1,
fontFamily: 'inherit',
transition: 'background 150ms ease-in-out, border-color 150ms ease-in-out',
'&:hover': launching ? {} : { background: c.bg.elevated, borderColor: c.border.strong },
}}
>
{prompt}
</Box>
))}
<Box sx={{ mt: 3, width: '100%', maxWidth: 560, pointerEvents: 'auto', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<AnimatePresence mode="wait" initial={false}>
{expanded === null ? (
<motion.div
key="categories"
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.18 }}
style={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center' }}
>
<Typography sx={{ color: c.text.ghost, fontSize: '0.8rem', mb: 1.2 }}>
or try one of these
</Typography>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1, width: '100%', maxWidth: 420 }}>
{STARTER_CATEGORIES.map((cat) => (
<Box
component="button"
key={cat.id}
onClick={() => setExpanded(cat.id)}
sx={{
display: 'flex', alignItems: 'center', gap: 1,
px: 1.6, py: 1.1,
borderRadius: 2.5,
border: `1px solid ${c.border.medium}`,
background: c.bg.surface,
color: c.text.secondary,
fontSize: '0.85rem', fontWeight: 500,
cursor: 'pointer', fontFamily: 'inherit',
transition: 'background 150ms, border-color 150ms',
'&:hover': { background: c.bg.elevated, borderColor: c.border.strong },
}}
>
<cat.Icon size={16} />
{cat.label}
</Box>
))}
</Box>
</motion.div>
) : (
<motion.div
key="specifics"
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.18 }}
style={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center' }}
>
<Box
component="button"
onClick={() => setExpanded(null)}
sx={{
display: 'inline-flex', alignItems: 'center', gap: 0.5,
mb: 1.2, px: 1, py: 0.4,
border: 'none', background: 'transparent',
color: c.text.ghost, fontSize: '0.8rem',
cursor: 'pointer', fontFamily: 'inherit',
'&:hover': { color: c.text.secondary },
}}
>
<ArrowLeft size={14} /> back
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.8, width: '100%', maxWidth: 460 }}>
{currentPrompts.map((prompt) => (
<Box
component="button"
key={prompt}
onClick={() => launch(prompt)}
disabled={launching}
sx={{
textAlign: 'left',
px: 1.6, py: 0.9,
borderRadius: 2,
border: `1px solid ${c.border.medium}`,
background: c.bg.surface,
color: c.text.secondary,
fontSize: '0.82rem',
cursor: launching ? 'default' : 'pointer',
opacity: launching ? 0.5 : 1,
fontFamily: 'inherit',
transition: 'background 150ms, border-color 150ms',
'&:hover': launching ? {} : { background: c.bg.elevated, borderColor: c.border.strong },
}}
>
{prompt}
</Box>
))}
</Box>
</motion.div>
)}
</AnimatePresence>
</Box>
)}
</Box>