diff --git a/app/login/page.tsx b/app/login/page.tsx index f8e6f3533..25fb39add 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -3,6 +3,7 @@ import { useEffect, useState, useRef, Suspense } from "react"; import { usePrivy, + useLogin, useLoginWithEmail, useLoginWithOAuth, } from "@privy-io/react-auth"; @@ -26,7 +27,8 @@ const DiscordIcon = ({ className }: { className?: string }) => ( ); function LoginPageContent() { - const { ready, authenticated, login, user } = usePrivy(); + const { ready, authenticated, user } = usePrivy(); + const { login } = useLogin(); const { sendCode, loginWithCode, state: emailState } = useLoginWithEmail(); const { initOAuth } = useLoginWithOAuth(); const router = useRouter(); @@ -168,10 +170,9 @@ function LoginPageContent() { lastLoginAttemptRef.current = now; setLoadingButton("wallet"); - // Use login() instead of connectWallet() for authentication - // This opens the Privy modal (non-blocking, returns immediately) + // Use login() with loginMethods restricted to 'wallet' to directly show wallet options // Authentication state changes are handled via the authenticated state in useEffect - login(); + login({ loginMethods: ["wallet"] }); // Reset the guard after a short delay to allow modal to open // If authentication succeeds, the useEffect will handle redirect diff --git a/components/character-builder/character-form.tsx b/components/character-builder/character-form.tsx index abd69f5df..4ffff317d 100644 --- a/components/character-builder/character-form.tsx +++ b/components/character-builder/character-form.tsx @@ -153,7 +153,7 @@ export function CharacterForm({ character, onChange }: CharacterFormProps) { htmlFor="username" className="text-xs font-medium text-white/70" > - Username + Username *
diff --git a/components/chat/character-build-mode.tsx b/components/chat/character-build-mode.tsx index aa8aead92..3a48f8236 100644 --- a/components/chat/character-build-mode.tsx +++ b/components/chat/character-build-mode.tsx @@ -168,6 +168,11 @@ export function CharacterBuildMode({ return; } + if (!character.username) { + toast.error("Username is required"); + return; + } + if (!character.bio) { toast.error("Character bio is required"); return; diff --git a/components/chat/eliza-chat-interface.tsx b/components/chat/eliza-chat-interface.tsx index 55b728f44..9469d4985 100644 --- a/components/chat/eliza-chat-interface.tsx +++ b/components/chat/eliza-chat-interface.tsx @@ -150,6 +150,8 @@ export function ElizaChatInterface({ const [agentInfo, setAgentInfo] = useState(null); const [inputText, setInputText] = useState(""); const inputTextRef = useRef(inputText); + const textareaRef = useRef(null); + const inputRef = useRef(null); const isPendingMessageProcessingRef = useRef(false); const pendingMessageToSendRef = useRef(null); const isCreatingRoomRef = useRef(false); @@ -764,12 +766,13 @@ export function ElizaChatInterface({ } }, onComplete: () => { - // Always reload rooms to update lastText and lastTime - // Use longer delay for newly created rooms to ensure server-side processing is complete + // Reload rooms to update lastText, lastTime, and AI-generated title + // Title is generated automatically by the server-side room-title service const delay = didCreateNewRoom ? 500 : 100; setTimeout(() => { loadRooms(); }, delay); + // Notify parent that a message was sent successfully (for anonymous message counting) if (onMessageSent) { onMessageSent(); @@ -1173,6 +1176,54 @@ export function ElizaChatInterface({ inputTextRef.current = inputText; }, [inputText]); + // Auto-resize textarea when inputText changes + useEffect(() => { + if (textareaRef.current) { + textareaRef.current.style.height = "auto"; + textareaRef.current.style.height = Math.min(textareaRef.current.scrollHeight, 400) + "px"; + } + }, [inputText]); + + // Track if input is expanded (multiline mode) + const isExpanded = inputText.includes("\n") || inputText.length > 80; + const wasExpandedRef = useRef(isExpanded); + + // Maintain focus when transitioning between layouts + useEffect(() => { + if (wasExpandedRef.current !== isExpanded) { + // Layout changed - restore focus to the appropriate input + requestAnimationFrame(() => { + if (isExpanded && textareaRef.current) { + textareaRef.current.focus(); + // Move cursor to end of text + const len = textareaRef.current.value.length; + textareaRef.current.setSelectionRange(len, len); + } else if (!isExpanded && inputRef.current) { + inputRef.current.focus(); + // Move cursor to end of text + const len = inputRef.current.value.length; + inputRef.current.setSelectionRange(len, len); + } + }); + } + wasExpandedRef.current = isExpanded; + }, [isExpanded]); + + // Focus the appropriate input when clicking the container + const handleContainerClick = useCallback((e: React.MouseEvent) => { + // Don't focus if clicking on a button or dropdown + const target = e.target as HTMLElement; + if (target.closest('button') || target.closest('[role="menu"]') || target.closest('[data-radix-popper-content-wrapper]')) { + return; + } + + if (isExpanded && textareaRef.current) { + textareaRef.current.focus(); + } else if (!isExpanded && inputRef.current) { + inputRef.current.focus(); + } + }, [isExpanded]); + // Auto-scroll to bottom when messages change // Uses smooth scrolling during streaming for a polished feel useEffect(() => { @@ -1247,12 +1298,12 @@ export function ElizaChatInterface({ }; return ( -
- {/* Main Chat Area - Centered with max width for readability */} -
- {/* Messages Area - No Header */} -
- +
+ {/* Messages Area - Full width scroll area for better scroll UX */} +
+ + {/* Centered content container */} +
{error && (
@@ -1419,18 +1470,23 @@ export function ElizaChatInterface({ ); })}
- -
+
+
+
- {/* Input Area - Buttons inside input like Gemini/ChatGPT */} -
{ - e.preventDefault(); - sendMessage(); - }} - className="border-t border-white/[0.06] p-4" - > -
+ {/* Input Area - ChatGPT style */} +
+
+ { + e.preventDefault(); + sendMessage(); + }} + > +
{/* Robot Eye Visor Scanner */} {loadingState.isSending && (
@@ -1455,38 +1511,149 @@ export function ElizaChatInterface({
)} - {/* Textarea */} -