mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-11 09:44:44 +08:00
feat: add deep think feature (#311)
* feat: implement backend logic * feat: implement api/config endpoint * rename the symbol * feat: re-implement configuration at client-side * feat: add client-side of deep thinking * fix backend bug * feat: add reasoning block * docs: update readme * fix: translate into English * fix: change icon to lightbulb * feat: ignore more bad cases * feat: adjust thinking layout, and implement auto scrolling * docs: add comments --------- Co-authored-by: Henry Li <henry1943@163.com>
This commit is contained in:
@@ -3,8 +3,8 @@
|
||||
|
||||
import { MagicWandIcon } from "@radix-ui/react-icons";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { ArrowUp, X } from "lucide-react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { ArrowUp, Lightbulb, X } from "lucide-react";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { Detective } from "~/components/deer-flow/icons/detective";
|
||||
import MessageInput, {
|
||||
@@ -15,8 +15,10 @@ import { Tooltip } from "~/components/deer-flow/tooltip";
|
||||
import { BorderBeam } from "~/components/magicui/border-beam";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { enhancePrompt } from "~/core/api";
|
||||
import { getConfig } from "~/core/api/config";
|
||||
import type { Option, Resource } from "~/core/messages";
|
||||
import {
|
||||
setEnableDeepThinking,
|
||||
setEnableBackgroundInvestigation,
|
||||
useSettingsStore,
|
||||
} from "~/core/store";
|
||||
@@ -44,9 +46,13 @@ export function InputBox({
|
||||
onCancel?: () => void;
|
||||
onRemoveFeedback?: () => void;
|
||||
}) {
|
||||
const enableDeepThinking = useSettingsStore(
|
||||
(state) => state.general.enableDeepThinking,
|
||||
);
|
||||
const backgroundInvestigation = useSettingsStore(
|
||||
(state) => state.general.enableBackgroundInvestigation,
|
||||
);
|
||||
const reasoningModel = useMemo(() => getConfig().models.reasoning?.[0], []);
|
||||
const reportStyle = useSettingsStore((state) => state.general.reportStyle);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<MessageInputRef>(null);
|
||||
@@ -203,6 +209,36 @@ export function InputBox({
|
||||
</div>
|
||||
<div className="flex items-center px-4 py-2">
|
||||
<div className="flex grow gap-2">
|
||||
{reasoningModel && (
|
||||
<Tooltip
|
||||
className="max-w-60"
|
||||
title={
|
||||
<div>
|
||||
<h3 className="mb-2 font-bold">
|
||||
Deep Thinking Mode: {enableDeepThinking ? "On" : "Off"}
|
||||
</h3>
|
||||
<p>
|
||||
When enabled, DeerFlow will use reasoning model (
|
||||
{reasoningModel}) to generate more thoughtful plans.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
className={cn(
|
||||
"rounded-2xl",
|
||||
enableDeepThinking && "!border-brand !text-brand",
|
||||
)}
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setEnableDeepThinking(!enableDeepThinking);
|
||||
}}
|
||||
>
|
||||
<Lightbulb /> Deep Thinking
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip
|
||||
className="max-w-60"
|
||||
title={
|
||||
|
||||
@@ -3,8 +3,14 @@
|
||||
|
||||
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 {
|
||||
Download,
|
||||
Headphones,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Lightbulb,
|
||||
} from "lucide-react";
|
||||
import React, { useCallback, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { LoadingAnimation } from "~/components/deer-flow/loading-animation";
|
||||
import { Markdown } from "~/components/deer-flow/markdown";
|
||||
@@ -23,6 +29,11 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "~/components/ui/collapsible";
|
||||
import type { Message, Option } from "~/core/messages";
|
||||
import {
|
||||
closeResearch,
|
||||
@@ -294,6 +305,114 @@ function ResearchCard({
|
||||
);
|
||||
}
|
||||
|
||||
function ThoughtBlock({
|
||||
className,
|
||||
content,
|
||||
isStreaming,
|
||||
hasMainContent,
|
||||
}: {
|
||||
className?: string;
|
||||
content: string;
|
||||
isStreaming?: boolean;
|
||||
hasMainContent?: boolean;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
const [hasAutoCollapsed, setHasAutoCollapsed] = useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasMainContent && !hasAutoCollapsed) {
|
||||
setIsOpen(false);
|
||||
setHasAutoCollapsed(true);
|
||||
}
|
||||
}, [hasMainContent, hasAutoCollapsed]);
|
||||
|
||||
if (!content || content.trim() === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("mb-6 w-full", className)}>
|
||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"h-auto w-full justify-start rounded-xl border px-6 py-4 text-left transition-all duration-200",
|
||||
"hover:bg-accent hover:text-accent-foreground",
|
||||
isStreaming
|
||||
? "border-primary/20 bg-primary/5 shadow-sm"
|
||||
: "border-border bg-card",
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full items-center gap-3">
|
||||
<Lightbulb
|
||||
size={18}
|
||||
className={cn(
|
||||
"shrink-0 transition-colors duration-200",
|
||||
isStreaming ? "text-primary" : "text-muted-foreground",
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
"leading-none font-semibold transition-colors duration-200",
|
||||
isStreaming ? "text-primary" : "text-foreground",
|
||||
)}
|
||||
>
|
||||
Deep Thinking
|
||||
</span>
|
||||
{isStreaming && <LoadingAnimation className="ml-2 scale-75" />}
|
||||
<div className="flex-grow" />
|
||||
{isOpen ? (
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className="text-muted-foreground transition-transform duration-200"
|
||||
/>
|
||||
) : (
|
||||
<ChevronRight
|
||||
size={16}
|
||||
className="text-muted-foreground transition-transform duration-200"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:slide-up-2 data-[state=open]:slide-down-2 mt-3">
|
||||
<Card
|
||||
className={cn(
|
||||
"transition-all duration-200",
|
||||
isStreaming ? "border-primary/20 bg-primary/5" : "border-border",
|
||||
)}
|
||||
>
|
||||
<CardContent>
|
||||
<div className="flex h-40 w-full overflow-y-auto">
|
||||
<ScrollContainer
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
scrollShadow={false}
|
||||
autoScrollToBottom
|
||||
>
|
||||
<Markdown
|
||||
className={cn(
|
||||
"prose dark:prose-invert max-w-none transition-colors duration-200",
|
||||
isStreaming ? "prose-primary" : "opacity-80",
|
||||
)}
|
||||
animated={isStreaming}
|
||||
>
|
||||
{content}
|
||||
</Markdown>
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const GREETINGS = ["Cool", "Sounds great", "Looks good", "Great", "Awesome"];
|
||||
function PlanCard({
|
||||
className,
|
||||
@@ -320,6 +439,17 @@ function PlanCard({
|
||||
}>(() => {
|
||||
return parseJSON(message.content ?? "", {});
|
||||
}, [message.content]);
|
||||
|
||||
const reasoningContent = message.reasoningContent;
|
||||
const hasMainContent = Boolean(
|
||||
message.content && message.content.trim() !== "",
|
||||
);
|
||||
|
||||
// 判断是否正在思考:有推理内容但还没有主要内容
|
||||
const isThinking = Boolean(reasoningContent && !hasMainContent);
|
||||
|
||||
// 判断是否应该显示计划:有主要内容就显示(无论是否还在流式传输)
|
||||
const shouldShowPlan = hasMainContent;
|
||||
const handleAccept = useCallback(async () => {
|
||||
if (onSendMessage) {
|
||||
onSendMessage(
|
||||
@@ -331,67 +461,90 @@ function PlanCard({
|
||||
}
|
||||
}, [onSendMessage]);
|
||||
return (
|
||||
<Card className={cn("w-full", className)}>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Markdown animated>
|
||||
{`### ${
|
||||
plan.title !== undefined && plan.title !== ""
|
||||
? plan.title
|
||||
: "Deep Research"
|
||||
}`}
|
||||
</Markdown>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Markdown className="opacity-80" animated>
|
||||
{plan.thought}
|
||||
</Markdown>
|
||||
{plan.steps && (
|
||||
<ul className="my-2 flex list-decimal flex-col gap-4 border-l-[2px] pl-8">
|
||||
{plan.steps.map((step, i) => (
|
||||
<li key={`step-${i}`}>
|
||||
<h3 className="mb text-lg font-medium">
|
||||
<Markdown animated>{step.title}</Markdown>
|
||||
</h3>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
<Markdown animated>{step.description}</Markdown>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-end">
|
||||
{!message.isStreaming && interruptMessage?.options?.length && (
|
||||
<motion.div
|
||||
className="flex gap-2"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.3 }}
|
||||
>
|
||||
{interruptMessage?.options.map((option) => (
|
||||
<Button
|
||||
key={option.value}
|
||||
variant={option.value === "accepted" ? "default" : "outline"}
|
||||
disabled={!waitForFeedback}
|
||||
onClick={() => {
|
||||
if (option.value === "accepted") {
|
||||
void handleAccept();
|
||||
} else {
|
||||
onFeedback?.({
|
||||
option,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{option.text}
|
||||
</Button>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<div className={cn("w-full", className)}>
|
||||
{reasoningContent && (
|
||||
<ThoughtBlock
|
||||
content={reasoningContent}
|
||||
isStreaming={isThinking}
|
||||
hasMainContent={hasMainContent}
|
||||
/>
|
||||
)}
|
||||
{shouldShowPlan && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Markdown animated={message.isStreaming}>
|
||||
{`### ${
|
||||
plan.title !== undefined && plan.title !== ""
|
||||
? plan.title
|
||||
: "Deep Research"
|
||||
}`}
|
||||
</Markdown>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Markdown className="opacity-80" animated={message.isStreaming}>
|
||||
{plan.thought}
|
||||
</Markdown>
|
||||
{plan.steps && (
|
||||
<ul className="my-2 flex list-decimal flex-col gap-4 border-l-[2px] pl-8">
|
||||
{plan.steps.map((step, i) => (
|
||||
<li key={`step-${i}`}>
|
||||
<h3 className="mb text-lg font-medium">
|
||||
<Markdown animated={message.isStreaming}>
|
||||
{step.title}
|
||||
</Markdown>
|
||||
</h3>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
<Markdown animated={message.isStreaming}>
|
||||
{step.description}
|
||||
</Markdown>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-end">
|
||||
{!message.isStreaming && interruptMessage?.options?.length && (
|
||||
<motion.div
|
||||
className="flex gap-2"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.3 }}
|
||||
>
|
||||
{interruptMessage?.options.map((option) => (
|
||||
<Button
|
||||
key={option.value}
|
||||
variant={
|
||||
option.value === "accepted" ? "default" : "outline"
|
||||
}
|
||||
disabled={!waitForFeedback}
|
||||
onClick={() => {
|
||||
if (option.value === "accepted") {
|
||||
void handleAccept();
|
||||
} else {
|
||||
onFeedback?.({
|
||||
option,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{option.text}
|
||||
</Button>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Geist } from "next/font/google";
|
||||
import Script from "next/script";
|
||||
|
||||
import { ThemeProviderWrapper } from "~/components/deer-flow/theme-provider-wrapper";
|
||||
import { loadConfig } from "~/core/api/config";
|
||||
import { env } from "~/env";
|
||||
|
||||
import { Toaster } from "../components/deer-flow/toaster";
|
||||
@@ -24,12 +25,14 @@ const geist = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
});
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
const conf = await loadConfig();
|
||||
return (
|
||||
<html lang="en" className={`${geist.variable}`} suppressHydrationWarning>
|
||||
<head>
|
||||
<script>{`window.__deerflowConfig = ${JSON.stringify(conf)}`}</script>
|
||||
{/* Define isSpace function globally to fix markdown-it issues with Next.js + Turbopack
|
||||
https://github.com/markdown-it/markdown-it/issues/1082#issuecomment-2749656365 */}
|
||||
<Script id="markdown-it-fix" strategy="beforeInteractive">
|
||||
|
||||
@@ -36,6 +36,7 @@ const generalFormSchema = z.object({
|
||||
}),
|
||||
// Others
|
||||
enableBackgroundInvestigation: z.boolean(),
|
||||
enableDeepThinking: z.boolean(),
|
||||
reportStyle: z.enum(["academic", "popular_science", "news", "social_media"]),
|
||||
});
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ export async function* chatStream(
|
||||
max_step_num: number;
|
||||
max_search_results?: number;
|
||||
interrupt_feedback?: string;
|
||||
enable_deep_thinking?: boolean;
|
||||
enable_background_investigation: boolean;
|
||||
report_style?: "academic" | "popular_science" | "news" | "social_media";
|
||||
mcp_settings?: {
|
||||
|
||||
25
web/src/core/api/config.ts
Normal file
25
web/src/core/api/config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { type DeerFlowConfig } from "../config/types";
|
||||
|
||||
import { resolveServiceURL } from "./resolve-service-url";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__deerflowConfig: DeerFlowConfig;
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadConfig() {
|
||||
const res = await fetch(resolveServiceURL("./config"));
|
||||
const config = await res.json();
|
||||
return config;
|
||||
}
|
||||
|
||||
export function getConfig(): DeerFlowConfig {
|
||||
if (
|
||||
typeof window === "undefined" ||
|
||||
typeof window.__deerflowConfig === "undefined"
|
||||
) {
|
||||
throw new Error("Config not loaded");
|
||||
}
|
||||
return window.__deerflowConfig;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { env } from "~/env";
|
||||
import { useReplay } from "../replay";
|
||||
|
||||
import { fetchReplayTitle } from "./chat";
|
||||
import { getRAGConfig } from "./rag";
|
||||
import { getConfig } from "./config";
|
||||
|
||||
export function useReplayMetadata() {
|
||||
const { isReplay } = useReplay();
|
||||
@@ -52,15 +52,8 @@ export function useRAGProvider() {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
getRAGConfig()
|
||||
.then(setProvider)
|
||||
.catch((e) => {
|
||||
setProvider(null);
|
||||
console.error("Failed to get RAG provider", e);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
setProvider(getConfig().rag.provider);
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
return { provider, loading };
|
||||
|
||||
@@ -10,15 +10,7 @@ export function queryRAGResources(query: string) {
|
||||
.then((res) => {
|
||||
return res.resources as Array<Resource>;
|
||||
})
|
||||
.catch((err) => {
|
||||
.catch(() => {
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
export function getRAGConfig() {
|
||||
return fetch(resolveServiceURL(`rag/config`), {
|
||||
method: "GET",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => res.provider);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ export interface MessageChunkEvent
|
||||
"message_chunk",
|
||||
{
|
||||
content?: string;
|
||||
reasoning_content?: string;
|
||||
}
|
||||
> {}
|
||||
|
||||
|
||||
1
web/src/core/config/index.ts
Normal file
1
web/src/core/config/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./types";
|
||||
13
web/src/core/config/types.ts
Normal file
13
web/src/core/config/types.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export interface ModelConfig {
|
||||
basic: string[];
|
||||
reasoning: string[];
|
||||
}
|
||||
|
||||
export interface RagConfig {
|
||||
provider: string;
|
||||
}
|
||||
|
||||
export interface DeerFlowConfig {
|
||||
rag: RagConfig;
|
||||
models: ModelConfig;
|
||||
}
|
||||
@@ -43,6 +43,11 @@ function mergeTextMessage(message: Message, event: MessageChunkEvent) {
|
||||
message.content += event.data.content;
|
||||
message.contentChunks.push(event.data.content);
|
||||
}
|
||||
if (event.data.reasoning_content) {
|
||||
message.reasoningContent = (message.reasoningContent ?? "") + event.data.reasoning_content;
|
||||
message.reasoningContentChunks = message.reasoningContentChunks ?? [];
|
||||
message.reasoningContentChunks.push(event.data.reasoning_content);
|
||||
}
|
||||
}
|
||||
|
||||
function mergeToolCallMessage(
|
||||
|
||||
@@ -17,6 +17,8 @@ export interface Message {
|
||||
isStreaming?: boolean;
|
||||
content: string;
|
||||
contentChunks: string[];
|
||||
reasoningContent?: string;
|
||||
reasoningContentChunks?: string[];
|
||||
toolCalls?: ToolCallRuntime[];
|
||||
options?: Option[];
|
||||
finishReason?: "stop" | "interrupt" | "tool_calls";
|
||||
|
||||
@@ -10,6 +10,7 @@ const SETTINGS_KEY = "deerflow.settings";
|
||||
const DEFAULT_SETTINGS: SettingsState = {
|
||||
general: {
|
||||
autoAcceptedPlan: false,
|
||||
enableDeepThinking: false,
|
||||
enableBackgroundInvestigation: false,
|
||||
maxPlanIterations: 1,
|
||||
maxStepNum: 3,
|
||||
@@ -24,6 +25,7 @@ const DEFAULT_SETTINGS: SettingsState = {
|
||||
export type SettingsState = {
|
||||
general: {
|
||||
autoAcceptedPlan: boolean;
|
||||
enableDeepThinking: boolean;
|
||||
enableBackgroundInvestigation: boolean;
|
||||
maxPlanIterations: number;
|
||||
maxStepNum: number;
|
||||
@@ -127,7 +129,9 @@ export const getChatStreamSettings = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export function setReportStyle(value: "academic" | "popular_science" | "news" | "social_media") {
|
||||
export function setReportStyle(
|
||||
value: "academic" | "popular_science" | "news" | "social_media",
|
||||
) {
|
||||
useSettingsStore.setState((state) => ({
|
||||
general: {
|
||||
...state.general,
|
||||
@@ -137,6 +141,16 @@ export function setReportStyle(value: "academic" | "popular_science" | "news" |
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
export function setEnableDeepThinking(value: boolean) {
|
||||
useSettingsStore.setState((state) => ({
|
||||
general: {
|
||||
...state.general,
|
||||
enableDeepThinking: value,
|
||||
},
|
||||
}));
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
export function setEnableBackgroundInvestigation(value: boolean) {
|
||||
useSettingsStore.setState((state) => ({
|
||||
general: {
|
||||
|
||||
@@ -104,6 +104,7 @@ export async function sendMessage(
|
||||
interrupt_feedback: interruptFeedback,
|
||||
resources,
|
||||
auto_accepted_plan: settings.autoAcceptedPlan,
|
||||
enable_deep_thinking: settings.enableDeepThinking ?? false,
|
||||
enable_background_investigation:
|
||||
settings.enableBackgroundInvestigation ?? true,
|
||||
max_plan_iterations: settings.maxPlanIterations,
|
||||
@@ -132,6 +133,8 @@ export async function sendMessage(
|
||||
role: data.role,
|
||||
content: "",
|
||||
contentChunks: [],
|
||||
reasoningContent: "",
|
||||
reasoningContentChunks: [],
|
||||
isStreaming: true,
|
||||
interruptFeedback,
|
||||
};
|
||||
@@ -296,6 +299,8 @@ export async function listenToPodcast(researchId: string) {
|
||||
agent: "podcast",
|
||||
content: JSON.stringify(podcastObject),
|
||||
contentChunks: [],
|
||||
reasoningContent: "",
|
||||
reasoningContentChunks: [],
|
||||
isStreaming: true,
|
||||
};
|
||||
appendMessage(podcastMessage);
|
||||
|
||||
@@ -7,7 +7,10 @@ export function parseJSON<T>(json: string | null | undefined, fallback: T) {
|
||||
try {
|
||||
const raw = json
|
||||
.trim()
|
||||
.replace(/^```js\s*/, "")
|
||||
.replace(/^```json\s*/, "")
|
||||
.replace(/^```ts\s*/, "")
|
||||
.replace(/^```plaintext\s*/, "")
|
||||
.replace(/^```\s*/, "")
|
||||
.replace(/\s*```$/, "");
|
||||
return parse(raw) as T;
|
||||
|
||||
Reference in New Issue
Block a user