diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e8cf57..57219fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,11 @@ and this project adheres to ## [Unreleased] -- 🎨(front) global layout modification +- 🎨(front) amelioration chat ux ### Changed +- 🎨(front) global layout modification - ✨(front) global layout UI - ♻️(chat) rewrite backend using Pydantic AI SDK #4 - 🗃️(chat) enforce messages stored JSON format #6 diff --git a/src/frontend/apps/conversations/src/features/chat/components/Chat.tsx b/src/frontend/apps/conversations/src/features/chat/components/Chat.tsx index 540e91a..19452a4 100644 --- a/src/frontend/apps/conversations/src/features/chat/components/Chat.tsx +++ b/src/frontend/apps/conversations/src/features/chat/components/Chat.tsx @@ -75,6 +75,9 @@ export const Chat = ({ } | null>(null); const [shouldAutoSubmit, setShouldAutoSubmit] = useState(false); const [hasInitialized, setHasInitialized] = useState(false); + const [streamingMessageHeight, setStreamingMessageHeight] = useState< + number | null + >(null); const { mutate: createChatConversation } = useCreateChatConversation(); @@ -172,34 +175,83 @@ export const Chat = ({ } }; - useEffect(() => { + // Calculer la hauteur pour le message de streaming + const calculateStreamingHeight = useCallback(() => { if (chatContainerRef.current) { - // Find the last user message + const container = chatContainerRef.current; + const containerHeight = container.clientHeight; + const userMessages = messages.filter((msg) => msg.role === 'user'); const lastUserMessage = userMessages[userMessages.length - 1]; if (lastUserMessage) { - // Find the element of the last user message - const messageElements = - chatContainerRef.current.querySelectorAll('[data-message-id]'); + const messageElements = container.querySelectorAll('[data-message-id]'); const lastUserMessageElement = Array.from(messageElements).find( (el) => el.getAttribute('data-message-id') === lastUserMessage.id, ); if (lastUserMessageElement) { - // Scroll to position the last user message at the very top - const messageTop = (lastUserMessageElement as HTMLElement).offsetTop; + const userMessageHeight = (lastUserMessageElement as HTMLElement) + .offsetHeight; - chatContainerRef.current.scrollTo({ - top: messageTop, - behavior: 'auto', - }); + const thinkingHeight = 90; + const availableHeight = + containerHeight - userMessageHeight - thinkingHeight; + + if (streamingMessageHeight !== availableHeight) { + setStreamingMessageHeight(availableHeight); + } } - } else { - scrollToBottom(); } } - }, [messages, hasInitialized, scrollToBottom]); + }, [messages, streamingMessageHeight]); + + useEffect(() => { + if (chatContainerRef.current && messages.length > 0) { + const userMessages = messages.filter((msg) => msg.role === 'user'); + const assistantMessages = messages.filter( + (msg) => msg.role === 'assistant', + ); + const lastMessage = messages[messages.length - 1]; + + // Gérer la hauteur de streaming + if ( + lastMessage && + lastMessage.role === 'user' && + assistantMessages.length > 0 && + status === 'ready' + ) { + // Nouveau message user détecté, réinitialiser la hauteur + setStreamingMessageHeight(null); + } else if (status === 'streaming' || status === 'submitted') { + // Calculer la hauteur pendant le streaming + calculateStreamingHeight(); + } + + if ( + hasInitialized && + (status === 'streaming' || status === 'submitted') + ) { + const lastUserMessage = userMessages[userMessages.length - 1]; + if (lastUserMessage) { + const messageElements = + chatContainerRef.current.querySelectorAll('[data-message-id]'); + const lastUserMessageElement = Array.from(messageElements).find( + (el) => el.getAttribute('data-message-id') === lastUserMessage.id, + ); + + if (lastUserMessageElement) { + const messageTop = (lastUserMessageElement as HTMLElement) + .offsetTop; + chatContainerRef.current.scrollTo({ + top: messageTop, + behavior: 'smooth', + }); + } + } + } + } + }, [messages, status, hasInitialized, calculateStreamingHeight]); // Synchronize conversationId state with prop when it changes (e.g., after navigation) useEffect(() => { @@ -262,6 +314,15 @@ export const Chat = ({ if (!ignore) { setInitialConversationMessages(conversation.messages); setHasInitialized(true); + + setTimeout(() => { + if (chatContainerRef.current) { + chatContainerRef.current.scrollTo({ + top: chatContainerRef.current.scrollHeight, + behavior: 'auto', + }); + } + }, 100); } } catch { // Optionally handle error (e.g., setInitialConversationMessages([]) or show error) @@ -357,7 +418,6 @@ export const Chat = ({ } else { baseHandleSubmit(event); } - // Attendre un peu avant de vider les fichiers pour s'assurer qu'ils sont traités setTimeout(() => { setFiles(null); @@ -396,101 +456,110 @@ export const Chat = ({ > {messages.length > 0 && ( - {messages.map((message) => ( - + {messages.map((message, index) => { + const isLastAssistantMessageInConversation = + message.role === 'assistant' && + index === + messages.findLastIndex((msg) => msg.role === 'assistant'); + const shouldApplyStreamingHeight = + isLastAssistantMessageInConversation && streamingMessageHeight; + + return ( - {/* Message content */} - {message.content && ( - + {/* Message content */} + {message.content && ( + - , - }} > - {message.content} - - - )} - - {/* Attachments section */} - {message.experimental_attachments && - message.experimental_attachments.length > 0 && ( - - + , + }} + > + {message.content} + )} - {/* Reasoning and tool invocations */} - - {message.parts - ?.filter( - (part) => - part.type === 'reasoning' || - part.type === 'tool-invocation', - ) - .map((part: ReasoningUIPart | ToolInvocationUIPart) => - part.type === 'reasoning' ? ( - - {part.reasoning} - - ) : part.type === 'tool-invocation' ? ( - 0 && ( + + - ) : null, + )} - - {message.role !== 'user' && ( - + + {/* Reasoning and tool invocations */} + + {message.parts + ?.filter( + (part) => + part.type === 'reasoning' || + part.type === 'tool-invocation', + ) + .map((part: ReasoningUIPart | ToolInvocationUIPart) => + part.type === 'reasoning' ? ( + + {part.reasoning} + + ) : part.type === 'tool-invocation' ? ( + + ) : null, + )} + + {message.role !== 'user' && ( + copyToClipboard(message.content)} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - copyToClipboard(message.content); - } - }} - role="button" - tabIndex={0} - > - - {!isMobile && ( - - {t('Copy')} - - )} - - {message.parts?.some( - (part) => part.type === 'source', - ) && ( - + + {!isMobile && ( + + {t('Copy')} + + )} + + {message.parts?.some( + (part) => part.type === 'source', + ) && ( + openSources(message.id)} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - openSources(message.id); - } - }} - role="button" - tabIndex={0} - > - - {!isMobile && ( - - {t('Show sources')} - - )} - - )} - - )} - {message.parts && isSourceOpen === message.id && ( - part.type === 'source', - )} - /> - )} + onClick={() => openSources(message.id)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + openSources(message.id); + } + }} + role="button" + tabIndex={0} + > + + {!isMobile && ( + + {t('Show sources')} + + )} + + )} + + )} + {message.parts && isSourceOpen === message.id && ( + + part.type === 'source', + )} + /> + )} + - - ))} + ); + })} )} - {(status === 'streaming' || status === 'submitted') && ( + {status !== 'ready' && ( { @@ -175,11 +175,6 @@ export const InputChat = ({