1- import { FC , useState , useRef , useEffect } from 'react' ;
1+ import { FC , useState , useRef , useEffect , useMemo } from 'react' ;
22import { useTranslation } from 'react-i18next' ;
3- import { Button , FormTextarea , Section } from 'components' ;
4- import { productionInference , ProductionInferenceRequest } from 'services/inference' ;
3+ import { Button , FormTextarea } from 'components' ;
54import { useToast } from 'hooks/useToast' ;
5+ import { useStreamingResponse } from 'hooks/useStreamingResponse' ;
66import './TestProductionLLM.scss' ;
7-
7+ import MessageContent from 'components/MessageContent' ;
88interface Message {
99 id : string ;
1010 content : string ;
@@ -15,139 +15,115 @@ interface Message {
1515const TestProductionLLM : FC = ( ) => {
1616 const { t } = useTranslation ( ) ;
1717 const toast = useToast ( ) ;
18- const [ message , setMessage ] = useState < string > ( '' ) ;
18+ const [ inputMessage , setInputMessage ] = useState < string > ( '' ) ;
1919 const [ messages , setMessages ] = useState < Message [ ] > ( [ ] ) ;
2020 const [ isLoading , setIsLoading ] = useState < boolean > ( false ) ;
2121 const messagesEndRef = useRef < HTMLDivElement > ( null ) ;
2222
23- const scrollToBottom = ( ) => {
24- messagesEndRef . current ?. scrollIntoView ( { behavior : 'smooth' } ) ;
25- } ;
23+ // Generate a unique channel ID for this session
24+ const channelId = useMemo ( ( ) => `channel- ${ Math . random ( ) . toString ( 36 ) . substring ( 2 , 15 ) } ` , [ ] ) ;
25+ const { startStreaming , stopStreaming , isStreaming } = useStreamingResponse ( channelId ) ;
2626
27+ // Auto-scroll to bottom
2728 useEffect ( ( ) => {
28- scrollToBottom ( ) ;
29+ messagesEndRef . current ?. scrollIntoView ( { behavior : 'smooth' } ) ;
2930 } , [ messages ] ) ;
3031
3132 const handleSendMessage = async ( ) => {
32- if ( ! message . trim ( ) ) {
33+ if ( ! inputMessage . trim ( ) ) {
3334 toast . open ( {
3435 type : 'warning' ,
35- title : t ( 'warningTitle' ) ,
36- message : t ( 'emptyMessageWarning' ) ,
36+ title : 'Warning' ,
37+ message : 'Please enter a message' ,
3738 } ) ;
3839 return ;
3940 }
4041
42+ const userMessageText = inputMessage . trim ( ) ;
43+
44+ // Add user message
4145 const userMessage : Message = {
4246 id : `user-${ Date . now ( ) } ` ,
43- content : message . trim ( ) ,
47+ content : userMessageText ,
4448 isUser : true ,
4549 timestamp : new Date ( ) . toISOString ( ) ,
4650 } ;
4751
48- // Add user message to chat
4952 setMessages ( prev => [ ...prev , userMessage ] ) ;
50- setMessage ( '' ) ;
53+ setInputMessage ( '' ) ;
5154 setIsLoading ( true ) ;
5255
53- try {
54- // Hardcoded values as requested
55- const request : ProductionInferenceRequest = {
56- chatId : 'test-chat-001' ,
57- message : userMessage . content ,
58- authorId : 'test-author-001' ,
59- conversationHistory : messages . map ( msg => ( {
60- authorRole : msg . isUser ? 'user' : 'bot' ,
61- message : msg . content ,
62- timestamp : msg . timestamp ,
63- } ) ) ,
64- url : 'https://test-url.example.com' ,
65- } ;
66-
67- let response ;
68- let attemptCount = 0 ;
69- const maxAttempts = 2 ;
70-
71- // Retry logic
72- while ( attemptCount < maxAttempts ) {
73- try {
74- attemptCount ++ ;
75- console . log ( `Production Inference Attempt ${ attemptCount } /${ maxAttempts } ` ) ;
76- response = await productionInference ( request ) ;
77-
78- // If we get a successful response, break out of retry loop
79- if ( ! response . status || response . status < 400 ) {
80- break ;
81- }
82-
83- // If first attempt failed with error status, retry once more
84- if ( attemptCount < maxAttempts && response . status >= 400 ) {
85- console . log ( 'Retrying due to error status...' ) ;
86- continue ;
87- }
88- } catch ( err ) {
89- // If first attempt threw an error, retry once more
90- if ( attemptCount < maxAttempts ) {
91- console . log ( 'Retrying due to exception...' ) ;
92- continue ;
93- }
94- throw err ; // Re-throw on final attempt
95- }
96- }
56+ // Create bot message ID
57+ const botMessageId = `bot-${ Date . now ( ) } ` ;
9758
98- console . log ( 'Production Inference Response:' , response ) ;
59+ // Prepare conversation history (exclude the current user message)
60+ const conversationHistory = messages . map ( msg => ( {
61+ authorRole : msg . isUser ? 'user' : 'bot' ,
62+ message : msg . content ,
63+ timestamp : msg . timestamp ,
64+ } ) ) ;
9965
100- // Create bot response message
101- let botContent = '' ;
102- let botMessageType : 'success' | 'error' = 'success' ;
66+ const streamingOptions = {
67+ authorId : 'test-user-456' ,
68+ conversationHistory,
69+ url : 'opensearch-dashboard-test' ,
70+ } ;
10371
104- if ( response . status && response . status >= 400 ) {
105- // Error response
106- botContent = response . content || 'An error occurred while processing your request.' ;
107- botMessageType = 'error' ;
108- } else {
109- // Success response
110- botContent = response ?. response ?. content || 'Response received successfully.' ;
72+ // Callbacks for streaming
73+ const onToken = ( token : string ) => {
74+ console . log ( '[Component] Received token:' , token ) ;
75+
76+ setMessages ( prev => {
77+ // Find the bot message
78+ const botMsgIndex = prev . findIndex ( msg => msg . id === botMessageId ) ;
11179
112- if ( response . questionOutOfLlmScope ) {
113- botContent += ' (Note: This question appears to be outside the LLM scope)' ;
80+ if ( botMsgIndex === - 1 ) {
81+ // First token - add the bot message
82+ console . log ( '[Component] Adding bot message with first token' ) ;
83+ setIsLoading ( false ) ;
84+ return [
85+ ...prev ,
86+ {
87+ id : botMessageId ,
88+ content : token ,
89+ isUser : false ,
90+ timestamp : new Date ( ) . toISOString ( ) ,
91+ }
92+ ] ;
93+ } else {
94+ // Append token to existing message
95+ console . log ( '[Component] Appending token to existing message' ) ;
96+ const updated = [ ...prev ] ;
97+ updated [ botMsgIndex ] = {
98+ ...updated [ botMsgIndex ] ,
99+ content : updated [ botMsgIndex ] . content + token ,
100+ } ;
101+ return updated ;
114102 }
115- }
116-
117- const botMessage : Message = {
118- id : `bot-${ Date . now ( ) } ` ,
119- content : botContent ,
120- isUser : false ,
121- timestamp : new Date ( ) . toISOString ( ) ,
122- } ;
123-
124- setMessages ( prev => [ ...prev , botMessage ] ) ;
103+ } ) ;
104+ } ;
125105
126- // Show toast notification
127- // toast.open({
128- // type: botMessageType,
129- // title: t('errorOccurred'),
130- // message: t('errorMessage'),
131- // });
106+ const onComplete = ( ) => {
107+ console . log ( '[Component] Stream completed' ) ;
108+ setIsLoading ( false ) ;
109+ } ;
132110
133- } catch ( error ) {
134- console . error ( 'Error sending message:' , error ) ;
111+ const onError = ( error : string ) => {
112+ console . error ( '[Component] Stream error:' , error ) ;
113+ setIsLoading ( false ) ;
135114
136- const errorMessage : Message = {
137- id : `error-${ Date . now ( ) } ` ,
138- content : 'Failed to send message. Please check your connection and try again.' ,
139- isUser : false ,
140- timestamp : new Date ( ) . toISOString ( ) ,
141- } ;
142-
143- setMessages ( prev => [ ...prev , errorMessage ] ) ;
144-
145115 toast . open ( {
146116 type : 'error' ,
147- title : 'Connection Error' ,
148- message : 'Unable to connect to the production LLM service.' ,
117+ title : 'Streaming Error' ,
118+ message : error ,
149119 } ) ;
150- } finally {
120+ } ;
121+
122+ // Start streaming
123+ try {
124+ await startStreaming ( userMessageText , streamingOptions , onToken , onComplete , onError ) ;
125+ } catch ( error ) {
126+ console . error ( '[Component] Failed to start streaming:' , error ) ;
151127 setIsLoading ( false ) ;
152128 }
153129 } ;
@@ -161,6 +137,7 @@ const TestProductionLLM: FC = () => {
161137
162138 const clearChat = ( ) => {
163139 setMessages ( [ ] ) ;
140+ stopStreaming ( ) ;
164141 toast . open ( {
165142 type : 'info' ,
166143 title : 'Chat Cleared' ,
@@ -195,7 +172,7 @@ const TestProductionLLM: FC = () => {
195172 } `}
196173 >
197174 < div className = "test-production-llm__message-content" >
198- { msg . content }
175+ < MessageContent content = { msg . content } />
199176 </ div >
200177 < div className = "test-production-llm__message-timestamp" >
201178 { new Date ( msg . timestamp ) . toLocaleTimeString ( ) }
@@ -222,20 +199,20 @@ const TestProductionLLM: FC = () => {
222199 < FormTextarea
223200 label = "Message"
224201 name = "message"
225- value = { message }
226- onChange = { ( e ) => setMessage ( e . target . value ) }
202+ value = { inputMessage }
203+ onChange = { ( e ) => setInputMessage ( e . target . value ) }
227204 onKeyDown = { handleKeyPress }
228205 placeholder = "Type your message here... (Press Enter to send, Shift+Enter for new line)"
229206 hideLabel
230207 maxRows = { 4 }
231- disabled = { isLoading }
208+ disabled = { isLoading || isStreaming }
232209 />
233210 < Button
234211 onClick = { handleSendMessage }
235- disabled = { isLoading || ! message . trim ( ) }
212+ disabled = { isLoading || isStreaming || ! inputMessage . trim ( ) }
236213 className = "test-production-llm__send-button"
237214 >
238- { isLoading ? 'Sending...' : 'Send' }
215+ { isLoading || isStreaming ? 'Sending...' : 'Send' }
239216 </ Button >
240217 </ div >
241218 </ div >
0 commit comments