11import { initCopyButton } from '../../../copybutton'
2+ import { GeneratingStatus } from './GeneratingStatus'
3+ import { References } from './RelatedResources'
24import { ChatMessage as ChatMessageType } from './chat.store'
35import { LlmGatewayMessage } from './useLlmGateway'
46import {
5- EuiAvatar ,
67 EuiButtonIcon ,
78 EuiCallOut ,
89 EuiCopy ,
910 EuiFlexGroup ,
1011 EuiFlexItem ,
1112 EuiIcon ,
1213 EuiLoadingElastic ,
13- EuiLoadingSpinner ,
1414 EuiPanel ,
1515 EuiSpacer ,
1616 EuiText ,
@@ -58,13 +58,90 @@ const getAccumulatedContent = (messages: LlmGatewayMessage[]) => {
5858 . join ( '' )
5959}
6060
61+ const splitContentAndReferences = (
62+ content : string
63+ ) : { mainContent : string ; referencesJson : string | null } => {
64+ const startDelimiter = '<!--REFERENCES'
65+ const endDelimiter = '-->'
66+
67+ const startIndex = content . indexOf ( startDelimiter )
68+ if ( startIndex === - 1 ) {
69+ return { mainContent : content , referencesJson : null }
70+ }
71+
72+ const endIndex = content . indexOf ( endDelimiter , startIndex )
73+ if ( endIndex === - 1 ) {
74+ return { mainContent : content , referencesJson : null }
75+ }
76+
77+ const mainContent = content . substring ( 0 , startIndex ) . trim ( )
78+ const referencesJson = content
79+ . substring ( startIndex + startDelimiter . length , endIndex )
80+ . trim ( )
81+
82+ return { mainContent, referencesJson }
83+ }
84+
6185const getMessageState = ( message : ChatMessageType ) => ( {
6286 isUser : message . type === 'user' ,
6387 isLoading : message . status === 'streaming' ,
6488 isComplete : message . status === 'complete' ,
6589 hasError : message . status === 'error' ,
6690} )
6791
92+ // Helper functions for computing AI status
93+ const getToolCallSearchQuery = (
94+ messages : LlmGatewayMessage [ ]
95+ ) : string | null => {
96+ const toolCallMessage = messages . find ( ( m ) => m . type === 'tool_call' )
97+ if ( ! toolCallMessage ) return null
98+
99+ try {
100+ const toolCalls = toolCallMessage . data ?. toolCalls
101+ if ( toolCalls && toolCalls . length > 0 ) {
102+ const firstToolCall = toolCalls [ 0 ]
103+ return firstToolCall . args ?. searchQuery || null
104+ }
105+ } catch ( e ) {
106+ console . error ( 'Error extracting search query from tool call:' , e )
107+ }
108+
109+ return null
110+ }
111+
112+ const hasContentStarted = ( messages : LlmGatewayMessage [ ] ) : boolean => {
113+ return messages . some ( ( m ) => m . type === 'ai_message_chunk' && m . data . content )
114+ }
115+
116+ const hasReachedReferences = ( messages : LlmGatewayMessage [ ] ) : boolean => {
117+ const accumulatedContent = messages
118+ . filter ( ( m ) => m . type === 'ai_message_chunk' )
119+ . map ( ( m ) => m . data . content )
120+ . join ( '' )
121+ return accumulatedContent . includes ( '<!--REFERENCES' )
122+ }
123+
124+ const computeAiStatus = (
125+ llmMessages : LlmGatewayMessage [ ] ,
126+ isComplete : boolean
127+ ) : string | null => {
128+ if ( isComplete ) return null
129+
130+ const searchQuery = getToolCallSearchQuery ( llmMessages )
131+ const contentStarted = hasContentStarted ( llmMessages )
132+ const reachedReferences = hasReachedReferences ( llmMessages )
133+
134+ if ( reachedReferences ) {
135+ return 'Gathering resources'
136+ } else if ( contentStarted ) {
137+ return 'Generating'
138+ } else if ( searchQuery ) {
139+ return `Searching for "${ searchQuery } "`
140+ }
141+
142+ return 'Thinking'
143+ }
144+
68145// Action bar for complete AI messages
69146const ActionBar = ( {
70147 content,
@@ -73,7 +150,7 @@ const ActionBar = ({
73150 content : string
74151 onRetry ?: ( ) => void
75152} ) => (
76- < EuiFlexGroup responsive = { false } component = "span" gutterSize = "s " >
153+ < EuiFlexGroup responsive = { false } component = "span" gutterSize = "none " >
77154 < EuiFlexItem grow = { false } >
78155 < EuiToolTip content = "This answer was helpful" >
79156 < EuiButtonIcon
@@ -137,34 +214,27 @@ export const ChatMessage = ({
137214
138215 if ( isUser ) {
139216 return (
140- < EuiFlexGroup
141- gutterSize = "s"
142- alignItems = "flexStart"
143- responsive = { false }
217+ < div
144218 data-message-type = "user"
145219 data-message-id = { message . id }
220+ css = { css `
221+ max-width : 50% ;
222+ justify-self : flex-end;
223+ ` }
146224 >
147- < EuiFlexItem grow = { false } >
148- < EuiAvatar
149- name = "User"
150- size = "m"
151- color = "#6DCCB1"
152- iconType = "user"
153- />
154- </ EuiFlexItem >
155- < EuiFlexItem >
156- < EuiPanel
157- paddingSize = "m"
158- hasShadow = { false }
159- hasBorder = { true }
160- css = { css `
161- background-color : ${ euiTheme . colors . emptyShade } ;
162- ` }
163- >
164- < EuiText size = "s" > { message . content } </ EuiText >
165- </ EuiPanel >
166- </ EuiFlexItem >
167- </ EuiFlexGroup >
225+ < EuiPanel
226+ paddingSize = "s"
227+ hasShadow = { false }
228+ hasBorder = { true }
229+ css = { css `
230+ border-radius : ${ euiTheme . border . radius . medium } ;
231+ background-color : ${ euiTheme . colors
232+ . backgroundLightText } ;
233+ ` }
234+ >
235+ < EuiText size = "s" > { message . content } </ EuiText >
236+ </ EuiPanel >
237+ </ div >
168238 )
169239 }
170240
@@ -176,10 +246,32 @@ export const ChatMessage = ({
176246
177247 const hasError = message . status === 'error' || ! ! error
178248
249+ // Only split content and references when complete for better performance
250+ const { mainContent, referencesJson } = useMemo ( ( ) => {
251+ if ( isComplete ) {
252+ return splitContentAndReferences ( content )
253+ }
254+ // During streaming, strip out unparsed references but don't parse them yet
255+ const startDelimiter = '<!--REFERENCES'
256+ const delimiterIndex = content . indexOf ( startDelimiter )
257+ if ( delimiterIndex !== - 1 ) {
258+ return {
259+ mainContent : content . substring ( 0 , delimiterIndex ) . trim ( ) ,
260+ referencesJson : null ,
261+ }
262+ }
263+ return { mainContent : content , referencesJson : null }
264+ } , [ content , isComplete ] )
265+
179266 const parsed = useMemo ( ( ) => {
180- const html = markedInstance . parse ( content ) as string
267+ const html = markedInstance . parse ( mainContent ) as string
181268 return DOMPurify . sanitize ( html )
182- } , [ content ] )
269+ } , [ mainContent ] )
270+
271+ const aiStatus = useMemo (
272+ ( ) => computeAiStatus ( llmMessages , isComplete ) ,
273+ [ llmMessages , isComplete ]
274+ )
183275
184276 const ref = React . useRef < HTMLDivElement > ( null )
185277
@@ -239,16 +331,12 @@ export const ChatMessage = ({
239331 < EuiPanel
240332 paddingSize = "m"
241333 hasShadow = { false }
242- hasBorder = { true }
334+ hasBorder = { false }
243335 css = { css `
244- background-color : ${ euiTheme . colors
245- . backgroundLightText } ;
336+ padding-top : 8px ;
246337 ` }
247338 >
248339 { content && (
249- // <EuiMarkdownFormat css={markdownFormatStyles}>
250- // {content}
251- // </EuiMarkdownFormat>
252340 < div
253341 ref = { ref }
254342 className = "markdown-content"
@@ -262,30 +350,20 @@ export const ChatMessage = ({
262350 />
263351 ) }
264352
265- { isLoading && (
266- < >
267- { content && < EuiSpacer size = "s" /> }
268- < EuiFlexGroup
269- alignItems = "center"
270- gutterSize = "s"
271- responsive = { false }
272- >
273- < EuiFlexItem grow = { false } >
274- < EuiLoadingSpinner size = "s" />
275- </ EuiFlexItem >
276- < EuiFlexItem grow = { false } >
277- < EuiText size = "xs" color = "subdued" >
278- Generating...
279- </ EuiText >
280- </ EuiFlexItem >
281- </ EuiFlexGroup >
282- </ >
353+ { referencesJson && (
354+ < References referencesJson = { referencesJson } />
283355 ) }
284356
357+ { content && isLoading && < EuiSpacer size = "m" /> }
358+ < GeneratingStatus status = { aiStatus } />
359+
285360 { isComplete && content && (
286361 < >
287362 < EuiSpacer size = "m" />
288- < ActionBar content = { content } onRetry = { onRetry } />
363+ < ActionBar
364+ content = { mainContent }
365+ onRetry = { onRetry }
366+ />
289367 </ >
290368 ) }
291369
0 commit comments