@@ -16,34 +16,20 @@ import type { Message } from './AIChat'
1616
1717const COPY_RESET_TIMEOUT_MS = 2000
1818const EXPORT_MENU_FORMATS : ExportAIMessageFormat [ ] = [ 'docx' , 'pdf' , 'txt' ]
19+ const SUGGESTION_ANIMATION_DELAY_MS = 70
1920
2021export type AIChatMessageProps = {
2122 message : Message
2223 onSuggestionClick ?: ( suggestion : string ) => void
2324 canExecuteAIFlow ?: boolean
2425}
2526
26- export const AIChatMessage : React . FC < AIChatMessageProps > = ( {
27- message,
28- onSuggestionClick,
29- canExecuteAIFlow = true ,
30- } ) => {
27+ const useCopyButton = ( message : Message ) => {
3128 const intl = useIntl ( )
32- const assistantMarkdownRef = useRef < HTMLDivElement > ( null )
3329 const [ copied , setCopied ] = useState ( false )
34- const [ exportLoadingByFormat , setExportLoadingByFormat ] = useState < Record < ExportAIMessageFormat , boolean > > ( {
35- txt : false ,
36- pdf : false ,
37- docx : false ,
38- } )
3930
4031 const copyLabel = intl . formatMessage ( { id : 'Copy' } )
4132 const copiedLabel = intl . formatMessage ( { id : 'Copied' } )
42- const downloadLabel = intl . formatMessage ( { id : 'Download' } )
43- const exportMenuItems = EXPORT_MENU_FORMATS . map ( ( format ) => ( {
44- key : format ,
45- label : `${ downloadLabel } ${ format . toUpperCase ( ) } ` ,
46- } ) )
4733
4834 const handleCopy = useCallback ( async ( ) => {
4935 if ( copied ) return
@@ -62,6 +48,86 @@ export const AIChatMessage: React.FC<AIChatMessageProps> = ({
6248 }
6349 } , [ copied , message . content . text , message . role ] )
6450
51+ const copyButton = (
52+ < Tooltip title = { copied ? copiedLabel : copyLabel } >
53+ < Button
54+ type = 'secondary'
55+ compact
56+ minimal
57+ size = 'medium'
58+ icon = { copied ? < Check size = 'small' /> : < Copy size = 'small' /> }
59+ onClick = { handleCopy }
60+ disabled = { copied }
61+ aria-label = { copied ? copiedLabel : copyLabel }
62+ />
63+ </ Tooltip >
64+ )
65+
66+ return copyButton
67+ }
68+
69+ const AIChatUserMessage : React . FC < { message : Message } > = ( { message } ) => {
70+ const copyButton = useCopyButton ( message )
71+
72+ return (
73+ < div
74+ data-message-id = { message . id }
75+ className = { `${ styles . messageWrapper } ${ styles . userMessage } ` }
76+ >
77+ < div className = { styles . userMessageContainer } >
78+ < div className = { styles . userMessageRow } >
79+ { message . copyable === true && message . content . text ?. trim ( ) && (
80+ < div className = { styles . userMessageActions } > { copyButton } </ div >
81+ ) }
82+ { ( message . content . text ?. trim ( ) || message . content . attachments ?. length ) ? (
83+ < div className = { styles . userMessageBubble } >
84+ { message . content . text ?. trim ( ) ? (
85+ < Typography . Text > { message . content . text } </ Typography . Text >
86+ ) : null }
87+ { message . content . attachments ?. length ? (
88+ < div className = { styles . userMessageAttachments } >
89+ { message . content . attachments . map ( ( attachment , index ) => (
90+ < AIChatDocument
91+ key = { `${ attachment . name } -${ index } ` }
92+ name = { attachment . name }
93+ />
94+ ) ) }
95+ </ div >
96+ ) : null }
97+ </ div >
98+ ) : null }
99+ </ div >
100+ </ div >
101+ </ div >
102+ )
103+ }
104+
105+ type AIChatAssistantMessageProps = {
106+ message : Message
107+ onSuggestionClick ?: ( suggestion : string ) => void
108+ canExecuteAIFlow : boolean
109+ }
110+
111+ const AIChatAssistantMessage : React . FC < AIChatAssistantMessageProps > = ( {
112+ message,
113+ onSuggestionClick,
114+ canExecuteAIFlow,
115+ } ) => {
116+ const intl = useIntl ( )
117+ const assistantMarkdownRef = useRef < HTMLDivElement > ( null )
118+ const copyButton = useCopyButton ( message )
119+ const [ exportLoadingByFormat , setExportLoadingByFormat ] = useState < Record < ExportAIMessageFormat , boolean > > ( {
120+ txt : false ,
121+ pdf : false ,
122+ docx : false ,
123+ } )
124+
125+ const downloadLabel = intl . formatMessage ( { id : 'Download' } )
126+ const exportMenuItems = EXPORT_MENU_FORMATS . map ( ( format ) => ( {
127+ key : format ,
128+ label : `${ downloadLabel } ${ format . toUpperCase ( ) } ` ,
129+ } ) )
130+
65131 const handleExport = useCallback ( async ( format : ExportAIMessageFormat ) => {
66132 if ( exportLoadingByFormat [ format ] ) return
67133
@@ -89,21 +155,6 @@ export const AIChatMessage: React.FC<AIChatMessageProps> = ({
89155
90156 const isExporting = exportLoadingByFormat . txt || exportLoadingByFormat . pdf || exportLoadingByFormat . docx
91157
92- const copyButton = (
93- < Tooltip title = { copied ? copiedLabel : copyLabel } >
94- < Button
95- type = 'secondary'
96- compact
97- minimal
98- size = 'medium'
99- icon = { copied ? < Check size = 'small' /> : < Copy size = 'small' /> }
100- onClick = { handleCopy }
101- disabled = { copied }
102- aria-label = { copied ? copiedLabel : copyLabel }
103- />
104- </ Tooltip >
105- )
106-
107158 const downloadButton = (
108159 < Dropdown
109160 trigger = { [ 'click' ] }
@@ -131,64 +182,55 @@ export const AIChatMessage: React.FC<AIChatMessageProps> = ({
131182 return (
132183 < div
133184 data-message-id = { message . id }
134- className = { `${ styles . messageWrapper } ${ message . role === 'user' ? styles . userMessage : styles . assistantMessage } ` }
185+ className = { `${ styles . messageWrapper } ${ styles . assistantMessage } ` }
135186 >
136- { message . role === 'user' ? (
137- < div className = { styles . userMessageContainer } >
138- < div className = { styles . userMessageRow } >
139- { message . copyable === true && message . content . text ?. trim ( ) && (
140- < div className = { styles . userMessageActions } > { copyButton } </ div >
141- ) }
142- { ( message . content . text ?. trim ( ) || message . content . attachments ?. length ) ? (
143- < div className = { styles . userMessageBubble } >
144- { message . content . text ?. trim ( ) ? (
145- < Typography . Text > { message . content . text } </ Typography . Text >
146- ) : null }
147- { message . content . attachments ?. length ? (
148- < div className = { styles . userMessageAttachments } >
149- { message . content . attachments . map ( ( attachment , index ) => (
150- < AIChatDocument
151- key = { `${ attachment . name } -${ index } ` }
152- name = { attachment . name }
153- />
154- ) ) }
155- </ div >
156- ) : null }
157- </ div >
158- ) : null }
187+ < div className = { styles . assistantMessageContainer } >
188+ { message . status === 'sending' && ! message . content . text ?. trim ( ) ? (
189+ < AIChatThinkingStatus />
190+ ) : (
191+ < div
192+ ref = { assistantMarkdownRef }
193+ className = { styles . assistantMarkdown }
194+ >
195+ < Markdown type = 'inline' > { message . content . text } </ Markdown >
159196 </ div >
160- </ div >
161- ) : (
162- < div className = { styles . assistantMessageContainer } >
163- { message . status === 'sending' && ! message . content . text ?. trim ( ) ? (
164- < AIChatThinkingStatus />
165- ) : (
166- < div
167- ref = { assistantMarkdownRef }
168- className = { styles . assistantMarkdown }
169- >
170- < Markdown type = 'inline' > { message . content . text } </ Markdown >
171- </ div >
172- ) }
173- { message . copyable === true && message . status !== 'sending' && (
174- < div className = { styles . assistantMessageActions } >
175- { copyButton }
176- { downloadButton }
177- </ div >
178- ) }
179- { message . content . suggestions ?. length > 0 && (
180- < AIChatSuggestions
181- items = { message . content . suggestions . map ( ( suggestion , index ) => ( {
182- key : `${ message . id } -${ suggestion } ` ,
183- label : suggestion ,
184- disabled : ! canExecuteAIFlow ,
185- animationDelayMs : index * 70 ,
186- onClick : ( ) => onSuggestionClick ?.( suggestion ) ,
187- } ) ) }
188- />
189- ) }
190- </ div >
191- ) }
197+ ) }
198+ { message . copyable === true && message . status !== 'sending' && (
199+ < div className = { styles . assistantMessageActions } >
200+ { copyButton }
201+ { downloadButton }
202+ </ div >
203+ ) }
204+ { message . content . suggestions ?. length > 0 && (
205+ < AIChatSuggestions
206+ items = { message . content . suggestions . map ( ( suggestion , index ) => ( {
207+ key : `${ message . id } -${ suggestion } ` ,
208+ label : suggestion ,
209+ disabled : ! canExecuteAIFlow ,
210+ animationDelayMs : index * SUGGESTION_ANIMATION_DELAY_MS ,
211+ onClick : ( ) => onSuggestionClick ?.( suggestion ) ,
212+ } ) ) }
213+ />
214+ ) }
215+ </ div >
192216 </ div >
193217 )
194218}
219+
220+ export const AIChatMessage : React . FC < AIChatMessageProps > = ( {
221+ message,
222+ onSuggestionClick,
223+ canExecuteAIFlow = true ,
224+ } ) => {
225+ if ( message . role === 'user' ) {
226+ return < AIChatUserMessage message = { message } />
227+ }
228+
229+ return (
230+ < AIChatAssistantMessage
231+ message = { message }
232+ onSuggestionClick = { onSuggestionClick }
233+ canExecuteAIFlow = { canExecuteAIFlow }
234+ />
235+ )
236+ }
0 commit comments