mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-11-10 21:32:43 +08:00
perf(web): virtualize chat message lists (#21790)
This commit is contained in:
1 parent
306541333b
commit
e77793cc6d
18 files changed
+415
-461
No files matched your search
@@ -0,0 +1,48 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { MessageVirtualizer } from '@/hooks/use-virtual-messages';
|
||||
import { VirtualMessageList } from './index';
|
||||
|
||||
function buildVirtualizer() {
|
||||
return {
|
||||
getTotalSize: () => 300,
|
||||
getVirtualItems: () => [
|
||||
{ index: 0, key: 'user_1', start: 0, size: 100, end: 100, lane: 0 },
|
||||
{ index: 2, key: 'user_3', start: 200, size: 100, end: 300, lane: 0 },
|
||||
],
|
||||
measureElement: jest.fn(),
|
||||
} as unknown as MessageVirtualizer;
|
||||
}
|
||||
|
||||
describe('VirtualMessageList', () => {
|
||||
it('renders only the virtual items the virtualizer returns', () => {
|
||||
render(
|
||||
<VirtualMessageList
|
||||
items={['one', 'two', 'three']}
|
||||
virtualizer={buildVirtualizer()}
|
||||
renderItem={(item, index) => <span>{`${index}:${item}`}</span>}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('0:one')).toBeInTheDocument();
|
||||
expect(screen.getByText('2:three')).toBeInTheDocument();
|
||||
expect(screen.queryByText('1:two')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('positions each item at the offset reported by the virtualizer', () => {
|
||||
render(
|
||||
<VirtualMessageList
|
||||
items={['one', 'two', 'three']}
|
||||
virtualizer={buildVirtualizer()}
|
||||
renderItem={(item) => <span>{item}</span>}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('one').parentElement).toHaveStyle({
|
||||
transform: 'translateY(0px)',
|
||||
});
|
||||
expect(screen.getByText('three').parentElement).toHaveStyle({
|
||||
transform: 'translateY(200px)',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { MessageVirtualizer } from '@/hooks/use-virtual-messages';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface VirtualMessageListProps<T> {
|
||||
items: T[];
|
||||
virtualizer: MessageVirtualizer;
|
||||
renderItem: (item: T, index: number) => ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function VirtualMessageList<T>({
|
||||
items,
|
||||
virtualizer,
|
||||
renderItem,
|
||||
className,
|
||||
}: VirtualMessageListProps<T>) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div
|
||||
className="relative w-full"
|
||||
style={{ height: `${virtualizer.getTotalSize()}px` }}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map((virtualItem) => {
|
||||
const item = items[virtualItem.index];
|
||||
if (item === undefined) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={virtualItem.key}
|
||||
data-index={virtualItem.index}
|
||||
ref={virtualizer.measureElement}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
transform: `translateY(${virtualItem.start}px)`,
|
||||
}}
|
||||
>
|
||||
{renderItem(item, virtualItem.index)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
jest.mock('eventsource-parser/stream', () => ({}));
|
||||
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { useScrollToBottom } from '../logic-hooks';
|
||||
|
||||
function createMockContainer({ atBottom = true } = {}) {
|
||||
const scrollTop = atBottom ? 100 : 0;
|
||||
const clientHeight = 100;
|
||||
const scrollHeight = 200;
|
||||
const listeners: Record<string, (...args: unknown[]) => void> = {};
|
||||
return {
|
||||
current: {
|
||||
scrollTop,
|
||||
clientHeight,
|
||||
scrollHeight,
|
||||
style: {},
|
||||
scrollTo: jest.fn(),
|
||||
addEventListener: jest.fn((event, cb) => {
|
||||
listeners[event] = cb;
|
||||
}),
|
||||
removeEventListener: jest.fn(),
|
||||
},
|
||||
listeners,
|
||||
} as any;
|
||||
}
|
||||
|
||||
// Helper to flush all timers and microtasks
|
||||
async function flushAll() {
|
||||
jest.runAllTimers();
|
||||
// Flush microtasks
|
||||
await Promise.resolve();
|
||||
// Sometimes, effects queue more timers, so run again
|
||||
jest.runAllTimers();
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
describe('useScrollToBottom', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should set isAtBottom true when user is at bottom', () => {
|
||||
const containerRef = createMockContainer({ atBottom: true });
|
||||
const { result } = renderHook(() => useScrollToBottom([], containerRef));
|
||||
expect(result.current.isAtBottom).toBe(true);
|
||||
});
|
||||
|
||||
it('should set isAtBottom false when user is not at bottom', () => {
|
||||
const containerRef = createMockContainer({ atBottom: false });
|
||||
const { result } = renderHook(() => useScrollToBottom([], containerRef));
|
||||
expect(result.current.isAtBottom).toBe(false);
|
||||
});
|
||||
|
||||
it('should scroll to bottom when isAtBottom is true and messages change', async () => {
|
||||
const containerRef = createMockContainer({ atBottom: true });
|
||||
|
||||
const { rerender } = renderHook(
|
||||
({ messages }) => useScrollToBottom(messages, containerRef),
|
||||
{ initialProps: { messages: [] as string[] } },
|
||||
);
|
||||
|
||||
rerender({ messages: ['msg1'] });
|
||||
await flushAll();
|
||||
|
||||
expect(containerRef.current.scrollTo).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should NOT scroll to bottom when isAtBottom is false and messages change', async () => {
|
||||
const containerRef = createMockContainer({ atBottom: false });
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
({ messages }) => useScrollToBottom(messages, containerRef),
|
||||
{ initialProps: { messages: [] as string[] } },
|
||||
);
|
||||
|
||||
// Simulate user scrolls up before messages change
|
||||
await act(async () => {
|
||||
containerRef.current.scrollTop = 0;
|
||||
containerRef.current.addEventListener.mock.calls[0][1]();
|
||||
await flushAll();
|
||||
// Advance fake timers by 10ms instead of real setTimeout
|
||||
jest.advanceTimersByTime(10);
|
||||
console.log('AFTER SCROLL: isAtBottom:', result.current.isAtBottom);
|
||||
});
|
||||
|
||||
rerender({ messages: ['msg1'] });
|
||||
await flushAll();
|
||||
|
||||
console.log('AFTER RERENDER: isAtBottom:', result.current.isAtBottom);
|
||||
|
||||
expect(containerRef.current.scrollTo).not.toHaveBeenCalled();
|
||||
|
||||
// Optionally, flush again after the assertion to see if it gets called late
|
||||
await flushAll();
|
||||
});
|
||||
|
||||
it('should indicate button should appear when user is not at bottom', () => {
|
||||
const containerRef = createMockContainer({ atBottom: false });
|
||||
const { result } = renderHook(() => useScrollToBottom([], containerRef));
|
||||
// The button should appear in the UI when isAtBottom is false
|
||||
expect(result.current.isAtBottom).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
const originalRAF = global.requestAnimationFrame;
|
||||
beforeAll(() => {
|
||||
global.requestAnimationFrame = (cb) => setTimeout(cb, 0);
|
||||
});
|
||||
afterAll(() => {
|
||||
global.requestAnimationFrame = originalRAF;
|
||||
});
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
import { useTranslate } from './common-hooks';
|
||||
import { useSetPaginationParams } from './route-hook';
|
||||
import { useSaveSetting } from './use-user-setting-request';
|
||||
import { useVirtualMessages } from './use-virtual-messages';
|
||||
|
||||
export function usePrevious<T>(value: T) {
|
||||
const ref = useRef<T>();
|
||||
@@ -445,129 +446,6 @@ export const useSpeechWithSse = (url: string = api.chatsTts) => {
|
||||
|
||||
//#region chat hooks
|
||||
|
||||
// Firefox reports a fractional `scrollTop`, while `scrollHeight` / `clientHeight`
|
||||
// are rounded. `scrollToBottom` therefore lands a sub-pixel *below* the position
|
||||
// a native clamp had produced, which reads as a decreasing `scrollTop`. Require a
|
||||
// real gesture's worth of movement so that jitter is not mistaken for one.
|
||||
const UserScrollUpThreshold = 2;
|
||||
|
||||
export const useScrollToBottom = (
|
||||
messages?: unknown,
|
||||
containerRef?: React.RefObject<HTMLDivElement>,
|
||||
) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [isAtBottom, setIsAtBottom] = useState(true);
|
||||
const isAtBottomRef = useRef(true);
|
||||
// `null` means "no baseline yet", so the very first measurement has to fall
|
||||
// back to the distance check instead of guessing a scroll direction.
|
||||
const lastScrollTopRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
isAtBottomRef.current = isAtBottom;
|
||||
}, [isAtBottom]);
|
||||
|
||||
// We pin the transcript to the bottom ourselves, so browser scroll anchoring is
|
||||
// pure interference: when a streamed answer re-lays out (markdown turning a
|
||||
// paragraph into a code block, a line re-wrapping), Firefox shifts `scrollTop`
|
||||
// to hold its anchor node still. That shift is indistinguishable from a user
|
||||
// scrolling up in the handler below, so it latched auto-follow off mid-answer.
|
||||
// Chrome suppresses the adjustment while pinned to the bottom, which is why
|
||||
// only Firefox drifted away from the bottom.
|
||||
useEffect(() => {
|
||||
if (!containerRef?.current) return;
|
||||
containerRef.current.style.overflowAnchor = 'none';
|
||||
}, [containerRef]);
|
||||
|
||||
const checkIfNearBottom = useCallback(() => {
|
||||
if (!containerRef?.current) return true;
|
||||
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
|
||||
// Content shorter than the viewport has nothing to scroll, so it is
|
||||
// trivially "at the bottom". Returning false here would latch auto-follow
|
||||
// off when a stream starts from a short transcript: growing content does
|
||||
// not fire a `scroll` event, so no later check would ever re-arm the flag
|
||||
// and the view would never track the incoming message.
|
||||
if (scrollHeight <= clientHeight) return true;
|
||||
return Math.abs(scrollTop + clientHeight - scrollHeight) < 60;
|
||||
}, [containerRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef?.current) return;
|
||||
const container = containerRef.current;
|
||||
|
||||
const handleScroll = () => {
|
||||
const previousScrollTop = lastScrollTopRef.current;
|
||||
const { scrollTop } = container;
|
||||
lastScrollTopRef.current = scrollTop;
|
||||
|
||||
const nearBottom = checkIfNearBottom();
|
||||
let atBottom: boolean;
|
||||
if (nearBottom) {
|
||||
atBottom = true;
|
||||
} else if (
|
||||
previousScrollTop === null ||
|
||||
scrollTop < previousScrollTop - UserScrollUpThreshold
|
||||
) {
|
||||
// With scroll anchoring off, only a user gesture (wheel, drag, keys,
|
||||
// touch) can shrink `scrollTop` by a meaningful amount, so this is the
|
||||
// one reliable signal that they want to leave the bottom.
|
||||
atBottom = false;
|
||||
} else {
|
||||
// We are far from the bottom yet `scrollTop` did not move: the gap comes
|
||||
// from content that grew after `scrollToBottom` ran but before this
|
||||
// event was dispatched. Disarming here would strand auto-follow forever,
|
||||
// because growing content never fires another `scroll` event to re-arm
|
||||
// it. Keep whatever the user last asked for.
|
||||
atBottom = isAtBottomRef.current;
|
||||
}
|
||||
|
||||
// Write the ref here rather than relying on the effect that mirrors
|
||||
// `isAtBottom`: that effect only runs after the next render, and while the
|
||||
// main thread is busy rendering a streaming answer an already scheduled
|
||||
// auto-scroll would still see the stale `true`.
|
||||
isAtBottomRef.current = atBottom;
|
||||
setIsAtBottom(atBottom);
|
||||
};
|
||||
|
||||
container.addEventListener('scroll', handleScroll);
|
||||
handleScroll();
|
||||
return () => container.removeEventListener('scroll', handleScroll);
|
||||
}, [containerRef, checkIfNearBottom]);
|
||||
|
||||
// Imperative scroll function
|
||||
const scrollToBottom = useCallback(() => {
|
||||
if (containerRef?.current) {
|
||||
const container = containerRef.current;
|
||||
// Overshoot and let the browser clamp. `scrollHeight - clientHeight` is a
|
||||
// difference of two rounded values, so in Firefox — where the real maximum
|
||||
// is fractional — it can land just short of the bottom.
|
||||
container.scrollTo({
|
||||
top: container.scrollHeight,
|
||||
behavior: 'auto',
|
||||
});
|
||||
}
|
||||
}, [containerRef]);
|
||||
|
||||
// Streaming replaces `messages` many times a second. The previous
|
||||
// rAF + setTimeout(100) chain always had several scrolls queued, and they read
|
||||
// `isAtBottomRef` long after the user had scrolled up — yanking the view back
|
||||
// down. One cancellable frame per change, gated on the latest position, keeps
|
||||
// auto-follow without fighting the user.
|
||||
useEffect(() => {
|
||||
if (!messages) return;
|
||||
if (!containerRef?.current) return;
|
||||
if (!isAtBottomRef.current) return;
|
||||
|
||||
const frame = requestAnimationFrame(() => {
|
||||
if (isAtBottomRef.current) {
|
||||
scrollToBottom();
|
||||
}
|
||||
});
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}, [messages, containerRef, scrollToBottom]);
|
||||
|
||||
return { scrollRef: ref, isAtBottom, scrollToBottom };
|
||||
};
|
||||
|
||||
export const useHandleMessageInputChange = () => {
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
@@ -584,14 +462,15 @@ export const useHandleMessageInputChange = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const useSelectDerivedMessages = () => {
|
||||
export const useSelectDerivedMessages = (resetKey?: string) => {
|
||||
const [derivedMessages, setDerivedMessages] = useState<IMessage[]>([]);
|
||||
|
||||
const messageContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { scrollRef, scrollToBottom } = useScrollToBottom(
|
||||
const { virtualizer, scrollToBottom } = useVirtualMessages(
|
||||
derivedMessages,
|
||||
messageContainerRef,
|
||||
{ resetKey },
|
||||
);
|
||||
|
||||
const addNewestQuestion = useCallback(
|
||||
@@ -771,7 +650,7 @@ export const useSelectDerivedMessages = () => {
|
||||
}, [setDerivedMessages]);
|
||||
|
||||
return {
|
||||
scrollRef,
|
||||
virtualizer,
|
||||
messageContainerRef,
|
||||
derivedMessages,
|
||||
setDerivedMessages,
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Virtualizer, useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { RefObject, useCallback, useEffect } from 'react';
|
||||
import { IMessage } from '@/interfaces/database/chat';
|
||||
import { buildMessageUuidWithRole } from '@/utils/chat';
|
||||
|
||||
// Until a message is measured, the virtualizer positions it with this
|
||||
// estimate. A chat turn is usually a short question and a long answer, so a
|
||||
// mid-sized value keeps the initial jump to the bottom close for both.
|
||||
const EstimatedMessageHeight = 160;
|
||||
|
||||
// Losing the bottom by more than this means the user scrolled up on purpose.
|
||||
const FollowBottomThreshold = 60;
|
||||
|
||||
export type MessageVirtualizer = Virtualizer<HTMLDivElement, HTMLDivElement>;
|
||||
|
||||
interface UseVirtualMessagesOptions {
|
||||
/** Changes when the displayed transcript is replaced, e.g. a conversation switch. */
|
||||
resetKey?: string;
|
||||
}
|
||||
|
||||
export const useVirtualMessages = (
|
||||
messages: IMessage[] | undefined,
|
||||
containerRef: RefObject<HTMLDivElement>,
|
||||
{ resetKey }: UseVirtualMessagesOptions = {},
|
||||
) => {
|
||||
const virtualizer = useVirtualizer<HTMLDivElement, HTMLDivElement>({
|
||||
count: messages?.length ?? 0,
|
||||
getScrollElement: () => containerRef.current,
|
||||
getItemKey: (index) => {
|
||||
const message = messages?.[index];
|
||||
return message ? buildMessageUuidWithRole(message) : index;
|
||||
},
|
||||
estimateSize: () => EstimatedMessageHeight,
|
||||
overscan: 4,
|
||||
// A transcript is read from the bottom: keep the distance to the end
|
||||
// stable when messages are appended or resized, and follow new output
|
||||
// only while the user is still at the bottom.
|
||||
anchorTo: 'end',
|
||||
followOnAppend: true,
|
||||
scrollEndThreshold: FollowBottomThreshold,
|
||||
});
|
||||
|
||||
const scrollToBottom = useCallback(() => {
|
||||
virtualizer.scrollToEnd();
|
||||
}, [virtualizer]);
|
||||
|
||||
// The transcript pins itself to the bottom, so browser scroll anchoring is
|
||||
// pure interference while an answer re-wraps and re-measures.
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (container) {
|
||||
container.style.overflowAnchor = 'none';
|
||||
}
|
||||
}, [containerRef]);
|
||||
|
||||
// Appends are handled by followOnAppend. A replaced transcript (mount or
|
||||
// conversation switch) has no append to follow, so open it at the bottom.
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [resetKey, scrollToBottom]);
|
||||
|
||||
return { virtualizer, scrollToBottom };
|
||||
};
|
||||
@@ -8,12 +8,12 @@ import MarkdownContent from '@/components/next-markdown-content';
|
||||
import MessageItem from '@/components/next-message-item';
|
||||
import PdfSheet from '@/components/pdf-drawer';
|
||||
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
|
||||
import { VirtualMessageList } from '@/components/virtual-message-list';
|
||||
import {
|
||||
useFetchAgent,
|
||||
useUploadAgentFileWithProgress,
|
||||
} from '@/hooks/use-agent-request';
|
||||
import { useFetchUserInfo } from '@/hooks/use-user-setting-request';
|
||||
import { buildMessageUuidWithRole } from '@/utils/chat';
|
||||
import { memo, useCallback, useContext, useEffect, useRef } from 'react';
|
||||
import { AgentChatContext } from '../context';
|
||||
import DebugContent from '../debug-content';
|
||||
@@ -24,7 +24,7 @@ function AgentChatBox() {
|
||||
const { data: canvasInfo, refetch } = useFetchAgent();
|
||||
const {
|
||||
value,
|
||||
scrollRef,
|
||||
virtualizer,
|
||||
messageContainerRef,
|
||||
sendLoading,
|
||||
derivedMessages,
|
||||
@@ -100,56 +100,53 @@ function AgentChatBox() {
|
||||
<>
|
||||
<section className="flex flex-1 flex-col px-5 min-h-0 pb-4">
|
||||
<div className="flex-1 overflow-auto min-h-0" ref={messageContainerRef}>
|
||||
<div>
|
||||
{!sendLoading && <div data-testid="agent-run-idle" />}
|
||||
{/* <Spin spinning={sendLoading}> */}
|
||||
{derivedMessages?.map((message, i) => {
|
||||
return (
|
||||
<MessageItem
|
||||
loading={
|
||||
message.role === MessageType.Assistant &&
|
||||
sendLoading &&
|
||||
derivedMessages.length - 1 === i
|
||||
}
|
||||
key={buildMessageUuidWithRole(message)}
|
||||
nickname={userInfo.nickname}
|
||||
avatar={userInfo.avatar}
|
||||
avatarDialog={canvasInfo.avatar}
|
||||
item={message}
|
||||
reference={findReferenceByMessageId(message.id)}
|
||||
clickDocumentButton={clickDocumentButton}
|
||||
index={i}
|
||||
showLikeButton={false}
|
||||
sendLoading={sendLoading && derivedMessages.length - 1 === i}
|
||||
>
|
||||
{message.role === MessageType.Assistant &&
|
||||
derivedMessages.length - 1 === i && (
|
||||
<DebugContent
|
||||
parameters={buildInputList(message)}
|
||||
message={message}
|
||||
ok={handleOk(message)}
|
||||
isNext={false}
|
||||
btnText={'Submit'}
|
||||
></DebugContent>
|
||||
)}
|
||||
{message.role === MessageType.Assistant &&
|
||||
derivedMessages.length - 1 !== i && (
|
||||
{!sendLoading && <div data-testid="agent-run-idle" />}
|
||||
<VirtualMessageList
|
||||
className="w-full"
|
||||
items={derivedMessages}
|
||||
virtualizer={virtualizer}
|
||||
renderItem={(message, i) => (
|
||||
<MessageItem
|
||||
loading={
|
||||
message.role === MessageType.Assistant &&
|
||||
sendLoading &&
|
||||
derivedMessages.length - 1 === i
|
||||
}
|
||||
nickname={userInfo.nickname}
|
||||
avatar={userInfo.avatar}
|
||||
avatarDialog={canvasInfo.avatar}
|
||||
item={message}
|
||||
reference={findReferenceByMessageId(message.id)}
|
||||
clickDocumentButton={clickDocumentButton}
|
||||
index={i}
|
||||
showLikeButton={false}
|
||||
sendLoading={sendLoading && derivedMessages.length - 1 === i}
|
||||
>
|
||||
{message.role === MessageType.Assistant &&
|
||||
derivedMessages.length - 1 === i && (
|
||||
<DebugContent
|
||||
parameters={buildInputList(message)}
|
||||
message={message}
|
||||
ok={handleOk(message)}
|
||||
isNext={false}
|
||||
btnText={'Submit'}
|
||||
></DebugContent>
|
||||
)}
|
||||
{message.role === MessageType.Assistant &&
|
||||
derivedMessages.length - 1 !== i && (
|
||||
<div>
|
||||
<MarkdownContent
|
||||
content={message?.data?.tips}
|
||||
loading={false}
|
||||
></MarkdownContent>
|
||||
<div>
|
||||
<MarkdownContent
|
||||
content={message?.data?.tips}
|
||||
loading={false}
|
||||
></MarkdownContent>
|
||||
<div>
|
||||
{buildInputList(message)?.map((item) => item.value)}
|
||||
</div>
|
||||
{buildInputList(message)?.map((item) => item.value)}
|
||||
</div>
|
||||
)}
|
||||
</MessageItem>
|
||||
);
|
||||
})}
|
||||
{/* </Spin> */}
|
||||
</div>
|
||||
<div ref={scrollRef} />
|
||||
</div>
|
||||
)}
|
||||
</MessageItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{isTaskMode || (
|
||||
<div ref={inputWrapperRef}>
|
||||
|
||||
@@ -47,7 +47,7 @@ jest.mock('@/hooks/logic-hooks', () => ({
|
||||
useSelectDerivedMessages: () => ({
|
||||
derivedMessages: [],
|
||||
setDerivedMessages: jest.fn(),
|
||||
scrollRef: { current: null },
|
||||
virtualizer: {},
|
||||
messageContainerRef: { current: null },
|
||||
removeLatestMessage: jest.fn(),
|
||||
removeMessageById: jest.fn(),
|
||||
|
||||
@@ -305,7 +305,7 @@ export const useSendAgentMessage = ({
|
||||
const prologue = useGetBeginNodePrologue();
|
||||
const {
|
||||
derivedMessages,
|
||||
scrollRef,
|
||||
virtualizer,
|
||||
messageContainerRef,
|
||||
removeLatestMessage,
|
||||
removeMessageById,
|
||||
@@ -316,7 +316,7 @@ export const useSendAgentMessage = ({
|
||||
scrollToBottom,
|
||||
addPrologue,
|
||||
setDerivedMessages,
|
||||
} = useSelectDerivedMessages();
|
||||
} = useSelectDerivedMessages(activeSessionId);
|
||||
const { addEventList: addEventListFun } = useContext(AgentChatLogContext);
|
||||
const {
|
||||
appendUploadResponseList,
|
||||
@@ -611,7 +611,7 @@ export const useSendAgentMessage = ({
|
||||
value,
|
||||
sendLoading: !done,
|
||||
derivedMessages,
|
||||
scrollRef,
|
||||
virtualizer,
|
||||
messageContainerRef,
|
||||
handlePressEnter,
|
||||
handleInputChange,
|
||||
|
||||
@@ -4,6 +4,7 @@ import MarkdownContent from '@/components/next-markdown-content';
|
||||
import MessageItem from '@/components/next-message-item';
|
||||
import PdfSheet from '@/components/pdf-drawer';
|
||||
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
|
||||
import { VirtualMessageList } from '@/components/virtual-message-list';
|
||||
import { MessageType } from '@/constants/chat';
|
||||
import { useUploadAgentFileWithProgress } from '@/hooks/use-agent-request';
|
||||
import { useFetchUserInfo } from '@/hooks/use-user-setting-request';
|
||||
@@ -13,7 +14,7 @@ import DebugContent from '@/pages/agent/debug-content';
|
||||
import { useAwaitComponentData } from '@/pages/agent/hooks/use-chat-logic';
|
||||
import { BeginQuery } from '@/pages/agent/interface';
|
||||
import { ParameterDialog } from '@/pages/agent/share/parameter-dialog';
|
||||
import { buildMessageUuidWithRole, buildReferenceObject } from '@/utils/chat';
|
||||
import { buildReferenceObject } from '@/utils/chat';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useExploreUrlParams } from '../hooks/use-explore-url-params';
|
||||
@@ -34,7 +35,7 @@ export function SessionChat({ session }: SessionChatProps) {
|
||||
const {
|
||||
value,
|
||||
derivedMessages,
|
||||
scrollRef,
|
||||
virtualizer,
|
||||
messageContainerRef,
|
||||
sendLoading,
|
||||
handleInputChange,
|
||||
@@ -160,8 +161,11 @@ export function SessionChat({ session }: SessionChatProps) {
|
||||
No messages in this session
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full pr-5">
|
||||
{derivedMessages.map((message, i) => {
|
||||
<VirtualMessageList
|
||||
className="w-full pr-5"
|
||||
items={derivedMessages}
|
||||
virtualizer={virtualizer}
|
||||
renderItem={(message, i) => {
|
||||
const inputList = buildInputList(message);
|
||||
const hasUserFillUpInputs =
|
||||
message.role === MessageType.Assistant &&
|
||||
@@ -174,7 +178,6 @@ export function SessionChat({ session }: SessionChatProps) {
|
||||
isStreamingActiveSession &&
|
||||
derivedMessages.length - 1 === i
|
||||
}
|
||||
key={buildMessageUuidWithRole(message)}
|
||||
item={message}
|
||||
nickname={userInfo.nickname}
|
||||
avatar={userInfo.avatar}
|
||||
@@ -215,10 +218,9 @@ export function SessionChat({ session }: SessionChatProps) {
|
||||
)}
|
||||
</MessageItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div ref={scrollRef} />
|
||||
</div>
|
||||
)}
|
||||
<section className="p-4">
|
||||
|
||||
@@ -5,6 +5,7 @@ import MessageItem from '@/components/next-message-item';
|
||||
import PdfSheet from '@/components/pdf-drawer';
|
||||
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
|
||||
import { useSyncThemeFromParams } from '@/components/theme-provider';
|
||||
import { VirtualMessageList } from '@/components/virtual-message-list';
|
||||
import { MessageType } from '@/constants/chat';
|
||||
import { useUploadAgentFileWithProgress } from '@/hooks/use-agent-request';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -12,7 +13,6 @@ import i18n, { changeLanguageAsync } from '@/locales/config';
|
||||
import DebugContent from '@/pages/agent/debug-content';
|
||||
import { useCacheChatLog } from '@/pages/agent/hooks/use-cache-chat-log';
|
||||
import { useAwaitComponentData } from '@/pages/agent/hooks/use-chat-logic';
|
||||
import { buildMessageUuidWithRole } from '@/utils/chat';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { forwardRef, useCallback } from 'react';
|
||||
import {
|
||||
@@ -47,7 +47,7 @@ const ChatContainer = () => {
|
||||
handleInputChange,
|
||||
value,
|
||||
sendLoading,
|
||||
scrollRef,
|
||||
virtualizer,
|
||||
messageContainerRef,
|
||||
derivedMessages,
|
||||
hasError,
|
||||
@@ -130,60 +130,59 @@ const ChatContainer = () => {
|
||||
)}
|
||||
ref={messageContainerRef}
|
||||
>
|
||||
<div>
|
||||
{derivedMessages?.map((message, i) => {
|
||||
return (
|
||||
<MessageItem
|
||||
visibleAvatar={visibleAvatar}
|
||||
conversationId={conversationId}
|
||||
currentEventListWithoutMessageById={
|
||||
currentEventListWithoutMessageById
|
||||
}
|
||||
setCurrentMessageId={setCurrentMessageId}
|
||||
key={buildMessageUuidWithRole(message)}
|
||||
item={message}
|
||||
nickname="You"
|
||||
reference={findReferenceByMessageId(message.id)}
|
||||
loading={
|
||||
message.role === MessageType.Assistant &&
|
||||
sendLoading &&
|
||||
derivedMessages?.length - 1 === i
|
||||
}
|
||||
isShare={true}
|
||||
avatarDialog={inputsData.avatar}
|
||||
agentName={inputsData.title}
|
||||
index={i}
|
||||
clickDocumentButton={clickDocumentButton}
|
||||
showLikeButton={false}
|
||||
showLoudspeaker={false}
|
||||
showLog={false}
|
||||
sendLoading={sendLoading}
|
||||
>
|
||||
{message.role === MessageType.Assistant &&
|
||||
derivedMessages.length - 1 === i && (
|
||||
<DebugContent
|
||||
parameters={buildInputList(message)}
|
||||
message={message}
|
||||
ok={handleOk(message)}
|
||||
isNext={false}
|
||||
btnText={'Submit'}
|
||||
></DebugContent>
|
||||
)}
|
||||
{message.role === MessageType.Assistant &&
|
||||
derivedMessages.length - 1 !== i && (
|
||||
<div>
|
||||
<div>{message?.data?.tips}</div>
|
||||
<VirtualMessageList
|
||||
className="w-full"
|
||||
items={derivedMessages}
|
||||
virtualizer={virtualizer}
|
||||
renderItem={(message, i) => (
|
||||
<MessageItem
|
||||
visibleAvatar={visibleAvatar}
|
||||
conversationId={conversationId}
|
||||
currentEventListWithoutMessageById={
|
||||
currentEventListWithoutMessageById
|
||||
}
|
||||
setCurrentMessageId={setCurrentMessageId}
|
||||
item={message}
|
||||
nickname="You"
|
||||
reference={findReferenceByMessageId(message.id)}
|
||||
loading={
|
||||
message.role === MessageType.Assistant &&
|
||||
sendLoading &&
|
||||
derivedMessages?.length - 1 === i
|
||||
}
|
||||
isShare={true}
|
||||
avatarDialog={inputsData.avatar}
|
||||
agentName={inputsData.title}
|
||||
index={i}
|
||||
clickDocumentButton={clickDocumentButton}
|
||||
showLikeButton={false}
|
||||
showLoudspeaker={false}
|
||||
showLog={false}
|
||||
sendLoading={sendLoading}
|
||||
>
|
||||
{message.role === MessageType.Assistant &&
|
||||
derivedMessages.length - 1 === i && (
|
||||
<DebugContent
|
||||
parameters={buildInputList(message)}
|
||||
message={message}
|
||||
ok={handleOk(message)}
|
||||
isNext={false}
|
||||
btnText={'Submit'}
|
||||
></DebugContent>
|
||||
)}
|
||||
{message.role === MessageType.Assistant &&
|
||||
derivedMessages.length - 1 !== i && (
|
||||
<div>
|
||||
<div>{message?.data?.tips}</div>
|
||||
|
||||
<div>
|
||||
{buildInputList(message)?.map((item) => item.value)}
|
||||
</div>
|
||||
<div>
|
||||
{buildInputList(message)?.map((item) => item.value)}
|
||||
</div>
|
||||
)}
|
||||
</MessageItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div ref={scrollRef} />
|
||||
</div>
|
||||
)}
|
||||
</MessageItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{isTaskMode || (
|
||||
<div className="flex w-full justify-center md:mb-8">
|
||||
|
||||
@@ -45,6 +45,9 @@ jest.mock('@/components/message-input/next', () => ({
|
||||
MockNextMessageInput(props),
|
||||
}));
|
||||
jest.mock('@/components/message-item', () => () => null);
|
||||
jest.mock('@/components/virtual-message-list', () => ({
|
||||
VirtualMessageList: () => null,
|
||||
}));
|
||||
jest.mock('@/components/pdf-drawer', () => () => null);
|
||||
jest.mock('@/components/pdf-drawer/hooks', () => ({
|
||||
useClickDrawer: () => ({
|
||||
@@ -61,7 +64,6 @@ jest.mock('@/hooks/logic-hooks', () => ({
|
||||
value: 'Question',
|
||||
setValue: jest.fn(),
|
||||
}),
|
||||
useScrollToBottom: () => ({ scrollRef: { current: null } }),
|
||||
}));
|
||||
jest.mock('@/hooks/use-chat-request', () => ({
|
||||
useFetchChat: () => ({
|
||||
|
||||
@@ -15,11 +15,9 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { VirtualMessageList } from '@/components/virtual-message-list';
|
||||
import { MessageType } from '@/constants/chat';
|
||||
import {
|
||||
useHandleMessageInputChange,
|
||||
useScrollToBottom,
|
||||
} from '@/hooks/logic-hooks';
|
||||
import { useHandleMessageInputChange } from '@/hooks/logic-hooks';
|
||||
import {
|
||||
useFetchChat,
|
||||
useGetChatSearchParams,
|
||||
@@ -28,7 +26,6 @@ import {
|
||||
import { useFindLlmByUuid } from '@/hooks/use-llm-request';
|
||||
import { useFetchUserInfo } from '@/hooks/use-user-setting-request';
|
||||
import { IClientConversation } from '@/interfaces/database/chat';
|
||||
import { buildMessageUuidWithRole } from '@/utils/chat';
|
||||
import {
|
||||
deriveModelPreset,
|
||||
removeUselessFieldsFromValues,
|
||||
@@ -110,18 +107,19 @@ const ChatCard = forwardRef(function ChatCard(
|
||||
const { id: dialogId } = useParams();
|
||||
const { updateChat } = useUpdateChat();
|
||||
|
||||
const { derivedMessages, handlePressEnter, sendLoading } =
|
||||
useSendSingleMessage({
|
||||
controller,
|
||||
value,
|
||||
setValue,
|
||||
files,
|
||||
clearFiles,
|
||||
});
|
||||
|
||||
const messageContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { scrollRef } = useScrollToBottom(derivedMessages, messageContainerRef);
|
||||
const {
|
||||
derivedMessages,
|
||||
handlePressEnter,
|
||||
sendLoading,
|
||||
messageContainerRef,
|
||||
virtualizer,
|
||||
} = useSendSingleMessage({
|
||||
controller,
|
||||
value,
|
||||
setValue,
|
||||
files,
|
||||
clearFiles,
|
||||
});
|
||||
|
||||
const messageReferences = useMessageReferences(derivedMessages);
|
||||
|
||||
@@ -264,32 +262,31 @@ const ChatCard = forwardRef(function ChatCard(
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 min-h-0">
|
||||
<div ref={messageContainerRef} className="h-full overflow-auto">
|
||||
<div className="w-full">
|
||||
{derivedMessages?.map((message, i) => {
|
||||
return (
|
||||
<MessageItem
|
||||
loading={
|
||||
message.role === MessageType.Assistant &&
|
||||
sendLoading &&
|
||||
derivedMessages.length - 1 === i
|
||||
}
|
||||
key={buildMessageUuidWithRole(message)}
|
||||
item={message}
|
||||
nickname={userInfo.nickname}
|
||||
avatar={userInfo.avatar}
|
||||
avatarDialog={currentDialog.icon}
|
||||
reference={messageReferences.get(message) ?? EmptyReference}
|
||||
// clickDocumentButton={clickDocumentButton}
|
||||
index={i}
|
||||
isLast={i === derivedMessages.length - 1}
|
||||
sendLoading={sendLoading}
|
||||
clickDocumentButton={clickDocumentButton}
|
||||
showLikeButton={false}
|
||||
></MessageItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div ref={scrollRef} />
|
||||
<VirtualMessageList
|
||||
className="w-full"
|
||||
items={derivedMessages ?? []}
|
||||
virtualizer={virtualizer}
|
||||
renderItem={(message, i) => (
|
||||
<MessageItem
|
||||
loading={
|
||||
message.role === MessageType.Assistant &&
|
||||
sendLoading &&
|
||||
derivedMessages.length - 1 === i
|
||||
}
|
||||
item={message}
|
||||
nickname={userInfo.nickname}
|
||||
avatar={userInfo.avatar}
|
||||
avatarDialog={currentDialog.icon}
|
||||
reference={messageReferences.get(message) ?? EmptyReference}
|
||||
// clickDocumentButton={clickDocumentButton}
|
||||
index={i}
|
||||
isLast={i === derivedMessages.length - 1}
|
||||
sendLoading={sendLoading}
|
||||
clickDocumentButton={clickDocumentButton}
|
||||
showLikeButton={false}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -10,6 +10,9 @@ jest.mock('@/components/message-input/next', () => ({
|
||||
NextMessageInput: () => null,
|
||||
}));
|
||||
jest.mock('@/components/message-item', () => () => null);
|
||||
jest.mock('@/components/virtual-message-list', () => ({
|
||||
VirtualMessageList: () => null,
|
||||
}));
|
||||
jest.mock('@/components/pdf-drawer', () => () => null);
|
||||
jest.mock('@/components/pdf-drawer/hooks', () => ({
|
||||
useClickDrawer: () => ({
|
||||
|
||||
@@ -2,11 +2,11 @@ import { NextMessageInput } from '@/components/message-input/next';
|
||||
import MessageItem from '@/components/message-item';
|
||||
import PdfSheet from '@/components/pdf-drawer';
|
||||
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
|
||||
import { VirtualMessageList } from '@/components/virtual-message-list';
|
||||
import { MessageType } from '@/constants/chat';
|
||||
import { useFetchChat, useGetChatSearchParams } from '@/hooks/use-chat-request';
|
||||
import { useFetchUserInfo } from '@/hooks/use-user-setting-request';
|
||||
import { IClientConversation } from '@/interfaces/database/chat';
|
||||
import { buildMessageUuidWithRole } from '@/utils/chat';
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
useGetSendButtonDisabled,
|
||||
@@ -25,7 +25,7 @@ interface IProps {
|
||||
export function SingleChatBox({ conversation }: IProps) {
|
||||
const {
|
||||
value,
|
||||
scrollRef,
|
||||
virtualizer,
|
||||
messageContainerRef,
|
||||
sendLoading,
|
||||
messages,
|
||||
@@ -82,31 +82,32 @@ export function SingleChatBox({ conversation }: IProps) {
|
||||
ref={messageContainerRef}
|
||||
className="p-5 flex-1 overflow-auto min-h-0 scrollbar-auto"
|
||||
>
|
||||
<div className="w-full pr-5">
|
||||
{messages?.map((message, i) => (
|
||||
<VirtualMessageList
|
||||
className="w-full pr-5"
|
||||
items={messages ?? []}
|
||||
virtualizer={virtualizer}
|
||||
renderItem={(message, i) => (
|
||||
<MessageItem
|
||||
loading={
|
||||
message.role === MessageType.Assistant &&
|
||||
sendLoading &&
|
||||
messages.length - 1 === i
|
||||
(messages?.length ?? 0) - 1 === i
|
||||
}
|
||||
key={buildMessageUuidWithRole(message)}
|
||||
item={message}
|
||||
nickname={userInfo.nickname}
|
||||
avatar={userInfo.avatar}
|
||||
avatarDialog={currentDialog.icon}
|
||||
item={message}
|
||||
reference={messageReferences.get(message) ?? EmptyReference}
|
||||
clickDocumentButton={clickDocumentButton}
|
||||
index={i}
|
||||
isLast={i === messages.length - 1}
|
||||
isLast={(messages?.length ?? 0) - 1 === i}
|
||||
removeMessageById={removeMessageById}
|
||||
deleteDisabled={message.awaitingServerId}
|
||||
regenerateMessage={regenerateMessage}
|
||||
sendLoading={sendLoading}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div ref={scrollRef} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-5 pt-0">
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { NextMessageInputOnPressEnterParameter } from '@/components/message-input/next';
|
||||
import { MessageType } from '@/constants/chat';
|
||||
import {
|
||||
useHandleMessageInputChange,
|
||||
useScrollToBottom,
|
||||
} from '@/hooks/logic-hooks';
|
||||
import { useHandleMessageInputChange } from '@/hooks/logic-hooks';
|
||||
import { useFetchChat, useGetChatSearchParams } from '@/hooks/use-chat-request';
|
||||
import { useVirtualMessages } from '@/hooks/use-virtual-messages';
|
||||
import { buildMessageListWithUuid } from '@/utils/chat';
|
||||
import { IMessage, Message } from '@/interfaces/database/chat';
|
||||
import notification from '@/utils/notification';
|
||||
@@ -40,7 +38,11 @@ export const useCurrentChatSession = () => {
|
||||
const isStreaming = useIsChatStreaming(conversationId);
|
||||
|
||||
const messageContainerRef = useRef<HTMLDivElement>(null);
|
||||
const { scrollRef } = useScrollToBottom(messages, messageContainerRef);
|
||||
const { virtualizer, scrollToBottom } = useVirtualMessages(
|
||||
messages,
|
||||
messageContainerRef,
|
||||
{ resetKey: conversationId },
|
||||
);
|
||||
|
||||
const ensureSession = useChatStreamStore((state) => state.ensureSession);
|
||||
const seedPrologue = useChatStreamStore((state) => state.seedPrologue);
|
||||
@@ -58,8 +60,9 @@ export const useCurrentChatSession = () => {
|
||||
return {
|
||||
messages,
|
||||
isStreaming,
|
||||
scrollRef,
|
||||
virtualizer,
|
||||
messageContainerRef,
|
||||
scrollToBottom,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -73,8 +76,13 @@ export const useSendMessage = () => {
|
||||
|
||||
const { id: chatId } = useParams();
|
||||
const { data: currentDialog } = useFetchChat();
|
||||
const { messages, isStreaming, scrollRef, messageContainerRef } =
|
||||
useCurrentChatSession();
|
||||
const {
|
||||
messages,
|
||||
isStreaming,
|
||||
virtualizer,
|
||||
messageContainerRef,
|
||||
scrollToBottom,
|
||||
} = useCurrentChatSession();
|
||||
|
||||
const appendQuestion = useChatStreamStore((state) => state.appendQuestion);
|
||||
const failStream = useChatStreamStore((state) => state.failStream);
|
||||
@@ -240,15 +248,7 @@ export const useSendMessage = () => {
|
||||
clearFiles();
|
||||
|
||||
// Auto scroll to bottom when sending new message
|
||||
if (messageContainerRef.current) {
|
||||
const el = messageContainerRef.current;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
el.scrollTo({
|
||||
top: el.scrollHeight,
|
||||
});
|
||||
});
|
||||
}
|
||||
requestAnimationFrame(scrollToBottom);
|
||||
},
|
||||
[
|
||||
value,
|
||||
@@ -260,7 +260,7 @@ export const useSendMessage = () => {
|
||||
clearFiles,
|
||||
setValue,
|
||||
sendMessage,
|
||||
messageContainerRef,
|
||||
scrollToBottom,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -275,7 +275,7 @@ export const useSendMessage = () => {
|
||||
setValue,
|
||||
regenerateMessage,
|
||||
sendLoading: isStreaming,
|
||||
scrollRef,
|
||||
virtualizer,
|
||||
messageContainerRef,
|
||||
messages,
|
||||
removeMessageById,
|
||||
|
||||
@@ -56,7 +56,7 @@ export const useSendSharedMessage = () => {
|
||||
removeLatestMessage,
|
||||
addNewestAnswer,
|
||||
addNewestQuestion,
|
||||
scrollRef,
|
||||
virtualizer,
|
||||
messageContainerRef,
|
||||
removeAllMessages,
|
||||
removeAllMessagesExceptFirst,
|
||||
@@ -165,7 +165,7 @@ export const useSendSharedMessage = () => {
|
||||
derivedMessages,
|
||||
hasError,
|
||||
stopOutputMessage,
|
||||
scrollRef,
|
||||
virtualizer,
|
||||
messageContainerRef,
|
||||
removeAllMessages,
|
||||
removeAllMessagesExceptFirst,
|
||||
|
||||
@@ -36,7 +36,7 @@ export function useSendSingleMessage({
|
||||
const { send, answer, done } = useSendMessageWithSse();
|
||||
|
||||
const {
|
||||
scrollRef,
|
||||
virtualizer,
|
||||
messageContainerRef,
|
||||
setDerivedMessages,
|
||||
derivedMessages,
|
||||
@@ -151,7 +151,7 @@ export function useSendSingleMessage({
|
||||
);
|
||||
|
||||
return {
|
||||
scrollRef,
|
||||
virtualizer,
|
||||
messageContainerRef,
|
||||
setDerivedMessages,
|
||||
derivedMessages,
|
||||
|
||||
@@ -4,10 +4,10 @@ import MessageItem from '@/components/message-item';
|
||||
import PdfSheet from '@/components/pdf-drawer';
|
||||
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
|
||||
import { useSyncThemeFromParams } from '@/components/theme-provider';
|
||||
import { VirtualMessageList } from '@/components/virtual-message-list';
|
||||
import { MessageType } from '@/constants/chat';
|
||||
import { useFetchExternalChatInfo } from '@/hooks/use-chat-request';
|
||||
import i18n, { changeLanguageAsync } from '@/locales/config';
|
||||
import { buildMessageUuidWithRole } from '@/utils/chat';
|
||||
import React, { forwardRef } from 'react';
|
||||
import { useSendButtonDisabled } from '../hooks/use-button-disabled';
|
||||
import {
|
||||
@@ -36,7 +36,7 @@ const ChatContainer = () => {
|
||||
derivedMessages,
|
||||
hasError,
|
||||
stopOutputMessage,
|
||||
scrollRef,
|
||||
virtualizer,
|
||||
messageContainerRef,
|
||||
removeAllMessagesExceptFirst,
|
||||
} = useSendSharedMessage();
|
||||
@@ -72,31 +72,29 @@ const ChatContainer = () => {
|
||||
}
|
||||
ref={messageContainerRef}
|
||||
>
|
||||
<div>
|
||||
{derivedMessages?.map((message, i) => {
|
||||
return (
|
||||
<MessageItem
|
||||
visibleAvatar={visibleAvatar}
|
||||
key={buildMessageUuidWithRole(message)}
|
||||
avatarDialog={avatarDialogSrc}
|
||||
item={message}
|
||||
nickname="You"
|
||||
reference={messageReferences.get(message) ?? EmptyReference}
|
||||
loading={
|
||||
message.role === MessageType.Assistant &&
|
||||
sendLoading &&
|
||||
derivedMessages?.length - 1 === i
|
||||
}
|
||||
index={i}
|
||||
isLast={i === derivedMessages.length - 1}
|
||||
clickDocumentButton={clickDocumentButton}
|
||||
showLikeButton={false}
|
||||
showLoudspeaker={false}
|
||||
></MessageItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div ref={scrollRef} />
|
||||
<VirtualMessageList
|
||||
className="w-full"
|
||||
items={derivedMessages}
|
||||
virtualizer={virtualizer}
|
||||
renderItem={(message, i) => (
|
||||
<MessageItem
|
||||
visibleAvatar={visibleAvatar}
|
||||
avatarDialog={avatarDialogSrc}
|
||||
item={message}
|
||||
nickname="You"
|
||||
reference={messageReferences.get(message) ?? EmptyReference}
|
||||
loading={
|
||||
message.role === MessageType.Assistant &&
|
||||
sendLoading &&
|
||||
derivedMessages.length - 1 === i
|
||||
}
|
||||
index={i}
|
||||
clickDocumentButton={clickDocumentButton}
|
||||
showLikeButton={false}
|
||||
showLoudspeaker={false}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full justify-center md:mb-8">
|
||||
<div className="w-full md:w-5/6">
|
||||
|
||||
Reference in new issue
Block a user