mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-23 22:24:46 +08:00
feat: implement MCP UIs
This commit is contained in:
@@ -30,7 +30,7 @@ export function InputBox({
|
||||
size?: "large" | "normal";
|
||||
responding?: boolean;
|
||||
feedback?: { option: Option } | null;
|
||||
onSend?: (message: string, feedback: { option: Option } | null) => void;
|
||||
onSend?: (message: string, options?: { interruptFeedback?: string }) => void;
|
||||
onCancel?: () => void;
|
||||
onRemoveFeedback?: () => void;
|
||||
}) {
|
||||
@@ -63,7 +63,9 @@ export function InputBox({
|
||||
return;
|
||||
}
|
||||
if (onSend) {
|
||||
onSend(message, feedback ?? null);
|
||||
onSend(message, {
|
||||
interruptFeedback: feedback?.option.value,
|
||||
});
|
||||
setMessage("");
|
||||
onRemoveFeedback?.();
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
import type { Message, Option } from "~/core/messages";
|
||||
import {
|
||||
openResearch,
|
||||
sendMessage,
|
||||
useMessage,
|
||||
useResearchTitle,
|
||||
useStore,
|
||||
@@ -35,9 +34,14 @@ 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) => {
|
||||
@@ -81,6 +85,7 @@ export function MessageListView({
|
||||
waitForFeedback={waitingForFeedbackMessageId === messageId}
|
||||
interruptMessage={interruptMessage}
|
||||
onFeedback={onFeedback}
|
||||
onSendMessage={onSendMessage}
|
||||
/>
|
||||
))}
|
||||
<div className="flex h-8 w-full shrink-0"></div>
|
||||
@@ -96,14 +101,19 @@ function MessageListItem({
|
||||
className,
|
||||
messageId,
|
||||
waitForFeedback,
|
||||
onFeedback,
|
||||
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) =>
|
||||
@@ -126,6 +136,7 @@ function MessageListItem({
|
||||
waitForFeedback={waitForFeedback}
|
||||
interruptMessage={interruptMessage}
|
||||
onFeedback={onFeedback}
|
||||
onSendMessage={onSendMessage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -269,11 +280,16 @@ function PlanCard({
|
||||
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<{
|
||||
@@ -284,13 +300,15 @@ function PlanCard({
|
||||
return parseJSON(message.content ?? "", {});
|
||||
}, [message.content]);
|
||||
const handleAccept = useCallback(async () => {
|
||||
await sendMessage(
|
||||
`${GREETINGS[Math.floor(Math.random() * GREETINGS.length)]}! ${Math.random() > 0.5 ? "Let's get started." : "Let's start."}`,
|
||||
{
|
||||
interruptFeedback: "accepted",
|
||||
},
|
||||
);
|
||||
}, []);
|
||||
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 (
|
||||
<Card className={cn("w-full", className)}>
|
||||
<CardHeader>
|
||||
|
||||
@@ -17,16 +17,15 @@ export function MessagesBlock({ className }: { className?: string }) {
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const [feedback, setFeedback] = useState<{ option: Option } | null>(null);
|
||||
const handleSend = useCallback(
|
||||
async (message: string) => {
|
||||
async (message: string, options?: { interruptFeedback?: string }) => {
|
||||
const abortController = new AbortController();
|
||||
abortControllerRef.current = abortController;
|
||||
try {
|
||||
await sendMessage(
|
||||
message,
|
||||
{
|
||||
maxPlanIterations: 1,
|
||||
maxStepNum: 3,
|
||||
interruptFeedback: feedback?.option.value,
|
||||
interruptFeedback:
|
||||
options?.interruptFeedback ?? feedback?.option.value,
|
||||
},
|
||||
{
|
||||
abortSignal: abortController.signal,
|
||||
@@ -37,6 +36,7 @@ export function MessagesBlock({ className }: { className?: string }) {
|
||||
[feedback],
|
||||
);
|
||||
const handleCancel = useCallback(() => {
|
||||
console.info("cancel");
|
||||
abortControllerRef.current?.abort();
|
||||
abortControllerRef.current = null;
|
||||
}, []);
|
||||
@@ -51,7 +51,11 @@ export function MessagesBlock({ className }: { className?: string }) {
|
||||
}, [setFeedback]);
|
||||
return (
|
||||
<div className={cn("flex h-full flex-col", className)}>
|
||||
<MessageListView className="flex flex-grow" onFeedback={handleFeedback} />
|
||||
<MessageListView
|
||||
className="flex flex-grow"
|
||||
onFeedback={handleFeedback}
|
||||
onSendMessage={handleSend}
|
||||
/>
|
||||
<div className="relative flex h-42 shrink-0 pb-4">
|
||||
{!responding && messageCount === 0 && (
|
||||
<ConversationStarter
|
||||
|
||||
@@ -4,12 +4,19 @@
|
||||
import { PythonOutlined } from "@ant-design/icons";
|
||||
import { motion } from "framer-motion";
|
||||
import { LRUCache } from "lru-cache";
|
||||
import { BookOpenText, Search } from "lucide-react";
|
||||
import { BookOpenText, PencilRuler, Search } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import { docco } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "~/components/ui/accordion";
|
||||
import { Skeleton } from "~/components/ui/skeleton";
|
||||
import { findMCPTool } from "~/core/mcp";
|
||||
import type { ToolCallRuntime } from "~/core/messages";
|
||||
import { useMessage, useStore } from "~/core/store";
|
||||
import { parseJSON } from "~/core/utils";
|
||||
@@ -20,6 +27,7 @@ import Image from "./image";
|
||||
import { LoadingAnimation } from "./loading-animation";
|
||||
import { Markdown } from "./markdown";
|
||||
import { RainbowText } from "./rainbow-text";
|
||||
import { Tooltip } from "./tooltip";
|
||||
|
||||
export function ResearchActivitiesBlock({
|
||||
className,
|
||||
@@ -85,6 +93,8 @@ function ActivityListItem({ messageId }: { messageId: string }) {
|
||||
return <CrawlToolCall key={toolCall.id} toolCall={toolCall} />;
|
||||
} else if (toolCall.name === "python_repl_tool") {
|
||||
return <PythonToolCall key={toolCall.id} toolCall={toolCall} />;
|
||||
} else {
|
||||
return <MCPToolCall key={toolCall.id} toolCall={toolCall} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,7 +152,7 @@ function WebSearchToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
className="flex items-center"
|
||||
animated={searchResults === undefined}
|
||||
>
|
||||
<Search className={"mr-2"} />
|
||||
<Search size={16} className={"mr-2"} />
|
||||
<span>Searching for </span>
|
||||
<span className="max-w-[500px] overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{(toolCall.args as { query: string }).query}
|
||||
@@ -229,12 +239,12 @@ function CrawlToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
const title = useMemo(() => __pageCache.get(url), [url]);
|
||||
return (
|
||||
<section className="mt-4 pl-4">
|
||||
<div className="font-medium italic">
|
||||
<div>
|
||||
<RainbowText
|
||||
className="flex items-center"
|
||||
className="flex items-center text-base font-medium italic"
|
||||
animated={toolCall.result === undefined}
|
||||
>
|
||||
<BookOpenText className={"mr-2"} />
|
||||
<BookOpenText size={16} className={"mr-2"} />
|
||||
<span>Reading</span>
|
||||
</RainbowText>
|
||||
</div>
|
||||
@@ -264,15 +274,22 @@ function PythonToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
}, [toolCall.args]);
|
||||
return (
|
||||
<section className="mt-4 pl-4">
|
||||
<div className="font-medium italic">
|
||||
<div className="flex items-center">
|
||||
<PythonOutlined className={"mr-2"} />
|
||||
<RainbowText animated={toolCall.result === undefined}>
|
||||
<RainbowText
|
||||
className="text-base font-medium italic"
|
||||
animated={toolCall.result === undefined}
|
||||
>
|
||||
Running Python code
|
||||
</RainbowText>
|
||||
</div>
|
||||
<div className="px-5">
|
||||
<div className="bg-accent mt-2 rounded-md p-2 text-sm">
|
||||
<SyntaxHighlighter language="python" style={docco}>
|
||||
<div className="bg-accent mt-2 max-h-[400px] w-[800px] overflow-y-auto rounded-md p-2 text-sm">
|
||||
<SyntaxHighlighter
|
||||
customStyle={{ background: "transparent" }}
|
||||
language="python"
|
||||
style={docco}
|
||||
>
|
||||
{code}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
@@ -280,3 +297,43 @@ function PythonToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function MCPToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
const tool = useMemo(() => findMCPTool(toolCall.name), [toolCall.name]);
|
||||
return (
|
||||
<section className="mt-4 pl-4">
|
||||
<div className="w-fit overflow-y-auto rounded-md py-0">
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>
|
||||
<Tooltip title={tool?.description}>
|
||||
<div className="flex items-center font-medium italic">
|
||||
<PencilRuler size={16} className={"mr-2"} />
|
||||
<RainbowText
|
||||
className="pr-0.5 text-base font-medium italic"
|
||||
animated={toolCall.result === undefined}
|
||||
>
|
||||
Running {toolCall.name ? toolCall.name + "()" : "MCP tool"}
|
||||
</RainbowText>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
{toolCall.result && (
|
||||
<div className="bg-accent max-h-[400px] w-[800px] overflow-y-auto rounded-md text-sm">
|
||||
<SyntaxHighlighter
|
||||
customStyle={{ background: "transparent" }}
|
||||
language="json"
|
||||
style={docco}
|
||||
>
|
||||
{toolCall.result}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
)}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export function Tooltip({
|
||||
title?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<ShadcnTooltip>
|
||||
<ShadcnTooltip delayDuration={750}>
|
||||
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||
<TooltipContent className={className}>{title}</TooltipContent>
|
||||
</ShadcnTooltip>
|
||||
|
||||
Reference in New Issue
Block a user