diff --git a/CHANGELOG.md b/CHANGELOG.md index f0333d5..0017a29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ and this project adheres to ## [0.0.11] - 2026-01-16 ### Changed -- 🐛(front) optimize chat + - 📦️(front) update react - ✨(chat) Generate and edit conversation title diff --git a/src/frontend/apps/conversations/src/components/Loader.tsx b/src/frontend/apps/conversations/src/components/Loader.tsx index b3ba0c3..609d0d0 100644 --- a/src/frontend/apps/conversations/src/components/Loader.tsx +++ b/src/frontend/apps/conversations/src/components/Loader.tsx @@ -1,8 +1,7 @@ import dynamic from 'next/dynamic'; -import searchingAnimation from '@/assets/lotties/searching'; - const Lottie = dynamic(() => import('lottie-react'), { ssr: false }); +import searchingAnimation from '@/assets/lotties/searching'; export function Loader() { return ( 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 2198a11..b563a07 100644 --- a/src/frontend/apps/conversations/src/features/chat/components/Chat.tsx +++ b/src/frontend/apps/conversations/src/features/chat/components/Chat.tsx @@ -1,4 +1,4 @@ -import { Message, SourceUIPart, ToolInvocationUIPart } from '@ai-sdk/ui-utils'; +import { Message, SourceUIPart } from '@ai-sdk/ui-utils'; import { Modal, ModalSize } from '@openfun/cunningham-react'; import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you import { useRouter } from 'next/router'; @@ -664,299 +664,19 @@ export const Chat = ({ return ( - - {message.experimental_attachments && - message.experimental_attachments.length > 0 && ( - - - - )} - - {/* Message content */} - {message.content && ( - - - {message.role === 'user' - ? t('You said: ') - : t('Assistant IA replied: ')} - - {message.role === 'user' ? ( - - {message.content} - - ) : ( - ( - - ), - a: ({ children, ...props }) => ( - - {children} - - ), - // eslint-disable-next-line @typescript-eslint/no-unused-vars - pre: ({ node, children, ...props }) => ( - {children} - ), - }} - > - {message.content} - - )} - - )} - - - {isCurrentlyStreaming && - isLastAssistantMessageInConversation && - status === 'streaming' && - message.parts?.some( - (part) => - part.type === 'tool-invocation' && - part.toolInvocation.toolName !== - 'document_parsing', - ) && ( - - - - {(() => { - const toolInvocation = message.parts?.find( - (part) => - part.type === 'tool-invocation' && - part.toolInvocation.toolName !== - 'document_parsing', - ); - if ( - toolInvocation?.type === - 'tool-invocation' && - toolInvocation.toolInvocation.toolName === - 'summarize' - ) { - return t('Summarizing...'); - } - return t('Search...'); - })()} - - - )} - {message.parts - ?.filter((part) => part.type === 'tool-invocation') - .map( - (part: ToolInvocationUIPart, partIndex: number) => - part.type === 'tool-invocation' && - isCurrentlyStreaming && - isLastAssistantMessageInConversation ? ( - - ) : null, - )} - - {message.role === 'assistant' && - !( - isLastAssistantMessageInConversation && - status === 'streaming' - ) && ( - - - 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', - ) && - (() => { - const sourceCount = - message.parts?.filter( - (part) => part.type === 'source', - ).length || 0; - return ( - openSources(message.id)} - onKeyDown={(e) => { - if ( - e.key === 'Enter' || - e.key === ' ' - ) { - e.preventDefault(); - openSources(message.id); - } - }} - role="button" - tabIndex={0} - > - - - {t('Show')} {sourceCount}{' '} - {sourceCount !== 1 - ? t('sources') - : t('source')} - - - ); - })()} - - - {/* We should display the button, but disabled if no trace linked */} - {conversationId && - message.id && - message.id.startsWith('trace-') && ( - - )} - - - )} - {message.parts && - isSourceOpen === message.id && - (() => { - const sourceParts = message.parts.filter( - (part): part is SourceUIPart => - part.type === 'source', - ); - return ( - - - - ); - })()} - - - + message={message} + isLastAssistantMessageInConversation={ + isLastAssistantMessageInConversation + } + shouldApplyStreamingHeight={shouldApplyStreamingHeight} + streamingMessageHeight={streamingMessageHeight} + isCurrentlyStreaming={isCurrentlyStreaming} + status={isCurrentlyStreaming ? status : 'ready'} // Ne passer le vrai status que si nécessaire + isSourceOpen={isSourceOpen} + conversationId={conversationId} + onOpenSources={openSources} + getMetadata={getMetadata} + /> ); })} diff --git a/src/frontend/apps/conversations/src/features/chat/components/ChatMessage.tsx b/src/frontend/apps/conversations/src/features/chat/components/ChatMessage.tsx deleted file mode 100644 index c9c463f..0000000 --- a/src/frontend/apps/conversations/src/features/chat/components/ChatMessage.tsx +++ /dev/null @@ -1,386 +0,0 @@ -import { - Message, - ReasoningUIPart, - SourceUIPart, - ToolInvocationUIPart, -} from '@ai-sdk/ui-utils'; -import 'katex/dist/katex.min.css'; -import { memo, useDeferredValue } from 'react'; -import { useTranslation } from 'react-i18next'; -import { MarkdownHooks } from 'react-markdown'; -import rehypeKatex from 'rehype-katex'; -import rehypePrettyCode from 'rehype-pretty-code'; -import remarkGfm from 'remark-gfm'; -import remarkMath from 'remark-math'; - -import { Box, Icon, Text } from '@/components'; -import { useClipboard } from '@/hook'; -import { useResponsiveStore } from '@/stores'; - -import { AttachmentList } from './AttachmentList'; -import { CodeBlock } from './CodeBlock'; -import { FeedbackButtons } from './FeedbackButtons'; -import { SourceItemList } from './SourceItemList'; -import { ToolInvocationItem } from './ToolInvocationItem'; - -// Mémoriser les plugins Markdown en dehors du composant pour éviter les recréations -const remarkPlugins = [remarkGfm, remarkMath]; -const rehypePlugins = [ - [ - rehypePrettyCode, - { - theme: 'github-dark-dimmed', - }, - ], - rehypeKatex, -]; - -// Composants Markdown mémorisés -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const markdownComponents: any = { - // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any - p: ({ node, ...props }: any) => ( - - ), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - a: ({ children, ...props }: any) => ( - - {children} - - ), - // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any - pre: ({ node, children, ...props }: any) => ( - {children} - ), -}; - -// Composant Markdown mémorisé pour éviter les recalculs inutiles -const MemoizedMarkdown = memo(function MemoizedMarkdown({ - content, -}: { - content: string; -}) { - return ( - - {content} - - ); -}); - -interface ChatMessageProps { - message: Message; - isLastAssistantMessageInConversation: boolean; - shouldApplyStreamingHeight: boolean; - streamingMessageHeight: number | null; - isCurrentlyStreaming: boolean; - status: 'idle' | 'streaming' | 'submitted' | 'ready' | 'error'; - isSourceOpen: string | null; - conversationId: string | undefined; - onOpenSources: (messageId: string) => void; - getMetadata: (url: string) => - | { - title: string | null; - favicon: string | null; - loading: boolean; - error: boolean; - } - | undefined; -} - -export const ChatMessage = memo(function ChatMessage({ - message, - isLastAssistantMessageInConversation, - shouldApplyStreamingHeight, - streamingMessageHeight, - isCurrentlyStreaming, - status, - isSourceOpen, - conversationId, - onOpenSources, - getMetadata, -}: ChatMessageProps) { - const { t } = useTranslation(); - const copyToClipboard = useClipboard(); - const { isMobile } = useResponsiveStore(); - - const deferredContent = useDeferredValue(message.content); - - const contentToRender = - message.role === 'assistant' ? deferredContent : message.content; - - return ( - - - {message.experimental_attachments && - message.experimental_attachments.length > 0 && ( - - - - )} - - {message.content && ( - - - {message.role === 'user' - ? t('You said: ') - : t('Assistant IA replied: ')} - - {message.role === 'user' ? ( - - {message.content} - - ) : ( - - )} - - )} - - - {isCurrentlyStreaming && - isLastAssistantMessageInConversation && - status === 'streaming' && - message.parts?.some( - (part) => - part.type === 'tool-invocation' && - part.toolInvocation.toolName !== 'document_parsing', - ) && ( - - - {(() => { - const toolInvocation = message.parts?.find( - (part) => - part.type === 'tool-invocation' && - part.toolInvocation.toolName !== 'document_parsing', - ); - if ( - toolInvocation?.type === 'tool-invocation' && - toolInvocation.toolInvocation.toolName === 'summarize' - ) { - return t('Summarizing...'); - } - return t('Search...'); - })()} - - - )} - {message.parts - ?.filter( - (part) => - part.type === 'reasoning' || part.type === 'tool-invocation', - ) - .map( - ( - part: ReasoningUIPart | ToolInvocationUIPart, - partIndex: number, - ) => - part.type === 'reasoning' ? ( - - {part.reasoning} - - ) : part.type === 'tool-invocation' && - isCurrentlyStreaming && - isLastAssistantMessageInConversation ? ( - - ) : null, - )} - - {message.role === 'assistant' && - !( - isLastAssistantMessageInConversation && status === 'streaming' - ) && ( - - - 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') && - (() => { - const sourceCount = - message.parts?.filter((part) => part.type === 'source') - .length || 0; - return ( - onOpenSources(message.id)} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - onOpenSources(message.id); - } - }} - role="button" - tabIndex={0} - > - - - {t('Show')} {sourceCount}{' '} - {sourceCount !== 1 ? t('sources') : t('source')} - - - ); - })()} - - - {conversationId && - message.id && - message.id.startsWith('trace-') && ( - - )} - - - )} - {message.parts && - isSourceOpen === message.id && - (() => { - const sourceParts = message.parts.filter( - (part): part is SourceUIPart => part.type === 'source', - ); - return ( - - - - ); - })()} - - - - ); -}); diff --git a/src/frontend/apps/conversations/src/features/chat/hooks/index.ts b/src/frontend/apps/conversations/src/features/chat/hooks/index.ts index 45845b9..ec6a610 100644 --- a/src/frontend/apps/conversations/src/features/chat/hooks/index.ts +++ b/src/frontend/apps/conversations/src/features/chat/hooks/index.ts @@ -1,3 +1,2 @@ export { useChatScroll } from './useChatScroll'; export { useSourceMetadataCache } from './useSourceMetadata'; -export { useModelSelection } from './useModelSelection'; diff --git a/src/frontend/apps/conversations/src/features/chat/hooks/useModelSelection.ts b/src/frontend/apps/conversations/src/features/chat/hooks/useModelSelection.ts deleted file mode 100644 index 0d21cba..0000000 --- a/src/frontend/apps/conversations/src/features/chat/hooks/useModelSelection.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; - -import { LLMModel, useLLMConfiguration } from '../api/useLLMConfiguration'; -import { useChatPreferencesStore } from '../stores/useChatPreferencesStore'; - -export const useModelSelection = () => { - const { data: llmConfig } = useLLMConfiguration(); - const { selectedModelHrid, setSelectedModelHrid } = useChatPreferencesStore(); - const [selectedModel, setSelectedModel] = useState(null); - const hasInitializedRef = useRef(false); - - useEffect(() => { - // Ne s'exécuter qu'une seule fois quand llmConfig est chargé - if (llmConfig?.models && !hasInitializedRef.current) { - let modelToSelect: LLMModel | undefined; - - if (selectedModelHrid) { - // Try to find the previously selected model - modelToSelect = llmConfig.models.find( - (model) => - model.hrid === selectedModelHrid && model.is_active !== false, - ); - } - - // If no saved model or saved model not found/inactive, use default - if (!modelToSelect) { - modelToSelect = llmConfig.models.find((model) => model.is_default); - } - - if (modelToSelect) { - setSelectedModel(modelToSelect); - setSelectedModelHrid(modelToSelect.hrid); - hasInitializedRef.current = true; - } - } - }, [llmConfig?.models, selectedModelHrid, setSelectedModelHrid]); - - const handleModelSelect = (model: LLMModel) => { - setSelectedModel(model); - setSelectedModelHrid(model.hrid); - }; - - return { selectedModel, handleModelSelect }; -};
- {message.role === 'user' - ? t('You said: ') - : t('Assistant IA replied: ')} -