1- import { useCallback , useMemo , useRef , useState } from 'react'
1+ import { useCallback , useEffect , useMemo , useRef , useState } from 'react'
22import { useChat } from 'ai/react'
3- import { MessageSquarePlus } from 'lucide-react'
3+ import { MessageSquarePlus , Clock } from 'lucide-react'
44import { useModel } from '../contexts/ModelContext'
55import { useUser } from '../contexts/UserContext'
66import { generateMessageId } from '../mcp/client'
@@ -14,6 +14,8 @@ import { BotMessage } from './BotMessage'
1414import { UserMessage } from './UserMessage'
1515import { CodeInterpreterToggle } from './CodeInterpreterToggle'
1616import { WebSearchToggle } from './WebSearchToggle'
17+ import { BackgroundToggle } from './BackgroundToggle'
18+ import { BackgroundJobsSidebar } from './BackgroundJobsSidebar'
1719import { ModelSelect } from './ModelSelect'
1820import { BotThinking } from './BotThinking'
1921import { BotError } from './BotError'
@@ -26,6 +28,7 @@ import { useStreamingChat } from '@/hooks/useStreamingChat'
2628import { getTimestamp } from '@/lib/utils/date'
2729import { isCodeInterpreterSupported } from '@/lib/utils/prompting'
2830import { useHasMounted } from '@/hooks/useHasMounted'
31+ import { useBackgroundJobs } from '@/hooks/useBackgroundJobs'
2932
3033const getEventKey = ( event : StreamEvent | Message , idx : number ) : string => {
3134 if ( 'type' in event ) {
@@ -61,14 +64,19 @@ const TIMEOUT_ERROR_MESSAGE =
6164export 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