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',
- ) && (
- 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',
+ ) && (
+ 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 = ({