Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 5 additions & 17 deletions src/components/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -823,15 +823,7 @@ export function Chat() {
[updateAssistantText, flushTextBuffer],
)

const {
messages,
input,
handleInputChange,
handleSubmit,
isLoading,
setMessages,
setInput,
} = useChat({
const { messages, isLoading, setMessages, append } = useChat({
body: chatBody,
onError: handleError,
onResponse: handleResponse,
Expand Down Expand Up @@ -885,10 +877,9 @@ export function Chat() {
timestamp: getTimestamp(),
},
])

handleSubmit(new Event('submit'))
append({ role: 'user', content: prompt })
},
[hasStartedChat, handleSubmit],
[hasStartedChat, append],
)

const handleServerToggle = useCallback((serverId: string) => {
Expand All @@ -908,16 +899,15 @@ export function Chat() {
setStreamBuffer([])
setStreaming(false)
setTimedOut(false)
setMessages([]) // Clear messages completely - initialMessage will be shown via renderEvents logic
setInput('')
setMessages([])
setFocusTimestamp(Date.now())
setUseCodeInterpreter(false)
setUseWebSearch(false)

textBufferRef.current = ''
lastAssistantIdRef.current = null
pendingStreamEventsRef.current = []
}, [setMessages, setInput])
}, [setMessages])

const handleScrollToBottom = useCallback(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
Expand Down Expand Up @@ -1190,8 +1180,6 @@ export function Chat() {
<ChatInput
onSendMessage={handleSendMessage}
disabled={isLoading || streaming}
value={input}
onChange={handleInputChange}
focusTimestamp={focusTimestamp}
/>
</div>
Expand Down
58 changes: 58 additions & 0 deletions src/components/ChatInput.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import { ChatInput } from './ChatInput'

const setup = (props = {}) => {
const onSendMessage = vi.fn()
render(<ChatInput onSendMessage={onSendMessage} {...props} />)

const textarea = screen.getByRole('textbox', {
name: 'Ask something...',
}) as HTMLTextAreaElement
const { form } = textarea

return { onSendMessage, textarea, form }
}

describe('ChatInput', () => {
it('renders the input and button', () => {
setup()
expect(screen.getByPlaceholderText(/ask something/i)).toBeInTheDocument()
expect(screen.getByRole('button')).toBeInTheDocument()
})

it('submits message on Enter', () => {
const { onSendMessage, textarea } = setup()
fireEvent.change(textarea, { target: { value: 'Hello world' } })
fireEvent.keyDown(textarea, { key: 'Enter', code: 'Enter', charCode: 13 })
expect(onSendMessage).toHaveBeenCalledWith('Hello world')
})

it('does not submit on Shift+Enter', () => {
const { onSendMessage, textarea } = setup()
fireEvent.change(textarea, { target: { value: 'Hello\nworld' } })
fireEvent.keyDown(textarea, { key: 'Enter', code: 'Enter', shiftKey: true })
expect(onSendMessage).not.toHaveBeenCalled()
})

it('disables input and button when disabled', () => {
setup({ disabled: true })
expect(screen.getByPlaceholderText(/ask something/i)).toBeDisabled()
expect(screen.getByRole('button')).toBeDisabled()
})

it('autofocuses', () => {
const { textarea } = setup({ focusTimestamp: Date.now() })

expect(document.activeElement).toBe(textarea)
})

it('clears after submit', () => {
const { textarea } = setup()

fireEvent.change(textarea, { target: { value: 'Clear me' } })
fireEvent.keyDown(textarea, { key: 'Enter', code: 'Enter', charCode: 13 })

expect(textarea).toHaveValue('')
})
})
36 changes: 15 additions & 21 deletions src/components/ChatInput.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
import React, { useRef, useEffect } from 'react'
import { useRef, useEffect, type KeyboardEvent } from 'react'
import { Button } from './ui/button'
import { Send } from 'lucide-react'
import { Textarea } from './ui/textarea'

type ChatInputProps = {
onSendMessage: (message: string) => void
disabled?: boolean
value: string
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
focusTimestamp?: number
}

export function ChatInput({
onSendMessage,
disabled = false,
value,
onChange,
focusTimestamp,
}: ChatInputProps) {
const textareaRef = useRef<HTMLTextAreaElement>(null)
Expand All @@ -28,31 +24,28 @@ export function ChatInput({

useEffect(() => {
if (textareaRef.current) {
// Reset height to calculate the right one
textareaRef.current.style.height = 'auto'
// Set new height based on scrollHeight (content)
textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 128)}px`
}
}, [value])
}, [textareaRef.current?.value])

const handleSubmit = (e: React.FormEvent) => {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const form = e.currentTarget as HTMLFormElement
const textarea = form.elements.namedItem('prompt') as HTMLTextAreaElement
const { value } = textarea

if (value.trim() && !disabled) {
onSendMessage(value)

// Reset the textarea height
if (textareaRef.current) {
textareaRef.current.style.height = 'auto'
}
form.reset()
textarea.style.height = 'auto'
}
}

const handleKeyDown = (e: React.KeyboardEvent) => {
// Submit on Enter without Shift
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSubmit(e)
e.currentTarget.form?.requestSubmit()
}
}

Expand All @@ -64,8 +57,7 @@ export function ChatInput({
<div className="relative flex-1 flex items-center">
<Textarea
ref={textareaRef}
value={value}
onChange={onChange}
name="prompt"
onKeyDown={handleKeyDown}
placeholder="Ask something..."
required
Expand All @@ -75,15 +67,17 @@ export function ChatInput({
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
aria-label="Ask something..."
className="w-full resize-none rounded-lg border-0 pr-12 text-base placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-400 disabled:opacity-50 transition-all"
/>
<Button
type="submit"
variant="default"
className="absolute right-2 size-8"
aria-label="Send message"
disabled={disabled}
>
<Send className="size-4" />
<span className="sr-only">Send message</span>
<Send className="size-4" aria-hidden="true" />
</Button>
</div>
</form>
Expand Down
Loading