mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-23 00:16:48 +00:00
Refactor API fetch calls to use a unified fetch function; enhance chat history loading with new hooks and UI components
- Replaced `fetchWithAuth` with a generic `fetch` function across various API modules for consistency. - Updated `useThreadStream` and `useThreadHistory` hooks to manage chat history loading, including loading states and pagination. - Introduced `LoadMoreHistoryIndicator` component for better user experience when loading more chat history. - Enhanced message handling in `MessageList` to accommodate new loading states and history management. - Added support for run messages in the thread context, improving the overall message handling logic. - Updated translations for loading indicators in English and Chinese.
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import type { BaseStream } from "@langchain/langgraph-sdk/react";
|
||||
import { ChevronUpIcon, Loader2Icon } from "lucide-react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
import {
|
||||
Conversation,
|
||||
ConversationContent,
|
||||
} from "@/components/ai-elements/conversation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import {
|
||||
extractContentFromMessage,
|
||||
@@ -18,7 +21,6 @@ import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
||||
import type { Subtask } from "@/core/tasks";
|
||||
import { useUpdateSubtask } from "@/core/tasks/context";
|
||||
import type { AgentThreadState } from "@/core/threads";
|
||||
import { useThreadMessageEnrichment } from "@/core/threads/hooks";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { ArtifactFileList } from "../artifacts/artifact-file-list";
|
||||
@@ -33,22 +35,134 @@ import { SubtaskCard } from "./subtask-card";
|
||||
export const MESSAGE_LIST_DEFAULT_PADDING_BOTTOM = 160;
|
||||
export const MESSAGE_LIST_FOLLOWUPS_EXTRA_PADDING_BOTTOM = 80;
|
||||
|
||||
const LOAD_MORE_HISTORY_THROTTLE_MS = 1200;
|
||||
|
||||
function LoadMoreHistoryIndicator({
|
||||
isLoading,
|
||||
hasMore,
|
||||
loadMore,
|
||||
}: {
|
||||
isLoading?: boolean;
|
||||
hasMore?: boolean;
|
||||
loadMore?: () => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const sentinelRef = useRef<HTMLDivElement | null>(null);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const lastLoadRef = useRef(0);
|
||||
|
||||
const throttledLoadMore = useCallback(() => {
|
||||
if (!hasMore || isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const remaining =
|
||||
LOAD_MORE_HISTORY_THROTTLE_MS - (now - lastLoadRef.current);
|
||||
|
||||
if (remaining <= 0) {
|
||||
lastLoadRef.current = now;
|
||||
loadMore?.();
|
||||
return;
|
||||
}
|
||||
|
||||
if (timeoutRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
timeoutRef.current = null;
|
||||
if (!hasMore || isLoading) {
|
||||
return;
|
||||
}
|
||||
lastLoadRef.current = Date.now();
|
||||
loadMore?.();
|
||||
}, remaining);
|
||||
}, [hasMore, isLoading, loadMore]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = sentinelRef.current;
|
||||
if (!element || !hasMore) {
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry?.isIntersecting) {
|
||||
throttledLoadMore();
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: "120px 0px 0px 0px",
|
||||
},
|
||||
);
|
||||
|
||||
observer.observe(element);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [hasMore, throttledLoadMore]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!hasMore && !isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={sentinelRef} className="flex w-full justify-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-muted-foreground hover:text-foreground rounded-full px-3"
|
||||
disabled={(isLoading ?? false) || !hasMore}
|
||||
onClick={throttledLoadMore}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2Icon className="mr-2 size-4 animate-spin" />
|
||||
{t.common.loading}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronUpIcon className="mr-2 size-4" />
|
||||
{t.common.loadMore}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MessageList({
|
||||
className,
|
||||
threadId,
|
||||
thread,
|
||||
paddingBottom = MESSAGE_LIST_DEFAULT_PADDING_BOTTOM,
|
||||
hasMoreHistory,
|
||||
loadMoreHistory,
|
||||
isHistoryLoading,
|
||||
}: {
|
||||
className?: string;
|
||||
threadId: string;
|
||||
thread: BaseStream<AgentThreadState>;
|
||||
paddingBottom?: number;
|
||||
hasMoreHistory?: boolean;
|
||||
loadMoreHistory?: () => void;
|
||||
isHistoryLoading?: boolean;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const rehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading);
|
||||
const updateSubtask = useUpdateSubtask();
|
||||
const messages = thread.messages;
|
||||
const { data: enrichment } = useThreadMessageEnrichment(threadId);
|
||||
|
||||
if (thread.isThreadLoading && messages.length === 0) {
|
||||
return <MessageListSkeleton />;
|
||||
@@ -57,19 +171,21 @@ export function MessageList({
|
||||
<Conversation
|
||||
className={cn("flex size-full flex-col justify-center", className)}
|
||||
>
|
||||
<ConversationContent className="mx-auto w-full max-w-(--container-width-md) gap-8 pt-12">
|
||||
<ConversationContent className="mx-auto w-full max-w-(--container-width-md) gap-8 pt-8">
|
||||
<LoadMoreHistoryIndicator
|
||||
isLoading={isHistoryLoading}
|
||||
hasMore={hasMoreHistory}
|
||||
loadMore={loadMoreHistory}
|
||||
/>
|
||||
{groupMessages(messages, (group) => {
|
||||
if (group.type === "human" || group.type === "assistant") {
|
||||
return group.messages.map((msg) => {
|
||||
const entry = msg.id ? enrichment?.get(msg.id) : undefined;
|
||||
return (
|
||||
<MessageListItem
|
||||
key={`${group.id}/${msg.id}`}
|
||||
threadId={threadId}
|
||||
message={msg}
|
||||
isLoading={thread.isLoading}
|
||||
runId={entry?.run_id}
|
||||
feedback={entry?.feedback}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user