// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates // SPDX-License-Identifier: MIT import { LoadingOutlined } from "@ant-design/icons"; import { motion } from "framer-motion"; import { Download, Headphones } from "lucide-react"; import { useCallback, useMemo, useRef, useState } from "react"; import { LoadingAnimation } from "~/components/deer-flow/loading-animation"; import { Markdown } from "~/components/deer-flow/markdown"; import { RainbowText } from "~/components/deer-flow/rainbow-text"; import { RollingText } from "~/components/deer-flow/rolling-text"; import { ScrollContainer, type ScrollContainerRef, } from "~/components/deer-flow/scroll-container"; import { Tooltip } from "~/components/deer-flow/tooltip"; import { Button } from "~/components/ui/button"; import { Card, CardContent, CardFooter, CardHeader, CardTitle, } from "~/components/ui/card"; import type { Message, Option } from "~/core/messages"; import { closeResearch, openResearch, useLastFeedbackMessageId, useLastInterruptMessage, useMessage, useMessageIds, useResearchMessage, useStore, } from "~/core/store"; import { parseJSON } from "~/core/utils"; import { cn } from "~/lib/utils"; export function MessageListView({ className, onFeedback, onSendMessage, }: { className?: string; onFeedback?: (feedback: { option: Option }) => void; onSendMessage?: ( message: string, options?: { interruptFeedback?: string }, ) => void; }) { const scrollContainerRef = useRef(null); const messageIds = useMessageIds(); const interruptMessage = useLastInterruptMessage(); const waitingForFeedbackMessageId = useLastFeedbackMessageId(); const responding = useStore((state) => state.responding); const noOngoingResearch = useStore( (state) => state.ongoingResearchId === null, ); const ongoingResearchIsOpen = useStore( (state) => state.ongoingResearchId === state.openResearchId, ); const handleToggleResearch = useCallback(() => { // Fix the issue where auto-scrolling to the bottom // occasionally fails when toggling research. const timer = setTimeout(() => { if (scrollContainerRef.current) { scrollContainerRef.current.scrollToBottom(); } }, 500); return () => { clearTimeout(timer); }; }, []); return ( {responding && (noOngoingResearch || !ongoingResearchIsOpen) && ( )} ); } function MessageListItem({ className, messageId, waitForFeedback, interruptMessage, onFeedback, onSendMessage, onToggleResearch, }: { className?: string; messageId: string; waitForFeedback?: boolean; onFeedback?: (feedback: { option: Option }) => void; interruptMessage?: Message | null; onSendMessage?: ( message: string, options?: { interruptFeedback?: string }, ) => void; onToggleResearch?: () => void; }) { const message = useMessage(messageId); const researchIds = useStore((state) => state.researchIds); const startOfResearch = useMemo(() => { return researchIds.includes(messageId); }, [researchIds, messageId]); if (message) { if ( message.role === "user" || message.agent === "coordinator" || message.agent === "planner" || message.agent === "podcast" || startOfResearch ) { let content: React.ReactNode; if (message.agent === "planner") { content = (
); } else if (message.agent === "podcast") { content = (
); } else if (startOfResearch) { content = (
); } else { content = message.content ? (
{message?.content}
) : null; } if (content) { return ( {content} ); } } return null; } } function MessageBubble({ className, message, children, }: { className?: string; message: Message; children: React.ReactNode; }) { return (
{children}
); } function ResearchCard({ className, researchId, onToggleResearch, }: { className?: string; researchId: string; onToggleResearch?: () => void; }) { const reportId = useStore((state) => state.researchReportIds.get(researchId)); const hasReport = reportId !== undefined; const reportGenerating = useStore( (state) => hasReport && state.messages.get(reportId)!.isStreaming, ); const openResearchId = useStore((state) => state.openResearchId); const state = useMemo(() => { if (hasReport) { return reportGenerating ? "Generating report..." : "Report generated"; } return "Researching..."; }, [hasReport, reportGenerating]); const msg = useResearchMessage(researchId); const title = useMemo(() => { if (msg) { return parseJSON(msg.content ?? "", { title: "" }).title; } return undefined; }, [msg]); const handleOpen = useCallback(() => { if (openResearchId === researchId) { closeResearch(); } else { openResearch(researchId); } onToggleResearch?.(); }, [openResearchId, researchId, onToggleResearch]); return ( {title !== undefined && title !== "" ? title : "Deep Research"}
{state}
); } const GREETINGS = ["Cool", "Sounds great", "Looks good", "Great", "Awesome"]; function PlanCard({ className, message, interruptMessage, onFeedback, waitForFeedback, onSendMessage, }: { className?: string; message: Message; interruptMessage?: Message | null; onFeedback?: (feedback: { option: Option }) => void; onSendMessage?: ( message: string, options?: { interruptFeedback?: string }, ) => void; waitForFeedback?: boolean; }) { const plan = useMemo<{ title?: string; thought?: string; steps?: { title?: string; description?: string }[]; }>(() => { return parseJSON(message.content ?? "", {}); }, [message.content]); const handleAccept = useCallback(async () => { if (onSendMessage) { onSendMessage( `${GREETINGS[Math.floor(Math.random() * GREETINGS.length)]}! ${Math.random() > 0.5 ? "Let's get started." : "Let's start."}`, { interruptFeedback: "accepted", }, ); } }, [onSendMessage]); return ( {`### ${ plan.title !== undefined && plan.title !== "" ? plan.title : "Deep Research" }`} {plan.thought} {plan.steps && (
    {plan.steps.map((step, i) => (
  • {step.title}

    {step.description}
  • ))}
)}
{!message.isStreaming && interruptMessage?.options?.length && ( {interruptMessage?.options.map((option) => ( ))} )}
); } function PodcastCard({ className, message, }: { className?: string; message: Message; }) { const data = useMemo(() => { return JSON.parse(message.content ?? ""); }, [message.content]); const title = useMemo(() => data?.title, [data]); const audioUrl = useMemo(() => data?.audioUrl, [data]); const isGenerating = useMemo(() => { return message.isStreaming; }, [message.isStreaming]); const [isPlaying, setIsPlaying] = useState(false); return (
{isGenerating ? : } {isGenerating ? "Generating podcast..." : isPlaying ? "Now playing podcast..." : "Podcast"}
{!isGenerating && ( )}
{title}
); }