mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-22 12:42:22 +02:00
[eric] onboarding: starter prompt opens the composer pre-filled (unsent), user reviews + hits send; replaces the ghost preview
This commit is contained in:
@@ -42,9 +42,12 @@ interface Props {
|
||||
thinkingLevel?: 'off' | 'low' | 'medium' | 'high' | 'auto';
|
||||
onThinkingLevelChange?: (level: 'off' | 'low' | 'medium' | 'high' | 'auto') => void;
|
||||
onActivityLabelChange?: (label: string | null) => void;
|
||||
// Seed the composer with this text (unsent), so a starter-prompt click opens
|
||||
// the chat with the message already typed, ready for the user to hit send.
|
||||
prefillPrompt?: string;
|
||||
}
|
||||
|
||||
const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode, onModeChange, model, onModelChange, provider, onProviderChange, isRunning, onStop, autoRunMode, contextEstimate, embedded, autoFocus, sessionId, queueLength = 0, thinkingLevel = 'auto', onThinkingLevelChange, onActivityLabelChange }, ref) => {
|
||||
const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode, onModeChange, model, onModelChange, provider, onProviderChange, isRunning, onStop, autoRunMode, contextEstimate, embedded, autoFocus, sessionId, queueLength = 0, thinkingLevel = 'auto', onThinkingLevelChange, onActivityLabelChange, prefillPrompt }, ref) => {
|
||||
const c = useClaudeTokens();
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -59,6 +62,19 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode,
|
||||
if (autoFocus) editorRef.current?.focus();
|
||||
}, [autoFocus]);
|
||||
|
||||
// Drop the seeded prompt into the editor when it arrives (starter-prompt click).
|
||||
const prefilledRef = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (!prefillPrompt || prefilledRef.current === prefillPrompt) return;
|
||||
const editor = editorRef.current;
|
||||
if (!editor) return;
|
||||
if (editor.tagName === 'TEXTAREA') (editor as unknown as HTMLTextAreaElement).value = prefillPrompt;
|
||||
else editor.textContent = prefillPrompt;
|
||||
setHasContent(true);
|
||||
prefilledRef.current = prefillPrompt;
|
||||
editor.focus();
|
||||
}, [prefillPrompt]);
|
||||
|
||||
useDraftLoad(editorRef, ownerId);
|
||||
|
||||
const [hasContent, setHasContent] = useState(() => !!loadDraft(ownerId));
|
||||
|
||||
@@ -57,6 +57,8 @@ interface Props {
|
||||
dashboardId?: string;
|
||||
newAgentBounce?: boolean;
|
||||
onNewAgentBounceEnd?: () => void;
|
||||
// Text to seed the composer with when it opens (starter-prompt click).
|
||||
prefillPrompt?: string;
|
||||
}
|
||||
|
||||
const TOOLBAR_OWNER_ID = '__toolbar__';
|
||||
@@ -100,7 +102,7 @@ function formatRelativeTime(dateStr: string | null): string {
|
||||
}
|
||||
|
||||
const DashboardToolbar = React.forwardRef<HTMLDivElement, Props>(
|
||||
({ inputOpen, onNewAgent, onCancel, onSend, onAddView, onHistoryResume, onAddBrowser, onAddNote, dashboardId, newAgentBounce, onNewAgentBounceEnd }, ref) => {
|
||||
({ inputOpen, onNewAgent, onCancel, onSend, onAddView, onHistoryResume, onAddBrowser, onAddNote, dashboardId, newAgentBounce, onNewAgentBounceEnd, prefillPrompt }, ref) => {
|
||||
const c = useClaudeTokens();
|
||||
const dispatch = useAppDispatch();
|
||||
const elementSelection = useElementSelection();
|
||||
@@ -434,6 +436,7 @@ const DashboardToolbar = React.forwardRef<HTMLDivElement, Props>(
|
||||
sessionId={TOOLBAR_OWNER_ID}
|
||||
thinkingLevel={thinkingLevel}
|
||||
onThinkingLevelChange={handleThinkingLevelChange}
|
||||
prefillPrompt={prefillPrompt}
|
||||
/>
|
||||
</div>
|
||||
) : historyOpen ? (
|
||||
|
||||
@@ -73,6 +73,8 @@ interface DashboardCanvasProps {
|
||||
onNewAgent: () => void;
|
||||
onToolbarCancel: () => void;
|
||||
onToolbarSend: (...args: any[]) => void;
|
||||
onStarterPrefill: (prompt: string) => void;
|
||||
toolbarPrefill?: string;
|
||||
onAddView: (outputId: string) => void;
|
||||
onHistoryResume: (sessionId: string) => void;
|
||||
onAddBrowser: () => void;
|
||||
@@ -130,6 +132,8 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
onNewAgent,
|
||||
onToolbarCancel,
|
||||
onToolbarSend,
|
||||
onStarterPrefill,
|
||||
toolbarPrefill,
|
||||
onAddView,
|
||||
onHistoryResume,
|
||||
onAddBrowser,
|
||||
@@ -215,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} />
|
||||
<DashboardEmptyState c={c} onLaunch={onToolbarSend} onPrefill={onStarterPrefill} />
|
||||
) : (
|
||||
<div
|
||||
ref={canvas.contentRef}
|
||||
@@ -289,6 +293,7 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
onFitToView={onFitToView}
|
||||
onTidy={onTidy}
|
||||
onSearchPaletteClose={onSearchPaletteClose}
|
||||
toolbarPrefill={toolbarPrefill}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
|
||||
@@ -62,7 +62,9 @@ const STARTER_CATEGORIES: StarterCategory[] = [
|
||||
const DashboardEmptyState: React.FC<{
|
||||
c: ClaudeTokens;
|
||||
onLaunch?: (prompt: string, mode: string, model: string) => void;
|
||||
}> = ({ c, onLaunch }) => {
|
||||
// Open the composer with the prompt typed in (unsent) so the user hits send.
|
||||
onPrefill?: (prompt: string) => void;
|
||||
}> = ({ c, onLaunch, onPrefill }) => {
|
||||
// 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();
|
||||
@@ -72,7 +74,6 @@ const DashboardEmptyState: React.FC<{
|
||||
const navigate = useNavigate();
|
||||
const [launching, setLaunching] = React.useState(false);
|
||||
const [expanded, setExpanded] = React.useState<string | null>(null);
|
||||
const [hoveredPrompt, setHoveredPrompt] = React.useState<string | null>(null);
|
||||
const currentCategory = STARTER_CATEGORIES.find((cat) => cat.id === expanded);
|
||||
const currentPrompts = currentCategory?.prompts ?? [];
|
||||
|
||||
@@ -87,6 +88,12 @@ const DashboardEmptyState: React.FC<{
|
||||
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);
|
||||
return;
|
||||
}
|
||||
if (!onLaunch) return;
|
||||
setLaunching(true); // empty state unmounts on first session, but guard a fast double-click
|
||||
onLaunch(prompt, mode, model);
|
||||
@@ -194,16 +201,12 @@ const DashboardEmptyState: React.FC<{
|
||||
>
|
||||
<ArrowLeft size={15} /> back
|
||||
</Box>
|
||||
<Box
|
||||
onMouseLeave={() => setHoveredPrompt(null)}
|
||||
sx={{ position: 'relative', display: 'flex', flexDirection: 'column', gap: 0.9, width: '100%', maxWidth: 480 }}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.9, width: '100%', maxWidth: 480 }}>
|
||||
{currentPrompts.map((prompt) => (
|
||||
<Box
|
||||
component="button"
|
||||
key={prompt}
|
||||
onClick={() => launch(prompt)}
|
||||
onMouseEnter={() => setHoveredPrompt(prompt)}
|
||||
disabled={launching}
|
||||
sx={{
|
||||
textAlign: 'left',
|
||||
@@ -223,45 +226,6 @@ const DashboardEmptyState: React.FC<{
|
||||
{prompt}
|
||||
</Box>
|
||||
))}
|
||||
|
||||
{/* Hover preview: a translucent ghost of the chat that would open,
|
||||
so the user sees their message land before committing. Pure
|
||||
divs (not the heavy AgentChat), so it costs nothing to render. */}
|
||||
<AnimatePresence>
|
||||
{hoveredPrompt && (
|
||||
<Box
|
||||
component={motion.div}
|
||||
key="ghost"
|
||||
initial={{ opacity: 0, x: -8 }}
|
||||
animate={{ opacity: 0.72, x: 0 }}
|
||||
exit={{ opacity: 0, x: -8 }}
|
||||
transition={{ duration: 0.16 }}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: '100%', top: '50%', ml: 3,
|
||||
transform: 'translateY(-50%)',
|
||||
width: 300,
|
||||
pointerEvents: 'none',
|
||||
bgcolor: c.bg.surface,
|
||||
border: `1px solid ${c.border.medium}`,
|
||||
borderRadius: 3,
|
||||
boxShadow: c.shadow.md,
|
||||
p: 1.5,
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ fontSize: '0.8rem', fontWeight: 600, color: c.text.tertiary, mb: 1.2 }}>
|
||||
New chat
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Box sx={{ maxWidth: '88%', bgcolor: c.bg.elevated, borderRadius: 2.5, px: 1.4, py: 0.9 }}>
|
||||
<Typography sx={{ fontSize: '0.85rem', color: c.text.primary, lineHeight: 1.4 }}>
|
||||
{hoveredPrompt}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Box>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@@ -41,6 +41,7 @@ interface DashboardOverlaysProps {
|
||||
onFitToView: () => void;
|
||||
onTidy: () => void;
|
||||
onSearchPaletteClose: () => void;
|
||||
toolbarPrefill?: string;
|
||||
}
|
||||
|
||||
const DashboardOverlays: React.FC<DashboardOverlaysProps> = ({
|
||||
@@ -68,6 +69,7 @@ const DashboardOverlays: React.FC<DashboardOverlaysProps> = ({
|
||||
onFitToView,
|
||||
onTidy,
|
||||
onSearchPaletteClose,
|
||||
toolbarPrefill,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
@@ -86,6 +88,7 @@ const DashboardOverlays: React.FC<DashboardOverlaysProps> = ({
|
||||
dashboardId={dashboardId}
|
||||
newAgentBounce={newAgentBounce}
|
||||
onNewAgentBounceEnd={onNewAgentBounceEnd}
|
||||
prefillPrompt={toolbarPrefill}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { useElementSelection } from '@/app/components/editor/ElementSelectionContext';
|
||||
import { useCanvasControls } from '../interaction/useCanvasControls';
|
||||
@@ -159,6 +159,18 @@ 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.
|
||||
const [toolbarPrefill, setToolbarPrefill] = useState<string | undefined>(undefined);
|
||||
const handleStarterPrefill = useCallback((prompt: string) => {
|
||||
setToolbarPrefill(prompt);
|
||||
setToolbarOpen(true);
|
||||
}, [setToolbarOpen]);
|
||||
useEffect(() => {
|
||||
if (!toolbarOpen && toolbarPrefill) setToolbarPrefill(undefined);
|
||||
}, [toolbarOpen, toolbarPrefill]);
|
||||
|
||||
useDashboardClipboard({
|
||||
isActive,
|
||||
dashboardId,
|
||||
@@ -253,6 +265,8 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
|
||||
focusedCardId, pendingFocusNoteId, multiDragDelta, shakeDirection,
|
||||
neighborDirections, toolbarOpen, searchPaletteOpen, newAgentBounce,
|
||||
toolbarRef, spawnOriginsRef, revealSpawnedRef, measuredHeightsRef, getCanvasState,
|
||||
toolbarPrefill,
|
||||
onStarterPrefill: handleStarterPrefill,
|
||||
onViewportMouseDown: handleViewportMouseDown,
|
||||
onViewportMouseMove: handleViewportMouseMove,
|
||||
onViewportMouseUp: handleViewportMouseUp,
|
||||
|
||||
Reference in New Issue
Block a user