Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a38b615
feat: add single message queue with Enter key handling and cleanup on…
hermit46 Aug 11, 2025
6f7b7ca
feat: add logic to process queued message after turn
hermit46 Aug 11, 2025
528db07
feat: add convenience hooks for thread-specific state access
hermit46 Aug 13, 2025
c4e3dd1
feat: integrate per-thread queue system in useChat
hermit46 Aug 13, 2025
5621e1e
feat: enhance ChatInput with multi-message queue support
hermit46 Sep 5, 2025
47cc18f
test: update ChatInput tests for new queue system
hermit46 Sep 6, 2025
1f53414
test: add comprehensive tests for enhanced queue system
hermit46 Aug 13, 2025
22188d5
test: add tests for thread state convenience hooks
hermit46 Aug 13, 2025
0091025
test: add comprehensive tests for useAppState per-thread functionality
hermit46 Aug 13, 2025
a0bf783
test: add migration tests for useAppState backward compatibility
hermit46 Aug 13, 2025
e6553da
test: add integration tests for per-thread system components
hermit46 Aug 13, 2025
348989f
test: add tests for useAppState method separation and organization
hermit46 Aug 13, 2025
e807aaf
perf: optimize useThreadState with individual hooks for better perfor…
hermit46 Aug 13, 2025
ec271a2
test: add performance tests for memoization optimization
hermit46 Aug 13, 2025
296fc21
test: add benchmark tests demonstrating performance improvements
hermit46 Aug 13, 2025
ebcbd2d
feat(queue): implement message queueing infrastructure
hermit46 Aug 18, 2025
655fb48
feat(scheduler): add inference coordination and scheduling engine
hermit46 Aug 18, 2025
d7fd309
feat(chat): integrate message queueing with chat interface
hermit46 Sep 6, 2025
9b77893
test(mvp): add comprehensive testing infrastructure for simultaneous …
hermit46 Sep 6, 2025
f9b196e
fix(spelling): var spelling (handleSendMessage)
hermit46 Aug 18, 2025
b29b5eb
fix: merge sendMessage function signatures for attachments and explic…
hermit46 Sep 6, 2025
f610a66
fix: remove extra deps & param passing bug
hermit46 Sep 6, 2025
1b58501
fix: fix tests to be concurrency-aware
hermit46 Sep 6, 2025
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
102 changes: 79 additions & 23 deletions web-app/src/containers/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,15 @@ import { useGeneralSetting } from '@/hooks/useGeneralSetting'
import { useModelProvider } from '@/hooks/useModelProvider'

import { useAppState } from '@/hooks/useAppState'
import { MovingBorder } from './MovingBorder'
import { useChat } from '@/hooks/useChat'
import { MovingBorder } from './MovingBorder'
import DropdownModelProvider from '@/containers/DropdownModelProvider'
import { ModelLoader } from '@/containers/loaders/ModelLoader'
import DropdownToolsAvailable from '@/containers/DropdownToolsAvailable'
import { useServiceHub } from '@/hooks/useServiceHub'
import { getConnectedServers } from '@/services/mcp'
import { useRouter } from '@tanstack/react-router'
import { route } from '@/constants/routes'

type ChatInputProps = {
className?: string
Expand All @@ -52,14 +55,32 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
loadingModel,
tools,
cancelToolCall,
addToThreadQueue,
getThreadQueueLength,
setThreadPrompt,
getThreadPrompt,
} = useAppState()
const { prompt, setPrompt } = usePrompt()
const { currentThreadId } = useThreads()
const { prompt: globalPrompt, setPrompt: setGlobalPrompt } = usePrompt()
const { currentThreadId, createThread } = useThreads()

// Use thread-aware prompt state
const prompt = currentThreadId
? getThreadPrompt(currentThreadId)
: globalPrompt
const setPrompt = currentThreadId
? (value: string) => setThreadPrompt(currentThreadId, value)
: setGlobalPrompt
const { t } = useTranslation()
const { spellCheckChatInput } = useGeneralSetting()
const router = useRouter()

const maxRows = 10

// Get current thread's queue information
const currentThreadQueueLength = currentThreadId
? getThreadQueueLength(currentThreadId)
: 0

