Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ba8a7ef
Add AskAI message feedback endpoint
reakaleek Dec 1, 2025
4ce3f48
Adjust otel naming
reakaleek Dec 1, 2025
1e52f45
Add UI onclick handler and other adjustments
reakaleek Dec 1, 2025
264ac88
Add log sanitization
reakaleek Dec 1, 2025
2d08faa
Use spans and create tests
reakaleek Dec 1, 2025
25eb256
Remove all control characters as suggested by CodeQL
reakaleek Dec 1, 2025
7926de7
Validate input
reakaleek Dec 1, 2025
4df1e5c
Refactor
reakaleek Dec 1, 2025
d0883cf
Persist reaction selection
reakaleek Dec 1, 2025
3dadebf
Add debounce if user quickly changes selection
reakaleek Dec 1, 2025
b48be34
Fix other CodeQL errors regarind log sanitization
reakaleek Dec 1, 2025
c2b7c3c
Fix formatting
reakaleek Dec 1, 2025
79e4117
Fix test
reakaleek Dec 1, 2025
5de5770
Dispose correctly
reakaleek Dec 1, 2025
a112b98
Change scope of services
reakaleek Dec 1, 2025
6eea0cb
Potential fix for pull request finding 'Missing Dispose call on local…
reakaleek Dec 1, 2025
b6b2cbf
Potential fix for pull request finding 'Missing Dispose call on local…
reakaleek Dec 1, 2025
5f6a852
Format
reakaleek Dec 1, 2025
07ff6e6
Fix logging format for conversation_id in AgentBuilder
reakaleek Dec 1, 2025
01cd487
Fix tests
reakaleek Dec 1, 2025
ce47e59
Potential fix for code scanning alert no. 38: Log entries created fro…
reakaleek Dec 1, 2025
e562c5c
Re-add fast path for common cases to avoid string allocation
reakaleek Dec 1, 2025
d313731
Potential fix for code scanning alert no. 35: Log entries created fro…
reakaleek Dec 1, 2025
ce6d0ea
Potential fix for code scanning alert no. 39: Log entries created fro…
reakaleek Dec 1, 2025
7dca09e
Potential fix for code scanning alert no. 43: Log entries created fro…
reakaleek Dec 1, 2025
65ad494
Also remove newlines
reakaleek Dec 1, 2025
f622191
Remove user input from logs
reakaleek Dec 1, 2025
717d3d2
Remove this troublesome log for now
reakaleek Dec 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
import { ApiError } from '../errorHandling'
import { ChatMessage } from './ChatMessage'
import { ChatMessage as ChatMessageType } from './chat.store'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen } from '@testing-library/react'
import * as React from 'react'

// Create a fresh QueryClient for each test
const createTestQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
})

// Wrapper component for tests that need React Query
const renderWithQueryClient = (ui: React.ReactElement) => {
const testQueryClient = createTestQueryClient()
return render(
<QueryClientProvider client={testQueryClient}>{ui}</QueryClientProvider>
)
}

