diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/love/epi/src/components/ChatUI/Composer.tsx	Mon Dec 01 20:35:56 2025 -0800
@@ -0,0 +1,101 @@
+import { useEffect, useRef } from 'react';
+import type { ReactNode } from 'react';
+import { SendHorizonal, Square } from 'lucide-react';
+import { useComposer } from '@/hooks/useComposer';
+import { useChat } from '@/hooks/useChat';
+import { broadcastDone } from '@/hooks/useChatWebsocket';
+import { apiUrl } from '@/utils';
+import { currentChatId } from '@/atoms/chatAtoms';
+import { useAtom } from 'jotai';
+import { useNavigate } from '@tanstack/react-router';
+
+interface ComposerProps {
+  chatId: string;
+}
+
+export function Composer({ chatId }: ComposerProps): ReactNode {
+  const { text, setText, isSending, isDisabled, setSending } = useComposer();
+  const [_, setCurrentChatId] = useAtom(currentChatId);
+  const { sendUserMessage } = useChat(chatId);
+
+  const textareaRef = useRef<HTMLTextAreaElement>(null);
+
+  // Auto-resize
+  useEffect(() => {
+    const el = textareaRef.current;
+    if (el) {
+      el.style.height = 'auto';
+      el.style.height = `${el.scrollHeight}px`;
+    }
+  }, [text]);
+  const navigate = useNavigate();
+
+  const handleSubmit = async () => {
+    console.log('before');
+    // Stoping the calls
+    if (isSending) {
+      console.log('called');
+      broadcastDone(chatId);
+      return;
+    }
+
+    // Don't send if it is diabled or no text
+    if (isDisabled || !text.trim()) return;
+
+    let newChatId: string = '';
+    if (!chatId || chatId === 'new') {
+      const res = await fetch(apiUrl('/chats'), { method: 'POST' });
+      const data = await res.json();
+      await navigate(
+        { to: '/chat/$chatId', params: { chatId: data.id } },
+      );
+      console.log('problem: 1');
+      setCurrentChatId(data.id);
+      newChatId = data.id;
+    }
+
+    const message = text.trim();
+    setSending();
+    setText('');
+    sendUserMessage(message, newChatId);
+  };
+
+  const handleKeyDown = (e: React.KeyboardEvent) => {
+    if (e.key === 'Enter' && !e.shiftKey) {
+      e.preventDefault();
+      handleSubmit();
+    }
+  };
+
+  return (
+    <div className="border-t border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900">
+      <div className="max-w-4xl mx-auto p-4">
+        <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">
+          <textarea
+            ref={textareaRef}
+            value={text}
+            onChange={(e) => setText(e.target.value)}
+            onKeyDown={handleKeyDown}
+            placeholder="Send a message..."
+            rows={1}
+            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"
+          />
+
+          <button
+            onClick={handleSubmit}
+            disabled={isDisabled}
+            className={`w-10 h-10 rounded-full flex items-center justify-center transition-all ${
+              isDisabled
+                ? 'bg-gray-300 dark:bg-gray-700 text-gray-500'
+                : isSending
+                  ? 'bg-red-500 hover:bg-red-600 text-white'
+                  : 'bg-blue-500 hover:bg-blue-600 text-white'
+            }`}
+          >
+            {isSending ? <Square className="w-5 h-5" /> : <SendHorizonal className="w-5 h-5" />}
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+}