Skip to content

Commit 01ea13e

Browse files
committed
Dynamic AI response status and related resources display
1 parent b3b15c4 commit 01ea13e

File tree

6 files changed

+317
-58
lines changed

6 files changed

+317
-58
lines changed

src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/Chat.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
} from '@elastic/eui'
1616
import { css } from '@emotion/react'
1717
import * as React from 'react'
18-
import { useCallback, useEffect, useRef } from 'react'
18+
import { useCallback, useEffect, useRef, useState } from 'react'
1919

2020
const containerStyles = css`
2121
height: 100%;
@@ -66,6 +66,7 @@ export const Chat = () => {
6666
const inputRef = useRef<HTMLInputElement>(null)
6767
const scrollRef = useRef<HTMLDivElement>(null)
6868
const lastMessageStatusRef = useRef<string | null>(null)
69+
const [inputValue, setInputValue] = useState('')
6970

7071
const dynamicScrollableStyles = css`
7172
${scrollableStyles}
@@ -81,6 +82,7 @@ export const Chat = () => {
8182
if (inputRef.current) {
8283
inputRef.current.value = ''
8384
}
85+
setInputValue('')
8486

8587
// Scroll to bottom after new message
8688
setTimeout(() => scrollToBottom(scrollRef.current), 100)
@@ -202,6 +204,7 @@ export const Chat = () => {
202204
inputRef={inputRef}
203205
fullWidth
204206
placeholder="Ask Elastic Docs AI Assistant"
207+
onChange={(e) => setInputValue(e.target.value)}
205208
onKeyDown={(e) => {
206209
if (e.key === 'Enter') {
207210
handleSubmit(e.currentTarget.value)
@@ -219,7 +222,7 @@ export const Chat = () => {
219222
`}
220223
color="primary"
221224
iconType="sortUp"
222-
display="base"
225+
display={inputValue.trim() ? 'fill' : 'base'}
223226
onClick={() => {
224227
if (inputRef.current) {
225228
handleSubmit(inputRef.current.value)

src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/ChatMessage.tsx

Lines changed: 56 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import { initCopyButton } from '../../../copybutton'
2+
import { GeneratingStatus } from './GeneratingStatus'
3+
import { References } from './RelatedResources'
24
import { ChatMessage as ChatMessageType } from './chat.store'
35
import { LlmGatewayMessage } from './useLlmGateway'
46
import {
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,6 +58,24 @@ const getAccumulatedContent = (messages: LlmGatewayMessage[]) => {
5858
.join('')
5959
}
6060

61+
const splitContentAndReferences = (
62+
content: string
63+
): { mainContent: string; referencesJson: string | null } => {
64+
const delimiter = '--- references ---'
65+
const delimiterIndex = content.indexOf(delimiter)
66+
67+
if (delimiterIndex === -1) {
68+
return { mainContent: content, referencesJson: null }
69+
}
70+
71+
const mainContent = content.substring(0, delimiterIndex).trim()
72+
const referencesJson = content
73+
.substring(delimiterIndex + delimiter.length)
74+
.trim()
75+
76+
return { mainContent, referencesJson }
77+
}
78+
6179
const getMessageState = (message: ChatMessageType) => ({
6280
isUser: message.type === 'user',
6381
isLoading: message.status === 'streaming',
@@ -73,7 +91,7 @@ const ActionBar = ({
7391
content: string
7492
onRetry?: () => void
7593
}) => (
76-
<EuiFlexGroup responsive={false} component="span" gutterSize="s">
94+
<EuiFlexGroup responsive={false} component="span" gutterSize="none">
7795
<EuiFlexItem grow={false}>
7896
<EuiToolTip content="This answer was helpful">
7997
<EuiButtonIcon
@@ -137,34 +155,27 @@ export const ChatMessage = ({
137155

138156
if (isUser) {
139157
return (
140-
<EuiFlexGroup
141-
gutterSize="s"
142-
alignItems="flexStart"
143-
responsive={false}
158+
<div
144159
data-message-type="user"
145160
data-message-id={message.id}
161+
css={css`
162+
max-width: 50%;
163+
justify-self: flex-end;
164+
`}
146165
>
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>
166+
<EuiPanel
167+
paddingSize="s"
168+
hasShadow={false}
169+
hasBorder={true}
170+
css={css`
171+
border-radius: ${euiTheme.border.radius.medium};
172+
background-color: ${euiTheme.colors
173+
.backgroundLightText};
174+
`}
175+
>
176+
<EuiText size="s">{message.content}</EuiText>
177+
</EuiPanel>
178+
</div>
168179
)
169180
}
170181

@@ -176,10 +187,15 @@ export const ChatMessage = ({
176187

177188
const hasError = message.status === 'error' || !!error
178189

190+
const { mainContent, referencesJson } = useMemo(
191+
() => splitContentAndReferences(content),
192+
[content]
193+
)
194+
179195
const parsed = useMemo(() => {
180-
const html = markedInstance.parse(content) as string
196+
const html = markedInstance.parse(mainContent) as string
181197
return DOMPurify.sanitize(html)
182-
}, [content])
198+
}, [mainContent])
183199

184200
const ref = React.useRef<HTMLDivElement>(null)
185201

@@ -239,16 +255,12 @@ export const ChatMessage = ({
239255
<EuiPanel
240256
paddingSize="m"
241257
hasShadow={false}
242-
hasBorder={true}
258+
hasBorder={false}
243259
css={css`
244-
background-color: ${euiTheme.colors
245-
.backgroundLightText};
260+
padding-top: 8px;
246261
`}
247262
>
248263
{content && (
249-
// <EuiMarkdownFormat css={markdownFormatStyles}>
250-
// {content}
251-
// </EuiMarkdownFormat>
252264
<div
253265
ref={ref}
254266
className="markdown-content"
@@ -262,26 +274,16 @@ export const ChatMessage = ({
262274
/>
263275
)}
264276

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-
</>
277+
{referencesJson && (
278+
<References referencesJson={referencesJson} />
283279
)}
284280

281+
{content && isLoading && <EuiSpacer size="m" />}
282+
<GeneratingStatus
283+
llmMessages={llmMessages}
284+
isComplete={isComplete}
285+
/>
286+
285287
{isComplete && content && (
286288
<>
287289
<EuiSpacer size="m" />

src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/ChatMessageList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const ChatMessageList = ({ messages }: ChatMessageListProps) => {
2121
isLast={index === messages.length - 1}
2222
/>
2323
)}
24-
{index < messages.length - 1 && <EuiSpacer size="m" />}
24+
{index < messages.length - 1 && <EuiSpacer size="l" />}
2525
</React.Fragment>
2626
))}
2727
</>
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/** @jsxImportSource @emotion/react */
2+
import { LlmGatewayMessage } from './useLlmGateway'
3+
import {
4+
EuiFlexGroup,
5+
EuiFlexItem,
6+
EuiLoadingSpinner,
7+
EuiText,
8+
} from '@elastic/eui'
9+
import * as React from 'react'
10+
11+
interface GeneratingStatusProps {
12+
llmMessages: LlmGatewayMessage[]
13+
isComplete?: boolean
14+
}
15+
16+
const getToolCallSearchQuery = (
17+
messages: LlmGatewayMessage[]
18+
): string | null => {
19+
const toolCallMessage = messages.find((m) => m.type === 'tool_call')
20+
if (!toolCallMessage) return null
21+
22+
try {
23+
const toolCalls = toolCallMessage.data?.toolCalls
24+
if (toolCalls && toolCalls.length > 0) {
25+
const firstToolCall = toolCalls[0]
26+
return firstToolCall.args?.searchQuery || null
27+
}
28+
} catch (e) {
29+
console.error('Error extracting search query from tool call:', e)
30+
}
31+
32+
return null
33+
}
34+
35+
const hasContentStarted = (messages: LlmGatewayMessage[]): boolean => {
36+
return messages.some((m) => m.type === 'ai_message_chunk' && m.data.content)
37+
}
38+
39+
const hasReachedReferences = (messages: LlmGatewayMessage[]): boolean => {
40+
const accumulatedContent = messages
41+
.filter((m) => m.type === 'ai_message_chunk')
42+
.map((m) => m.data.content)
43+
.join('')
44+
return accumulatedContent.includes('--- references ---')
45+
}
46+
47+
export const GeneratingStatus = ({
48+
llmMessages,
49+
isComplete = false,
50+
}: GeneratingStatusProps) => {
51+
const searchQuery = getToolCallSearchQuery(llmMessages)
52+
const contentStarted = hasContentStarted(llmMessages)
53+
const reachedReferences = hasReachedReferences(llmMessages)
54+
55+
// If complete, don't show anything
56+
if (isComplete) {
57+
return null
58+
}
59+
60+
// Loading states
61+
let statusText = 'Thinking'
62+
63+
if (reachedReferences) {
64+
statusText = 'Finding sources'
65+
} else if (contentStarted) {
66+
statusText = 'Generating'
67+
} else if (searchQuery) {
68+
statusText = `Searching for "${searchQuery}"`
69+
}
70+
71+
return (
72+
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
73+
<EuiFlexItem grow={false}>
74+
<EuiLoadingSpinner size="s" />
75+
</EuiFlexItem>
76+
<EuiFlexItem grow={false}>
77+
<EuiText size="xs" color="subdued">
78+
{statusText}...
79+
</EuiText>
80+
</EuiFlexItem>
81+
</EuiFlexGroup>
82+
)
83+
}

0 commit comments

Comments
 (0)