diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cd29c1..2d4aeea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to ## [Unreleased] ### Added - +- ✨(front) add code copy button - ✨(RAG) add generic collection RAG tools #159 ### Fixed 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 438d754..2b6dc80 100644 --- a/src/frontend/apps/conversations/src/features/chat/components/Chat.tsx +++ b/src/frontend/apps/conversations/src/features/chat/components/Chat.tsx @@ -8,6 +8,7 @@ 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'; import { useCallback, useEffect, useRef, useState } from 'react'; +import type { ChangeEvent, FormEvent } from 'react'; import { useTranslation } from 'react-i18next'; import { MarkdownHooks } from 'react-markdown'; import rehypeKatex from 'rehype-katex'; @@ -26,6 +27,7 @@ import { useLLMConfiguration, } from '@/features/chat/api/useLLMConfiguration'; import { AttachmentList } from '@/features/chat/components/AttachmentList'; +import { CodeBlock } from '@/features/chat/components/CodeBlock'; import { FeedbackButtons } from '@/features/chat/components/FeedbackButtons'; import { InputChat } from '@/features/chat/components/InputChat'; import { SourceItemList } from '@/features/chat/components/SourceItemList'; @@ -153,7 +155,7 @@ export const Chat = ({ const [initialConversationMessages, setInitialConversationMessages] = useState(undefined); const [pendingFirstMessage, setPendingFirstMessage] = useState<{ - event: React.FormEvent; + event: FormEvent; attachments?: Attachment[]; forceWebSearch?: boolean; } | null>(null); @@ -244,7 +246,7 @@ export const Chat = ({ void stopGeneration(); }; - const handleSubmitWrapper = (event: React.FormEvent) => { + const handleSubmitWrapper = (event: FormEvent) => { void handleSubmit(event); }; @@ -361,7 +363,7 @@ export const Chat = ({ if (initialConversationId !== conversationId) { handleInputChange({ target: { value: '' }, - } as React.ChangeEvent); + } as ChangeEvent); setHasInitialized(false); // Réinitialiser pour permettre le scroll au prochain chargement } }, [initialConversationId, conversationId, handleInputChange]); @@ -375,7 +377,7 @@ export const Chat = ({ if (pendingInput) { const syntheticEvent = { target: { value: pendingInput }, - } as React.ChangeEvent; + } as ChangeEvent; handleInputChange(syntheticEvent); } if (pendingFiles) { @@ -397,7 +399,7 @@ export const Chat = ({ const syntheticFormEvent = { preventDefault: () => {}, target: form, - } as unknown as React.FormEvent; + } as unknown as FormEvent; void handleSubmit(syntheticFormEvent); setShouldAutoSubmit(false); } @@ -457,7 +459,7 @@ export const Chat = ({ }, [hasInitialized, messages.length]); // Custom handleSubmit to include attachments and handle chat creation - const handleSubmit = async (event: React.FormEvent) => { + const handleSubmit = async (event: FormEvent) => { event.preventDefault(); // Upload files to server and get URLs @@ -677,6 +679,10 @@ export const Chat = ({ {children} ), + // eslint-disable-next-line @typescript-eslint/no-unused-vars + pre: ({ node, children, ...props }) => ( + {children} + ), }} > {message.content} diff --git a/src/frontend/apps/conversations/src/features/chat/components/CodeBlock.tsx b/src/frontend/apps/conversations/src/features/chat/components/CodeBlock.tsx new file mode 100644 index 0000000..875f49e --- /dev/null +++ b/src/frontend/apps/conversations/src/features/chat/components/CodeBlock.tsx @@ -0,0 +1,80 @@ +import { useRef } from 'react'; +import type { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Box, Icon, Text } from '@/components'; +import { useClipboard } from '@/hook'; + +interface CopyCodeButtonProps { + onCopy: () => void; +} + +const CopyCodeButton = ({ onCopy }: CopyCodeButtonProps) => { + const { t } = useTranslation(); + + return ( + + + + {t('Copy code')} + + + ); +}; + +interface CodeBlockProps { + children: ReactNode; + [key: string]: unknown; +} + +export const CodeBlock = ({ children, ...props }: CodeBlockProps) => { + const preRef = useRef(null); + const copyToClipboard = useClipboard(); + + const handleCopy = () => { + const code = preRef.current?.querySelector('code'); + copyToClipboard(code?.textContent || ''); + }; + + return ( + <> + + + {children} + + + ); +}; diff --git a/src/frontend/apps/conversations/src/pages/globals.css b/src/frontend/apps/conversations/src/pages/globals.css index a20f07e..75d0c89 100644 --- a/src/frontend/apps/conversations/src/pages/globals.css +++ b/src/frontend/apps/conversations/src/pages/globals.css @@ -235,6 +235,7 @@ ul a:hover { figure[data-rehype-pretty-code-figure] { margin-left: 0; margin-right: 0; + position: relative; } figure[data-rehype-pretty-code-figure] > pre {