mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-25 23:14:46 +08:00
feat: RAG Integration (#238)
* feat: add rag provider and retriever * feat: retriever tool * feat: add retriever tool to the researcher node * feat: add rag http apis * feat: new message input supports resource mentions * feat: new message input component support resource mentions * refactor: need_web_search to need_search * chore: RAG integration docs * chore: change example api host * fix: user message color in dark mode * fix: mentions style * feat: add local_search_tool to researcher prompt * chore: research prompt * fix: ragflow page size and reporter with * docs: ragflow integration and add acknowledgment projects * chore: format
This commit is contained in:
@@ -3,18 +3,15 @@
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { ArrowUp, X } from "lucide-react";
|
||||
import {
|
||||
type KeyboardEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useCallback, useRef } from "react";
|
||||
|
||||
import { Detective } from "~/components/deer-flow/icons/detective";
|
||||
import MessageInput, {
|
||||
type MessageInputRef,
|
||||
} from "~/components/deer-flow/message-input";
|
||||
import { Tooltip } from "~/components/deer-flow/tooltip";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import type { Option } from "~/core/messages";
|
||||
import type { Option, Resource } from "~/core/messages";
|
||||
import {
|
||||
setEnableBackgroundInvestigation,
|
||||
useSettingsStore,
|
||||
@@ -23,7 +20,6 @@ import { cn } from "~/lib/utils";
|
||||
|
||||
export function InputBox({
|
||||
className,
|
||||
size,
|
||||
responding,
|
||||
feedback,
|
||||
onSend,
|
||||
@@ -34,72 +30,52 @@ export function InputBox({
|
||||
size?: "large" | "normal";
|
||||
responding?: boolean;
|
||||
feedback?: { option: Option } | null;
|
||||
onSend?: (message: string, options?: { interruptFeedback?: string }) => void;
|
||||
onSend?: (
|
||||
message: string,
|
||||
options?: {
|
||||
interruptFeedback?: string;
|
||||
resources?: Array<Resource>;
|
||||
},
|
||||
) => void;
|
||||
onCancel?: () => void;
|
||||
onRemoveFeedback?: () => void;
|
||||
}) {
|
||||
const [message, setMessage] = useState("");
|
||||
const [imeStatus, setImeStatus] = useState<"active" | "inactive">("inactive");
|
||||
const [indent, setIndent] = useState(0);
|
||||
const backgroundInvestigation = useSettingsStore(
|
||||
(state) => state.general.enableBackgroundInvestigation,
|
||||
);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<MessageInputRef>(null);
|
||||
const feedbackRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (feedback) {
|
||||
setMessage("");
|
||||
|
||||
setTimeout(() => {
|
||||
if (feedbackRef.current) {
|
||||
setIndent(feedbackRef.current.offsetWidth);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
setTimeout(() => {
|
||||
textareaRef.current?.focus();
|
||||
}, 0);
|
||||
}, [feedback]);
|
||||
|
||||
const handleSendMessage = useCallback(() => {
|
||||
if (responding) {
|
||||
onCancel?.();
|
||||
} else {
|
||||
if (message.trim() === "") {
|
||||
return;
|
||||
}
|
||||
if (onSend) {
|
||||
onSend(message, {
|
||||
interruptFeedback: feedback?.option.value,
|
||||
});
|
||||
setMessage("");
|
||||
onRemoveFeedback?.();
|
||||
}
|
||||
}
|
||||
}, [responding, onCancel, message, onSend, feedback, onRemoveFeedback]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
const handleSendMessage = useCallback(
|
||||
(message: string, resources: Array<Resource>) => {
|
||||
console.log(message, resources);
|
||||
if (responding) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
event.key === "Enter" &&
|
||||
!event.shiftKey &&
|
||||
!event.metaKey &&
|
||||
!event.ctrlKey &&
|
||||
imeStatus === "inactive"
|
||||
) {
|
||||
event.preventDefault();
|
||||
handleSendMessage();
|
||||
onCancel?.();
|
||||
} else {
|
||||
if (message.trim() === "") {
|
||||
return;
|
||||
}
|
||||
if (onSend) {
|
||||
onSend(message, {
|
||||
interruptFeedback: feedback?.option.value,
|
||||
resources,
|
||||
});
|
||||
onRemoveFeedback?.();
|
||||
}
|
||||
}
|
||||
},
|
||||
[responding, imeStatus, handleSendMessage],
|
||||
[responding, onCancel, onSend, feedback, onRemoveFeedback],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("bg-card relative rounded-[24px] border", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"bg-card relative flex h-full w-full flex-col rounded-[24px] border",
|
||||
className,
|
||||
)}
|
||||
ref={containerRef}
|
||||
>
|
||||
<div className="w-full">
|
||||
<AnimatePresence>
|
||||
{feedback && (
|
||||
@@ -122,25 +98,10 @@ export function InputBox({
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className={cn(
|
||||
"m-0 w-full resize-none border-none px-4 py-3 text-lg",
|
||||
size === "large" ? "min-h-32" : "min-h-4",
|
||||
)}
|
||||
style={{ textIndent: feedback ? `${indent}px` : 0 }}
|
||||
placeholder={
|
||||
feedback
|
||||
? `Describe how you ${feedback.option.text.toLocaleLowerCase()}?`
|
||||
: "What can I do for you?"
|
||||
}
|
||||
value={message}
|
||||
onCompositionStart={() => setImeStatus("active")}
|
||||
onCompositionEnd={() => setImeStatus("inactive")}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={(event) => {
|
||||
setMessage(event.target.value);
|
||||
}}
|
||||
<MessageInput
|
||||
className={cn("h-24 px-4 pt-3")}
|
||||
ref={inputRef}
|
||||
onEnter={handleSendMessage}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center px-4 py-2">
|
||||
@@ -181,7 +142,7 @@ export function InputBox({
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn("h-10 w-10 rounded-full")}
|
||||
onClick={handleSendMessage}
|
||||
onClick={() => inputRef.current?.submit()}
|
||||
>
|
||||
{responding ? (
|
||||
<div className="flex h-10 w-10 items-center justify-center">
|
||||
|
||||
@@ -174,7 +174,14 @@ function MessageListItem({
|
||||
>
|
||||
<MessageBubble message={message}>
|
||||
<div className="flex w-full flex-col">
|
||||
<Markdown>{message?.content}</Markdown>
|
||||
<Markdown
|
||||
className={cn(
|
||||
message.role === "user" &&
|
||||
"prose-invert not-dark:text-secondary dark:text-inherit",
|
||||
)}
|
||||
>
|
||||
{message?.content}
|
||||
</Markdown>
|
||||
</div>
|
||||
</MessageBubble>
|
||||
</div>
|
||||
@@ -214,9 +221,8 @@ function MessageBubble({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
`flex w-fit max-w-[85%] flex-col rounded-2xl px-4 py-3 shadow`,
|
||||
message.role === "user" &&
|
||||
"text-primary-foreground bg-brand rounded-ee-none",
|
||||
`group flex w-fit max-w-[85%] flex-col rounded-2xl px-4 py-3 text-nowrap shadow`,
|
||||
message.role === "user" && "bg-brand rounded-ee-none",
|
||||
message.role === "assistant" && "bg-card rounded-es-none",
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "~/components/ui/card";
|
||||
import { fastForwardReplay } from "~/core/api";
|
||||
import { useReplayMetadata } from "~/core/api/hooks";
|
||||
import type { Option } from "~/core/messages";
|
||||
import type { Option, Resource } from "~/core/messages";
|
||||
import { useReplay } from "~/core/replay";
|
||||
import { sendMessage, useMessageIds, useStore } from "~/core/store";
|
||||
import { env } from "~/env";
|
||||
@@ -36,7 +36,13 @@ 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, options?: { interruptFeedback?: string }) => {
|
||||
async (
|
||||
message: string,
|
||||
options?: {
|
||||
interruptFeedback?: string;
|
||||
resources?: Array<Resource>;
|
||||
},
|
||||
) => {
|
||||
const abortController = new AbortController();
|
||||
abortControllerRef.current = abortController;
|
||||
try {
|
||||
@@ -45,6 +51,7 @@ export function MessagesBlock({ className }: { className?: string }) {
|
||||
{
|
||||
interruptFeedback:
|
||||
options?.interruptFeedback ?? feedback?.option.value,
|
||||
resources: options?.resources,
|
||||
},
|
||||
{
|
||||
abortSignal: abortController.signal,
|
||||
|
||||
@@ -276,8 +276,8 @@ function CrawlToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
}
|
||||
|
||||
function PythonToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
const code = useMemo<string>(() => {
|
||||
return (toolCall.args as { code: string }).code;
|
||||
const code = useMemo<string | undefined>(() => {
|
||||
return (toolCall.args as { code?: string }).code;
|
||||
}, [toolCall.args]);
|
||||
const { resolvedTheme } = useTheme();
|
||||
return (
|
||||
@@ -302,7 +302,7 @@ function PythonToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
boxShadow: "none",
|
||||
}}
|
||||
>
|
||||
{code.trim()}
|
||||
{code?.trim() ?? ""}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,10 +53,7 @@ export function ResearchReportBlock({
|
||||
// }, [isCompleted]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={cn("relative flex flex-col pt-4 pb-8", className)}
|
||||
>
|
||||
<div ref={contentRef} className={cn("w-full pt-4 pb-8", className)}>
|
||||
{!isReplay && isCompleted && editing ? (
|
||||
<ReportEditor
|
||||
content={message?.content}
|
||||
|
||||
Reference in New Issue
Block a user