const { selectedModel, selectedProvider } = useModelProvider()
const { sendMessage } = useChat()
const [message, setMessage] = useState('')
Expand Down Expand Up @@ -105,7 +126,9 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
try {
// Only check mmproj for llamacpp provider
if (selectedProvider === 'llamacpp') {
const hasLocalMmproj = await serviceHub.models().checkMmprojExists(selectedModel.id)
const hasLocalMmproj = await serviceHub
.models()
.checkMmprojExists(selectedModel.id)
setHasMmproj(hasLocalMmproj)
}
// For non-llamacpp providers, only check vision capability
Expand All @@ -130,7 +153,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
// Check if there are active MCP servers
const hasActiveMCPServers = connectedServers.length > 0 || tools.length > 0

const handleSendMesage = (prompt: string) => {
const handleSendMessage = async (prompt: string) => {
if (!selectedModel) {
setMessage('Please select a model to start chatting.')
return
Expand All @@ -139,12 +162,42 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
return
}
setMessage('')
sendMessage(
prompt,
true,
uploadedFiles.length > 0 ? uploadedFiles : undefined
)
setUploadedFiles([])

// Create thread if none exists, otherwise use current thread
if (!currentThreadId) {
try {
// Create a new thread for the initial message
if (!selectedModel) {
setMessage('Please select a model to create a new conversation.')
return
}
const threadModel: ThreadModel = {
id: selectedModel.id,
provider: selectedProvider,
}
const newThread = await createThread(
threadModel,
prompt.trim().slice(0, 50)
)

// Navigate to the new thread
router.navigate({
to: route.threadsDetail,
params: { threadId: newThread.id },
})

// Queue the message after navigation
addToThreadQueue(newThread.id, prompt.trim())
} catch (error) {
console.error('Failed to create thread:', error)
setMessage('Failed to create new conversation.')
return
}
} else {
// Always queue messages - let scheduler decide when to process
addToThreadQueue(currentThreadId, prompt.trim())
}
setPrompt('')
}

useEffect(() => {
Expand Down Expand Up @@ -543,12 +596,11 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
)}
<TextareaAutosize
ref={textareaRef}
disabled={Boolean(streamingContent)}
minRows={2}
rows={1}
maxRows={10}
value={prompt}
data-testid={'chat-input'}
data-test-id={'chat-input'}
onChange={(e) => {
setPrompt(e.target.value)
// Count the number of newlines to estimate rows
Expand All @@ -559,17 +611,11 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
// e.keyCode 229 is for IME input with Safari
const isComposing =
e.nativeEvent.isComposing || e.keyCode === 229
if (
e.key === 'Enter' &&
!e.shiftKey &&
prompt.trim() &&
!isComposing
) {
if (e.key === 'Enter' && !isComposing && !e.shiftKey) {
e.preventDefault()
// Submit the message when Enter is pressed without Shift
handleSendMesage(prompt)
// When Shift+Enter is pressed, a new line is added (default behavior)
handleSendMessage(prompt) // Use same handler as send button
}
// Shift+Enter: Allow default behavior (new line)
}}
onPaste={handlePaste}
placeholder={t('common:placeholder.chatInput')}
Expand Down Expand Up @@ -748,6 +794,16 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
</div>
</div>

{/* Enhanced Queue Indicator */}
{currentThreadQueueLength > 0 && (
<div className="flex items-center gap-2">
<div className="bg-accent text-accent-fg text-xs px-2 py-1 rounded-full font-medium">
{currentThreadQueueLength} message
{currentThreadQueueLength === 1 ? '' : 's'} queued
</div>
</div>
)}

{streamingContent ? (
<Button
variant="destructive"
Expand All @@ -768,7 +824,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
size="icon"
disabled={!prompt.trim() && uploadedFiles.length === 0}
data-test-id="send-message-button"
onClick={() => handleSendMesage(prompt)}
onClick={() => handleSendMessage(prompt)}
>
{streamingContent ? (
<span className="animate-spin h-4 w-4 border-2 border-current border-t-transparent rounded-full" />
Expand Down
Loading