// Mock EuiCallOut and EuiSpacer for SearchOrAskAiErrorCallout
jest.mock('@elastic/eui', () => {
const actual = jest.requireActual('@elastic/eui')
Expand Down Expand Up @@ -141,7 +159,7 @@ describe('ChatMessage Component', () => {

it('should render AI message with correct content', () => {
// Act
render(<ChatMessage message={aiMessage} />)
renderWithQueryClient(<ChatMessage message={aiMessage} />)

// Assert
expect(
Expand All @@ -153,7 +171,7 @@ describe('ChatMessage Component', () => {

it('should show feedback buttons', () => {
// Act
render(<ChatMessage message={aiMessage} />)
renderWithQueryClient(<ChatMessage message={aiMessage} />)

// Assert
expect(
Expand All @@ -170,7 +188,7 @@ describe('ChatMessage Component', () => {

it('should display Elastic logo icon', () => {
// Act
render(<ChatMessage message={aiMessage} />)
renderWithQueryClient(<ChatMessage message={aiMessage} />)

// Assert
const messageElement = screen
Expand Down Expand Up @@ -199,7 +217,7 @@ describe('ChatMessage Component', () => {

it('should show loading icon when streaming', () => {
// Act
render(<ChatMessage message={streamingMessage} />)
renderWithQueryClient(<ChatMessage message={streamingMessage} />)

// Assert
// Loading elastic icon should be present
Expand All @@ -211,7 +229,7 @@ describe('ChatMessage Component', () => {

it('should not show feedback buttons when streaming', () => {
// Act
render(<ChatMessage message={streamingMessage} />)
renderWithQueryClient(<ChatMessage message={streamingMessage} />)

// Assert
expect(
Expand Down Expand Up @@ -244,7 +262,7 @@ describe('ChatMessage Component', () => {

it('should show error message', () => {
// Act
render(<ChatMessage message={errorMessage} />)
renderWithQueryClient(<ChatMessage message={errorMessage} />)

// Assert
const callout = screen.getByTestId('eui-callout')
Expand All @@ -258,7 +276,7 @@ describe('ChatMessage Component', () => {

it('should display previous content before error occurred', () => {
// Act
render(<ChatMessage message={errorMessage} />)
renderWithQueryClient(<ChatMessage message={errorMessage} />)

// Assert
// When there's an error, the content is hidden, only the error callout is shown
Expand All @@ -282,7 +300,7 @@ describe('ChatMessage Component', () => {

it('should render markdown content', () => {
// Act
render(<ChatMessage message={messageWithMarkdown} />)
renderWithQueryClient(<ChatMessage message={messageWithMarkdown} />)

// Assert - EuiMarkdownFormat will render the markdown
expect(screen.getByText(/Bold text/)).toBeInTheDocument()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { ApiError } from '../errorHandling'
import { AskAiEvent, ChunkEvent, EventTypes } from './AskAiEvent'
import { GeneratingStatus } from './GeneratingStatus'
import { References } from './RelatedResources'
import { ChatMessage as ChatMessageType } from './chat.store'
import { ChatMessage as ChatMessageType, useConversationId } from './chat.store'
import { useMessageFeedback } from './useMessageFeedback'
import { useStatusMinDisplay } from './useStatusMinDisplay'
import {
EuiButtonIcon,
Expand Down Expand Up @@ -186,62 +187,80 @@ const computeAiStatus = (
// Action bar for complete AI messages
const ActionBar = ({
content,
messageId,
onRetry,
}: {
content: string
messageId: string
onRetry?: () => void
}) => (
<EuiFlexGroup responsive={false} component="span" gutterSize="none">
<EuiFlexItem grow={false}>
<EuiToolTip content="This answer was helpful">
<EuiButtonIcon
aria-label="This answer was helpful"
iconType="thumbUp"
color="success"
size="s"
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip content="This answer was not helpful">
<EuiButtonIcon
aria-label="This answer was not helpful"
iconType="thumbDown"
color="danger"
size="s"
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiCopy
textToCopy={content}
beforeMessage="Copy markdown"
afterMessage="Copied!"
>
{(copy) => (
}) => {
const conversationId = useConversationId()
const { selectedReaction, submitFeedback } = useMessageFeedback(
messageId,
conversationId
)

return (
<EuiFlexGroup responsive={false} component="span" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiToolTip content="This answer was helpful">
<EuiButtonIcon
aria-label="Copy markdown"
iconType="copy"
aria-label="This answer was helpful"
iconType="thumbUp"
color="success"
size="s"
onClick={copy}
display={
selectedReaction === 'thumbsUp' ? 'base' : 'empty'
}
onClick={() => submitFeedback('thumbsUp')}
/>
)}
</EuiCopy>
</EuiFlexItem>
{onRetry && (
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip content="Request a new answer">
<EuiToolTip content="This answer was not helpful">
<EuiButtonIcon
aria-label="Request a new answer"
iconType="refresh"
onClick={onRetry}
aria-label="This answer was not helpful"
iconType="thumbDown"
color="danger"
size="s"
display={
selectedReaction === 'thumbsDown' ? 'base' : 'empty'
}
onClick={() => submitFeedback('thumbsDown')}
/>
</EuiToolTip>
</EuiFlexItem>
)}
</EuiFlexGroup>
)
<EuiFlexItem grow={false}>
<EuiCopy
textToCopy={content}
beforeMessage="Copy markdown"
afterMessage="Copied!"
>
{(copy) => (
<EuiButtonIcon
aria-label="Copy markdown"
iconType="copy"
size="s"
onClick={copy}
/>
)}
</EuiCopy>
</EuiFlexItem>
{onRetry && (
<EuiFlexItem grow={false}>
<EuiToolTip content="Request a new answer">
<EuiButtonIcon
aria-label="Request a new answer"
iconType="refresh"
onClick={onRetry}
size="s"
/>
</EuiToolTip>
</EuiFlexItem>
)}
</EuiFlexGroup>
)
}

export const ChatMessage = ({
message,
Expand Down Expand Up @@ -412,6 +431,7 @@ export const ChatMessage = ({
<EuiSpacer size="m" />
<ActionBar
content={mainContent}
messageId={message.id}
onRetry={onRetry}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { v4 as uuidv4 } from 'uuid'
import { create } from 'zustand/react'

export type AiProvider = 'AgentBuilder' | 'LlmGateway'
export type Reaction = 'thumbsUp' | 'thumbsDown'

export interface ChatMessage {
id: string
Expand All @@ -22,6 +23,7 @@ interface ChatState {
chatMessages: ChatMessage[]
conversationId: string | null
aiProvider: AiProvider
messageFeedback: Record<string, Reaction> // messageId -> reaction
actions: {
submitQuestion: (question: string) => void
updateAiMessage: (
Expand All @@ -37,13 +39,15 @@ interface ChatState {
hasMessageBeenSent: (id: string) => boolean
markMessageAsSent: (id: string) => void
cancelStreaming: () => void
setMessageFeedback: (messageId: string, reaction: Reaction) => void
}
}

export const chatStore = create<ChatState>((set) => ({
chatMessages: [],
conversationId: null, // Start with null - will be set by backend on first request
aiProvider: 'LlmGateway', // Default to LLM Gateway
messageFeedback: {},
actions: {
submitQuestion: (question: string) => {
set((state) => {
Expand Down Expand Up @@ -98,7 +102,7 @@ export const chatStore = create<ChatState>((set) => ({

clearChat: () => {
sentAiMessageIds.clear()
set({ chatMessages: [], conversationId: null })
set({ chatMessages: [], conversationId: null, messageFeedback: {} })
},

clearNon429Errors: () => {
Expand Down Expand Up @@ -136,6 +140,15 @@ export const chatStore = create<ChatState>((set) => ({
),
}))
},

setMessageFeedback: (messageId: string, reaction: Reaction) => {
set((state) => ({
messageFeedback: {
...state.messageFeedback,
[messageId]: reaction,
},
}))
},
},
}))

Expand All @@ -144,3 +157,5 @@ export const useConversationId = () =>
chatStore((state) => state.conversationId)
export const useAiProvider = () => chatStore((state) => state.aiProvider)
export const useChatActions = () => chatStore((state) => state.actions)
export const useMessageReaction = (messageId: string) =>
chatStore((state) => state.messageFeedback[messageId] ?? null)
Loading
Loading