deerflow2/web/src/app/chat/components/messages-block.tsx

186 lines
6.5 KiB
TypeScript

// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
// SPDX-License-Identifier: MIT
import { motion } from "framer-motion";
import { FastForward, Play } from "lucide-react";
import { useCallback, useRef, useState } from "react";
import { RainbowText } from "~/components/deer-flow/rainbow-text";
import { Button } from "~/components/ui/button";
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { fastForwardReplay } from "~/core/api";
import { useReplayMetadata } from "~/core/api/hooks";
import type { Option } from "~/core/messages";
import { useReplay } from "~/core/replay";
import { sendMessage, useStore } from "~/core/store";
import { env } from "~/env";
import { cn } from "~/lib/utils";
import { ConversationStarter } from "./conversation-starter";
import { InputBox } from "./input-box";
import { MessageListView } from "./message-list-view";
import { Welcome } from "./welcome";
export function MessagesBlock({ className }: { className?: string }) {
const messageCount = useStore((state) => state.messageIds.length);
const responding = useStore((state) => state.responding);
const { isReplay } = useReplay();
const { title: replayTitle, hasError: replayHasError } = useReplayMetadata();
const [replayStarted, setReplayStarted] = useState(false);
const abortControllerRef = useRef<AbortController | null>(null);
const [feedback, setFeedback] = useState<{ option: Option } | null>(null);
const handleSend = useCallback(
async (message: string, options?: { interruptFeedback?: string }) => {
const abortController = new AbortController();
abortControllerRef.current = abortController;
try {
await sendMessage(
message,
{
interruptFeedback:
options?.interruptFeedback ?? feedback?.option.value,
},
{
abortSignal: abortController.signal,
},
);
} catch {}
},
[feedback],
);
const handleCancel = useCallback(() => {
abortControllerRef.current?.abort();
abortControllerRef.current = null;
}, []);
const handleFeedback = useCallback(
(feedback: { option: Option }) => {
setFeedback(feedback);
},
[setFeedback],
);
const handleRemoveFeedback = useCallback(() => {
setFeedback(null);
}, [setFeedback]);
const handleStartReplay = useCallback(() => {
setReplayStarted(true);
void sendMessage();
}, [setReplayStarted]);
const [fastForwarding, setFastForwarding] = useState(false);
const handleFastForwardReplay = useCallback(() => {
setFastForwarding(!fastForwarding);
fastForwardReplay(!fastForwarding);
}, [fastForwarding]);
return (
<div className={cn("flex h-full flex-col", className)}>
<MessageListView
className="flex flex-grow"
onFeedback={handleFeedback}
onSendMessage={handleSend}
/>
{!isReplay ? (
<div className="relative flex h-42 shrink-0 pb-4">
{!responding && messageCount === 0 && (
<ConversationStarter
className="absolute top-[-218px] left-0"
onSend={handleSend}
/>
)}
<InputBox
className="h-full w-full"
responding={responding}
feedback={feedback}
onSend={handleSend}
onCancel={handleCancel}
onRemoveFeedback={handleRemoveFeedback}
/>
</div>
) : (
<>
<div
className={cn(
"fixed bottom-[calc(50vh+80px)] left-0 transition-all duration-500 ease-out",
replayStarted && "pointer-events-none scale-150 opacity-0",
)}
>
<Welcome />
</div>
<motion.div
className="mb-4 h-fit w-full items-center justify-center"
initial={{ opacity: 0, y: "20vh" }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Card
className={cn(
"w-full transition-all duration-300",
!replayStarted && "translate-y-[-40vh]",
)}
>
<div className="flex items-center justify-between">
<div className="flex-grow">
<CardHeader>
<CardTitle>
<RainbowText animated={responding}>
{responding ? "Replaying" : `${replayTitle}`}
</RainbowText>
</CardTitle>
<CardDescription>
<RainbowText animated={responding}>
{responding
? "DeerFlow is now replaying the conversation..."
: replayStarted
? "The replay has been stopped."
: `You're now in DeerFlow's replay mode. Click the "Play" button on the right to start.`}
</RainbowText>
</CardDescription>
</CardHeader>
</div>
{!replayHasError && (
<div className="pr-4">
{responding && (
<Button
className={cn(fastForwarding && "animate-pulse")}
variant={fastForwarding ? "default" : "outline"}
onClick={handleFastForwardReplay}
>
<FastForward size={16} />
Fast Forward
</Button>
)}
{!replayStarted && (
<Button className="w-24" onClick={handleStartReplay}>
<Play size={16} />
Play
</Button>
)}
</div>
)}
</div>
</Card>
{!replayStarted && env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY && (
<div className="text-muted-foreground w-full text-center text-xs">
* This site is for demo purposes only. If you want to try your
own question, please{" "}
<a
className="underline"
href="https://github.com/bytedance/deer-flow"
target="_blank"
rel="noopener noreferrer"
>
click here
</a>{" "}
to clone it locally and run it.
</div>
)}
</motion.div>
</>
)}
</div>
);
}