Implement chat message feedback for UI chat window
This commit is contained in:
parent
ac35d5f667
commit
131eccef45
|
|
@ -595,7 +595,27 @@ export class App {
|
||||||
|
|
||||||
// Get internal chatmessages from chatflowid
|
// Get internal chatmessages from chatflowid
|
||||||
this.app.get('/api/v1/internal-chatmessage/:id', async (req: Request, res: Response) => {
|
this.app.get('/api/v1/internal-chatmessage/:id', async (req: Request, res: Response) => {
|
||||||
const chatmessages = await this.getChatMessage(req.params.id, chatType.INTERNAL)
|
const sortOrder = req.query?.order as string | undefined
|
||||||
|
const chatId = req.query?.chatId as string | undefined
|
||||||
|
const memoryType = req.query?.memoryType as string | undefined
|
||||||
|
const sessionId = req.query?.sessionId as string | undefined
|
||||||
|
const messageId = req.query?.messageId as string | undefined
|
||||||
|
const startDate = req.query?.startDate as string | undefined
|
||||||
|
const endDate = req.query?.endDate as string | undefined
|
||||||
|
const feedback = req.query?.feedback as boolean | undefined
|
||||||
|
|
||||||
|
const chatmessages = await this.getChatMessage(
|
||||||
|
req.params.id,
|
||||||
|
chatType.INTERNAL,
|
||||||
|
sortOrder,
|
||||||
|
chatId,
|
||||||
|
memoryType,
|
||||||
|
sessionId,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
messageId,
|
||||||
|
feedback
|
||||||
|
)
|
||||||
return res.json(chatmessages)
|
return res.json(chatmessages)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import client from './client'
|
import client from './client'
|
||||||
|
|
||||||
const getInternalChatmessageFromChatflow = (id) => client.get(`/internal-chatmessage/${id}`)
|
const getInternalChatmessageFromChatflow = (id, params = {}) =>
|
||||||
|
client.get(`/internal-chatmessage/${id}`, { params: { feedback: true, ...params } })
|
||||||
const getAllChatmessageFromChatflow = (id, params = {}) =>
|
const getAllChatmessageFromChatflow = (id, params = {}) =>
|
||||||
client.get(`/chatmessage/${id}`, { params: { order: 'DESC', feedback: true, ...params } })
|
client.get(`/chatmessage/${id}`, { params: { order: 'DESC', feedback: true, ...params } })
|
||||||
const getChatmessageFromPK = (id, params = {}) => client.get(`/chatmessage/${id}`, { params: { order: 'ASC', feedback: true, ...params } })
|
const getChatmessageFromPK = (id, params = {}) => client.get(`/chatmessage/${id}`, { params: { order: 'ASC', feedback: true, ...params } })
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import client from './client'
|
||||||
|
|
||||||
|
const addFeedback = (id, body) => client.post(`/feedback/${id}`, body)
|
||||||
|
const updateFeedback = (id, body) => client.put(`/feedback/${id}`, body)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
addFeedback,
|
||||||
|
updateFeedback
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { IconButton } from '@mui/material'
|
||||||
|
import { IconClipboard } from '@tabler/icons'
|
||||||
|
|
||||||
|
const CopyToClipboardButton = (props) => {
|
||||||
|
const customization = useSelector((state) => state.customization)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
disabled={props.isDisabled || props.isLoading}
|
||||||
|
onClick={props.onClick}
|
||||||
|
size='small'
|
||||||
|
sx={{ background: 'transparent', border: 'none' }}
|
||||||
|
title='Copy to clipboard'
|
||||||
|
>
|
||||||
|
<IconClipboard
|
||||||
|
style={{ width: '20px', height: '20px' }}
|
||||||
|
color={props.isLoading ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
CopyToClipboardButton.propTypes = {
|
||||||
|
isDisabled: PropTypes.bool,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
onClick: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CopyToClipboardButton
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { IconButton } from '@mui/material'
|
||||||
|
import { IconThumbDown } from '@tabler/icons'
|
||||||
|
|
||||||
|
const ThumbsDownButton = (props) => {
|
||||||
|
const customization = useSelector((state) => state.customization)
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
disabled={props.isDisabled || props.isLoading}
|
||||||
|
onClick={props.onClick}
|
||||||
|
size='small'
|
||||||
|
sx={{ background: 'transparent', border: 'none' }}
|
||||||
|
title='Thumbs Down'
|
||||||
|
>
|
||||||
|
<IconThumbDown
|
||||||
|
style={{ width: '20px', height: '20px' }}
|
||||||
|
color={props.rating === 'THUMBS_DOWN' ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ThumbsDownButton.propTypes = {
|
||||||
|
isDisabled: PropTypes.bool,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
rating: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThumbsDownButton
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { IconButton } from '@mui/material'
|
||||||
|
import { IconThumbUp } from '@tabler/icons'
|
||||||
|
|
||||||
|
const ThumbsUpButton = (props) => {
|
||||||
|
const customization = useSelector((state) => state.customization)
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
disabled={props.isDisabled || props.isLoading}
|
||||||
|
onClick={props.onClick}
|
||||||
|
size='small'
|
||||||
|
sx={{ background: 'transparent', border: 'none' }}
|
||||||
|
title='Thumbs Up'
|
||||||
|
>
|
||||||
|
<IconThumbUp
|
||||||
|
style={{ width: '20px', height: '20px' }}
|
||||||
|
color={props.rating === 'THUMBS_UP' ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ThumbsUpButton.propTypes = {
|
||||||
|
isDisabled: PropTypes.bool,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
rating: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThumbsUpButton
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { useCallback, useEffect } from 'react'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import { useDispatch } from 'react-redux'
|
||||||
|
|
||||||
|
// material-ui
|
||||||
|
import { Button, Dialog, DialogContent, DialogTitle, DialogActions, Box, OutlinedInput } from '@mui/material'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
// Project import
|
||||||
|
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||||
|
|
||||||
|
// store
|
||||||
|
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
|
||||||
|
|
||||||
|
const ChatFeedbackContentDialog = ({ show, onCancel, onConfirm }) => {
|
||||||
|
const portalElement = document.getElementById('portal')
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
|
const [feedbackContent, setFeedbackContent] = useState('')
|
||||||
|
|
||||||
|
const onChange = useCallback((e) => setFeedbackContent(e.target.value), [setFeedbackContent])
|
||||||
|
|
||||||
|
const onSave = () => {
|
||||||
|
onConfirm(feedbackContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
|
||||||
|
else dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||||
|
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||||
|
}, [show, dispatch])
|
||||||
|
|
||||||
|
const component = show ? (
|
||||||
|
<Dialog
|
||||||
|
onClose={onCancel}
|
||||||
|
open={show}
|
||||||
|
fullWidth
|
||||||
|
maxWidth='sm'
|
||||||
|
aria-labelledby='alert-dialog-title'
|
||||||
|
aria-describedby='alert-dialog-description'
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
|
||||||
|
Provide additional feedback
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Box sx={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<OutlinedInput
|
||||||
|
// eslint-disable-next-line
|
||||||
|
autoFocus
|
||||||
|
id='feedbackContentInput'
|
||||||
|
multiline={true}
|
||||||
|
name='feedbackContentInput'
|
||||||
|
onChange={onChange}
|
||||||
|
placeholder='What do you think of the response?'
|
||||||
|
rows={4}
|
||||||
|
value={feedbackContent}
|
||||||
|
sx={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={onCancel}>Cancel</Button>
|
||||||
|
<StyledButton variant='contained' onClick={onSave}>
|
||||||
|
Submit Feedback
|
||||||
|
</StyledButton>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
) : null
|
||||||
|
|
||||||
|
return createPortal(component, portalElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChatFeedbackContentDialog
|
||||||
|
|
@ -32,9 +32,13 @@ import audioUploadSVG from 'assets/images/wave-sound.jpg'
|
||||||
import { CodeBlock } from 'ui-component/markdown/CodeBlock'
|
import { CodeBlock } from 'ui-component/markdown/CodeBlock'
|
||||||
import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown'
|
import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown'
|
||||||
import SourceDocDialog from 'ui-component/dialog/SourceDocDialog'
|
import SourceDocDialog from 'ui-component/dialog/SourceDocDialog'
|
||||||
|
import ChatFeedbackContentDialog from 'ui-component/dialog/ChatFeedbackContentDialog'
|
||||||
import StarterPromptsCard from 'ui-component/cards/StarterPromptsCard'
|
import StarterPromptsCard from 'ui-component/cards/StarterPromptsCard'
|
||||||
import { cancelAudioRecording, startAudioRecording, stopAudioRecording } from './audio-recording'
|
import { cancelAudioRecording, startAudioRecording, stopAudioRecording } from './audio-recording'
|
||||||
import { ImageButton, ImageSrc, ImageBackdrop, ImageMarked } from 'ui-component/button/ImageButton'
|
import { ImageButton, ImageSrc, ImageBackdrop, ImageMarked } from 'ui-component/button/ImageButton'
|
||||||
|
import CopyToClipboardButton from 'ui-component/button/CopyToClipboardButton'
|
||||||
|
import ThumbsUpButton from 'ui-component/button/ThumbsUpButton'
|
||||||
|
import ThumbsDownButton from 'ui-component/button/ThumbsDownButton'
|
||||||
import './ChatMessage.css'
|
import './ChatMessage.css'
|
||||||
import './audio-recording.css'
|
import './audio-recording.css'
|
||||||
|
|
||||||
|
|
@ -42,6 +46,7 @@ import './audio-recording.css'
|
||||||
import chatmessageApi from 'api/chatmessage'
|
import chatmessageApi from 'api/chatmessage'
|
||||||
import chatflowsApi from 'api/chatflows'
|
import chatflowsApi from 'api/chatflows'
|
||||||
import predictionApi from 'api/prediction'
|
import predictionApi from 'api/prediction'
|
||||||
|
import chatmessagefeedbackApi from 'api/chatmessagefeedback'
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
import useApi from 'hooks/useApi'
|
import useApi from 'hooks/useApi'
|
||||||
|
|
@ -86,6 +91,9 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
|
||||||
const getChatflowConfig = useApi(chatflowsApi.getSpecificChatflow)
|
const getChatflowConfig = useApi(chatflowsApi.getSpecificChatflow)
|
||||||
|
|
||||||
const [starterPrompts, setStarterPrompts] = useState([])
|
const [starterPrompts, setStarterPrompts] = useState([])
|
||||||
|
const [chatFeedbackStatus, setChatFeedbackStatus] = useState(false)
|
||||||
|
const [feedbackId, setFeedbackId] = useState('')
|
||||||
|
const [showFeedbackContentDialog, setShowFeedbackContentDialog] = useState(false)
|
||||||
|
|
||||||
// drag & drop and file input
|
// drag & drop and file input
|
||||||
const fileUploadRef = useRef(null)
|
const fileUploadRef = useRef(null)
|
||||||
|
|
@ -318,6 +326,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
|
||||||
let allMessages = [...cloneDeep(prevMessages)]
|
let allMessages = [...cloneDeep(prevMessages)]
|
||||||
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
|
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
|
||||||
allMessages[allMessages.length - 1].message += text
|
allMessages[allMessages.length - 1].message += text
|
||||||
|
allMessages[allMessages.length - 1].feedback = null
|
||||||
return allMessages
|
return allMessages
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -389,6 +398,14 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
const data = response.data
|
const data = response.data
|
||||||
|
|
||||||
|
setMessages((prevMessages) => {
|
||||||
|
let allMessages = [...cloneDeep(prevMessages)]
|
||||||
|
if (allMessages[allMessages.length - 1].type === 'apiMessage') {
|
||||||
|
allMessages[allMessages.length - 1].id = data?.chatMessageId
|
||||||
|
}
|
||||||
|
return allMessages
|
||||||
|
})
|
||||||
|
|
||||||
if (!chatId) setChatId(data.chatId)
|
if (!chatId) setChatId(data.chatId)
|
||||||
|
|
||||||
if (input === '' && data.question) {
|
if (input === '' && data.question) {
|
||||||
|
|
@ -412,10 +429,12 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
|
||||||
...prevMessages,
|
...prevMessages,
|
||||||
{
|
{
|
||||||
message: text,
|
message: text,
|
||||||
|
id: data?.chatMessageId,
|
||||||
sourceDocuments: data?.sourceDocuments,
|
sourceDocuments: data?.sourceDocuments,
|
||||||
usedTools: data?.usedTools,
|
usedTools: data?.usedTools,
|
||||||
fileAnnotations: data?.fileAnnotations,
|
fileAnnotations: data?.fileAnnotations,
|
||||||
type: 'apiMessage'
|
type: 'apiMessage',
|
||||||
|
feedback: null
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
@ -474,7 +493,9 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
|
||||||
setChatId(chatId)
|
setChatId(chatId)
|
||||||
const loadedMessages = getChatmessageApi.data.map((message) => {
|
const loadedMessages = getChatmessageApi.data.map((message) => {
|
||||||
const obj = {
|
const obj = {
|
||||||
|
id: message.id,
|
||||||
message: message.content,
|
message: message.content,
|
||||||
|
feedback: message.feedback,
|
||||||
type: message.role
|
type: message.role
|
||||||
}
|
}
|
||||||
if (message.sourceDocuments) obj.sourceDocuments = JSON.parse(message.sourceDocuments)
|
if (message.sourceDocuments) obj.sourceDocuments = JSON.parse(message.sourceDocuments)
|
||||||
|
|
@ -527,6 +548,9 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
|
||||||
})
|
})
|
||||||
setStarterPrompts(inputFields)
|
setStarterPrompts(inputFields)
|
||||||
}
|
}
|
||||||
|
if (config.chatFeedback) {
|
||||||
|
setChatFeedbackStatus(config.chatFeedback.status)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
|
@ -604,6 +628,83 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, [previews])
|
}, [previews])
|
||||||
|
|
||||||
|
const copyMessageToClipboard = async (text) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text || '')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error copying to clipboard:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onThumbsUpClick = async (messageId) => {
|
||||||
|
const body = {
|
||||||
|
chatflowid,
|
||||||
|
chatId,
|
||||||
|
messageId,
|
||||||
|
rating: 'THUMBS_UP',
|
||||||
|
content: ''
|
||||||
|
}
|
||||||
|
const result = await chatmessagefeedbackApi.addFeedback(chatflowid, body)
|
||||||
|
if (result.data) {
|
||||||
|
const data = result.data
|
||||||
|
let id = ''
|
||||||
|
if (data && data.id) id = data.id
|
||||||
|
setMessages((prevMessages) => {
|
||||||
|
const allMessages = [...cloneDeep(prevMessages)]
|
||||||
|
return allMessages.map((message) => {
|
||||||
|
if (message.id === messageId) {
|
||||||
|
message.feedback = {
|
||||||
|
rating: 'THUMBS_UP'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
})
|
||||||
|
})
|
||||||
|
setFeedbackId(id)
|
||||||
|
setShowFeedbackContentDialog(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onThumbsDownClick = async (messageId) => {
|
||||||
|
const body = {
|
||||||
|
chatflowid,
|
||||||
|
chatId,
|
||||||
|
messageId,
|
||||||
|
rating: 'THUMBS_DOWN',
|
||||||
|
content: ''
|
||||||
|
}
|
||||||
|
const result = await chatmessagefeedbackApi.addFeedback(chatflowid, body)
|
||||||
|
if (result.data) {
|
||||||
|
const data = result.data
|
||||||
|
let id = ''
|
||||||
|
if (data && data.id) id = data.id
|
||||||
|
setMessages((prevMessages) => {
|
||||||
|
const allMessages = [...cloneDeep(prevMessages)]
|
||||||
|
return allMessages.map((message) => {
|
||||||
|
if (message.id === messageId) {
|
||||||
|
message.feedback = {
|
||||||
|
rating: 'THUMBS_DOWN'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
})
|
||||||
|
})
|
||||||
|
setFeedbackId(id)
|
||||||
|
setShowFeedbackContentDialog(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitFeedbackContent = async (text) => {
|
||||||
|
const body = {
|
||||||
|
content: text
|
||||||
|
}
|
||||||
|
const result = await chatmessagefeedbackApi.updateFeedback(feedbackId, body)
|
||||||
|
if (result.data) {
|
||||||
|
setFeedbackId('')
|
||||||
|
setShowFeedbackContentDialog(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onDragEnter={handleDrag}>
|
<div onDragEnter={handleDrag}>
|
||||||
{isDragActive && (
|
{isDragActive && (
|
||||||
|
|
@ -747,6 +848,36 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
|
||||||
{message.message}
|
{message.message}
|
||||||
</MemoizedReactMarkdown>
|
</MemoizedReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
|
{message.type === 'apiMessage' && message.id && chatFeedbackStatus ? (
|
||||||
|
<>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'start', gap: 1 }}>
|
||||||
|
<CopyToClipboardButton onClick={() => copyMessageToClipboard(message.message)} />
|
||||||
|
{!message.feedback ||
|
||||||
|
message.feedback.rating === '' ||
|
||||||
|
message.feedback.rating === 'THUMBS_UP' ? (
|
||||||
|
<ThumbsUpButton
|
||||||
|
isDisabled={message.feedback && message.feedback.rating === 'THUMBS_UP'}
|
||||||
|
rating={message.feedback ? message.feedback.rating : ''}
|
||||||
|
onClick={() => onThumbsUpClick(message.id)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{!message.feedback ||
|
||||||
|
message.feedback.rating === '' ||
|
||||||
|
message.feedback.rating === 'THUMBS_DOWN' ? (
|
||||||
|
<ThumbsDownButton
|
||||||
|
isDisabled={message.feedback && message.feedback.rating === 'THUMBS_DOWN'}
|
||||||
|
rating={message.feedback ? message.feedback.rating : ''}
|
||||||
|
onClick={() => onThumbsDownClick(message.id)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
<ChatFeedbackContentDialog
|
||||||
|
show={showFeedbackContentDialog}
|
||||||
|
onCancel={() => setShowFeedbackContentDialog(false)}
|
||||||
|
onConfirm={submitFeedbackContent}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
{message.fileAnnotations && (
|
{message.fileAnnotations && (
|
||||||
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
|
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
|
||||||
{message.fileAnnotations.map((fileAnnotation, index) => {
|
{message.fileAnnotations.map((fileAnnotation, index) => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue