From f279524e3208545ee15f0bd7c6c9b00938206826 Mon Sep 17 00:00:00 2001 From: kumarabhirup Date: Thu, 5 Mar 2026 21:20:25 -0800 Subject: [PATCH] feat(chat): add hero state with prompt suggestions for new conversations Show an animated hero screen with rotating prompt suggestion cards when the chat is empty, using framer-motion for layout transitions. Add setText method to ChatEditor for programmatic content insertion. --- apps/web/app/components/chat-panel.tsx | 777 +++++++++++------- .../web/app/components/tiptap/chat-editor.tsx | 6 + 2 files changed, 493 insertions(+), 290 deletions(-) diff --git a/apps/web/app/components/chat-panel.tsx b/apps/web/app/components/chat-panel.tsx index 8b093714070..abfe2772cfb 100644 --- a/apps/web/app/components/chat-panel.tsx +++ b/apps/web/app/components/chat-panel.tsx @@ -11,6 +11,12 @@ import { useRef, useState, } from "react"; +import { motion, LayoutGroup } from "framer-motion"; +import { + Mail, Users, DollarSign, Calendar, Zap, FileText, Database, + Code, Bug, Clock, BarChart3, PenTool, Globe, Search, Sparkles, + FolderOpen, Table, BrainCircuit, MessageSquare, Workflow, +} from "lucide-react"; import { ChatMessage } from "./chat-message"; import { FilePickerModal, @@ -25,6 +31,131 @@ import { } from "./ui/dropdown-menu"; import { UnicodeSpinner } from "./unicode-spinner"; +// ── Prompt suggestions for new chat hero ── + +const PROMPT_SUGGESTIONS = [ + { + id: "email-draft", + label: "Draft an Email", + icon: Mail, + prompt: "Draft a professional follow-up email to a client after our initial meeting, thanking them and summarizing the next steps we discussed", + }, + { + id: "enrich-contacts", + label: "Enrich Leads", + icon: Users, + prompt: "When a new contact is added to my CRM, find their LinkedIn profile and company details and update the record", + }, + { + id: "invoice-reminder", + label: "Invoice Reminder", + icon: DollarSign, + prompt: "Draft a friendly payment reminder email for an invoice that is 7 days overdue, including the invoice number and amount", + }, + { + id: "schedule-report", + label: "Weekly Report", + icon: Calendar, + prompt: "Set up a cron job that runs every Friday at 4pm to compile a summary of all completed tasks this week and email it to the team", + }, + { + id: "auto-tasks", + label: "Auto Tasks", + icon: Zap, + prompt: "Create a workflow that automatically creates a task whenever someone mentions me in an email with a request or action item", + }, + { + id: "summarize-docs", + label: "Summarize Docs", + icon: FileText, + prompt: "Read through all the documents in my workspace and create a concise summary of the key information across all files", + }, + { + id: "query-data", + label: "Query Database", + icon: Database, + prompt: "Connect to my database and show me the top 10 customers by revenue this quarter, including their contact details", + }, + { + id: "code-review", + label: "Review Code", + icon: Code, + prompt: "Review the code in my workspace for potential bugs, security issues, and performance improvements. Prioritize critical findings", + }, + { + id: "debug-error", + label: "Debug Error", + icon: Bug, + prompt: "Help me debug this error I'm seeing in production. Walk me through the likely causes and how to fix each one", + }, + { + id: "daily-digest", + label: "Daily Digest", + icon: Clock, + prompt: "Set up a daily digest that runs every morning at 9am summarizing my unread emails, calendar events, and pending tasks", + }, + { + id: "analyze-csv", + label: "Analyze Data", + icon: BarChart3, + prompt: "Analyze the CSV file in my workspace — find trends, outliers, and generate a visual report with key insights", + }, + { + id: "write-content", + label: "Write Content", + icon: PenTool, + prompt: "Write a compelling blog post about how AI automation is transforming small business operations in 2026", + }, + { + id: "web-research", + label: "Web Research", + icon: Globe, + prompt: "Research the top 5 competitors in my industry and create a comparison table with their pricing, features, and market position", + }, + { + id: "search-files", + label: "Search Files", + icon: Search, + prompt: "Search through all files in my workspace and find every mention of customer feedback, complaints, or feature requests", + }, + { + id: "brainstorm", + label: "Brainstorm Ideas", + icon: Sparkles, + prompt: "Help me brainstorm 10 creative marketing campaign ideas for launching a new product to our existing customer base", + }, + { + id: "organize-files", + label: "Organize Files", + icon: FolderOpen, + prompt: "Look at all the files in my workspace and suggest a better folder structure. Then reorganize them for me", + }, + { + id: "create-spreadsheet", + label: "Build Spreadsheet", + icon: Table, + prompt: "Create a project tracking spreadsheet with columns for task name, assignee, status, priority, due date, and notes", + }, + { + id: "ai-strategy", + label: "AI Strategy", + icon: BrainCircuit, + prompt: "Help me create an AI adoption strategy for my team — which tasks should we automate first for the biggest impact?", + }, + { + id: "meeting-prep", + label: "Meeting Prep", + icon: MessageSquare, + prompt: "Prepare a briefing document for my upcoming client meeting. Include their recent activity, open issues, and talking points", + }, + { + id: "build-workflow", + label: "Build Workflow", + icon: Workflow, + prompt: "Design an automated onboarding workflow for new clients — from welcome email to document collection to account setup", + }, +]; + // ── Attachment types & helpers ── type AttachedFile = { @@ -741,6 +872,41 @@ export const ChatPanel = forwardRef( const [queuedMessages, setQueuedMessages] = useState([]); const [rawView, _setRawView] = useState(false); + // ── Hero state (new chat screen) ── + const [greeting, setGreeting] = useState(""); + const [visiblePrompts, setVisiblePrompts] = useState([]); + + useEffect(() => { + const greetings = [ + "Ready to build?", + "Let's automate something?", + "What shall we tackle?", + "Ready to get things done?", + "Let's get to work?", + "How can I help?", + ]; + const getTimeGreeting = () => { + const hour = new Date().getHours(); + if (hour < 12) return "Good morning!"; + if (hour < 17) return "Good afternoon!"; + return "Good evening!"; + }; + const allGreetings = [getTimeGreeting(), ...greetings]; + setGreeting(allGreetings[Math.floor(Math.random() * allGreetings.length)]); + }, []); + + useEffect(() => { + const shuffled = [...PROMPT_SUGGESTIONS].sort(() => 0.5 - Math.random()); + setVisiblePrompts(shuffled.slice(0, 7)); + }, []); + + const handlePromptClick = useCallback((promptId: string) => { + const prompt = PROMPT_SUGGESTIONS.find((p) => p.id === promptId); + if (!prompt) return; + editorRef.current?.setText(prompt.prompt); + setEditorEmpty(false); + }, []); + const filePath = fileContext?.path ?? null; // ── Ref-based session ID for transport ── @@ -1712,9 +1878,205 @@ export const ChatPanel = forwardRef( lastMsg.parts.some((p) => p.type === "text" && (p as { text: string }).text.length > 0); const showInlineSpinner = isStreaming && !lastAssistantHasText; + const showHeroState = messages.length === 0 && !compact && !isSubagentMode && !loadingSession; + + // ── Input bar content (shared between hero and bottom positions) ── + + const inputBarContent = ( + <> + {queuedMessages.length > 0 && ( +
+
+
+ Queue ({queuedMessages.length}) +
+
+ {queuedMessages.map((msg, idx) => ( + + ))} +
+
+
+ )} + + {!isSubagentMode && ( + + )} + + setEditorEmpty(isEmpty)} + onNativeFileDrop={isSubagentMode ? undefined : uploadAndAttachNativeFiles} + placeholder={ + showHeroState + ? "Build a workflow to automate your tasks" + : isSubagentMode + ? (isStreaming ? "Type to queue a message..." : "Type @ to mention files...") + : compact && fileContext + ? `Ask about ${fileContext.isDirectory ? "this folder" : fileContext.filename}...` + : isStreaming + ? "Type to queue a message..." + : attachedFiles.length > 0 + ? "Add a message or send files..." + : "Type @ to mention files..." + } + disabled={loadingSession} + compact={compact} + /> + +
+
+ {!isSubagentMode && ( + + )} +
+
+ {isStreaming && ( + + )} + {isStreaming ? ( + + ) : ( + + )} +
+
+ + ); + + const inputBarContainer = (onDragOverHandler: React.DragEventHandler, onDragLeaveHandler: React.DragEventHandler, onDropHandler: React.DragEventHandler) => ( +
+ {inputBarContent} +
+ ); + + const handleInputDragOver: React.DragEventHandler = (e) => { + if ( + e.dataTransfer?.types.includes("application/x-file-mention") || + e.dataTransfer?.types.includes("Files") + ) { + e.preventDefault(); + e.dataTransfer.dropEffect = "copy"; + (e.currentTarget as HTMLElement).setAttribute("data-drag-hover", ""); + } + }; + const handleInputDragLeave: React.DragEventHandler = (e) => { + if (!e.currentTarget.contains(e.relatedTarget as Node)) { + (e.currentTarget as HTMLElement).removeAttribute("data-drag-hover"); + } + }; + const handleInputDrop: React.DragEventHandler = (e) => { + (e.currentTarget as HTMLElement).removeAttribute("data-drag-hover"); + const data = e.dataTransfer?.getData("application/x-file-mention"); + if (data) { + e.preventDefault(); + e.stopPropagation(); + try { + const { name, path } = JSON.parse(data) as { name: string; path: string }; + if (name && path) { + editorRef.current?.insertFileMention(name, path); + } + } catch { /* ignore */ } + return; + } + const files = e.dataTransfer?.files; + if (files && files.length > 0) { + e.preventDefault(); + e.stopPropagation(); + uploadAndAttachNativeFiles(files); + } + }; + // ── Render ── return ( +
(

+ ) : showHeroState ? ( +
+ {/* Hero greeting */} + {greeting && ( + + {greeting.split(" ").map((word, i) => ( + + {word} + + ))} + + )} + + {/* Centered input bar */} + + + {inputBarContainer(handleInputDragOver, handleInputDragLeave, handleInputDrop)} + + + + {/* Prompt suggestion pills */} + +
+ {visiblePrompts.slice(0, 3).map((template) => { + const Icon = template.icon; + return ( + + ); + })} +
+
+ {visiblePrompts.slice(3, 7).map((template) => { + const Icon = template.icon; + return ( + + ); + })} +
+
+
) : messages.length === 0 ? (
- {compact ? ( -

- Ask about this file -

- ) : ( - <> -

- What can I help with? -

-

- Send a message to start a - conversation with your - agent. -

- - )} +

+ Ask about this file +

) : ( @@ -2016,270 +2459,23 @@ export const ChatPanel = forwardRef( )} - {/* Input bar at bottom */} -
+ {/* Input bar at bottom (hidden when hero state is active) */} + {!showHeroState && (
-
{ - if ( - e.dataTransfer?.types.includes("application/x-file-mention") || - e.dataTransfer?.types.includes("Files") - ) { - e.preventDefault(); - e.dataTransfer.dropEffect = "copy"; - // visual feedback - (e.currentTarget as HTMLElement).setAttribute("data-drag-hover", ""); - } - }} - onDragLeave={(e) => { - // Only remove when leaving the container itself (not entering a child) - if (!e.currentTarget.contains(e.relatedTarget as Node)) { - (e.currentTarget as HTMLElement).removeAttribute("data-drag-hover"); - } - }} - onDrop={(e) => { - (e.currentTarget as HTMLElement).removeAttribute("data-drag-hover"); - - // Sidebar file mention drop - const data = e.dataTransfer?.getData("application/x-file-mention"); - if (data) { - e.preventDefault(); - e.stopPropagation(); - try { - const { name, path } = JSON.parse(data) as { name: string; path: string }; - if (name && path) { - editorRef.current?.insertFileMention(name, path); - } - } catch { - // ignore malformed data - } - return; - } - - // Native file drop (from OS file manager / Desktop) - const files = e.dataTransfer?.files; - if (files && files.length > 0) { - e.preventDefault(); - e.stopPropagation(); - uploadAndAttachNativeFiles(files); - } - }} - > - {/* Queued messages indicator */} - {queuedMessages.length > 0 && ( -
-
-
- Queue ({queuedMessages.length}) -
-
- {queuedMessages.map((msg, idx) => ( - - ))} -
-
-
- )} - - {/* Attachment preview strip (hidden in subagent mode) */} - {!isSubagentMode && ( - - )} - - - setEditorEmpty(isEmpty) - } - onNativeFileDrop={isSubagentMode ? undefined : uploadAndAttachNativeFiles} - placeholder={ - isSubagentMode - ? (isStreaming ? "Type to queue a message..." : "Type @ to mention files...") - : compact && fileContext - ? `Ask about ${fileContext.isDirectory ? "this folder" : fileContext.filename}...` - : isStreaming - ? "Type to queue a message..." - : attachedFiles.length > - 0 - ? "Add a message or send files..." - : "Type @ to mention files..." - } - disabled={loadingSession} - compact={compact} - /> - - {/* Toolbar row */} -
+ -
- {!isSubagentMode && ( - - )} -
- {/* Send / Stop / Queue buttons */} -
- {isStreaming && ( - - )} - {isStreaming ? ( - - ) : ( - - )} -
-
+ {inputBarContainer(handleInputDragOver, handleInputDragLeave, handleInputDrop)} +
-
+ )} {/* File picker modal (not in subagent mode) */} {!isSubagentMode && ( @@ -2293,6 +2489,7 @@ export const ChatPanel = forwardRef( )} +
); }, ); diff --git a/apps/web/app/components/tiptap/chat-editor.tsx b/apps/web/app/components/tiptap/chat-editor.tsx index c45f692e546..81fd0fd5f5d 100644 --- a/apps/web/app/components/tiptap/chat-editor.tsx +++ b/apps/web/app/components/tiptap/chat-editor.tsx @@ -31,6 +31,8 @@ export type ChatEditorHandle = { isEmpty: () => boolean; /** Programmatically submit the current content. */ submit: () => void; + /** Replace the editor content with the given text and focus at end. */ + setText: (text: string) => void; }; type ChatEditorProps = { @@ -413,6 +415,10 @@ export const ChatEditor = forwardRef( editor.commands.clearContent(true); } }, + setText: (text: string) => { + editor?.commands.setContent(text); + editor?.commands.focus("end"); + }, })); return (