Created
June 6, 2025 09:03
-
-
Save ad22sinha/4d8d7f2d9b7c866152901a2cc4abbf02 to your computer and use it in GitHub Desktop.
src/components/dashboard/chatinterface.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use client'; | |
import { useState, useRef, useEffect } from 'react'; | |
import { PaperAirplaneIcon, ArrowPathIcon, XCircleIcon, XMarkIcon } from '@heroicons/react/24/solid'; | |
import { ChatMessage } from '@/types/email'; | |
interface EmailTag { | |
email: string; | |
isValid: boolean; | |
} | |
interface ChatInterfaceProps { | |
onSubmit: (prompt: string) => Promise<void>; | |
onSendToMany?: (emails: string[], subject?: string) => Promise<void>; | |
isGenerating: boolean; | |
messages: ChatMessage[]; | |
currentEmailCardId?: string; | |
isNewChat?: boolean; | |
initialSubject?: string; | |
onCancelGeneration?: () => void; | |
} | |
// Added from Version 2 | |
const typeText = ( | |
text: string, | |
setPrompt: React.Dispatch<React.SetStateAction<string>>, | |
promptTextareaRef: React.RefObject<HTMLTextAreaElement>, | |
setIsTyping: React.Dispatch<React.SetStateAction<boolean>>, | |
onComplete?: () => void | |
) => { | |
let index = 0; | |
setPrompt(''); // Clear existing text | |
setIsTyping(true); | |
const typeInterval = setInterval(() => { | |
if (index < text.length) { | |
setPrompt(text.substring(0, index + 1)); | |
index++; | |
} else { | |
clearInterval(typeInterval); | |
setIsTyping(false); | |
// Focus the textarea after typing is complete | |
setTimeout(() => { | |
promptTextareaRef.current?.focus(); | |
// Call the completion callback after focusing | |
if (onComplete) { | |
onComplete(); | |
} | |
}, 100); | |
} | |
}, 20); // Adjust speed here (lower = faster, higher = slower) | |
}; | |
const ChatInterface = ({ | |
onSubmit, | |
onSendToMany, | |
isGenerating, | |
messages, | |
currentEmailCardId, | |
isNewChat = false, | |
initialSubject = '', | |
onCancelGeneration | |
}: ChatInterfaceProps) => { | |
const [prompt, setPrompt] = useState(''); | |
const [isSendMode, setIsSendMode] = useState(false); | |
const [emailTags, setEmailTags] = useState<EmailTag[]>([]); | |
const [currentEmail, setCurrentEmail] = useState(''); | |
const [emailSubject, setEmailSubject] = useState(initialSubject); | |
const messagesEndRef = useRef<HTMLDivElement>(null); | |
const promptTextareaRef = useRef<HTMLTextAreaElement>(null); | |
const emailInputRef = useRef<HTMLInputElement>(null); | |
const [isTyping, setIsTyping] = useState(false); // isTyping state used by typeText and prompt examples | |
// useEffect for subject management from Version 1 (robust version) | |
useEffect(() => { | |
if (!isSendMode) { | |
if (initialSubject !== emailSubject) { // Only update if different | |
setEmailSubject(initialSubject); | |
} | |
} | |
}, [initialSubject, isSendMode, emailSubject]); | |
// useEffect for activateSendMode custom event from Version 1 | |
useEffect(() => { | |
const activateSendModeHandler = (event: CustomEvent) => { | |
if (event.detail?.activate && !isSendMode) { // Ensure not already in send mode | |
setIsSendMode(true); | |
setPrompt(''); | |
setEmailTags([]); | |
if (typeof event.detail.subject === 'string') { | |
setEmailSubject(event.detail.subject); | |
} else { | |
// If no event subject, use initialSubject (which defaults to ''). | |
setEmailSubject(initialSubject); | |
} | |
setTimeout(() => emailInputRef.current?.focus(), 0); | |
} | |
}; | |
document.addEventListener('activateSendMode', activateSendModeHandler as EventListener); | |
return () => { | |
document.removeEventListener('activateSendMode', activateSendModeHandler as EventListener); | |
}; | |
}, [isSendMode, initialSubject]); // emailSubject removed as a dependency, as this effect sets it. | |
useEffect(() => { | |
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); | |
}, [messages]); | |
const adjustTextareaHeight = () => { | |
if (promptTextareaRef.current) { | |
promptTextareaRef.current.style.height = 'auto'; | |
const scrollHeight = promptTextareaRef.current.scrollHeight; | |
const minHeight = 40; | |
const maxHeight = 120; | |
const newHeight = Math.max(minHeight, Math.min(scrollHeight, maxHeight)); | |
promptTextareaRef.current.style.height = newHeight + 'px'; | |
promptTextareaRef.current.style.overflowY = scrollHeight > maxHeight ? 'auto' : 'hidden'; | |
} | |
}; | |
useEffect(() => { | |
adjustTextareaHeight(); | |
}, [prompt]); | |
const isValidEmail = (email: string): boolean => { | |
const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; | |
return emailRegex.test(email.trim()); | |
}; | |
// useEffect for /send command from Version 1 | |
useEffect(() => { | |
if (!isSendMode && prompt.trim().toLowerCase().startsWith('/send')) { // Ensure not already in send mode | |
setIsSendMode(true); | |
const commandText = prompt.trim().substring(5).trim(); | |
setPrompt(''); | |
setEmailTags([]); | |
if (commandText) { | |
const potentialEmails = commandText.split(/[\s,]+/).filter(Boolean); | |
potentialEmails.forEach(emailStr => { | |
const cleanedPotentialEmail = emailStr.trim().replace(/,$/, ''); | |
if (cleanedPotentialEmail && isValidEmail(cleanedPotentialEmail)) { | |
setEmailTags(prev => [...prev, { email: cleanedPotentialEmail, isValid: true }]); | |
} | |
}); | |
} | |
// When entering send mode via /send, set subject from initialSubject (which defaults to ''). | |
setEmailSubject(initialSubject); | |
setTimeout(() => emailInputRef.current?.focus(), 0); | |
} | |
}, [prompt, isSendMode, initialSubject]); // emailSubject removed as a dependency, as this effect sets it. | |
const handleSubmit = async (e: React.FormEvent) => { | |
e.preventDefault(); | |
if (isSendMode) { | |
let finalEmailTags = [...emailTags]; | |
if (currentEmail.trim()) { | |
const cleanedEmail = currentEmail.trim().replace(/,$/, ''); | |
if (!finalEmailTags.some(tag => tag.email.toLowerCase() === cleanedEmail.toLowerCase())) { | |
finalEmailTags.push({ email: cleanedEmail, isValid: isValidEmail(cleanedEmail) }); | |
} | |
setCurrentEmail(''); // Clear input after processing | |
} | |
const validEmails = finalEmailTags | |
.filter(tag => tag.isValid) | |
.map(tag => tag.email); | |
const uniqueValidEmails = Array.from(new Set(validEmails.map(email => email.toLowerCase()))).map(lowerEmail => { | |
return validEmails.find(e => e.toLowerCase() === lowerEmail)!; | |
}); | |
if (uniqueValidEmails.length > 0 && emailSubject.trim() && onSendToMany) { | |
await onSendToMany(uniqueValidEmails, emailSubject); | |
// setEmailTags(finalEmailTags); // Update state if keeping tags after send | |
// exitSendMode(); // Optional: exit send mode after sending | |
} else if (uniqueValidEmails.length === 0) { | |
console.warn("No valid emails to send."); | |
emailInputRef.current?.focus(); | |
} else if (!emailSubject.trim()) { | |
console.warn("Email subject is required."); | |
// Potentially focus the subject input here if it's empty | |
} | |
// Persist the tags that were just processed, including the one from currentEmail input | |
setEmailTags(finalEmailTags.filter(tag => !tag.email.toLowerCase().includes(currentEmail.trim().toLowerCase()) || isValidEmail(currentEmail.trim()))); | |
return; | |
} | |
if (!prompt.trim()) return; | |
const currentPrompt = prompt; | |
setPrompt(''); | |
setIsTyping(false); // Ensure isTyping is false after submission | |
if (promptTextareaRef.current) { | |
promptTextareaRef.current.style.height = '40px'; | |
promptTextareaRef.current.style.overflowY = 'hidden'; | |
} | |
onSubmit(currentPrompt); | |
}; | |
const addEmailTag = (email: string) => { | |
if (!email.trim()) return; | |
const cleanedEmail = email.trim().replace(/,$/, ''); | |
if (emailTags.some(tag => tag.email.toLowerCase() === cleanedEmail.toLowerCase())) { | |
setCurrentEmail(''); | |
emailInputRef.current?.focus(); | |
return; | |
} | |
const isValid = isValidEmail(cleanedEmail); | |
setEmailTags(prevTags => [...prevTags, { email: cleanedEmail, isValid }]); | |
setCurrentEmail(''); | |
emailInputRef.current?.focus(); | |
}; | |
const removeEmailTag = (index: number) => { | |
setEmailTags(prevTags => prevTags.filter((_, i) => i !== index)); | |
emailInputRef.current?.focus(); | |
}; | |
// exitSendMode from Version 1 (explicitly resets subject) | |
const exitSendMode = () => { | |
setIsSendMode(false); | |
setEmailTags([]); | |
setCurrentEmail(''); | |
setEmailSubject(initialSubject); // Explicitly reset to initialSubject on exit | |
setTimeout(() => promptTextareaRef.current?.focus(), 0); | |
}; | |
const clearInput = () => { | |
if (isSendMode) { | |
setCurrentEmail(''); | |
emailInputRef.current?.focus(); | |
} else { | |
setPrompt(''); | |
setIsTyping(false); | |
if (promptTextareaRef.current) { | |
promptTextareaRef.current.style.height = '40px'; | |
promptTextareaRef.current.style.overflowY = 'hidden'; | |
promptTextareaRef.current.focus(); | |
} | |
} | |
}; | |
const formatTimestamp = (timestamp: string) => { | |
const date = new Date(timestamp); | |
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | |
}; | |
// handleKeyDown from Version 1 (includes subject input handling) | |
const handleKeyDown = (e: React.KeyboardEvent) => { | |
if (isSendMode) { | |
if (e.target !== emailInputRef.current && e.target !== document.getElementById('emailSubject')) return; | |
if (e.target === emailInputRef.current) { | |
if (e.key === 'Enter' && !e.shiftKey && !currentEmail.trim() && emailTags.length > 0 && emailSubject.trim()) { | |
e.preventDefault(); | |
handleSubmit(e as unknown as React.FormEvent); | |
return; | |
} | |
if ((e.key === 'Enter' && !e.shiftKey) || e.key === ',' || e.key === ' ') { | |
if (currentEmail.trim()) { | |
e.preventDefault(); | |
addEmailTag(currentEmail); | |
} else if (e.key === ' ' || e.key === ',') { | |
e.preventDefault(); // Prevent space/comma if input is empty | |
} | |
return; | |
} | |
if (e.key === 'Backspace' && !currentEmail && emailTags.length > 0) { | |
e.preventDefault(); | |
removeEmailTag(emailTags.length - 1); | |
return; | |
} | |
} | |
// Allow Enter on subject field to potentially submit form if conditions met | |
if (e.target === document.getElementById('emailSubject') && e.key === 'Enter' && !e.shiftKey) { | |
if (emailTags.length > 0 && emailSubject.trim()) { | |
e.preventDefault(); | |
handleSubmit(e as unknown as React.FormEvent); | |
return; | |
} | |
} | |
if (e.key === 'Escape') { | |
e.preventDefault(); | |
exitSendMode(); | |
return; | |
} | |
return; | |
} | |
// Not in send mode (prompt input) | |
if (e.target !== promptTextareaRef.current) return; | |
if (e.key === 'Enter' && !e.shiftKey) { | |
e.preventDefault(); | |
if (prompt.trim()) { | |
handleSubmit(e as unknown as React.FormEvent); | |
} | |
} | |
}; | |
return ( | |
<div className="h-full overflow-hidden flex flex-col rounded-lg bg-white dark:bg-gray-900 shadow-sm"> | |
<div className="flex-1 overflow-y-auto p-3 space-y-4 bg-gray-50 dark:bg-gray-800"> | |
{messages.length === 0 ? ( | |
<div className="flex flex-col items-center justify-center text-center text-gray-500 dark:text-gray-400 py-12"> | |
<div className="w-16 h-16 bg-white dark:bg-gray-700 rounded-full flex items-center justify-center mb-4 shadow-sm border border-gray-100 dark:border-gray-600"> | |
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" className="text-indigo-600 dark:text-indigo-400"> | |
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M12 5.75V18.25"></path> | |
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M18.25 12L5.75 12"></path> | |
</svg> | |
</div> | |
<p className="text-base font-medium text-gray-900 dark:text-white mb-2">Start a conversation</p> | |
<p className="text-sm max-w-sm text-gray-500 dark:text-gray-400"> | |
Describe the email you want to create. Be specific about content, style, and purpose. | |
</p> | |
</div> | |
) : ( | |
messages.map((message) => ( | |
<div | |
key={message.id} | |
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`} | |
> | |
<div | |
className={`max-w-[85%] rounded-2xl shadow-sm ${ | |
message.isSystem | |
? 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 border border-gray-200 dark:border-gray-600' | |
: message.role === 'user' | |
? 'bg-indigo-600 text-white' | |
: 'bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-200 border border-gray-100 dark:border-gray-600' | |
} animate-in fade-in-0 slide-in-from-${message.role === 'user' ? 'right' : 'left'}-2 duration-300`} | |
> | |
<div className="p-3"> | |
<div className="text-sm whitespace-pre-wrap">{message.content}</div> | |
<div className={`text-[10px] mt-1.5 flex justify-end ${ | |
message.role === 'user' ? 'text-indigo-200' : 'text-gray-400 dark:text-gray-500' | |
}`}> | |
{formatTimestamp(message.timestamp)} | |
</div> | |
</div> | |
</div> | |
</div> | |
)) | |
)} | |
{isGenerating && !isSendMode && ( | |
<div className="flex justify-start"> | |
<div className="flex px-4 py-3 space-x-1 bg-white dark:bg-gray-700 rounded-2xl border border-gray-100 dark:border-gray-600 shadow-sm"> | |
<div className="h-2 w-2 bg-indigo-600 dark:bg-indigo-400 rounded-full animate-bounce delay-0"></div> | |
<div className="h-2 w-2 bg-indigo-500 dark:bg-indigo-500 rounded-full animate-bounce delay-150"></div> | |
<div className="h-2 w-2 bg-indigo-400 dark:bg-indigo-600 rounded-full animate-bounce delay-300"></div> | |
</div> | |
</div> | |
)} | |
<div ref={messagesEndRef} className="h-2" /> | |
</div> | |
<div className="flex-shrink-0 border-t border-gray-200 dark:border-gray-700 p-3 relative bg-white dark:bg-gray-900 backdrop-blur-md backdrop-saturate-150 sticky bottom-0"> | |
<form onSubmit={handleSubmit} className="relative"> | |
{isSendMode && ( | |
<div className="mb-2 bg-white dark:bg-gray-800 p-3 rounded-lg border border-gray-200 dark:border-gray-700 flex flex-col shadow-sm"> | |
<div className="flex items-center mb-2"> | |
<div className="text-xs font-medium text-gray-800 dark:text-gray-200 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded-md"> | |
Send to: | |
</div> | |
<button | |
type="button" | |
onClick={exitSendMode} | |
className="text-xs text-gray-500 dark:text-gray-400 hover:text-red-500 dark:hover:text-red-400 ml-auto flex items-center" | |
> | |
<XMarkIcon className="h-3.5 w-3.5 mr-1" /> | |
Cancel | |
</button> | |
</div> | |
<div className="flex flex-wrap gap-2 min-h-[36px] p-2 rounded-md bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600"> | |
{emailTags.map((tag, index) => ( | |
<div | |
key={index} | |
className={`flex items-center text-sm px-2 py-0.5 rounded-md ${ | |
tag.isValid | |
? 'bg-indigo-100 dark:bg-indigo-900/40 text-indigo-800 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800' | |
: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300 border border-red-200 dark:border-red-800' | |
}`} | |
> | |
<span className="max-w-[150px] truncate font-medium">{tag.email}</span> | |
<button | |
type="button" | |
onClick={() => removeEmailTag(index)} | |
className="ml-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 p-0.5 rounded-full hover:bg-gray-200/50 dark:hover:bg-gray-600/50" | |
> | |
<XMarkIcon className="h-3.5 w-3.5" /> | |
</button> | |
</div> | |
))} | |
<input | |
ref={emailInputRef} | |
type="text" | |
value={currentEmail} | |
onChange={(e) => setCurrentEmail(e.target.value)} | |
onKeyDown={handleKeyDown} | |
placeholder={emailTags.length === 0 ? "Type email addresses..." : ""} | |
className="flex-1 min-w-[150px] text-sm text-gray-900 dark:text-gray-100 outline-none bg-transparent dark:bg-transparent py-0.5" | |
autoFocus | |
/> | |
</div> | |
<div className="mt-3"> | |
<label htmlFor="emailSubject" className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1"> | |
Subject: | |
</label> | |
<input | |
type="text" | |
id="emailSubject" // Used by handleKeyDown | |
value={emailSubject} | |
onChange={(e) => setEmailSubject(e.target.value)} | |
onKeyDown={handleKeyDown} // Added from Version 1 | |
placeholder="Enter email subject..." | |
className="w-full px-3 py-1.5 rounded-md bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 text-sm text-gray-900 dark:text-white focus:ring-1 focus:ring-indigo-300 dark:focus:ring-indigo-700 focus:border-indigo-500 dark:focus:border-indigo-500 outline-none" | |
/> | |
</div> | |
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400"> | |
Press <span className="px-1 py-0.5 bg-gray-100 dark:bg-gray-700 rounded text-gray-700 dark:text-gray-300 font-medium">Space</span>, | |
<span className="px-1 py-0.5 bg-gray-100 dark:bg-gray-700 rounded text-gray-700 dark:text-gray-300 font-medium mx-1">,</span> | |
or <span className="px-1 py-0.5 bg-gray-100 dark:bg-gray-700 rounded text-gray-700 dark:text-gray-300 font-medium ml-1">Enter</span> | |
to add each email. Press <span className="px-1 py-0.5 bg-gray-100 dark:bg-gray-700 rounded text-gray-700 dark:text-gray-300 font-medium mx-1">Enter</span> | |
when done to send. | |
</div> | |
</div> | |
)} | |
{!isSendMode && ( | |
<textarea | |
ref={promptTextareaRef} | |
className="w-full border border-gray-200 dark:border-gray-700 rounded-xl py-2.5 px-3.5 pr-16 outline-none focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 dark:focus:ring-indigo-900/30 transition-all text-gray-900 dark:text-white resize-none text-sm shadow-sm bg-white dark:bg-gray-800" | |
placeholder={isNewChat | |
? "Describe the email you want to create..." | |
: "Ask for changes to your email..."} | |
value={prompt} | |
onChange={(e) => setPrompt(e.target.value)} | |
disabled={isGenerating || isTyping} // Disable if AI is generating OR user is "typing" via example click | |
onKeyDown={handleKeyDown} | |
rows={1} | |
style={{ | |
minHeight: "40px", | |
maxHeight: "120px", | |
overflowY: promptTextareaRef.current && promptTextareaRef.current.scrollHeight > 120 ? 'auto' : 'hidden' | |
}} | |
/> | |
)} | |
{!isSendMode && !isGenerating && (prompt.length > 0 || isTyping) && ( // Show clear button if user has typed, or if typeText is active | |
<button | |
type="button" | |
onClick={clearInput} | |
className="absolute right-14 bottom-2.5 p-1 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors z-10" | |
aria-label="Clear input" | |
> | |
<XCircleIcon className="h-4 w-4" /> | |
</button> | |
)} | |
{isSendMode ? ( | |
<button | |
type="submit" | |
className={`absolute right-3 bottom-2 py-1 px-3 rounded-lg ${ | |
(isGenerating || emailTags.filter(tag => tag.isValid).length === 0 || !emailSubject.trim()) | |
? 'bg-gray-200 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed' | |
: 'bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm' | |
} transition-all duration-200 text-xs font-medium flex items-center z-10`} | |
disabled={isGenerating || emailTags.filter(tag => tag.isValid).length === 0 || !emailSubject.trim()} | |
aria-label="Send emails" | |
> | |
{isGenerating ? ( | |
<> | |
<ArrowPathIcon className="h-3.5 w-3.5 mr-1 animate-spin" /> | |
<span>Sending...</span> | |
</> | |
) : ( | |
<> | |
<PaperAirplaneIcon className="h-3.5 w-3.5 mr-1" /> | |
<span>Send ({emailTags.filter(tag => tag.isValid).length})</span> | |
</> | |
)} | |
</button> | |
) : ( | |
(!isGenerating ? ( | |
<button | |
type="submit" | |
className={`absolute right-3 bottom-2 p-1.5 rounded-lg ${ | |
(!prompt.trim() || isTyping) | |
? 'text-gray-300 dark:text-gray-600 cursor-not-allowed' | |
: 'text-white bg-indigo-600 hover:bg-indigo-700 shadow-sm' | |
} transition-all duration-200 z-10`} | |
disabled={!prompt.trim() || isTyping} | |
aria-label="Send message" | |
> | |
<PaperAirplaneIcon className="h-4 w-4" /> | |
</button> | |
) : ( | |
<button | |
type="button" | |
onClick={onCancelGeneration} | |
className="absolute right-3 bottom-2 p-1.5 rounded-lg bg-red-100 dark:bg-red-900 text-red-600 dark:text-red-300 hover:bg-red-200 dark:hover:bg-red-800 shadow-sm transition-all duration-200 z-10 border border-red-200 dark:border-red-700 flex items-center" | |
aria-label="Cancel email generation" | |
> | |
<XMarkIcon className="h-4 w-4" /> | |
</button> | |
)) | |
)} | |
</form> | |
{/* Prompt examples - Redesigned for a more aesthetic and compact look */} | |
{!isSendMode && messages.length <= 1 && ( | |
<div className="grid grid-cols-3 gap-1.5 mt-3 px-1 justify-center"> | |
{[ | |
{ | |
icon: ( | |
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={1.5} viewBox="0 0 24 24"> | |
<circle cx="12" cy="8" r="4" /> | |
<path d="M4 20c0-4 16-4 16 0" /> | |
</svg> | |
), | |
text: 'Welcome', | |
prompt: 'Create a welcoming email for new customers introducing our company and services', | |
tooltip: 'Generate a friendly welcome email for new customers' | |
}, | |
{ | |
icon: ( | |
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={1.5} viewBox="0 0 24 24"> | |
<circle cx="9" cy="20" r="1" /> | |
<circle cx="17" cy="20" r="1" /> | |
<path d="M5 6h2l1 7h10l1-5H7" /> | |
</svg> | |
), | |
text: 'E-commerce', | |
prompt: 'Write a promotional email for our latest product launch with a special discount offer', | |
tooltip: 'Create promotional emails with discounts and product launches' | |
}, | |
{ | |
icon: ( | |
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={1.5} viewBox="0 0 24 24"> | |
<rect x="3" y="5" width="18" height="14" rx="2" /> | |
<path d="M3 7l9 6 9-6" /> | |
</svg> | |
), | |
text: 'Invite', | |
prompt: 'Create a professional invitation email for our upcoming webinar or event', | |
tooltip: 'Draft professional invitations for events and webinars' | |
}, | |
{ | |
icon: ( | |
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={1.5} viewBox="0 0 24 24"> | |
<rect x="3" y="5" width="18" height="14" rx="2" /> | |
<path d="M7 8h10M7 12h6" /> | |
</svg> | |
), | |
text: 'Newsletter', | |
prompt: 'Draft a monthly newsletter with company updates and industry insights', | |
tooltip: 'Write engaging newsletters with updates and insights' | |
}, | |
{ | |
icon: ( | |
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={1.5} viewBox="0 0 24 24"> | |
<rect x="6" y="4" width="12" height="16" rx="2" /> | |
<path d="M9 8h6M9 12h6M9 16h2" /> | |
</svg> | |
), | |
text: 'Invoice', | |
prompt: 'Write a polite invoice reminder email for overdue payments', | |
tooltip: 'Create polite payment reminders and invoice emails' | |
}, | |
{ | |
icon: ( | |
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={1.5} viewBox="0 0 24 24"> | |
<path d="M6 7V6a6 6 0 1 1 12 0v1" /> | |
<rect x="5" y="7" width="14" height="13" rx="2" /> | |
</svg> | |
), | |
text: 'Cart', | |
prompt: 'Create a cart abandonment email to encourage customers to complete their purchase', | |
tooltip: 'Recover abandoned carts with persuasive follow-up emails' | |
} | |
].map((example, index) => ( | |
<button | |
key={index} | |
type="button" | |
title={example.tooltip} | |
onClick={() => { | |
typeText( | |
example.prompt, | |
setPrompt, | |
promptTextareaRef, | |
setIsTyping, | |
() => { // This onComplete logic is from Version 2 | |
setPrompt(''); // Clear the visual prompt | |
setIsTyping(false); // Ensure isTyping is reset | |
if (promptTextareaRef.current) { // Reset textarea style | |
promptTextareaRef.current.style.height = '40px'; | |
promptTextareaRef.current.style.overflowY = 'hidden'; | |
promptTextareaRef.current.focus(); | |
} | |
onSubmit(example.prompt); // Submit the original example prompt | |
} | |
); | |
}} | |
className="flex items-center justify-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-400 transition-all duration-200 ease-in-out hover:border-indigo-300/70 dark:hover:border-indigo-600/70 hover:bg-indigo-50/70 dark:hover:bg-indigo-500/10 hover:text-indigo-600 dark:hover:text-indigo-400 hover:shadow-sm disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none" | |
disabled={isTyping || isGenerating} | |
> | |
{example.icon} | |
<span>{example.text}</span> | |
</button> | |
))} | |
</div> | |
)} | |
{/* Hints section from Version 1 */} | |
{!isSendMode && ( | |
<div className="flex items-center justify-between text-[10px] text-gray-500 dark:text-gray-400 mt-2 px-1"> | |
<div className="flex items-center"> | |
<span className="bg-gray-100 dark:bg-gray-700 px-1.5 py-0.5 rounded text-[10px] font-medium mr-1 border border-gray-200 dark:border-gray-600"> | |
<span className="font-semibold">Enter</span> | |
</span> | |
to send | |
</div> | |
<div className="flex items-center"> | |
<span className="bg-gray-100 dark:bg-gray-700 px-1.5 py-0.5 rounded text-[10px] font-medium mr-1 border border-gray-200 dark:border-gray-600"> | |
<span className="font-semibold">Shift</span>+<span className="font-semibold">Enter</span> | |
</span> | |
for new line | |
</div> | |
<div className="flex items-center"> | |
<span className="bg-gray-100 dark:bg-gray-700 px-1.5 py-0.5 rounded text-[10px] font-medium mr-1 border border-gray-200 dark:border-gray-600">/send</span> | |
to send to multiple | |
</div> | |
</div> | |
)} | |
</div> | |
</div> | |
); | |
}; | |
export default ChatInterface; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment