mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-14 10:44:46 +08:00
feat: implement enhance prompt (#294)
* feat: implement enhance prompt * add unit test * fix prompt * fix: fix eslint and compiling issues * feat: add border-beam animation * fix: fix importing issues --------- Co-authored-by: Henry Li <henry1943@163.com>
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { MagicWandIcon } from "@radix-ui/react-icons";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { ArrowUp, X } from "lucide-react";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
|
||||
import { Detective } from "~/components/deer-flow/icons/detective";
|
||||
import MessageInput, {
|
||||
@@ -11,7 +12,9 @@ import MessageInput, {
|
||||
} from "~/components/deer-flow/message-input";
|
||||
import { ReportStyleDialog } from "~/components/deer-flow/report-style-dialog";
|
||||
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 type { Option, Resource } from "~/core/messages";
|
||||
import {
|
||||
setEnableBackgroundInvestigation,
|
||||
@@ -44,10 +47,16 @@ export function InputBox({
|
||||
const backgroundInvestigation = useSettingsStore(
|
||||
(state) => state.general.enableBackgroundInvestigation,
|
||||
);
|
||||
const reportStyle = useSettingsStore((state) => state.general.reportStyle);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<MessageInputRef>(null);
|
||||
const feedbackRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Enhancement state
|
||||
const [isEnhancing, setIsEnhancing] = useState(false);
|
||||
const [isEnhanceAnimating, setIsEnhanceAnimating] = useState(false);
|
||||
const [currentPrompt, setCurrentPrompt] = useState("");
|
||||
|
||||
const handleSendMessage = useCallback(
|
||||
(message: string, resources: Array<Resource>) => {
|
||||
if (responding) {
|
||||
@@ -62,12 +71,50 @@ export function InputBox({
|
||||
resources,
|
||||
});
|
||||
onRemoveFeedback?.();
|
||||
// Clear enhancement animation after sending
|
||||
setIsEnhanceAnimating(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[responding, onCancel, onSend, feedback, onRemoveFeedback],
|
||||
);
|
||||
|
||||
const handleEnhancePrompt = useCallback(async () => {
|
||||
if (currentPrompt.trim() === "" || isEnhancing) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsEnhancing(true);
|
||||
setIsEnhanceAnimating(true);
|
||||
|
||||
try {
|
||||
const enhancedPrompt = await enhancePrompt({
|
||||
prompt: currentPrompt,
|
||||
report_style: reportStyle.toUpperCase(),
|
||||
});
|
||||
|
||||
// Add a small delay for better UX
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Update the input with the enhanced prompt with animation
|
||||
if (inputRef.current) {
|
||||
inputRef.current.setContent(enhancedPrompt);
|
||||
setCurrentPrompt(enhancedPrompt);
|
||||
}
|
||||
|
||||
// Keep animation for a bit longer to show the effect
|
||||
setTimeout(() => {
|
||||
setIsEnhanceAnimating(false);
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error("Failed to enhance prompt:", error);
|
||||
setIsEnhanceAnimating(false);
|
||||
// Could add toast notification here
|
||||
} finally {
|
||||
setIsEnhancing(false);
|
||||
}
|
||||
}, [currentPrompt, isEnhancing, reportStyle]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -97,11 +144,61 @@ export function InputBox({
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{isEnhanceAnimating && (
|
||||
<motion.div
|
||||
className="pointer-events-none absolute inset-0 z-20"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="relative h-full w-full">
|
||||
{/* Sparkle effect overlay */}
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-[24px] bg-gradient-to-r from-blue-500/10 via-purple-500/10 to-blue-500/10"
|
||||
animate={{
|
||||
background: [
|
||||
"linear-gradient(45deg, rgba(59, 130, 246, 0.1), rgba(147, 51, 234, 0.1), rgba(59, 130, 246, 0.1))",
|
||||
"linear-gradient(225deg, rgba(147, 51, 234, 0.1), rgba(59, 130, 246, 0.1), rgba(147, 51, 234, 0.1))",
|
||||
"linear-gradient(45deg, rgba(59, 130, 246, 0.1), rgba(147, 51, 234, 0.1), rgba(59, 130, 246, 0.1))",
|
||||
],
|
||||
}}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
/>
|
||||
{/* Floating sparkles */}
|
||||
{[...Array(6)].map((_, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
className="absolute h-2 w-2 rounded-full bg-blue-400"
|
||||
style={{
|
||||
left: `${20 + i * 12}%`,
|
||||
top: `${30 + (i % 2) * 40}%`,
|
||||
}}
|
||||
animate={{
|
||||
y: [-10, -20, -10],
|
||||
opacity: [0, 1, 0],
|
||||
scale: [0.5, 1, 0.5],
|
||||
}}
|
||||
transition={{
|
||||
duration: 1.5,
|
||||
repeat: Infinity,
|
||||
delay: i * 0.2,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<MessageInput
|
||||
className={cn("h-24 px-4 pt-5", feedback && "pt-9")}
|
||||
className={cn(
|
||||
"h-24 px-4 pt-5",
|
||||
feedback && "pt-9",
|
||||
isEnhanceAnimating && "transition-all duration-500",
|
||||
)}
|
||||
ref={inputRef}
|
||||
onEnter={handleSendMessage}
|
||||
onChange={setCurrentPrompt}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center px-4 py-2">
|
||||
@@ -137,6 +234,26 @@ export function InputBox({
|
||||
<ReportStyleDialog />
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<Tooltip title="Enhance prompt with AI">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"hover:bg-accent h-10 w-10",
|
||||
isEnhancing && "animate-pulse",
|
||||
)}
|
||||
onClick={handleEnhancePrompt}
|
||||
disabled={isEnhancing || currentPrompt.trim() === ""}
|
||||
>
|
||||
{isEnhancing ? (
|
||||
<div className="flex h-10 w-10 items-center justify-center">
|
||||
<div className="bg-foreground h-3 w-3 animate-bounce rounded-full opacity-70" />
|
||||
</div>
|
||||
) : (
|
||||
<MagicWandIcon className="text-brand" />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title={responding ? "Stop" : "Send"}>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -155,6 +272,21 @@ export function InputBox({
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{isEnhancing && (
|
||||
<>
|
||||
<BorderBeam
|
||||
duration={5}
|
||||
size={250}
|
||||
className="from-transparent via-red-500 to-transparent"
|
||||
/>
|
||||
<BorderBeam
|
||||
duration={5}
|
||||
delay={3}
|
||||
size={250}
|
||||
className="from-transparent via-blue-500 to-transparent"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
42
web/src/components/deer-flow/icons/enhance.tsx
Normal file
42
web/src/components/deer-flow/icons/enhance.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function Enhance(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M12 2L13.09 8.26L20 9L13.09 9.74L12 16L10.91 9.74L4 9L10.91 8.26L12 2Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M19 14L19.5 16.5L22 17L19.5 17.5L19 20L18.5 17.5L16 17L18.5 16.5L19 14Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M5 6L5.5 7.5L7 8L5.5 8.5L5 10L4.5 8.5L3 8L4.5 7.5L5 6Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import { LoadingOutlined } from "@ant-design/icons";
|
||||
export interface MessageInputRef {
|
||||
focus: () => void;
|
||||
submit: () => void;
|
||||
setContent: (content: string) => void;
|
||||
}
|
||||
|
||||
export interface MessageInputProps {
|
||||
@@ -82,8 +83,9 @@ const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
const debouncedUpdates = useDebouncedCallback(
|
||||
async (editor: EditorInstance) => {
|
||||
if (onChange) {
|
||||
const markdown = editor.storage.markdown.getMarkdown();
|
||||
onChange(markdown);
|
||||
// Get the plain text content for prompt enhancement
|
||||
const { text } = formatMessage(editor.getJSON() ?? []);
|
||||
onChange(text);
|
||||
}
|
||||
},
|
||||
200,
|
||||
@@ -101,6 +103,11 @@ const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
onEnter(text, resources);
|
||||
}
|
||||
},
|
||||
setContent: (content: string) => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.commands.setContent(content);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
94
web/src/components/magicui/border-beam.tsx
Normal file
94
web/src/components/magicui/border-beam.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "~/lib/utils";
|
||||
import { motion, type MotionStyle, type Transition } from "motion/react";
|
||||
|
||||
interface BorderBeamProps {
|
||||
/**
|
||||
* The size of the border beam.
|
||||
*/
|
||||
size?: number;
|
||||
/**
|
||||
* The duration of the border beam.
|
||||
*/
|
||||
duration?: number;
|
||||
/**
|
||||
* The delay of the border beam.
|
||||
*/
|
||||
delay?: number;
|
||||
/**
|
||||
* The color of the border beam from.
|
||||
*/
|
||||
colorFrom?: string;
|
||||
/**
|
||||
* The color of the border beam to.
|
||||
*/
|
||||
colorTo?: string;
|
||||
/**
|
||||
* The motion transition of the border beam.
|
||||
*/
|
||||
transition?: Transition;
|
||||
/**
|
||||
* The class name of the border beam.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* The style of the border beam.
|
||||
*/
|
||||
style?: React.CSSProperties;
|
||||
/**
|
||||
* Whether to reverse the animation direction.
|
||||
*/
|
||||
reverse?: boolean;
|
||||
/**
|
||||
* The initial offset position (0-100).
|
||||
*/
|
||||
initialOffset?: number;
|
||||
}
|
||||
|
||||
export const BorderBeam = ({
|
||||
className,
|
||||
size = 50,
|
||||
delay = 0,
|
||||
duration = 6,
|
||||
colorFrom = "#ffaa40",
|
||||
colorTo = "#9c40ff",
|
||||
transition,
|
||||
style,
|
||||
reverse = false,
|
||||
initialOffset = 0,
|
||||
}: BorderBeamProps) => {
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 rounded-[inherit] border border-transparent [mask-image:linear-gradient(transparent,transparent),linear-gradient(#000,#000)] [mask-composite:intersect] [mask-clip:padding-box,border-box]">
|
||||
<motion.div
|
||||
className={cn(
|
||||
"absolute aspect-square",
|
||||
"bg-gradient-to-l from-[var(--color-from)] via-[var(--color-to)] to-transparent",
|
||||
className,
|
||||
)}
|
||||
style={
|
||||
{
|
||||
width: size,
|
||||
offsetPath: `rect(0 auto auto 0 round ${size}px)`,
|
||||
"--color-from": colorFrom,
|
||||
"--color-to": colorTo,
|
||||
...style,
|
||||
} as MotionStyle
|
||||
}
|
||||
initial={{ offsetDistance: `${initialOffset}%` }}
|
||||
animate={{
|
||||
offsetDistance: reverse
|
||||
? [`${100 - initialOffset}%`, `${-initialOffset}%`]
|
||||
: [`${initialOffset}%`, `${100 + initialOffset}%`],
|
||||
}}
|
||||
transition={{
|
||||
repeat: Infinity,
|
||||
ease: "linear",
|
||||
duration,
|
||||
delay: -delay,
|
||||
...transition,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -4,4 +4,5 @@
|
||||
export * from "./chat";
|
||||
export * from "./mcp";
|
||||
export * from "./podcast";
|
||||
export * from "./prompt-enhancer";
|
||||
export * from "./types";
|
||||
|
||||
62
web/src/core/api/prompt-enhancer.ts
Normal file
62
web/src/core/api/prompt-enhancer.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { resolveServiceURL } from "./resolve-service-url";
|
||||
|
||||
export interface EnhancePromptRequest {
|
||||
prompt: string;
|
||||
context?: string;
|
||||
report_style?: string;
|
||||
}
|
||||
|
||||
export interface EnhancePromptResponse {
|
||||
enhanced_prompt: string;
|
||||
}
|
||||
|
||||
export async function enhancePrompt(
|
||||
request: EnhancePromptRequest,
|
||||
): Promise<string> {
|
||||
const response = await fetch(resolveServiceURL("prompt/enhance"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Raw API response:", data); // Debug log
|
||||
|
||||
// The backend now returns the enhanced prompt directly in the result field
|
||||
let enhancedPrompt = data.result;
|
||||
|
||||
// If the result is somehow still a JSON object, extract the enhanced_prompt
|
||||
if (typeof enhancedPrompt === "object" && enhancedPrompt.enhanced_prompt) {
|
||||
enhancedPrompt = enhancedPrompt.enhanced_prompt;
|
||||
}
|
||||
|
||||
// If the result is a JSON string, try to parse it
|
||||
if (typeof enhancedPrompt === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(enhancedPrompt);
|
||||
if (parsed.enhanced_prompt) {
|
||||
enhancedPrompt = parsed.enhanced_prompt;
|
||||
}
|
||||
} catch {
|
||||
// If parsing fails, use the string as-is (which is what we want)
|
||||
console.log("Using enhanced prompt as-is:", enhancedPrompt);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to original prompt if something went wrong
|
||||
if (!enhancedPrompt || enhancedPrompt.trim() === "") {
|
||||
console.warn("No enhanced prompt received, using original");
|
||||
enhancedPrompt = request.prompt;
|
||||
}
|
||||
|
||||
return enhancedPrompt;
|
||||
}
|
||||
Reference in New Issue
Block a user