mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-09 19:27:45 +02:00
[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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user