Skip to content

Commit 6b89d37

Browse files
committed
feat: integrate background job handling in Chat component with UI updates
1 parent ed814d8 commit 6b89d37

File tree

1 file changed

+133
-11
lines changed

1 file changed

+133
-11
lines changed

src/components/Chat.tsx

Lines changed: 133 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { useCallback, useMemo, useRef, useState } from 'react'
1+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
22
import { useChat } from 'ai/react'
3-
import { MessageSquarePlus } from 'lucide-react'
3+
import { MessageSquarePlus, Clock } from 'lucide-react'
44
import { useModel } from '../contexts/ModelContext'
55
import { useUser } from '../contexts/UserContext'
66
import { generateMessageId } from '../mcp/client'
@@ -14,6 +14,8 @@ import { BotMessage } from './BotMessage'
1414
import { UserMessage } from './UserMessage'
1515
import { CodeInterpreterToggle } from './CodeInterpreterToggle'
1616
import { WebSearchToggle } from './WebSearchToggle'
17+
import { BackgroundToggle } from './BackgroundToggle'
18+
import { BackgroundJobsSidebar } from './BackgroundJobsSidebar'
1719
import { ModelSelect } from './ModelSelect'
1820
import { BotThinking } from './BotThinking'
1921
import { BotError } from './BotError'
@@ -26,6 +28,7 @@ import { useStreamingChat } from '@/hooks/useStreamingChat'
2628
import { getTimestamp } from '@/lib/utils/date'
2729
import { isCodeInterpreterSupported } from '@/lib/utils/prompting'
2830
import { useHasMounted } from '@/hooks/useHasMounted'
31+
import { useBackgroundJobs } from '@/hooks/useBackgroundJobs'
2932

