deerflow2/web/src/app/chat/components/input-box.tsx

160 lines
4.8 KiB
TypeScript

// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
// SPDX-License-Identifier: MIT
import { AnimatePresence, motion } from "framer-motion";
import { ArrowUp, X } from "lucide-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, Resource } from "~/core/messages";
import {
setEnableBackgroundInvestigation,
useSettingsStore,
} from "~/core/store";
import { cn } from "~/lib/utils";
export function InputBox({
className,
responding,
feedback,
onSend,
onCancel,
onRemoveFeedback,
}: {
className?: string;
size?: "large" | "normal";
responding?: boolean;
feedback?: { option: Option } | null;
onSend?: (
message: string,
options?: {
interruptFeedback?: string;
resources?: Array<Resource>;
},
) => void;
onCancel?: () => void;
onRemoveFeedback?: () => void;
}) {
const backgroundInvestigation = useSettingsStore(
(state) => state.general.enableBackgroundInvestigation,
);
const containerRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<MessageInputRef>(null);
const feedbackRef = useRef<HTMLDivElement>(null);
const handleSendMessage = useCallback(
(message: string, resources: Array<Resource>) => {
console.log(message, resources);
if (responding) {
onCancel?.();
} else {
if (message.trim() === "") {
return;
}
if (onSend) {
onSend(message, {
interruptFeedback: feedback?.option.value,
resources,
});
onRemoveFeedback?.();
}
}
},
[responding, onCancel, onSend, feedback, onRemoveFeedback],
);
return (
<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 && (
<motion.div
ref={feedbackRef}
className="bg-background border-brand absolute top-0 left-0 mt-2 ml-4 flex items-center justify-center gap-1 rounded-2xl border px-2 py-0.5"
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0 }}
transition={{ duration: 0.2, ease: "easeInOut" }}
>
<div className="text-brand flex h-full w-full items-center justify-center text-sm opacity-90">
{feedback.option.text}
</div>
<X
className="cursor-pointer opacity-60"
size={16}
onClick={onRemoveFeedback}
/>
</motion.div>
)}
</AnimatePresence>
<MessageInput
className={cn("h-24 px-4 pt-5", feedback && "pt-9")}
ref={inputRef}
onEnter={handleSendMessage}
/>
</div>
<div className="flex items-center px-4 py-2">
<div className="flex grow">
<Tooltip
className="max-w-60"
title={
<div>
<h3 className="mb-2 font-bold">
Investigation Mode: {backgroundInvestigation ? "On" : "Off"}
</h3>
<p>
When enabled, DeerFlow will perform a quick search before
planning. This is useful for researches related to ongoing
events and news.
</p>
</div>
}
>
<Button
className={cn(
"rounded-2xl",
backgroundInvestigation && "!border-brand !text-brand",
)}
variant="outline"
onClick={() =>
setEnableBackgroundInvestigation(!backgroundInvestigation)
}
>
<Detective /> Investigation
</Button>
</Tooltip>
</div>
<div className="flex shrink-0 items-center gap-2">
<Tooltip title={responding ? "Stop" : "Send"}>
<Button
variant="outline"
size="icon"
className={cn("h-10 w-10 rounded-full")}
onClick={() => inputRef.current?.submit()}
>
{responding ? (
<div className="flex h-10 w-10 items-center justify-center">
<div className="bg-foreground h-4 w-4 rounded-sm opacity-70" />
</div>
) : (
<ArrowUp />
)}
</Button>
</Tooltip>
</div>
</div>
</div>
);
}