diff --git a/frontend/src/app/pages/Dashboard/canvas/DashboardEmptyState.tsx b/frontend/src/app/pages/Dashboard/canvas/DashboardEmptyState.tsx index f60e6a84..afa4561c 100644 --- a/frontend/src/app/pages/Dashboard/canvas/DashboardEmptyState.tsx +++ b/frontend/src/app/pages/Dashboard/canvas/DashboardEmptyState.tsx @@ -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(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<{ {showChips && ( - - - or try one of these - - {STARTER_PROMPTS.map((prompt) => ( - 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} - - ))} + + + {expanded === null ? ( + + + or try one of these + + + {STARTER_CATEGORIES.map((cat) => ( + 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.label} + + ))} + + + ) : ( + + 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 }, + }} + > + back + + + {currentPrompts.map((prompt) => ( + 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} + + ))} + + + )} + )}