diff --git a/AGENTS.md b/AGENTS.md index cd176f8a2d..202c4c6e02 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,6 @@ See `docs/content/platform/getting-started.md` for setup instructions. - Format Python code with `poetry run format`. - Format frontend code using `pnpm format`. - ## Frontend guidelines: See `/frontend/CONTRIBUTING.md` for complete patterns. Quick reference: @@ -33,14 +32,17 @@ See `/frontend/CONTRIBUTING.md` for complete patterns. Quick reference: 4. **Styling**: Tailwind CSS only, use design tokens, Phosphor Icons only 5. **Testing**: Add Storybook stories for new components, Playwright for E2E 6. **Code conventions**: Function declarations (not arrow functions) for components/handlers + - Component props should be `interface Props { ... }` (not exported) unless the interface needs to be used outside the component - Separate render logic from business logic (component.tsx + useComponent.ts + helpers.ts) - Colocate state when possible and avoid creating large components, use sub-components ( local `/components` folder next to the parent component ) when sensible - Avoid large hooks, abstract logic into `helpers.ts` files when sensible - Use function declarations for components, arrow functions only for callbacks - No barrel files or `index.ts` re-exports -- Do not use `useCallback` or `useMemo` unless strictly needed - Avoid comments at all times unless the code is very complex +- Do not use `useCallback` or `useMemo` unless asked to optimise a given function +- Do not type hook returns, let Typescript infer as much as possible +- Never type with `any`, if not types available use `unknown` ## Testing @@ -49,22 +51,8 @@ See `/frontend/CONTRIBUTING.md` for complete patterns. Quick reference: Always run the relevant linters and tests before committing. Use conventional commit messages for all commits (e.g. `feat(backend): add API`). - Types: - - feat - - fix - - refactor - - ci - - dx (developer experience) - Scopes: - - platform - - platform/library - - platform/marketplace - - backend - - backend/executor - - frontend - - frontend/library - - frontend/marketplace - - blocks +Types: - feat - fix - refactor - ci - dx (developer experience) +Scopes: - platform - platform/library - platform/marketplace - backend - backend/executor - frontend - frontend/library - frontend/marketplace - blocks ## Pull requests diff --git a/autogpt_platform/CLAUDE.md b/autogpt_platform/CLAUDE.md index 2c76e7db80..bc7834f1b6 100644 --- a/autogpt_platform/CLAUDE.md +++ b/autogpt_platform/CLAUDE.md @@ -85,17 +85,6 @@ pnpm format pnpm types ``` -**📖 Complete Guide**: See `/frontend/CONTRIBUTING.md` and `/frontend/.cursorrules` for comprehensive frontend patterns. - -**Key Frontend Conventions:** - -- Separate render logic from data/behavior in components -- Use generated API hooks from `@/app/api/__generated__/endpoints/` -- Use function declarations (not arrow functions) for components/handlers -- Use design system components from `src/components/` (atoms, molecules, organisms) -- Only use Phosphor Icons -- Never use `src/components/__legacy__/*` or deprecated `BackendAPI` - ## Architecture Overview ### Backend Architecture @@ -217,14 +206,17 @@ See `/frontend/CONTRIBUTING.md` for complete patterns. Quick reference: 4. **Styling**: Tailwind CSS only, use design tokens, Phosphor Icons only 5. **Testing**: Add Storybook stories for new components, Playwright for E2E 6. **Code conventions**: Function declarations (not arrow functions) for components/handlers + - Component props should be `interface Props { ... }` (not exported) unless the interface needs to be used outside the component - Separate render logic from business logic (component.tsx + useComponent.ts + helpers.ts) - Colocate state when possible and avoid creating large components, use sub-components ( local `/components` folder next to the parent component ) when sensible - Avoid large hooks, abstract logic into `helpers.ts` files when sensible - Use function declarations for components, arrow functions only for callbacks - No barrel files or `index.ts` re-exports -- Do not use `useCallback` or `useMemo` unless strictly needed +- Do not use `useCallback` or `useMemo` unless asked to optimise a given function - Avoid comments at all times unless the code is very complex +- Do not type hook returns, let Typescript infer as much as possible +- Never type with `any`, if not types available use `unknown` ### Security Implementation 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 7af7eea9a9..65319a9ad7 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 @@ -1,5 +1,4 @@ import { Button } from "@/components/atoms/Button/Button"; -import { useToast } from "@/components/molecules/Toast/use-toast"; import { cn } from "@/lib/utils"; import { ArrowUpIcon, @@ -7,7 +6,6 @@ import { MicrophoneIcon, StopIcon, } from "@phosphor-icons/react"; -import { KeyboardEvent, useCallback, useEffect } from "react"; import { RecordingIndicator } from "./components/RecordingIndicator"; import { useChatInput } from "./useChatInput"; import { useVoiceRecording } from "./useVoiceRecording"; @@ -44,60 +42,22 @@ export function ChatInput({ inputId, }); - const handleTranscription = useCallback( - (text: string) => { - setValue((prev) => { - const trimmedPrev = prev.trim(); - if (trimmedPrev) { - return `${trimmedPrev} ${text}`; - } - return text; - }); - }, - [setValue], - ); - const { isRecording, isTranscribing, - error: voiceError, elapsedTime, toggleRecording, - isSupported: isVoiceSupported, + handleKeyDown, + showMicButton, + isInputDisabled, } = useVoiceRecording({ - onTranscription: handleTranscription, + setValue, disabled: disabled || isStreaming, + isStreaming, + value, + baseHandleKeyDown, }); - const { toast } = useToast(); - - // Show voice recording errors via toast - useEffect(() => { - if (voiceError) { - toast({ - title: "Voice recording failed", - description: voiceError, - variant: "destructive", - }); - } - }, [voiceError, toast]); - - const handleKeyDown = useCallback( - (event: KeyboardEvent) => { - // Space key toggles recording when input is empty - if (event.key === " " && !value.trim() && !isTranscribing) { - event.preventDefault(); - toggleRecording(); - return; - } - baseHandleKeyDown(event); - }, - [value, isTranscribing, toggleRecording, baseHandleKeyDown], - ); - - const showMicButton = isVoiceSupported && !isStreaming; - const isInputDisabled = disabled || isStreaming || isTranscribing; - return (
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/useChatInput.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/useChatInput.ts index 6fa8e7252b..a053e6080f 100644 --- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/useChatInput.ts +++ b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/useChatInput.ts @@ -6,7 +6,7 @@ import { useState, } from "react"; -interface UseChatInputArgs { +interface Args { onSend: (message: string) => void; disabled?: boolean; maxRows?: number; @@ -18,7 +18,7 @@ export function useChatInput({ disabled = false, maxRows = 5, inputId = "chat-input", -}: UseChatInputArgs) { +}: Args) { const [value, setValue] = useState(""); const [hasMultipleLines, setHasMultipleLines] = useState(false); 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 8c72fb5bc8..f7895916a8 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 @@ -1,27 +1,29 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useToast } from "@/components/molecules/Toast/use-toast"; +import React, { + KeyboardEvent, + useCallback, + useEffect, + useRef, + useState, +} from "react"; const MAX_RECORDING_DURATION = 2 * 60 * 1000; // 2 minutes in ms -interface UseVoiceRecordingArgs { - onTranscription: (text: string) => void; +interface Args { + setValue: React.Dispatch>; disabled?: boolean; -} - -interface UseVoiceRecordingReturn { - isRecording: boolean; - isTranscribing: boolean; - error: string | null; - elapsedTime: number; - startRecording: () => Promise; - stopRecording: () => void; - toggleRecording: () => void; - isSupported: boolean; + isStreaming?: boolean; + value: string; + baseHandleKeyDown: (event: KeyboardEvent) => void; } export function useVoiceRecording({ - onTranscription, + setValue, disabled = false, -}: UseVoiceRecordingArgs): UseVoiceRecordingReturn { + isStreaming = false, + value, + baseHandleKeyDown, +}: Args) { const [isRecording, setIsRecording] = useState(false); const [isTranscribing, setIsTranscribing] = useState(false); const [error, setError] = useState(null); @@ -55,6 +57,19 @@ export function useVoiceRecording({ setElapsedTime(0); }, [clearTimer]); + const handleTranscription = useCallback( + (text: string) => { + setValue((prev) => { + const trimmedPrev = prev.trim(); + if (trimmedPrev) { + return `${trimmedPrev} ${text}`; + } + return text; + }); + }, + [setValue], + ); + const transcribeAudio = useCallback( async (audioBlob: Blob) => { setIsTranscribing(true); @@ -76,7 +91,7 @@ export function useVoiceRecording({ const data = await response.json(); if (data.text) { - onTranscription(data.text); + handleTranscription(data.text); } } catch (err) { const message = @@ -87,7 +102,7 @@ export function useVoiceRecording({ setIsTranscribing(false); } }, - [onTranscription], + [handleTranscription], ); const stopRecording = useCallback(() => { @@ -178,6 +193,33 @@ export function useVoiceRecording({ } }, [isRecording, startRecording, stopRecording]); + const { toast } = useToast(); + + useEffect(() => { + if (error) { + toast({ + title: "Voice recording failed", + description: error, + variant: "destructive", + }); + } + }, [error, toast]); + + const handleKeyDown = useCallback( + (event: KeyboardEvent) => { + if (event.key === " " && !value.trim() && !isTranscribing) { + event.preventDefault(); + toggleRecording(); + return; + } + baseHandleKeyDown(event); + }, + [value, isTranscribing, toggleRecording, baseHandleKeyDown], + ); + + const showMicButton = isSupported && !isStreaming; + const isInputDisabled = disabled || isStreaming || isTranscribing; + // Cleanup on unmount useEffect(() => { return () => { @@ -194,5 +236,8 @@ export function useVoiceRecording({ stopRecording, toggleRecording, isSupported, + handleKeyDown, + showMicButton, + isInputDisabled, }; }