diff --git a/backend/apps/agents/manager/predict_prompts.py b/backend/apps/agents/manager/predict_prompts.py index f41ebad8..5eb73da6 100644 --- a/backend/apps/agents/manager/predict_prompts.py +++ b/backend/apps/agents/manager/predict_prompts.py @@ -17,6 +17,10 @@ logger = logging.getLogger(__name__) MAX_TOPICS = 24 MAX_SUGGESTIONS = 5 +# Don't predict someone's next prompt until we ACTUALLY know their patterns. Below this many real +# past chats, any guess is just noise (onboarding starters alone are what they browsed at setup, not +# a read on what they want now), so we stay silent and let the neutral placeholder stand. +MIN_REAL_TOPICS = 4 # Names the aux title-gen hands out for empty/greeting chats; they carry no topic signal. P_SKIP_NAMES = {"untitled", "new chat", "greeting", "chat", ""} @@ -70,8 +74,9 @@ async def predict_prompts(count: int = MAX_SUGGESTIONS) -> List[str]: for s in (global_settings.personalized_starters or []) if getattr(s, "prompt", None) ] - # Nothing to personalize from: let the composer keep its static placeholder. - if not topics and not starters: + # Only predict once there's a real track record. Onboarding starters can enrich a prediction + # but never trigger one on their own: a brand-new user hasn't shown us what they want yet. + if len(topics) < MIN_REAL_TOPICS: return [] aux_model = (await resolve_aux_model(global_settings, preferred_tier="haiku"))[0] diff --git a/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx b/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx index 179a0848..c3c438dd 100644 --- a/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx +++ b/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx @@ -99,7 +99,6 @@ const DashboardToolbar = React.forwardRef( // load (cached), then one is shown at a time and cycled while the composer sits idle+empty. Empty // list (no signal / no provider / error) just leaves the static "What should I do sir..." placeholder. const [ghostList, setGhostList] = useState([]); - const [ghostIdx, setGhostIdx] = useState(0); const ghostFetchedRef = useRef(false); useEffect(() => { if (!inputOpen || ghostFetchedRef.current) return; @@ -116,14 +115,10 @@ const DashboardToolbar = React.forwardRef( } catch { /* fail open: keep the static placeholder */ } })(); }, [inputOpen]); - // Rotate the visible suggestion every few seconds while the composer is open, so the user sees a - // few different ideas instead of one. Cheap; the list is already fetched and cached. - useEffect(() => { - if (!inputOpen || ghostList.length <= 1) return undefined; - const t = setInterval(() => setGhostIdx((i) => (i + 1) % ghostList.length), 4500); - return () => clearInterval(t); - }, [inputOpen, ghostList.length]); - const ghostSuggestion = ghostList.length ? ghostList[ghostIdx % ghostList.length] : undefined; + // ONE stable suggestion, never a rotating carousel: cycling guesses every few seconds reads as + // "the app is throwing darts." The backend only returns anything when it has real usage history to + // predict from (see predict_prompts.py), so an empty list just leaves the neutral placeholder. + const ghostSuggestion = ghostList.length ? ghostList[0] : undefined; // Reset defaults on each new compose session so in-session picks don't leak into the next new-chat draft. const prevInputOpen = useRef(false);