From 5a878e0af038eda438df38bc1f5ad2af242b7538 Mon Sep 17 00:00:00 2001 From: Lluis Agusti Date: Fri, 6 Feb 2026 00:07:08 +0800 Subject: [PATCH] chore: update styles + add mobile drawer --- .../ChatContainer/ChatContainer.tsx | 72 ++++--- .../ChatMessagesContainer.tsx | 186 +++++++++--------- .../components/ChatSidebar/ChatSidebar.tsx | 169 +++++++++------- .../components/EmptySession/EmptySession.tsx | 107 +++++----- .../components/EmptySession/helpers.ts | 11 ++ .../components/MobileDrawer/MobileDrawer.tsx | 140 +++++++++++++ .../components/MobileHeader/MobileHeader.tsx | 22 +++ .../src/app/(platform)/copilot-2/page.tsx | 60 +++--- .../(platform)/copilot-2/useCopilotPage.ts | 107 ++++++---- .../components/ai-elements/conversation.tsx | 3 +- .../atoms/OverflowText/OverflowText.tsx | 2 +- .../Chat/components/ChatInput/ChatInput.tsx | 12 +- .../components/ChatInput/useVoiceRecording.ts | 22 ++- .../frontend/src/components/ui/sidebar.tsx | 15 +- 14 files changed, 588 insertions(+), 340 deletions(-) create mode 100644 autogpt_platform/frontend/src/app/(platform)/copilot-2/components/EmptySession/helpers.ts create mode 100644 autogpt_platform/frontend/src/app/(platform)/copilot-2/components/MobileDrawer/MobileDrawer.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/copilot-2/components/MobileHeader/MobileHeader.tsx diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatContainer/ChatContainer.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatContainer/ChatContainer.tsx index 61fba990be..beba3434a3 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatContainer/ChatContainer.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatContainer/ChatContainer.tsx @@ -1,10 +1,10 @@ "use client"; -import { UIDataTypes, UITools, UIMessage } from "ai"; -import { ChatMessagesContainer } from "../ChatMessagesContainer/ChatMessagesContainer"; -import { EmptySession } from "../EmptySession/EmptySession"; import { ChatInput } from "@/components/contextual/Chat/components/ChatInput/ChatInput"; -import { CopilotChatActionsProvider } from "../CopilotChatActionsProvider/CopilotChatActionsProvider"; +import { UIDataTypes, UIMessage, UITools } from "ai"; import { LayoutGroup, motion } from "framer-motion"; +import { ChatMessagesContainer } from "../ChatMessagesContainer/ChatMessagesContainer"; +import { CopilotChatActionsProvider } from "../CopilotChatActionsProvider/CopilotChatActionsProvider"; +import { EmptySession } from "../EmptySession/EmptySession"; export interface ChatContainerProps { messages: UIMessage[]; @@ -29,40 +29,38 @@ export const ChatContainer = ({ return ( -
-
- {sessionId ? ( -
- - -
- {}} - placeholder="You can search or just ask" - /> - -
- ) : ( - + {sessionId ? ( +
+ - )} -
+ +
+ {}} + placeholder="What else can I help with?" + /> + +
+ ) : ( + + )}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatMessagesContainer/ChatMessagesContainer.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatMessagesContainer/ChatMessagesContainer.tsx index 047fc0ca85..a10423c147 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatMessagesContainer/ChatMessagesContainer.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatMessagesContainer/ChatMessagesContainer.tsx @@ -1,7 +1,6 @@ import { Conversation, ConversationContent, - ConversationEmptyState, ConversationScrollButton, } from "@/components/ai-elements/conversation"; import { @@ -9,8 +8,7 @@ import { MessageContent, MessageResponse, } from "@/components/ai-elements/message"; -import { MessageSquareIcon } from "lucide-react"; -import { UIMessage, UIDataTypes, UITools, ToolUIPart } from "ai"; +import { UIDataTypes, UIMessage, UITools, ToolUIPart } from "ai"; import { FindBlocksTool } from "../../tools/FindBlocks/FindBlocks"; import { FindAgentsTool } from "../../tools/FindAgents/FindAgents"; import { SearchDocsTool } from "../../tools/SearchDocs/SearchDocs"; @@ -32,103 +30,97 @@ export const ChatMessagesContainer = ({ error, }: ChatMessagesContainerProps) => { return ( - - - {messages.length === 0 ? ( - } - title="Start a conversation" - description="Type a message below to begin chatting" - /> - ) : ( - messages.map((message) => ( - - + + {messages.map((message) => ( + + + {message.parts.map((part, i) => { + switch (part.type) { + case "text": + return ( + + {part.text} + + ); + case "tool-find_block": + return ( + + ); + case "tool-find_agent": + case "tool-find_library_agent": + return ( + + ); + case "tool-search_docs": + case "tool-get_doc_page": + return ( + + ); + case "tool-run_block": + return ( + + ); + case "tool-run_agent": + case "tool-schedule_agent": + return ( + + ); + case "tool-create_agent": + return ( + + ); + case "tool-edit_agent": + return ( + + ); + case "tool-view_agent_output": + return ( + + ); + default: + return null; } - > - {message.parts.map((part, i) => { - switch (part.type) { - case "text": - return ( - - {part.text} - - ); - case "tool-find_block": - return ( - - ); - case "tool-find_agent": - case "tool-find_library_agent": - return ( - - ); - case "tool-search_docs": - case "tool-get_doc_page": - return ( - - ); - case "tool-run_block": - return ( - - ); - case "tool-run_agent": - case "tool-schedule_agent": - return ( - - ); - case "tool-create_agent": - return ( - - ); - case "tool-edit_agent": - return ( - - ); - case "tool-view_agent_output": - return ( - - ); - default: - return null; - } - })} - - - )) - )} + })} + + + ))} {status === "submitted" && ( - -

Thinking...

+ + + Thinking... +
)} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatSidebar/ChatSidebar.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatSidebar/ChatSidebar.tsx index b0ae5e87d4..ff4278a24d 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatSidebar/ChatSidebar.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/ChatSidebar/ChatSidebar.tsx @@ -1,35 +1,28 @@ "use client"; +import { useGetV2ListSessions } from "@/app/api/__generated__/endpoints/chat/chat"; +import { Button } from "@/components/atoms/Button/Button"; +import { Text } from "@/components/atoms/Text/Text"; import { Sidebar, - SidebarHeader, SidebarContent, SidebarFooter, + SidebarHeader, SidebarTrigger, useSidebar, } from "@/components/ui/sidebar"; import { cn } from "@/lib/utils"; import { + PlusCircleIcon, PlusIcon, SpinnerGapIcon, - ChatCircleIcon, } from "@phosphor-icons/react"; import { motion } from "framer-motion"; -import { useState } from "react"; import { parseAsString, useQueryState } from "nuqs"; -import { - postV2CreateSession, - useGetV2ListSessions, - getGetV2ListSessionsQueryKey, -} from "@/app/api/__generated__/endpoints/chat/chat"; -import { Button } from "@/components/atoms/Button/Button"; -import { useQueryClient } from "@tanstack/react-query"; export function ChatSidebar() { const { state } = useSidebar(); const isCollapsed = state === "collapsed"; - const [isCreating, setIsCreating] = useState(false); const [sessionId, setSessionId] = useQueryState("sessionId", parseAsString); - const queryClient = useQueryClient(); const { data: sessionsResponse, isLoading: isLoadingSessions } = useGetV2ListSessions({ limit: 50 }); @@ -37,22 +30,8 @@ export function ChatSidebar() { const sessions = sessionsResponse?.status === 200 ? sessionsResponse.data.sessions : []; - async function handleNewChat() { - if (isCreating) return; - setIsCreating(true); - try { - const response = await postV2CreateSession({ - body: JSON.stringify({}), - }); - if (response.status === 200 && response.data?.id) { - setSessionId(response.data.id); - queryClient.invalidateQueries({ - queryKey: getGetV2ListSessionsQueryKey(), - }); - } - } finally { - setIsCreating(false); - } + function handleNewChat() { + setSessionId(null); } function handleSelectSession(id: string) { @@ -68,14 +47,27 @@ export function ChatSidebar() { if (diffDays === 0) return "Today"; if (diffDays === 1) return "Yesterday"; if (diffDays < 7) return `${diffDays} days ago`; - return date.toLocaleDateString(); + + const day = date.getDate(); + const ordinal = + day % 10 === 1 && day !== 11 + ? "st" + : day % 10 === 2 && day !== 12 + ? "nd" + : day % 10 === 3 && day !== 13 + ? "rd" + : "th"; + const month = date.toLocaleDateString("en-US", { month: "short" }); + const year = date.getFullYear(); + + return `${day}${ordinal} ${month} ${year}`; } return ( {isCollapsed && ( - {isCollapsed && ( -
- -
- )} +
+ + +
)} - -
- - {!isCollapsed && ( -
+ + Your chats + +
- )} -
+ + )} {!isCollapsed && ( -
+ {isLoadingSessions ? (
@@ -148,27 +137,55 @@ export function ChatSidebar() { key={session.id} onClick={() => handleSelectSession(session.id)} className={cn( - "flex items-center gap-3 rounded-lg px-3 py-2 text-left text-sm transition-colors hover:bg-neutral-100 dark:hover:bg-neutral-800", - sessionId === session.id && - "bg-neutral-100 dark:bg-neutral-800", + "w-full rounded-lg px-3 py-2.5 text-left transition-colors", + session.id === sessionId + ? "bg-zinc-100" + : "hover:bg-zinc-50", )} > - -
- - {session.title || `Untitled chat`} - - +
+
+ + {session.title || `Untitled chat`} + +
+ {formatDate(session.updated_at)} - +
)) )} -
+ )} - + {!isCollapsed && sessionId && ( + + + + + + )} ); } diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/EmptySession/EmptySession.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/EmptySession/EmptySession.tsx index b64582a54d..5768f5d02b 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/EmptySession/EmptySession.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/EmptySession/EmptySession.tsx @@ -2,15 +2,16 @@ import { getGreetingName, + getInputPlaceholder, getQuickActions, } from "@/app/(platform)/copilot/helpers"; import { Button } from "@/components/atoms/Button/Button"; import { Text } from "@/components/atoms/Text/Text"; import { ChatInput } from "@/components/contextual/Chat/components/ChatInput/ChatInput"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; -import { SparkleIcon, SpinnerGapIcon } from "@phosphor-icons/react"; +import { SpinnerGapIcon } from "@phosphor-icons/react"; import { motion } from "framer-motion"; -import { useState } from "react"; +import { useEffect, useState } from "react"; interface Props { inputLayoutId: string; @@ -28,6 +29,13 @@ export function EmptySession({ const greetingName = getGreetingName(user); const quickActions = getQuickActions(); const [loadingAction, setLoadingAction] = useState(null); + const [inputPlaceholder, setInputPlaceholder] = useState( + getInputPlaceholder(), + ); + + useEffect(() => { + setInputPlaceholder(getInputPlaceholder(window.innerWidth)); + }, [window.innerWidth]); async function handleQuickActionClick(action: string) { if (isCreatingSession || loadingAction) return; @@ -41,66 +49,61 @@ export function EmptySession({ } return ( -
+
-
-
- - Autopilot runs for you 24/7 -
- - +
+ Hey, {greetingName} - - What do you want to automate? + + Tell me about your work — I'll find what to automate. + +
+ + + +
-
- - - - -
- {quickActions.map((action) => ( - - ))} -
+
+ {quickActions.map((action) => ( + + ))}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/EmptySession/helpers.ts b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/EmptySession/helpers.ts new file mode 100644 index 0000000000..faccd7b2d7 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/EmptySession/helpers.ts @@ -0,0 +1,11 @@ +export function getInputPlaceholder(width?: number) { + if (!width) return "What's your role and what eats up most of your day?"; + + if (width < 500) { + return "I'm a chef and I hate..."; + } + if (width <= 1080) { + return "What's your role and what eats up most of your day?"; + } + return "What's your role and what eats up most of your day? e.g. 'I'm a recruiter and I hate...'"; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/MobileDrawer/MobileDrawer.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/MobileDrawer/MobileDrawer.tsx new file mode 100644 index 0000000000..74b80ef499 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/MobileDrawer/MobileDrawer.tsx @@ -0,0 +1,140 @@ +import type { SessionSummaryResponse } from "@/app/api/__generated__/models/sessionSummaryResponse"; +import { Button } from "@/components/atoms/Button/Button"; +import { Text } from "@/components/atoms/Text/Text"; +import { scrollbarStyles } from "@/components/styles/scrollbars"; +import { cn } from "@/lib/utils"; +import { PlusIcon, SpinnerGapIcon, X } from "@phosphor-icons/react"; +import { Drawer } from "vaul"; + +interface Props { + isOpen: boolean; + sessions: SessionSummaryResponse[]; + currentSessionId: string | null; + isLoading: boolean; + onSelectSession: (sessionId: string) => void; + onNewChat: () => void; + onClose: () => void; + onOpenChange: (open: boolean) => void; +} + +function formatDate(dateString: string) { + const date = new Date(dateString); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffDays === 0) return "Today"; + if (diffDays === 1) return "Yesterday"; + if (diffDays < 7) return `${diffDays} days ago`; + + const day = date.getDate(); + const ordinal = + day % 10 === 1 && day !== 11 + ? "st" + : day % 10 === 2 && day !== 12 + ? "nd" + : day % 10 === 3 && day !== 13 + ? "rd" + : "th"; + const month = date.toLocaleDateString("en-US", { month: "short" }); + const year = date.getFullYear(); + + return `${day}${ordinal} ${month} ${year}`; +} + +export function MobileDrawer({ + isOpen, + sessions, + currentSessionId, + isLoading, + onSelectSession, + onNewChat, + onClose, + onOpenChange, +}: Props) { + return ( + + + + +
+
+ + Your chats + + +
+
+
+ {isLoading ? ( +
+ +
+ ) : sessions.length === 0 ? ( +

+ No conversations yet +

+ ) : ( + sessions.map((session) => ( + + )) + )} +
+ {currentSessionId && ( +
+ +
+ )} +
+
+
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/MobileHeader/MobileHeader.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/MobileHeader/MobileHeader.tsx new file mode 100644 index 0000000000..e0d6161744 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/components/MobileHeader/MobileHeader.tsx @@ -0,0 +1,22 @@ +import { Button } from "@/components/atoms/Button/Button"; +import { NAVBAR_HEIGHT_PX } from "@/lib/constants"; +import { ListIcon } from "@phosphor-icons/react"; + +interface Props { + onOpenDrawer: () => void; +} + +export function MobileHeader({ onOpenDrawer }: Props) { + return ( + + ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/page.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot-2/page.tsx index 8209670b1c..68bdec1e92 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/page.tsx @@ -1,51 +1,41 @@ "use client"; -import { ChatSidebar } from "./components/ChatSidebar/ChatSidebar"; +import { SidebarProvider } from "@/components/ui/sidebar"; import { ChatContainer } from "./components/ChatContainer/ChatContainer"; -import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar"; -import { Button } from "@/components/ui/button"; -import { CopyIcon, CheckIcon } from "@phosphor-icons/react"; +import { ChatSidebar } from "./components/ChatSidebar/ChatSidebar"; +import { MobileDrawer } from "./components/MobileDrawer/MobileDrawer"; +import { MobileHeader } from "./components/MobileHeader/MobileHeader"; import { useCopilotPage } from "./useCopilotPage"; export default function Page() { const { - copied, sessionId, messages, status, error, isCreatingSession, - handleCopySessionId, createSession, onSend, + // Mobile drawer + isMobile, + isDrawerOpen, + sessions, + isLoadingSessions, + handleOpenDrawer, + handleCloseDrawer, + handleDrawerOpenChange, + handleSelectSession, + handleNewChat, } = useCopilotPage(); return ( - - - {sessionId && ( -
-
- {sessionId.slice(0, 8)}... - -
-
- )} + {!isMobile && } +
+ {isMobile && }
- +
+ {isMobile && ( + + )}
); } diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot-2/useCopilotPage.ts b/autogpt_platform/frontend/src/app/(platform)/copilot-2/useCopilotPage.ts index a5a3e7373b..52aa89e228 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot-2/useCopilotPage.ts +++ b/autogpt_platform/frontend/src/app/(platform)/copilot-2/useCopilotPage.ts @@ -1,19 +1,25 @@ -import { useChat } from "@ai-sdk/react"; -import { DefaultChatTransport } from "ai"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { parseAsString, useQueryState } from "nuqs"; import { getGetV2ListSessionsQueryKey, getV2GetSession, postV2CreateSession, + useGetV2ListSessions, } from "@/app/api/__generated__/endpoints/chat/chat"; -import { convertChatSessionMessagesToUiMessages } from "./helpers/convertChatSessionToUiMessages"; +import { useBreakpoint } from "@/lib/hooks/useBreakpoint"; +import { useChat } from "@ai-sdk/react"; import { useQueryClient } from "@tanstack/react-query"; +import { DefaultChatTransport } from "ai"; +import { parseAsString, useQueryState } from "nuqs"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { convertChatSessionMessagesToUiMessages } from "./helpers/convertChatSessionToUiMessages"; export function useCopilotPage() { - const [copied, setCopied] = useState(false); const [isCreatingSession, setIsCreatingSession] = useState(false); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [sessionId, setSessionId] = useQueryState("sessionId", parseAsString); + + const breakpoint = useBreakpoint(); + const isMobile = + breakpoint === "base" || breakpoint === "sm" || breakpoint === "md"; const hydrationSeq = useRef(0); const lastHydratedSessionIdRef = useRef(null); const createSessionPromiseRef = useRef | null>(null); @@ -21,31 +27,23 @@ export function useCopilotPage() { const queuedFirstMessageResolverRef = useRef<(() => void) | null>(null); const queryClient = useQueryClient(); - function handleCopySessionId() { - if (!sessionId) return; - navigator.clipboard.writeText(sessionId); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } - - const transport = useMemo(() => { - if (!sessionId) return null; - return new DefaultChatTransport({ - api: `/api/chat/sessions/${sessionId}/stream`, - prepareSendMessagesRequest: ({ messages }) => { - const last = messages[messages.length - 1]; - return { - body: { - message: last.parts - ?.map((p) => (p.type === "text" ? p.text : "")) - .join(""), - is_user_message: last.role === "user", - context: null, - }, - }; - }, - }); - }, [sessionId]); + const transport = sessionId + ? new DefaultChatTransport({ + api: `/api/chat/sessions/${sessionId}/stream`, + prepareSendMessagesRequest: ({ messages }) => { + const last = messages[messages.length - 1]; + return { + body: { + message: last.parts + ?.map((p) => (p.type === "text" ? p.text : "")) + .join(""), + is_user_message: last.role === "user", + context: null, + }, + }; + }, + }) + : null; const { messages, sendMessage, status, error, setMessages } = useChat({ id: sessionId ?? undefined, @@ -58,7 +56,7 @@ export function useCopilotPage() { messagesRef.current = messages; }, [messages]); - async function createSession(): Promise { + async function createSession() { if (sessionId) return sessionId; if (createSessionPromiseRef.current) return createSessionPromiseRef.current; @@ -166,15 +164,56 @@ export function useCopilotPage() { await sentPromise; } + // Sessions list for mobile drawer + const { data: sessionsResponse, isLoading: isLoadingSessions } = + useGetV2ListSessions({ limit: 50 }); + + const sessions = + sessionsResponse?.status === 200 ? sessionsResponse.data.sessions : []; + + // Drawer handlers + const handleOpenDrawer = useCallback(() => { + setIsDrawerOpen(true); + }, []); + + const handleCloseDrawer = useCallback(() => { + setIsDrawerOpen(false); + }, []); + + const handleDrawerOpenChange = useCallback((open: boolean) => { + setIsDrawerOpen(open); + }, []); + + const handleSelectSession = useCallback( + (id: string) => { + setSessionId(id); + if (isMobile) setIsDrawerOpen(false); + }, + [setSessionId, isMobile], + ); + + const handleNewChat = useCallback(() => { + setSessionId(null); + if (isMobile) setIsDrawerOpen(false); + }, [setSessionId, isMobile]); + return { - copied, sessionId, messages, status, error, isCreatingSession, - handleCopySessionId, createSession, onSend, + // Mobile drawer + isMobile, + isDrawerOpen, + sessions, + isLoadingSessions, + handleOpenDrawer, + handleCloseDrawer, + handleDrawerOpenChange, + handleSelectSession, + handleNewChat, }; } diff --git a/autogpt_platform/frontend/src/components/ai-elements/conversation.tsx b/autogpt_platform/frontend/src/components/ai-elements/conversation.tsx index ea2ff4e421..a32ebeea1a 100644 --- a/autogpt_platform/frontend/src/components/ai-elements/conversation.tsx +++ b/autogpt_platform/frontend/src/components/ai-elements/conversation.tsx @@ -1,6 +1,7 @@ "use client"; import { Button } from "@/components/ui/button"; +import { scrollbarStyles } from "@/components/styles/scrollbars"; import { cn } from "@/lib/utils"; import { ArrowDownIcon } from "lucide-react"; import type { ComponentProps } from "react"; @@ -11,7 +12,7 @@ export type ConversationProps = ComponentProps; export const Conversation = ({ className, ...props }: ConversationProps) => ( - + {value} diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/ChatInput.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/ChatInput.tsx index a023d7a105..af44232a4a 100644 --- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/ChatInput.tsx +++ b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/ChatInput.tsx @@ -6,6 +6,7 @@ import { MicrophoneIcon, StopIcon, } from "@phosphor-icons/react"; +import { ChangeEvent, useCallback } from "react"; import { RecordingIndicator } from "./components/RecordingIndicator"; import { useChatInput } from "./useChatInput"; import { useVoiceRecording } from "./useVoiceRecording"; @@ -34,7 +35,7 @@ export function ChatInput({ setValue, handleKeyDown: baseHandleKeyDown, handleSubmit, - handleChange, + handleChange: baseHandleChange, hasMultipleLines, } = useChatInput({ onSend, @@ -61,6 +62,15 @@ export function ChatInput({ inputId, }); + // Block text changes when recording + const handleChange = useCallback( + (e: ChangeEvent) => { + if (isRecording) return; + baseHandleChange(e); + }, + [isRecording, baseHandleChange], + ); + return (
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/useVoiceRecording.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/useVoiceRecording.ts index 4fd500d96a..2d605a4a4e 100644 --- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/useVoiceRecording.ts +++ b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/useVoiceRecording.ts @@ -214,17 +214,33 @@ export function useVoiceRecording({ const handleKeyDown = useCallback( (event: KeyboardEvent) => { - if (event.key === " " && !value.trim() && !isTranscribing) { + // Allow space to toggle recording (start when empty, stop when recording) + if (event.key === " " && !isTranscribing) { + if (isRecordingRef.current) { + // Stop recording on space + event.preventDefault(); + stopRecording(); + return; + } else if (!value.trim()) { + // Start recording on space when input is empty + event.preventDefault(); + void startRecording(); + return; + } + } + // Block all key events when recording (except space handled above) + if (isRecordingRef.current) { event.preventDefault(); - toggleRecording(); return; } baseHandleKeyDown(event); }, - [value, isTranscribing, toggleRecording, baseHandleKeyDown], + [value, isTranscribing, stopRecording, startRecording, baseHandleKeyDown], ); const showMicButton = isSupported; + // Don't include isRecording in disabled state - we need key events to work + // Text input is blocked via handleKeyDown instead const isInputDisabled = disabled || isStreaming || isTranscribing; // Cleanup on unmount diff --git a/autogpt_platform/frontend/src/components/ui/sidebar.tsx b/autogpt_platform/frontend/src/components/ui/sidebar.tsx index 127420c3e3..1b6f69e27b 100644 --- a/autogpt_platform/frontend/src/components/ui/sidebar.tsx +++ b/autogpt_platform/frontend/src/components/ui/sidebar.tsx @@ -1,12 +1,9 @@ "use client"; -import * as React from "react"; import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; -import { PanelLeft } from "lucide-react"; +import * as React from "react"; -import { useIsMobile } from "@/hooks/use-mobile"; -import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; @@ -24,6 +21,9 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { cn } from "@/lib/utils"; +import { SidebarSimpleIcon } from "@phosphor-icons/react"; const SIDEBAR_COOKIE_NAME = "sidebar_state"; const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; @@ -280,7 +280,7 @@ Sidebar.displayName = "Sidebar"; const SidebarTrigger = React.forwardRef< React.ElementRef, React.ComponentProps ->(({ className, onClick, ...props }, ref) => { +>(({ onClick }, ref) => { const { toggleSidebar } = useSidebar(); return ( @@ -288,15 +288,12 @@ const SidebarTrigger = React.forwardRef< ref={ref} data-sidebar="trigger" variant="ghost" - size="icon" - className={cn("h-7 w-7", className)} onClick={(event) => { onClick?.(event); toggleSidebar(); }} - {...props} > - + Toggle Sidebar );