@@ -29,15 +29,13 @@ import {
29
29
useRef ,
30
30
useState ,
31
31
} from 'react'
32
- import toast from 'react-hot-toast'
33
- import { Button , Container , Text , Title } from '@mantine/core'
32
+ import { Button , Text } from '@mantine/core'
34
33
import { useTranslation } from 'next-i18next'
35
34
36
35
import { getEndpoint } from '@/utils/app/api'
37
36
import {
38
37
saveConversation ,
39
38
saveConversations ,
40
- updateConversation ,
41
39
} from '@/utils/app/conversation'
42
40
import { throttle } from '@/utils/data/throttle'
43
41
@@ -46,6 +44,7 @@ import {
46
44
type ChatBody ,
47
45
type Conversation ,
48
46
type Message ,
47
+ Content ,
49
48
} from '@/types/chat'
50
49
import { type Plugin } from '@/types/plugin'
51
50
@@ -55,7 +54,7 @@ import { ChatInput } from './ChatInput'
55
54
import { ChatLoader } from './ChatLoader'
56
55
import { ErrorMessageDiv } from './ErrorMessageDiv'
57
56
import { MemoizedChatMessage } from './MemoizedChatMessage'
58
- import { fetchPresignedUrl } from '~/components/UIUC-Components/ContextCards '
57
+ import { fetchPresignedUrl } from '~/utils/apiUtils '
59
58
60
59
import { type CourseMetadata } from '~/types/courseMetadata'
61
60
@@ -75,7 +74,6 @@ import ChatNavbar from '../UIUC-Components/navbars/ChatNavbar'
75
74
import { notifications } from '@mantine/notifications'
76
75
import { Montserrat } from 'next/font/google'
77
76
import { montserrat_heading , montserrat_paragraph } from 'fonts'
78
- import { NextResponse } from 'next/server'
79
77
80
78
const montserrat_med = Montserrat ( {
81
79
weight : '500' ,
@@ -114,6 +112,7 @@ export const Chat = memo(({ stopConversationRef, courseMetadata }: Props) => {
114
112
loading,
115
113
prompts,
116
114
showModelSettings,
115
+ isImg2TextLoading
117
116
} ,
118
117
handleUpdateConversation,
119
118
dispatch : homeDispatch ,
@@ -173,14 +172,90 @@ export const Chat = memo(({ stopConversationRef, courseMetadata }: Props) => {
173
172
}
174
173
}
175
174
175
+ const handleImageContent = async ( message : Message , endpoint : string , updatedConversation : Conversation , searchQuery : string , controller : AbortController ) => {
176
+ const imageContent = ( message . content as Content [ ] ) . filter ( content => content . type === 'image_url' ) ;
177
+ if ( imageContent . length > 0 ) {
178
+ homeDispatch ( { field : 'isImg2TextLoading' , value : true } )
179
+ const chatBody : ChatBody = {
180
+ model : updatedConversation . model ,
181
+ messages : [
182
+ {
183
+ ...message ,
184
+ content : [
185
+ ...imageContent ,
186
+ { type : 'text' , text : 'Provide detailed description of the image(s) focusing on any text (OCR information), distinct objects, colors, and actions depicted. Include contextual information, subtle details, and specific terminologies relevant for semantic document retrieval.' }
187
+ ]
188
+ }
189
+ ] ,
190
+ key : courseMetadata ?. openai_api_key && courseMetadata ?. openai_api_key != '' ? courseMetadata . openai_api_key : apiKey ,
191
+ prompt : updatedConversation . prompt ,
192
+ temperature : updatedConversation . temperature ,
193
+ course_name : getCurrentPageName ( ) ,
194
+ stream : false ,
195
+ } ;
196
+
197
+ try {
198
+ const response = await fetch ( endpoint , {
199
+ method : 'POST' ,
200
+ headers : {
201
+ 'Content-Type' : 'application/json' ,
202
+ } ,
203
+ body : JSON . stringify ( chatBody ) ,
204
+ signal : controller . signal ,
205
+ } ) ;
206
+
207
+ if ( ! response . ok ) {
208
+ const final_response = await response . json ( ) ;
209
+ homeDispatch ( { field : 'loading' , value : false } ) ;
210
+ homeDispatch ( { field : 'messageIsStreaming' , value : false } ) ;
211
+ throw new Error ( final_response . message ) ;
212
+ }
213
+
214
+ const data = await response . json ( ) ;
215
+ const imgDesc = data . choices [ 0 ] . message . content || '' ;
216
+
217
+ searchQuery += ` Image description: ${ imgDesc } ` ;
218
+
219
+ const imgDescIndex = ( message . content as Content [ ] ) . findIndex ( content => content . type === 'text' && ( content . text as string ) . startsWith ( 'Image description: ' ) ) ;
220
+
221
+ if ( imgDescIndex !== - 1 ) {
222
+ ( message . content as Content [ ] ) [ imgDescIndex ] = { type : 'text' , text : `Image description: ${ imgDesc } ` } ;
223
+ } else {
224
+ ( message . content as Content [ ] ) . push ( { type : 'text' , text : `Image description: ${ imgDesc } ` } ) ;
225
+ }
226
+ } catch ( error ) {
227
+ console . error ( 'Error in chat.tsx running onResponseCompletion():' , error ) ;
228
+ controller . abort ( ) ;
229
+ } finally {
230
+ homeDispatch ( { field : 'isImg2TextLoading' , value : false } )
231
+ } ;
232
+ }
233
+ return searchQuery ;
234
+ }
235
+
236
+ const handleContextSearch = async ( message : Message , selectedConversation : Conversation , searchQuery : string ) => {
237
+ if ( getCurrentPageName ( ) != 'gpt4' ) {
238
+ const token_limit = OpenAIModels [ selectedConversation ?. model . id as OpenAIModelID ] . tokenLimit
239
+ await fetchContexts ( getCurrentPageName ( ) , searchQuery , token_limit ) . then ( ( curr_contexts ) => {
240
+ message . contexts = curr_contexts as ContextWithMetadata [ ]
241
+ } )
242
+ }
243
+ }
244
+
176
245
// THIS IS WHERE MESSAGES ARE SENT.
177
246
const handleSend = useCallback (
178
247
async ( message : Message , deleteCount = 0 , plugin : Plugin | null = null ) => {
248
+
249
+ setCurrentMessage ( message )
179
250
// New way with React Context API
180
251
// TODO: MOVE THIS INTO ChatMessage
181
252
// console.log('IN handleSend: ', message)
182
253
// setSearchQuery(message.content)
183
- const searchQuery = message . content
254
+ let searchQuery = Array . isArray ( message . content )
255
+ ? message . content . map ( ( content ) => content . text ) . join ( ' ' )
256
+ : message . content ;
257
+
258
+ // console.log("QUERY: ", searchQuery)
184
259
185
260
if ( selectedConversation ) {
186
261
let updatedConversation : Conversation
@@ -206,21 +281,18 @@ export const Chat = memo(({ stopConversationRef, courseMetadata }: Props) => {
206
281
homeDispatch ( { field : 'loading' , value : true } )
207
282
homeDispatch ( { field : 'messageIsStreaming' , value : true } )
208
283
209
- // Run context search, attach to Message object.
210
- if ( getCurrentPageName ( ) != 'gpt4' ) {
211
- // THE ONLY place we fetch contexts (except ExtremePromptStuffing is still in api/chat.ts)
212
- const token_limit =
213
- OpenAIModels [ selectedConversation ?. model . id as OpenAIModelID ]
214
- . tokenLimit
215
- await fetchContexts (
216
- getCurrentPageName ( ) ,
217
- searchQuery ,
218
- token_limit ,
219
- ) . then ( ( curr_contexts ) => {
220
- message . contexts = curr_contexts as ContextWithMetadata [ ]
221
- } )
284
+ const endpoint = getEndpoint ( plugin ) ;
285
+
286
+ const controller = new AbortController ( )
287
+
288
+ // Run image to text conversion, attach to Message object.
289
+ if ( Array . isArray ( message . content ) ) {
290
+ searchQuery = await handleImageContent ( message , endpoint , updatedConversation , searchQuery , controller ) ;
222
291
}
223
292
293
+ // Run context search, attach to Message object.
294
+ await handleContextSearch ( message , selectedConversation , searchQuery ) ;
295
+
224
296
const chatBody : ChatBody = {
225
297
model : updatedConversation . model ,
226
298
messages : updatedConversation . messages ,
@@ -232,8 +304,9 @@ export const Chat = memo(({ stopConversationRef, courseMetadata }: Props) => {
232
304
prompt : updatedConversation . prompt ,
233
305
temperature : updatedConversation . temperature ,
234
306
course_name : getCurrentPageName ( ) ,
307
+ stream : true
235
308
}
236
- const endpoint = getEndpoint ( plugin ) // THIS is where we could support EXTREME prompt stuffing.
309
+
237
310
let body
238
311
if ( ! plugin ) {
239
312
body = JSON . stringify ( chatBody )
@@ -248,7 +321,8 @@ export const Chat = memo(({ stopConversationRef, courseMetadata }: Props) => {
248
321
?. requiredKeys . find ( ( key ) => key . key === 'GOOGLE_CSE_ID' ) ?. value ,
249
322
} )
250
323
}
251
- const controller = new AbortController ( )
324
+
325
+ // This is where we call the OpenAI API
252
326
const response = await fetch ( endpoint , {
253
327
method : 'POST' ,
254
328
headers : {
@@ -301,13 +375,17 @@ export const Chat = memo(({ stopConversationRef, courseMetadata }: Props) => {
301
375
}
302
376
if ( ! plugin ) {
303
377
if ( updatedConversation . messages . length === 1 ) {
304
- const { content } = message
378
+ const { content } = message ;
379
+ // Use only texts instead of content itself
380
+ const contentText = Array . isArray ( content )
381
+ ? content . map ( ( content ) => content . text ) . join ( ' ' )
382
+ : content ;
305
383
const customName =
306
- content . length > 30 ? content . substring ( 0 , 30 ) + '...' : content
384
+ contentText . length > 30 ? contentText . substring ( 0 , 30 ) + '...' : contentText ;
307
385
updatedConversation = {
308
386
...updatedConversation ,
309
387
name : customName ,
310
- }
388
+ } ;
311
389
}
312
390
homeDispatch ( { field : 'loading' , value : false } )
313
391
const reader = data . getReader ( )
@@ -390,6 +468,7 @@ export const Chat = memo(({ stopConversationRef, courseMetadata }: Props) => {
390
468
updatedConversations . push ( updatedConversation )
391
469
}
392
470
homeDispatch ( { field : 'conversations' , value : updatedConversations } )
471
+ console . log ( 'updatedConversations: ' , updatedConversations )
393
472
saveConversations ( updatedConversations )
394
473
homeDispatch ( { field : 'messageIsStreaming' , value : false } )
395
474
} else {
@@ -434,6 +513,20 @@ export const Chat = memo(({ stopConversationRef, courseMetadata }: Props) => {
434
513
] ,
435
514
)
436
515
516
+ const handleRegenerate = useCallback ( ( ) => {
517
+ if ( currentMessage && Array . isArray ( currentMessage . content ) ) {
518
+ // Find the index of the existing image description
519
+ const imgDescIndex = ( currentMessage . content as Content [ ] ) . findIndex ( content => content . type === 'text' && ( content . text as string ) . startsWith ( 'Image description: ' ) ) ;
520
+
521
+ if ( imgDescIndex !== - 1 ) {
522
+ // Remove the existing image description
523
+ ( currentMessage . content as Content [ ] ) . splice ( imgDescIndex , 1 ) ;
524
+ }
525
+
526
+ handleSend ( currentMessage , 2 , null ) ;
527
+ }
528
+ } , [ currentMessage , handleSend ] ) ;
529
+
437
530
const scrollToBottom = useCallback ( ( ) => {
438
531
if ( autoScrollEnabled ) {
439
532
messagesEndRef . current ?. scrollIntoView ( { behavior : 'smooth' } )
@@ -575,6 +668,64 @@ export const Chat = memo(({ stopConversationRef, courseMetadata }: Props) => {
575
668
</ div >
576
669
)
577
670
}
671
+ // Inside Chat function before the return statement
672
+ const renderMessageContent = ( message : Message ) => {
673
+ if ( Array . isArray ( message . content ) ) {
674
+ return (
675
+ < >
676
+ { message . content . map ( ( content , index ) => {
677
+ if ( content . type === 'image' && content . image_url ) {
678
+ return < img key = { index } src = { content . image_url . url } alt = "Uploaded content" /> ;
679
+ }
680
+ return < span key = { index } > { content . text } </ span > ;
681
+ } ) }
682
+ </ >
683
+ ) ;
684
+ }
685
+ return < span > { message . content } </ span > ;
686
+ } ;
687
+
688
+ const updateMessages = ( updatedMessage : Message , messageIndex : number ) => {
689
+ return selectedConversation ?. messages . map ( ( message , index ) => {
690
+ return index === messageIndex ? updatedMessage : message ;
691
+ } ) ;
692
+ } ;
693
+
694
+ const updateConversations = ( updatedConversation : Conversation ) => {
695
+ return conversations . map ( ( conversation ) =>
696
+ conversation . id === selectedConversation ?. id ? updatedConversation : conversation
697
+ ) ;
698
+ } ;
699
+
700
+ const onImageUrlsUpdate = useCallback ( ( updatedMessage : Message , messageIndex : number ) => {
701
+ if ( ! selectedConversation ) {
702
+ throw new Error ( "No selected conversation found" ) ;
703
+ }
704
+
705
+ const updatedMessages = updateMessages ( updatedMessage , messageIndex ) ;
706
+ if ( ! updatedMessages ) {
707
+ throw new Error ( "Failed to update messages" ) ;
708
+ }
709
+
710
+ const updatedConversation = {
711
+ ...selectedConversation ,
712
+ messages : updatedMessages ,
713
+ } ;
714
+
715
+ homeDispatch ( {
716
+ field : 'selectedConversation' ,
717
+ value : updatedConversation ,
718
+ } ) ;
719
+
720
+ const updatedConversations = updateConversations ( updatedConversation ) ;
721
+ if ( ! updatedConversations ) {
722
+ throw new Error ( "Failed to update conversations" ) ;
723
+ }
724
+
725
+ homeDispatch ( { field : 'conversations' , value : updatedConversations } ) ;
726
+ saveConversations ( updatedConversations ) ;
727
+ } , [ selectedConversation , conversations ] ) ;
728
+
578
729
579
730
return (
580
731
< div className = "overflow-wrap relative flex h-screen w-full flex-col overflow-hidden bg-white dark:bg-[#15162c]" >
@@ -671,14 +822,16 @@ export const Chat = memo(({ stopConversationRef, courseMetadata }: Props) => {
671
822
< MemoizedChatMessage
672
823
key = { index }
673
824
message = { message }
825
+ contentRenderer = { renderMessageContent }
674
826
messageIndex = { index }
675
827
onEdit = { ( editedMessage ) => {
676
- setCurrentMessage ( editedMessage )
828
+ // setCurrentMessage(editedMessage)
677
829
handleSend (
678
830
editedMessage ,
679
831
selectedConversation ?. messages . length - index ,
680
832
)
681
833
} }
834
+ onImageUrlsUpdate = { onImageUrlsUpdate }
682
835
/>
683
836
) ) }
684
837
{ loading && < ChatLoader /> }
@@ -694,18 +847,15 @@ export const Chat = memo(({ stopConversationRef, courseMetadata }: Props) => {
694
847
stopConversationRef = { stopConversationRef }
695
848
textareaRef = { textareaRef }
696
849
onSend = { ( message , plugin ) => {
697
- setCurrentMessage ( message )
850
+ // setCurrentMessage(message)
698
851
handleSend ( message , 0 , plugin )
699
852
} }
700
853
onScrollDownClick = { handleScrollDown }
701
- onRegenerate = { ( ) => {
702
- if ( currentMessage ) {
703
- handleSend ( currentMessage , 2 , null )
704
- }
705
- } }
854
+ onRegenerate = { handleRegenerate }
706
855
showScrollDownButton = { showScrollDownButton }
707
856
inputContent = { inputContent }
708
857
setInputContent = { setInputContent }
858
+ courseName = { getCurrentPageName ( ) }
709
859
/>
710
860
{ /* </div> */ }
711
861
</ >
0 commit comments