Skip to content

Commit e08f7cb

Browse files
authored
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
1 parent 2c7ae66 commit e08f7cb

File tree

18 files changed

+167
-119
lines changed

18 files changed

+167
-119
lines changed

cypress/e2e/remove_elements/spec.cy.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ describe('remove_elements', () => {
99
cy.get('#step-tool1').should('exist');
1010
cy.get('#step-tool1').click();
1111
cy.get('#step-tool1')
12+
.parent()
1213
.parent()
1314
.find('.inline-image')
1415
.should('have.length', 1);

cypress/e2e/streaming/spec.cy.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ function toolStream(tool: string) {
1313
const toolCall = cy.get(`#step-${tool}`);
1414
toolCall.click();
1515
for (const token of tokenList) {
16-
toolCall.parent().should('contain', token);
16+
toolCall.parent().parent().should('contain', token);
1717
}
18-
toolCall.parent().should('contain', tokenList.join(' '));
18+
toolCall.parent().parent().should('contain', tokenList.join(' '));
1919
}
2020

2121
describe('Streaming', () => {

cypress/e2e/update_step/spec.cy.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ describe('Update Step', () => {
99
cy.get(`#step-tool1`).click();
1010
cy.get('.step').should('have.length', 2);
1111
cy.get('.step').eq(0).should('contain', 'Hello!');
12-
cy.get(`#step-tool1`).parent().should('contain', 'Foo');
12+
cy.get(`#step-tool1`).parent().parent().should('contain', 'Foo');
1313

1414
cy.get('.step').eq(0).should('contain', 'Hello again!');
15-
cy.get(`#step-tool1`).parent().should('contain', 'Foo Bar');
15+
cy.get(`#step-tool1`).parent().parent().should('contain', 'Foo Bar');
1616
});
1717
});

frontend/src/components/CodeSnippet.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { cn } from '@/lib/utils';
12
import hljs from 'highlight.js';
23
import { useEffect, useRef } from 'react';
34

@@ -64,7 +65,9 @@ export default function CodeSnippet({ ...props }: CodeProps) {
6465
) : null;
6566

6667
const nonHighlightedCode = showSyntaxHighlighter ? null : (
67-
<div className="p-2 rounded-b-md min-h-20 overflow-x-auto bg-accent">
68+
<div
69+
className={cn('rounded-b-md overflow-x-auto bg-accent', code && 'p-2')}
70+
>
6871
<code className="whitespace-pre-wrap">{code}</code>
6972
</div>
7073
);

frontend/src/components/MarkdownAlert.tsx

-2
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,6 @@ const AlertComponent = ({
174174
const style = variantStyles[variant];
175175
const Icon = style.Icon;
176176

177-
// console.log('AlertComponent rendering:', variant, children);
178-
179177
return (
180178
<div className={cn('rounded-lg p-4 mb-4', style.container)}>
181179
<div className="flex">

frontend/src/components/ReadOnlyThread.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ const ReadOnlyThread = ({ id }: Props) => {
146146
return {
147147
allowHtml: config?.features?.unsafe_allow_html,
148148
latex: config?.features?.latex,
149+
editable: false,
149150
loading: false,
150151
showFeedbackButtons: !!config?.dataPersistence,
151152
uiName: config?.ui?.name || '',

frontend/src/components/chat/Footer.tsx

+3-11
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,26 @@
11
import { cn, hasMessage } from '@/lib/utils';
2+
import { MutableRefObject } from 'react';
23

34
import { FileSpec, useChatMessages } from '@chainlit/react-client';
45

56
import WaterMark from '@/components/WaterMark';
67

78
import MessageComposer from './MessageComposer';
8-
import ScrollDownButton from './ScrollDownButton';
99

1010
interface Props {
1111
fileSpec: FileSpec;
1212
onFileUpload: (payload: File[]) => void;
1313
onFileUploadError: (error: string) => void;
14-
setAutoScroll: (autoScroll: boolean) => void;
15-
autoScroll: boolean;
14+
autoScrollRef: MutableRefObject<boolean>;
1615
showIfEmptyThread?: boolean;
1716
}
1817

19-
export default function ChatFooter({
20-
autoScroll,
21-
showIfEmptyThread,
22-
...props
23-
}: Props) {
18+
export default function ChatFooter({ showIfEmptyThread, ...props }: Props) {
2419
const { messages } = useChatMessages();
2520
if (!hasMessage(messages) && !showIfEmptyThread) return null;
2621

2722
return (
2823
<div className={cn('relative flex flex-col items-center gap-2 w-full')}>
29-
{!autoScroll ? (
30-
<ScrollDownButton onClick={() => props.setAutoScroll(true)} />
31-
) : null}
3224
<MessageComposer {...props} />
3325
<WaterMark />
3426
</div>

frontend/src/components/chat/MessageComposer/index.tsx

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useRef, useState } from 'react';
1+
import { MutableRefObject, useCallback, useRef, useState } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import { useRecoilState, useSetRecoilState } from 'recoil';
44
import { v4 as uuidv4 } from 'uuid';
@@ -29,14 +29,14 @@ interface Props {
2929
fileSpec: FileSpec;
3030
onFileUpload: (payload: File[]) => void;
3131
onFileUploadError: (error: string) => void;
32-
setAutoScroll: (autoScroll: boolean) => void;
32+
autoScrollRef: MutableRefObject<boolean>;
3333
}
3434

3535
export default function MessageComposer({
3636
fileSpec,
3737
onFileUpload,
3838
onFileUploadError,
39-
setAutoScroll
39+
autoScrollRef
4040
}: Props) {
4141
const inputRef = useRef<InputMethods>(null);
4242
const [value, setValue] = useState('');
@@ -88,7 +88,9 @@ export default function MessageComposer({
8888
?.filter((a) => !!a.serverId)
8989
.map((a) => ({ id: a.serverId! }));
9090

91-
setAutoScroll(true);
91+
if (autoScrollRef) {
92+
autoScrollRef.current = true;
93+
}
9294
sendMessage(message, fileReferences);
9395
},
9496
[user, sendMessage]
@@ -107,7 +109,9 @@ export default function MessageComposer({
107109
};
108110

109111
replyMessage(message);
110-
setAutoScroll(true);
112+
if (autoScrollRef) {
113+
autoScrollRef.current = true;
114+
}
111115
},
112116
[user, replyMessage]
113117
);

frontend/src/components/chat/Messages/Message/Content/index.tsx

+8-11
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,9 @@ const MessageContent = memo(
3838
const isMessage = message.type.includes('message');
3939

4040
const outputMarkdown = (
41-
<div className="flex flex-col gap-2">
42-
{!isMessage && displayInput ? (
43-
<div className="text-lg font-semibold leading-none tracking-tight">
44-
Output
45-
</div>
41+
<>
42+
{!isMessage && displayInput && message.output ? (
43+
<div className="font-medium">Output</div>
4644
) : null}
4745
<Markdown
4846
allowHtml={allowHtml}
@@ -51,7 +49,7 @@ const MessageContent = memo(
5149
>
5250
{output}
5351
</Markdown>
54-
</div>
52+
</>
5553
);
5654

5755
let inputMarkdown;
@@ -73,18 +71,17 @@ const MessageContent = memo(
7371
});
7472

7573
inputMarkdown = (
76-
<div className="flex flex-col gap-2">
77-
<div className="text-lg font-semibold leading-none tracking-tight">
78-
Input
79-
</div>
74+
<>
75+
<div className="font-medium">Input</div>
76+
8077
<Markdown
8178
allowHtml={allowHtml}
8279
latex={latex}
8380
refElements={inputRefElements}
8481
>
8582
{input}
8683
</Markdown>
87-
</div>
84+
</>
8885
);
8986
}
9087

Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import { cn } from '@/lib/utils';
2-
import { ChevronDown, ChevronUp } from 'lucide-react';
3-
import { PropsWithChildren, useMemo, useState } from 'react';
2+
import { PropsWithChildren, useMemo } from 'react';
43

54
import type { IStep } from '@chainlit/react-client';
65

6+
import {
7+
Accordion,
8+
AccordionContent,
9+
AccordionItem,
10+
AccordionTrigger
11+
} from '@/components/ui/accordion';
712
import { Translator } from 'components/i18n';
813

914
interface Props {
@@ -16,52 +21,71 @@ export default function Step({
1621
children,
1722
isRunning
1823
}: PropsWithChildren<Props>) {
19-
const [open, setOpen] = useState(false);
2024
const using = useMemo(() => {
2125
return isRunning && step.start && !step.end && !step.isError;
2226
}, [step, isRunning]);
2327

2428
const hasContent = step.input || step.output || step.steps?.length;
2529
const isError = step.isError;
26-
2730
const stepName = step.name;
2831

29-
return (
30-
<div className="flex flex-col flex-grow w-0">
31-
<p
32-
className={cn(
33-
'flex items-center gap-1 group/step',
34-
isError && 'text-red-500',
35-
hasContent && 'cursor-pointer',
36-
!using && 'text-muted-foreground hover:text-foreground',
37-
using && 'loading-shimmer'
38-
)}
39-
onClick={() => setOpen(!open)}
40-
id={`step-${stepName}`}
41-
>
42-
{using ? (
43-
<>
44-
<Translator path="chat.messages.status.using" /> {stepName}
45-
</>
46-
) : (
47-
<>
48-
<Translator path="chat.messages.status.used" /> {stepName}
49-
</>
50-
)}
51-
{hasContent ? (
52-
open ? (
53-
<ChevronUp className="invisible group-hover/step:visible !size-4" />
32+
// If there's no content, just render the status without accordion
33+
if (!hasContent) {
34+
return (
35+
<div className="flex flex-col flex-grow w-0">
36+
<p
37+
className={cn(
38+
'flex items-center gap-1 font-medium',
39+
isError && 'text-red-500',
40+
!using && 'text-muted-foreground',
41+
using && 'loading-shimmer'
42+
)}
43+
id={`step-${stepName}`}
44+
>
45+
{using ? (
46+
<>
47+
<Translator path="chat.messages.status.using" /> {stepName}
48+
</>
5449
) : (
55-
<ChevronDown className="invisible group-hover/step:visible !size-4" />
56-
)
57-
) : null}
58-
</p>
50+
<>
51+
<Translator path="chat.messages.status.used" /> {stepName}
52+
</>
53+
)}
54+
</p>
55+
</div>
56+
);
57+
}
5958

60-
{open && (
61-
<div className="flex-grow mt-4 ml-2 pl-4 border-l-2 border-primary">
62-
{children}
63-
</div>
64-
)}
59+
return (
60+
<div className="flex flex-col flex-grow w-0">
61+
<Accordion type="single" collapsible className="w-full">
62+
<AccordionItem value={step.id} className="border-none">
63+
<AccordionTrigger
64+
className={cn(
65+
'flex items-center gap-1 justify-start transition-none p-0 hover:no-underline',
66+
isError && 'text-red-500',
67+
!using && 'text-muted-foreground hover:text-foreground',
68+
using && 'loading-shimmer'
69+
)}
70+
id={`step-${stepName}`}
71+
>
72+
{using ? (
73+
<>
74+
<Translator path="chat.messages.status.using" /> {stepName}
75+
</>
76+
) : (
77+
<>
78+
<Translator path="chat.messages.status.used" /> {stepName}
79+
</>
80+
)}
81+
</AccordionTrigger>
82+
<AccordionContent>
83+
<div className="flex-grow mt-4 ml-1 pl-4 border-l-2 border-primary">
84+
{children}
85+
</div>
86+
</AccordionContent>
87+
</AccordionItem>
88+
</Accordion>
6589
</div>
6690
);
6791
}

frontend/src/components/chat/Messages/Message/UserMessage.tsx

+4-8
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import {
77
IMessageElement,
88
IStep,
99
messagesState,
10-
useChatInteract,
11-
useConfig
10+
useChatInteract
1211
} from '@chainlit/react-client';
1312

1413
import AutoResizeTextarea from '@/components/AutoResizeTextarea';
@@ -28,8 +27,7 @@ export default function UserMessage({
2827
elements,
2928
children
3029
}: React.PropsWithChildren<Props>) {
31-
const config = useConfig();
32-
const { askUser, loading } = useContext(MessageContext);
30+
const { askUser, loading, editable } = useContext(MessageContext);
3331
const { editMessage } = useChatInteract();
3432
const setMessages = useSetRecoilState(messagesState);
3533
const disabled = loading || !!askUser;
@@ -42,8 +40,6 @@ export default function UserMessage({
4240
);
4341
}, [message.id, elements]);
4442

45-
const isEditable = !!config.config?.features.edit_message;
46-
4743
const handleEdit = () => {
4844
if (editValue) {
4945
setMessages((prev) => {
@@ -65,7 +61,7 @@ export default function UserMessage({
6561
<InlinedElements elements={inlineElements} className="items-end" />
6662

6763
<div className="flex flex-row items-center gap-1 w-full group">
68-
{!isEditing && isEditable && (
64+
{!isEditing && editable && (
6965
<Button
7066
variant="ghost"
7167
size="icon"
@@ -84,7 +80,7 @@ export default function UserMessage({
8480
'px-5 py-2.5 relative bg-accent rounded-3xl',
8581
inlineElements.length ? 'rounded-tr-lg' : '',
8682
isEditing ? 'w-full flex-grow' : 'max-w-[70%] flex-grow-0',
87-
isEditable ? '' : 'ml-auto'
83+
editable ? '' : 'ml-auto'
8884
)}
8985
>
9086
{isEditing ? (

frontend/src/components/chat/MessagesContainer/index.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ const MessagesContainer = ({ navigate }: Props) => {
122122
askUser,
123123
allowHtml: config?.features?.unsafe_allow_html,
124124
latex: config?.features?.latex,
125+
editable: !!config?.features.edit_message,
125126
loading,
126127
showFeedbackButtons: enableFeedback,
127128
uiName: config?.ui?.name || '',

0 commit comments

Comments
 (0)