3033
const getEventKey = (event: StreamEvent | Message, idx: number): string => {
3134
if ('type' in event) {
@@ -61,14 +64,19 @@ const TIMEOUT_ERROR_MESSAGE =
6164
export function Chat() {
6265
const hasMounted = useHasMounted()
6366
const messagesEndRef = useRef<HTMLDivElement>(null)
67+
const lastPromptRef = useRef<string>('')
6468
const [hasStartedChat, setHasStartedChat] = useState(false)
6569
const [focusTimestamp, setFocusTimestamp] = useState(Date.now())
6670
const [servers, setServers] = useState<Servers>({})
6771
const [selectedServers, setSelectedServers] = useState<Array<string>>([])
6872
const [useCodeInterpreter, setUseCodeInterpreter] = useState(false)
6973
const [useWebSearch, setUseWebSearch] = useState(false)
74+
const [useBackground, setUseBackground] = useState(false)
75+
const [backgroundJobsSidebarOpen, setBackgroundJobsSidebarOpen] =
76+
useState(false)
7077
const { selectedModel, setSelectedModel } = useModel()
7178
const { user } = useUser()
79+
const { jobs: backgroundJobs } = useBackgroundJobs()
7280

7381
const {
7482
streamBuffer,
@@ -110,6 +118,7 @@ export function Chat() {
110118
userId: user?.id,
111119
codeInterpreter: useCodeInterpreter,
112120
webSearch: useWebSearch,
121+
background: useBackground,
113122
}),
114123
[
115124
selectedServers,
@@ -118,13 +127,28 @@ export function Chat() {
118127
user?.id,
119128
useCodeInterpreter,
120129
useWebSearch,
130+
useBackground,
121131
],
122132
)
123133

134+
const handleResponseWithBackground = useCallback(
135+
(response: Response) => {
136+
if (useBackground) {
137+
// The background job title is the last user prompt
138+
const title = lastPromptRef.current
139+
140+
handleResponse(response, { background: true, title })
141+
} else {
142+
handleResponse(response, { background: false })
143+
}
144+
},
145+
[handleResponse, useBackground],
146+
)
147+
124148
const { messages, isLoading, setMessages, append, stop } = useChat({
125149
body: chatBody,
126150
onError: handleError,
127-
onResponse: handleResponse,
151+
onResponse: handleResponseWithBackground,
128152
})
129153

130154
const renderEvents = useMemo<Array<StreamEvent | Message>>(() => {
@@ -143,6 +167,7 @@ export function Chat() {
143167
if (!hasStartedChat) {
144168
setHasStartedChat(true)
145169
}
170+
lastPromptRef.current = prompt // Store the prompt for background job title
146171
addUserMessage(prompt)
147172
append({ role: 'user', content: prompt })
148173
},
@@ -167,8 +192,66 @@ export function Chat() {
167192
setFocusTimestamp(Date.now())
168193
setUseCodeInterpreter(false)
169194
setUseWebSearch(false)
195+
setUseBackground(false)
170196
}, [setMessages, stop, cancelStream, clearBuffer])
171197

198+
// Check if max concurrent background jobs limit is reached
199+
const maxJobsReached = useMemo(() => {
200+
if (!hasMounted) return false
201+
const runningJobs = backgroundJobs.filter((job) => job.status === 'running')
202+
return runningJobs.length >= 5 // Default limit from PRD
203+
}, [hasMounted, backgroundJobs])
204+
205+
// Update chat messages when background jobs update (for streaming loaded responses)
206+
useEffect(() => {
207+
setMessages((prevMessages) =>
208+
prevMessages.map((message) => {
209+
if (message) {
210+
const job = backgroundJobs.find(
211+
(j) => j.id === message.backgroundJobId,
212+
)
213+
if (job && job.response && job.response !== message.content) {
214+
// Update message content with latest job response
215+
return { ...message, content: job.response }
216+
}
217+
}
218+
return message
219+
}),
220+
)
221+
}, [backgroundJobs])
222+
223+
const handleLoadJobResponse = useCallback(
224+
async (jobId: string) => {
225+
const job = backgroundJobs.find((j) => j.id === jobId)
226+
227+
if (!job || job.status === 'failed') {
228+
console.warn('Job failed, cannot load response', job)
229+
return
230+
}
231+
232+
try {
233+
const url = new URL('/api/background-jobs', window.location.origin)
234+
url.searchParams.set('id', job.id)
235+
const streamResponse = await fetch(url.toString(), {
236+
method: 'GET',
237+
})
238+
239+
setBackgroundJobsSidebarOpen(false)
240+
handleResponse(streamResponse, { background: false })
241+
return
242+
} catch (error) {
243+
console.error('Failed to stream background job response:', error)
244+
handleError(new Error('Failed to load background job'))
245+
}
246+
},
247+
[backgroundJobs],
248+
)
249+
250+
const handleCancelJob = useCallback((jobId: string) => {
251+
// TODO: Implement actual job cancellation via OpenAI API
252+
console.log('Cancelling job:', jobId)
253+
}, [])
254+
172255
const handleScrollToBottom = useCallback(() => {
173256
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
174257
}, [])
@@ -200,19 +283,46 @@ export function Chat() {
200283
/>
201284
),
202285
},
286+
{
287+
key: 'background',
288+
isActive: useBackground,
289+
component: (
290+
<BackgroundToggle
291+
key="background"
292+
useBackground={useBackground}
293+
onToggle={setUseBackground}
294+
selectedModel={selectedModel}
295+
disabled={hasStartedChat}
296+
maxJobsReached={maxJobsReached}
297+
/>
298+
),
299+
},
203300
]
204301

205302
return (
206303
<div className="flex flex-col min-h-full relative">
207304
<div className="sticky top-0 z-10 bg-background border-b px-4 py-2 flex justify-between items-center">
208-
<Button
209-
variant="outline"
210-
onClick={handleNewChat}
211-
className="flex items-center gap-2"
212-
>
213-
<MessageSquarePlus className="size-4" />
214-
<span className="sr-only sm:not-sr-only">New Chat</span>
215-
</Button>
305+
<div className="flex items-center gap-2">
306+
<Button
307+
variant="outline"
308+
onClick={handleNewChat}
309+
className="flex items-center gap-2"
310+
>
311+
<MessageSquarePlus className="size-4" />
312+
<span className="sr-only sm:not-sr-only">New Chat</span>
313+
</Button>
314+
{hasMounted && (
315+
<Button
316+
variant="outline"
317+
onClick={() => setBackgroundJobsSidebarOpen(true)}
318+
className="flex items-center gap-2"
319+
title="Background Jobs"
320+
>
321+
<Clock className="size-4" />
322+
<span className="sr-only sm:not-sr-only">Jobs</span>
323+
</Button>
324+
)}
325+
</div>
216326
<ModelSelect value={selectedModel} onValueChange={handleModelChange} />
217327
</div>
218328
<div className="flex-1">
@@ -313,6 +423,12 @@ export function Chat() {
313423
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
314424
} else if ('type' in event && event.type === 'web_search') {
315425
return <WebSearchMessage key={key} event={event} />
426+
} else if (
427+
'type' in event &&
428+
event.type === 'background_job_created'
429+
) {
430+
// Background job handled by streaming hook - no UI rendering needed
431+
return null
316432
} else {
317433
// Fallback for Message type (from useChat)
318434
const message = event
@@ -439,6 +555,12 @@ export function Chat() {
439555
focusTimestamp={focusTimestamp}
440556
/>
441557
</div>
558+
<BackgroundJobsSidebar
559+
isOpen={backgroundJobsSidebarOpen}
560+
onClose={() => setBackgroundJobsSidebarOpen(false)}
561+
onLoadResponse={handleLoadJobResponse}
562+
onCancelJob={handleCancelJob}
563+
/>
442564
</div>
443565
)
444566
}

0 commit comments

Comments
 (0)