Mercurial
comparison love/epi/src/components/ChatUI/Composer.tsx @ 38:cf9caa4abc3e
[Love] FE and BE. Can chat and render images. Also created MCP for powerpoint generations.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 01 Dec 2025 20:35:56 -0800 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 37:fb9bcd3145cb | 38:cf9caa4abc3e |
|---|---|
| 1 import { useEffect, useRef } from 'react'; | |
| 2 import type { ReactNode } from 'react'; | |
| 3 import { SendHorizonal, Square } from 'lucide-react'; | |
| 4 import { useComposer } from '@/hooks/useComposer'; | |
| 5 import { useChat } from '@/hooks/useChat'; | |
| 6 import { broadcastDone } from '@/hooks/useChatWebsocket'; | |
| 7 import { apiUrl } from '@/utils'; | |
| 8 import { currentChatId } from '@/atoms/chatAtoms'; | |
| 9 import { useAtom } from 'jotai'; | |
| 10 import { useNavigate } from '@tanstack/react-router'; | |
| 11 | |
| 12 interface ComposerProps { | |
| 13 chatId: string; | |
| 14 } | |
| 15 | |
| 16 export function Composer({ chatId }: ComposerProps): ReactNode { | |
| 17 const { text, setText, isSending, isDisabled, setSending } = useComposer(); | |
| 18 const [_, setCurrentChatId] = useAtom(currentChatId); | |
| 19 const { sendUserMessage } = useChat(chatId); | |
| 20 | |
| 21 const textareaRef = useRef<HTMLTextAreaElement>(null); | |
| 22 | |
| 23 // Auto-resize | |
| 24 useEffect(() => { | |
| 25 const el = textareaRef.current; | |
| 26 if (el) { | |
| 27 el.style.height = 'auto'; | |
| 28 el.style.height = `${el.scrollHeight}px`; | |
| 29 } | |
| 30 }, [text]); | |
| 31 const navigate = useNavigate(); | |
| 32 | |
| 33 const handleSubmit = async () => { | |
| 34 console.log('before'); | |
| 35 // Stoping the calls | |
| 36 if (isSending) { | |
| 37 console.log('called'); | |
| 38 broadcastDone(chatId); | |
| 39 return; | |
| 40 } | |
| 41 | |
| 42 // Don't send if it is diabled or no text | |
| 43 if (isDisabled || !text.trim()) return; | |
| 44 | |
| 45 let newChatId: string = ''; | |
| 46 if (!chatId || chatId === 'new') { | |
| 47 const res = await fetch(apiUrl('/chats'), { method: 'POST' }); | |
| 48 const data = await res.json(); | |
| 49 await navigate( | |
| 50 { to: '/chat/$chatId', params: { chatId: data.id } }, | |
| 51 ); | |
| 52 console.log('problem: 1'); | |
| 53 setCurrentChatId(data.id); | |
| 54 newChatId = data.id; | |
| 55 } | |
| 56 | |
| 57 const message = text.trim(); | |
| 58 setSending(); | |
| 59 setText(''); | |
| 60 sendUserMessage(message, newChatId); | |
| 61 }; | |
| 62 | |
| 63 const handleKeyDown = (e: React.KeyboardEvent) => { | |
| 64 if (e.key === 'Enter' && !e.shiftKey) { | |
| 65 e.preventDefault(); | |
| 66 handleSubmit(); | |
| 67 } | |
| 68 }; | |
| 69 | |
| 70 return ( | |
| 71 <div className="border-t border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900"> | |
| 72 <div className="max-w-4xl mx-auto p-4"> | |
| 73 <div className="flex items-end gap-3 bg-gray-50 dark:bg-gray-800/50 rounded-2xl px-4 py-3 shadow-sm ring-1 ring-gray-200 dark:ring-gray-700 focus-within:ring-2 focus-within:ring-blue-500"> | |
| 74 <textarea | |
| 75 ref={textareaRef} | |
| 76 value={text} | |
| 77 onChange={(e) => setText(e.target.value)} | |
| 78 onKeyDown={handleKeyDown} | |
| 79 placeholder="Send a message..." | |
| 80 rows={1} | |
| 81 className="flex-1 resize-none bg-transparent outline-none text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 text-base leading-6 overflow-hidden max-h-96" | |
| 82 /> | |
| 83 | |
| 84 <button | |
| 85 onClick={handleSubmit} | |
| 86 disabled={isDisabled} | |
| 87 className={`w-10 h-10 rounded-full flex items-center justify-center transition-all ${ | |
| 88 isDisabled | |
| 89 ? 'bg-gray-300 dark:bg-gray-700 text-gray-500' | |
| 90 : isSending | |
| 91 ? 'bg-red-500 hover:bg-red-600 text-white' | |
| 92 : 'bg-blue-500 hover:bg-blue-600 text-white' | |
| 93 }`} | |
| 94 > | |
| 95 {isSending ? <Square className="w-5 h-5" /> : <SendHorizonal className="w-5 h-5" />} | |
| 96 </button> | |
| 97 </div> | |
| 98 </div> | |
| 99 </div> | |
| 100 ); | |
| 101 } |