// 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, useState } from "react";
import { Button } from "~/components/ui/button";
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import type { Message, Option } from "~/core/messages";
import {
openResearch,
useMessage,
useResearchTitle,
useStore,
} from "~/core/store";
import { parseJSON } from "~/core/utils";
import { cn } from "~/lib/utils";
import { LoadingAnimation } from "./loading-animation";
import { Markdown } from "./markdown";
import { RainbowText } from "./rainbow-text";
import { RollingText } from "./rolling-text";
import { ScrollContainer } from "./scroll-container";
import { Tooltip } from "./tooltip";
export function MessageListView({
className,
onFeedback,
onSendMessage,
}: {
className?: string;
onFeedback?: (feedback: { option: Option }) => void;
onSendMessage?: (
message: string,
options?: { interruptFeedback?: string },
) => void;
}) {
const messageIds = useStore((state) => state.messageIds);
const interruptMessage = useStore((state) => {
if (messageIds.length >= 2) {
const lastMessage = state.messages.get(
messageIds[messageIds.length - 1]!,
);
return lastMessage?.finishReason === "interrupt" ? lastMessage : null;
}
return null;
});
const waitingForFeedbackMessageId = useStore((state) => {
if (messageIds.length >= 2) {
const lastMessage = state.messages.get(
messageIds[messageIds.length - 1]!,
);
if (lastMessage && lastMessage.finishReason === "interrupt") {
return state.messageIds[state.messageIds.length - 2];
}
}
return null;
});
const responding = useStore((state) => state.responding);
const noOngoingResearch = useStore(
(state) => state.ongoingResearchId === null,
);
const ongoingResearchIsOpen = useStore(
(state) => state.ongoingResearchId === state.openResearchId,
);
return (
{messageIds.map((messageId) => (
))}
{responding && (noOngoingResearch || !ongoingResearchIsOpen) && (
)}
);
}
function MessageListItem({
className,
messageId,
waitForFeedback,
interruptMessage,
onFeedback,
onSendMessage,
}: {
className?: string;
messageId: string;
waitForFeedback?: boolean;
onFeedback?: (feedback: { option: Option }) => void;
interruptMessage?: Message | null;
onSendMessage?: (
message: string,
options?: { interruptFeedback?: string },
) => void;
}) {
const message = useMessage(messageId);
const startOfResearch = useStore((state) =>
state.researchIds.includes(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 ? (
) : 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,
}: {
className?: string;
researchId: string;
}) {
const reportId = useStore((state) =>
state.researchReportIds.get(researchId),
);
const hasReport = useStore((state) =>
state.researchReportIds.has(researchId),
);
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 title = useResearchTitle(researchId);
const handleOpen = useCallback(() => {
if (openResearchId === researchId) {
openResearch(null);
} else {
openResearch(researchId);
}
}, [openResearchId, researchId]);
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}
);
}