mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-21 21:24:46 +08:00
feat: enable podcast
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import {
|
||||
DownloadOutlined,
|
||||
SoundOutlined,
|
||||
LoadingOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { parse } from "best-effort-json-parser";
|
||||
import { motion } from "framer-motion";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
@@ -13,6 +18,11 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "~/components/ui/tooltip";
|
||||
import type { Message, Option } from "~/core/messages";
|
||||
import {
|
||||
openResearch,
|
||||
@@ -114,6 +124,7 @@ function MessageListItem({
|
||||
message.role === "user" ||
|
||||
message.agent === "coordinator" ||
|
||||
message.agent === "planner" ||
|
||||
message.agent === "podcast" ||
|
||||
startOfResearch
|
||||
) {
|
||||
let content: React.ReactNode;
|
||||
@@ -128,6 +139,12 @@ function MessageListItem({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (message.agent === "podcast") {
|
||||
content = (
|
||||
<div className="w-full px-4">
|
||||
<PodcastCard message={message} />
|
||||
</div>
|
||||
);
|
||||
} else if (startOfResearch) {
|
||||
content = (
|
||||
<div className="w-full px-4">
|
||||
@@ -348,3 +365,63 @@ function PlanCard({
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function PodcastCard({
|
||||
className,
|
||||
message,
|
||||
}: {
|
||||
className?: string;
|
||||
message: Message;
|
||||
}) {
|
||||
const data = useMemo(() => {
|
||||
return 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 (
|
||||
<Card className={cn("w-[508px] bg-white", className)}>
|
||||
<CardHeader>
|
||||
<div className="text-muted-foreground flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
{isGenerating ? <LoadingOutlined /> : <SoundOutlined />}
|
||||
<RainbowText animated={isGenerating}>
|
||||
{isGenerating
|
||||
? "Generating podcast..."
|
||||
: isPlaying
|
||||
? "Now playing podcast..."
|
||||
: "Podcast"}
|
||||
</RainbowText>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<DownloadOutlined />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Download podcast</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<CardTitle>
|
||||
<div className="text-lg font-medium">
|
||||
<RainbowText animated={isGenerating}>{title}</RainbowText>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<audio
|
||||
className="w-full"
|
||||
src={audioUrl}
|
||||
controls
|
||||
onPlay={() => setIsPlaying(true)}
|
||||
onPause={() => setIsPlaying(false)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,8 +80,12 @@ export function ResearchBlock({
|
||||
</div>
|
||||
<TabsContent className="h-full min-h-0 flex-grow px-8" value="report">
|
||||
<ScrollContainer className="px-5pb-20 h-full">
|
||||
{reportId && (
|
||||
<ResearchReportBlock className="mt-4" messageId={reportId} />
|
||||
{reportId && researchId && (
|
||||
<ResearchReportBlock
|
||||
className="mt-4"
|
||||
researchId={researchId}
|
||||
messageId={reportId}
|
||||
/>
|
||||
)}
|
||||
</ScrollContainer>
|
||||
</TabsContent>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { PauseCircleOutlined, SoundOutlined } from "@ant-design/icons";
|
||||
import { SoundOutlined } from "@ant-design/icons";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
|
||||
import { Button } from "~/components/ui/button";
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
} from "~/components/ui/tooltip";
|
||||
import { useMessage } from "~/core/store";
|
||||
import { listenToPodcast, useMessage } from "~/core/store";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
import { LoadingAnimation } from "./loading-animation";
|
||||
@@ -18,29 +18,23 @@ import { Markdown } from "./markdown";
|
||||
|
||||
export function ResearchReportBlock({
|
||||
className,
|
||||
researchId,
|
||||
messageId,
|
||||
}: {
|
||||
className?: string;
|
||||
researchId: string;
|
||||
messageId: string;
|
||||
}) {
|
||||
const message = useMessage(messageId);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [isTTS, setIsTTS] = useState(false);
|
||||
const handleTTS = useCallback(() => {
|
||||
if (contentRef.current) {
|
||||
if (isTTS) {
|
||||
window.speechSynthesis.cancel();
|
||||
setIsTTS(false);
|
||||
} else {
|
||||
const text = contentRef.current.textContent;
|
||||
if (text) {
|
||||
const utterance = new SpeechSynthesisUtterance(text);
|
||||
setIsTTS(true);
|
||||
window.speechSynthesis.speak(utterance);
|
||||
}
|
||||
}
|
||||
const [isGenerated, setGenerated] = useState(false);
|
||||
const handleListenToReport = useCallback(async () => {
|
||||
if (isGenerated) {
|
||||
return;
|
||||
}
|
||||
}, [isTTS]);
|
||||
setGenerated(true);
|
||||
await listenToPodcast(researchId);
|
||||
}, [isGenerated, researchId]);
|
||||
return (
|
||||
<div
|
||||
ref={contentRef}
|
||||
@@ -54,14 +48,16 @@ export function ResearchReportBlock({
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
handleTTS();
|
||||
void handleListenToReport();
|
||||
}}
|
||||
>
|
||||
{isTTS ? <PauseCircleOutlined /> : <SoundOutlined />}
|
||||
<SoundOutlined />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{isTTS ? "Pause" : "Listen to the report"}</p>
|
||||
<p>
|
||||
{isGenerated ? "The podcast is generated" : "Generate podcast"}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user