[eric] onboarding: HOVER a starter opens the composer (translucent preview), leave closes it, CLICK locks the query in to send

This commit is contained in:
ciregenz
2026-06-11 01:43:08 -07:00
parent f8afdf6b37
commit 6beace94a4
4 changed files with 38 additions and 22 deletions
@@ -76,7 +76,6 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode,
prefilledRef.current = prefillPrompt;
editor.style.opacity = '0.5';
editor.style.transition = 'opacity 0.15s';
editor.focus();
const solidify = () => {
editor.style.opacity = '';
editor.removeEventListener('keydown', solidify);
@@ -73,7 +73,7 @@ interface DashboardCanvasProps {
onNewAgent: () => void;
onToolbarCancel: () => void;
onToolbarSend: (...args: any[]) => void;
onStarterPrefill: (prompt: string) => void;
onStarter: (action: 'hover' | 'leave' | 'commit', prompt?: string) => void;
toolbarPrefill?: string;
onAddView: (outputId: string) => void;
onHistoryResume: (sessionId: string) => void;
@@ -132,7 +132,7 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
onNewAgent,
onToolbarCancel,
onToolbarSend,
onStarterPrefill,
onStarter,
toolbarPrefill,
onAddView,
onHistoryResume,
@@ -219,7 +219,7 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
/>
{sessionList.length === 0 && Object.keys(viewCards).length === 0 && Object.keys(browserCards).length === 0 ? (
<DashboardEmptyState c={c} onLaunch={onToolbarSend} onPrefill={onStarterPrefill} />
<DashboardEmptyState c={c} onLaunch={onToolbarSend} onStarter={onStarter} />
) : (
<div
ref={canvas.contentRef}
@@ -62,9 +62,10 @@ const STARTER_CATEGORIES: StarterCategory[] = [
const DashboardEmptyState: React.FC<{
c: ClaudeTokens;
onLaunch?: (prompt: string, mode: string, model: string) => void;
// Open the composer with the prompt typed in (unsent) so the user hits send.
onPrefill?: (prompt: string) => void;
}> = ({ c, onLaunch, onPrefill }) => {
// hover = preview-open the composer (translucent), leave = close it,
// commit = lock it open so the user can move to send.
onStarter?: (action: 'hover' | 'leave' | 'commit', prompt?: string) => void;
}> = ({ c, onLaunch, onStarter }) => {
// The host hides Dashboard with visibility:hidden (not display:none), which keeps
// CSS animations ticking; gate on active so the shimmer only burns while watched.
const active = useDashboardActive();
@@ -81,17 +82,17 @@ const DashboardEmptyState: React.FC<{
// model connected); otherwise fall back to the plain hint.
const showChips = !!onLaunch && canRun;
const isAppBuilder = currentCategory?.target === 'app-builder';
// Click commits the query into the composer (locks it open); the user then sends.
const launch = (prompt: string) => {
if (launching) return;
// Build prompts open the App Builder (live preview) with the prompt auto-sent.
if (currentCategory?.target === 'app-builder') {
if (isAppBuilder) {
navigate(`/apps/new?prompt=${encodeURIComponent(prompt)}`);
return;
}
// Open the composer with the prompt typed in, unsent: the user sees the chat
// open with their message ready and clicks send. Falls back to direct launch.
if (onPrefill) {
onPrefill(prompt);
if (onStarter) {
onStarter('commit', prompt);
return;
}
if (!onLaunch) return;
@@ -201,12 +202,16 @@ const DashboardEmptyState: React.FC<{
>
<ArrowLeft size={15} /> back
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.9, width: '100%', maxWidth: 480 }}>
<Box
onMouseLeave={() => { if (!isAppBuilder) onStarter?.('leave'); }}
sx={{ display: 'flex', flexDirection: 'column', gap: 0.9, width: '100%', maxWidth: 480 }}
>
{currentPrompts.map((prompt) => (
<Box
component="button"
key={prompt}
onClick={() => launch(prompt)}
onMouseEnter={() => { if (!isAppBuilder) onStarter?.('hover', prompt); }}
disabled={launching}
sx={{
textAlign: 'left',
@@ -159,16 +159,28 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
setSearchPaletteOpen,
});
// Starter-prompt prefill: opens the composer with the prompt typed in (unsent),
// so the user reviews and hits send. Bump a nonce so re-clicking the same prompt
// (after editing/clearing) still re-seeds. Cleared when the composer closes.
// Starter-prompt preview: HOVER opens the composer with the prompt typed in
// (translucent, unsent); leaving closes it again; CLICK locks it open so the
// user can move to the send button without it vanishing. Then they hit send.
const [toolbarPrefill, setToolbarPrefill] = useState<string | undefined>(undefined);
const handleStarterPrefill = useCallback((prompt: string) => {
setToolbarPrefill(prompt);
setToolbarOpen(true);
const starterLockedRef = useRef(false);
const handleStarter = useCallback((action: 'hover' | 'leave' | 'commit', prompt?: string) => {
if (action === 'hover' && prompt) {
setToolbarPrefill(prompt);
setToolbarOpen(true);
} else if (action === 'commit' && prompt) {
starterLockedRef.current = true;
setToolbarPrefill(prompt);
setToolbarOpen(true);
} else if (action === 'leave' && !starterLockedRef.current) {
setToolbarOpen(false);
}
}, [setToolbarOpen]);
useEffect(() => {
if (!toolbarOpen && toolbarPrefill) setToolbarPrefill(undefined);
if (!toolbarOpen) {
starterLockedRef.current = false;
if (toolbarPrefill) setToolbarPrefill(undefined);
}
}, [toolbarOpen, toolbarPrefill]);
useDashboardClipboard({
@@ -266,7 +278,7 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
neighborDirections, toolbarOpen, searchPaletteOpen, newAgentBounce,
toolbarRef, spawnOriginsRef, revealSpawnedRef, measuredHeightsRef, getCanvasState,
toolbarPrefill,
onStarterPrefill: handleStarterPrefill,
onStarter: handleStarter,
onViewportMouseDown: handleViewportMouseDown,
onViewportMouseMove: handleViewportMouseMove,
onViewportMouseUp: handleViewportMouseUp,