From e08f7cbae847d1ba08ef308d5e8c4f0fbaddde5c Mon Sep 17 00:00:00 2001 From: Willy Douhard Date: Sat, 8 Feb 2025 12:50:04 -0800 Subject: [PATCH] fix: use accordion for steps and fix streaming auto scroll (#1874) * fix: use accordion for steps and fix streaming auto scroll * fix: test * fix: test * fix: only show output title in step if output is defined * fix: should not be able to edit a message in read only thread * fix: scroll down button --- cypress/e2e/remove_elements/spec.cy.ts | 1 + cypress/e2e/streaming/spec.cy.ts | 4 +- cypress/e2e/update_step/spec.cy.ts | 4 +- frontend/src/components/CodeSnippet.tsx | 5 +- frontend/src/components/MarkdownAlert.tsx | 2 - frontend/src/components/ReadOnlyThread.tsx | 1 + frontend/src/components/chat/Footer.tsx | 14 +-- .../components/chat/MessageComposer/index.tsx | 14 ++- .../chat/Messages/Message/Content/index.tsx | 19 ++-- .../components/chat/Messages/Message/Step.tsx | 100 +++++++++++------- .../chat/Messages/Message/UserMessage.tsx | 12 +-- .../chat/MessagesContainer/index.tsx | 1 + .../src/components/chat/ScrollContainer.tsx | 74 +++++++++---- .../src/components/chat/WelcomeScreen.tsx | 10 +- frontend/src/components/chat/index.tsx | 11 +- frontend/src/contexts/MessageContext.tsx | 1 + frontend/src/types/messageContext.ts | 1 + libs/copilot/src/chat/body.tsx | 12 +-- 18 files changed, 167 insertions(+), 119 deletions(-) diff --git a/cypress/e2e/remove_elements/spec.cy.ts b/cypress/e2e/remove_elements/spec.cy.ts index 3f9d17ea7f..9a1f9952c5 100644 --- a/cypress/e2e/remove_elements/spec.cy.ts +++ b/cypress/e2e/remove_elements/spec.cy.ts @@ -9,6 +9,7 @@ describe('remove_elements', () => { cy.get('#step-tool1').should('exist'); cy.get('#step-tool1').click(); cy.get('#step-tool1') + .parent() .parent() .find('.inline-image') .should('have.length', 1); diff --git a/cypress/e2e/streaming/spec.cy.ts b/cypress/e2e/streaming/spec.cy.ts index 704d811702..2530664436 100644 --- a/cypress/e2e/streaming/spec.cy.ts +++ b/cypress/e2e/streaming/spec.cy.ts @@ -13,9 +13,9 @@ function toolStream(tool: string) { const toolCall = cy.get(`#step-${tool}`); toolCall.click(); for (const token of tokenList) { - toolCall.parent().should('contain', token); + toolCall.parent().parent().should('contain', token); } - toolCall.parent().should('contain', tokenList.join(' ')); + toolCall.parent().parent().should('contain', tokenList.join(' ')); } describe('Streaming', () => { diff --git a/cypress/e2e/update_step/spec.cy.ts b/cypress/e2e/update_step/spec.cy.ts index f8f02f922f..18d4ab229e 100644 --- a/cypress/e2e/update_step/spec.cy.ts +++ b/cypress/e2e/update_step/spec.cy.ts @@ -9,9 +9,9 @@ describe('Update Step', () => { cy.get(`#step-tool1`).click(); cy.get('.step').should('have.length', 2); cy.get('.step').eq(0).should('contain', 'Hello!'); - cy.get(`#step-tool1`).parent().should('contain', 'Foo'); + cy.get(`#step-tool1`).parent().parent().should('contain', 'Foo'); cy.get('.step').eq(0).should('contain', 'Hello again!'); - cy.get(`#step-tool1`).parent().should('contain', 'Foo Bar'); + cy.get(`#step-tool1`).parent().parent().should('contain', 'Foo Bar'); }); }); diff --git a/frontend/src/components/CodeSnippet.tsx b/frontend/src/components/CodeSnippet.tsx index 4bb900173a..727c8de98f 100644 --- a/frontend/src/components/CodeSnippet.tsx +++ b/frontend/src/components/CodeSnippet.tsx @@ -1,3 +1,4 @@ +import { cn } from '@/lib/utils'; import hljs from 'highlight.js'; import { useEffect, useRef } from 'react'; @@ -64,7 +65,9 @@ export default function CodeSnippet({ ...props }: CodeProps) { ) : null; const nonHighlightedCode = showSyntaxHighlighter ? null : ( -
+
{code}
); diff --git a/frontend/src/components/MarkdownAlert.tsx b/frontend/src/components/MarkdownAlert.tsx index 54125ee9d5..acb83d1795 100644 --- a/frontend/src/components/MarkdownAlert.tsx +++ b/frontend/src/components/MarkdownAlert.tsx @@ -174,8 +174,6 @@ const AlertComponent = ({ const style = variantStyles[variant]; const Icon = style.Icon; - // console.log('AlertComponent rendering:', variant, children); - return (
diff --git a/frontend/src/components/ReadOnlyThread.tsx b/frontend/src/components/ReadOnlyThread.tsx index e06d8da61c..8719c79b7f 100644 --- a/frontend/src/components/ReadOnlyThread.tsx +++ b/frontend/src/components/ReadOnlyThread.tsx @@ -146,6 +146,7 @@ const ReadOnlyThread = ({ id }: Props) => { return { allowHtml: config?.features?.unsafe_allow_html, latex: config?.features?.latex, + editable: false, loading: false, showFeedbackButtons: !!config?.dataPersistence, uiName: config?.ui?.name || '', diff --git a/frontend/src/components/chat/Footer.tsx b/frontend/src/components/chat/Footer.tsx index a6bdf1c28e..5a42a6e733 100644 --- a/frontend/src/components/chat/Footer.tsx +++ b/frontend/src/components/chat/Footer.tsx @@ -1,34 +1,26 @@ import { cn, hasMessage } from '@/lib/utils'; +import { MutableRefObject } from 'react'; import { FileSpec, useChatMessages } from '@chainlit/react-client'; import WaterMark from '@/components/WaterMark'; import MessageComposer from './MessageComposer'; -import ScrollDownButton from './ScrollDownButton'; interface Props { fileSpec: FileSpec; onFileUpload: (payload: File[]) => void; onFileUploadError: (error: string) => void; - setAutoScroll: (autoScroll: boolean) => void; - autoScroll: boolean; + autoScrollRef: MutableRefObject; showIfEmptyThread?: boolean; } -export default function ChatFooter({ - autoScroll, - showIfEmptyThread, - ...props -}: Props) { +export default function ChatFooter({ showIfEmptyThread, ...props }: Props) { const { messages } = useChatMessages(); if (!hasMessage(messages) && !showIfEmptyThread) return null; return (
- {!autoScroll ? ( - props.setAutoScroll(true)} /> - ) : null}
diff --git a/frontend/src/components/chat/MessageComposer/index.tsx b/frontend/src/components/chat/MessageComposer/index.tsx index 99b39a4ed7..889a86313d 100644 --- a/frontend/src/components/chat/MessageComposer/index.tsx +++ b/frontend/src/components/chat/MessageComposer/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useRef, useState } from 'react'; +import { MutableRefObject, useCallback, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useRecoilState, useSetRecoilState } from 'recoil'; import { v4 as uuidv4 } from 'uuid'; @@ -29,14 +29,14 @@ interface Props { fileSpec: FileSpec; onFileUpload: (payload: File[]) => void; onFileUploadError: (error: string) => void; - setAutoScroll: (autoScroll: boolean) => void; + autoScrollRef: MutableRefObject; } export default function MessageComposer({ fileSpec, onFileUpload, onFileUploadError, - setAutoScroll + autoScrollRef }: Props) { const inputRef = useRef(null); const [value, setValue] = useState(''); @@ -88,7 +88,9 @@ export default function MessageComposer({ ?.filter((a) => !!a.serverId) .map((a) => ({ id: a.serverId! })); - setAutoScroll(true); + if (autoScrollRef) { + autoScrollRef.current = true; + } sendMessage(message, fileReferences); }, [user, sendMessage] @@ -107,7 +109,9 @@ export default function MessageComposer({ }; replyMessage(message); - setAutoScroll(true); + if (autoScrollRef) { + autoScrollRef.current = true; + } }, [user, replyMessage] ); diff --git a/frontend/src/components/chat/Messages/Message/Content/index.tsx b/frontend/src/components/chat/Messages/Message/Content/index.tsx index 49d816cdac..1b7e83296d 100644 --- a/frontend/src/components/chat/Messages/Message/Content/index.tsx +++ b/frontend/src/components/chat/Messages/Message/Content/index.tsx @@ -38,11 +38,9 @@ const MessageContent = memo( const isMessage = message.type.includes('message'); const outputMarkdown = ( -
- {!isMessage && displayInput ? ( -
- Output -
+ <> + {!isMessage && displayInput && message.output ? ( +
Output
) : null} {output} -
+ ); let inputMarkdown; @@ -73,10 +71,9 @@ const MessageContent = memo( }); inputMarkdown = ( -
-
- Input -
+ <> +
Input
+ {input} -
+ ); } diff --git a/frontend/src/components/chat/Messages/Message/Step.tsx b/frontend/src/components/chat/Messages/Message/Step.tsx index bb15a913dd..4f995525cd 100644 --- a/frontend/src/components/chat/Messages/Message/Step.tsx +++ b/frontend/src/components/chat/Messages/Message/Step.tsx @@ -1,9 +1,14 @@ import { cn } from '@/lib/utils'; -import { ChevronDown, ChevronUp } from 'lucide-react'; -import { PropsWithChildren, useMemo, useState } from 'react'; +import { PropsWithChildren, useMemo } from 'react'; import type { IStep } from '@chainlit/react-client'; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger +} from '@/components/ui/accordion'; import { Translator } from 'components/i18n'; interface Props { @@ -16,52 +21,71 @@ export default function Step({ children, isRunning }: PropsWithChildren) { - const [open, setOpen] = useState(false); const using = useMemo(() => { return isRunning && step.start && !step.end && !step.isError; }, [step, isRunning]); const hasContent = step.input || step.output || step.steps?.length; const isError = step.isError; - const stepName = step.name; - return ( -
-

setOpen(!open)} - id={`step-${stepName}`} - > - {using ? ( - <> - {stepName} - - ) : ( - <> - {stepName} - - )} - {hasContent ? ( - open ? ( - + // If there's no content, just render the status without accordion + if (!hasContent) { + return ( +

+

+ {using ? ( + <> + {stepName} + ) : ( - - ) - ) : null} -

+ <> + {stepName} + + )} +

+
+ ); + } - {open && ( -
- {children} -
- )} + return ( +
+ + + + {using ? ( + <> + {stepName} + + ) : ( + <> + {stepName} + + )} + + +
+ {children} +
+
+
+
); } diff --git a/frontend/src/components/chat/Messages/Message/UserMessage.tsx b/frontend/src/components/chat/Messages/Message/UserMessage.tsx index 7986fdbb95..32403dcf0f 100644 --- a/frontend/src/components/chat/Messages/Message/UserMessage.tsx +++ b/frontend/src/components/chat/Messages/Message/UserMessage.tsx @@ -7,8 +7,7 @@ import { IMessageElement, IStep, messagesState, - useChatInteract, - useConfig + useChatInteract } from '@chainlit/react-client'; import AutoResizeTextarea from '@/components/AutoResizeTextarea'; @@ -28,8 +27,7 @@ export default function UserMessage({ elements, children }: React.PropsWithChildren) { - const config = useConfig(); - const { askUser, loading } = useContext(MessageContext); + const { askUser, loading, editable } = useContext(MessageContext); const { editMessage } = useChatInteract(); const setMessages = useSetRecoilState(messagesState); const disabled = loading || !!askUser; @@ -42,8 +40,6 @@ export default function UserMessage({ ); }, [message.id, elements]); - const isEditable = !!config.config?.features.edit_message; - const handleEdit = () => { if (editValue) { setMessages((prev) => { @@ -65,7 +61,7 @@ export default function UserMessage({
- {!isEditing && isEditable && ( + {!isEditing && editable && ( +
+ ) : null}
); } diff --git a/frontend/src/components/chat/WelcomeScreen.tsx b/frontend/src/components/chat/WelcomeScreen.tsx index bf5356b33f..65beac34ad 100644 --- a/frontend/src/components/chat/WelcomeScreen.tsx +++ b/frontend/src/components/chat/WelcomeScreen.tsx @@ -1,5 +1,11 @@ import { cn, hasMessage } from '@/lib/utils'; -import { useContext, useEffect, useMemo, useState } from 'react'; +import { + MutableRefObject, + useContext, + useEffect, + useMemo, + useState +} from 'react'; import { ChainlitContext, @@ -19,7 +25,7 @@ interface Props { fileSpec: FileSpec; onFileUpload: (payload: File[]) => void; onFileUploadError: (error: string) => void; - setAutoScroll: (autoScroll: boolean) => void; + autoScrollRef: MutableRefObject; } export default function WelcomeScreen(props: Props) { diff --git a/frontend/src/components/chat/index.tsx b/frontend/src/components/chat/index.tsx index 6b39f751f5..9cfdfd7f98 100644 --- a/frontend/src/components/chat/index.tsx +++ b/frontend/src/components/chat/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; import { toast } from 'sonner'; @@ -35,7 +35,7 @@ const Chat = () => { const setAttachments = useSetRecoilState(attachmentsState); const setThreads = useSetRecoilState(threadHistoryState); - const [autoScroll, setAutoScroll] = useState(true); + const autoScrollRef = useRef(true); const { error, disabled } = useChatData(); const { uploadFile } = useChatInteract(); const uploadFileRef = useRef(uploadFile); @@ -200,7 +200,7 @@ const Chat = () => {
) : null} - +
{ fileSpec={fileSpec} onFileUpload={onFileUpload} onFileUploadError={onFileUploadError} - setAutoScroll={setAutoScroll} + autoScrollRef={autoScrollRef} />
@@ -227,8 +227,7 @@ const Chat = () => { fileSpec={fileSpec} onFileUpload={onFileUpload} onFileUploadError={onFileUploadError} - setAutoScroll={setAutoScroll} - autoScroll={autoScroll} + autoScrollRef={autoScrollRef} />
diff --git a/frontend/src/contexts/MessageContext.tsx b/frontend/src/contexts/MessageContext.tsx index 0952158fb8..45735b61f2 100644 --- a/frontend/src/contexts/MessageContext.tsx +++ b/frontend/src/contexts/MessageContext.tsx @@ -5,6 +5,7 @@ import { IMessageContext } from 'types/messageContext'; const defaultMessageContext = { highlightedMessage: null, loading: false, + editable: false, onElementRefClick: undefined, onFeedbackUpdated: undefined, showFeedbackButtons: true, diff --git a/frontend/src/types/messageContext.ts b/frontend/src/types/messageContext.ts index d51ca12003..72440b092f 100644 --- a/frontend/src/types/messageContext.ts +++ b/frontend/src/types/messageContext.ts @@ -13,6 +13,7 @@ interface IMessageContext { ) => { xhr: XMLHttpRequest; promise: Promise }; cot: 'hidden' | 'tool_call' | 'full'; askUser?: IAsk; + editable: boolean; loading: boolean; showFeedbackButtons: boolean; uiName: string; diff --git a/libs/copilot/src/chat/body.tsx b/libs/copilot/src/chat/body.tsx index 6ad5b26af2..1a02778e33 100644 --- a/libs/copilot/src/chat/body.tsx +++ b/libs/copilot/src/chat/body.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useSetRecoilState } from 'recoil'; import { toast } from 'sonner'; import { v4 as uuidv4 } from 'uuid'; @@ -29,7 +29,7 @@ const Chat = () => { const layoutMaxWidth = useLayoutMaxWidth(); const setAttachments = useSetRecoilState(attachmentsState); const setThreads = useSetRecoilState(threadHistoryState); - const [autoScroll, setAutoScroll] = useState(true); + const autoScrollRef = useRef(true); const { error, disabled, callFn } = useChatData(); const { uploadFile } = useChatInteract(); const uploadFileRef = useRef(uploadFile); @@ -171,10 +171,7 @@ const Chat = () => { ) : null} - +
{ fileSpec={fileSpec} onFileUpload={onFileUpload} onFileUploadError={onFileUploadError} - setAutoScroll={setAutoScroll} - autoScroll={autoScroll} + autoScrollRef={autoScrollRef} />