mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-09 08:44:45 +08:00
171 lines
4.9 KiB
TypeScript
171 lines
4.9 KiB
TypeScript
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
import { ArrowUpOutlined, CloseOutlined } from "@ant-design/icons";
|
|
import { AnimatePresence, motion } from "framer-motion";
|
|
import {
|
|
type KeyboardEvent,
|
|
useCallback,
|
|
useEffect,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
|
|
import { Button } from "~/components/ui/button";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from "~/components/ui/tooltip";
|
|
import type { Option } from "~/core/messages";
|
|
import { cn } from "~/lib/utils";
|
|
|
|
export function InputBox({
|
|
className,
|
|
size,
|
|
responding,
|
|
feedback,
|
|
onSend,
|
|
onCancel,
|
|
onRemoveFeedback,
|
|
}: {
|
|
className?: string;
|
|
size?: "large" | "normal";
|
|
responding?: boolean;
|
|
feedback?: { option: Option } | null;
|
|
onSend?: (message: string, feedback: { option: Option } | null) => void;
|
|
onCancel?: () => void;
|
|
onRemoveFeedback?: () => void;
|
|
}) {
|
|
const [message, setMessage] = useState("");
|
|
const [imeStatus, setImeStatus] = useState<"active" | "inactive">("inactive");
|
|
const [indent, setIndent] = useState(0);
|
|
const textareaRef = useRef<HTMLTextAreaElement>(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, feedback ?? null);
|
|
setMessage("");
|
|
onRemoveFeedback?.();
|
|
}
|
|
}
|
|
}, [responding, onCancel, message, onSend, feedback, onRemoveFeedback]);
|
|
|
|
const handleKeyDown = useCallback(
|
|
(event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
if (responding) {
|
|
return;
|
|
}
|
|
if (
|
|
event.key === "Enter" &&
|
|
!event.shiftKey &&
|
|
!event.metaKey &&
|
|
!event.ctrlKey &&
|
|
imeStatus === "inactive"
|
|
) {
|
|
event.preventDefault();
|
|
handleSendMessage();
|
|
}
|
|
},
|
|
[responding, imeStatus, handleSendMessage],
|
|
);
|
|
|
|
return (
|
|
<div className={cn("relative rounded-[24px] border bg-white", className)}>
|
|
<div className="w-full">
|
|
<AnimatePresence>
|
|
{feedback && (
|
|
<motion.div
|
|
ref={feedbackRef}
|
|
className="absolute top-0 left-0 mt-3 ml-2 flex items-center justify-center gap-1 rounded-2xl border border-[#007aff] bg-white 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="flex h-full w-full items-center justify-center text-sm text-[#007aff] opacity-90">
|
|
{feedback.option.text}
|
|
</div>
|
|
<CloseOutlined
|
|
className="cursor-pointer text-[9px]"
|
|
onClick={onRemoveFeedback}
|
|
/>
|
|
</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);
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center px-4 py-2">
|
|
<div className="flex grow"></div>
|
|
<div className="flex shrink-0 items-center gap-2">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
className={cn(
|
|
"h-10 w-10 rounded-full",
|
|
responding ? "bg-button-hover" : "bg-button",
|
|
)}
|
|
onClick={handleSendMessage}
|
|
>
|
|
{responding ? (
|
|
<div className="flex h-10 w-10 items-center justify-center">
|
|
<div className="h-4 w-4 rounded-sm bg-red-300" />
|
|
</div>
|
|
) : (
|
|
<ArrowUpOutlined />
|
|
)}
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>{responding ? "Stop" : "Send"}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|