Files
deer-flow/frontend/src/components/workspace/input-box.tsx

352 lines
12 KiB
TypeScript
Raw Normal View History

2026-01-20 14:06:47 +08:00
"use client";
2026-01-15 23:40:21 +08:00
import type { ChatStatus } from "ai";
2026-01-22 13:43:45 +08:00
import {
CheckIcon,
GraduationCapIcon,
LightbulbIcon,
2026-01-29 16:17:41 +08:00
PaperclipIcon,
2026-01-22 13:43:45 +08:00
ZapIcon,
} from "lucide-react";
2026-01-16 09:37:04 +08:00
import { useCallback, useMemo, useState, type ComponentProps } from "react";
2026-01-15 23:40:21 +08:00
import {
PromptInput,
2026-01-22 13:43:45 +08:00
PromptInputActionMenu,
PromptInputActionMenuContent,
PromptInputActionMenuItem,
PromptInputActionMenuTrigger,
PromptInputAttachment,
PromptInputAttachments,
2026-01-15 23:40:21 +08:00
PromptInputBody,
PromptInputButton,
2026-01-15 23:40:21 +08:00
PromptInputFooter,
PromptInputSubmit,
PromptInputTextarea,
2026-01-22 13:43:45 +08:00
PromptInputTools,
2026-01-29 16:17:41 +08:00
usePromptInputAttachments,
2026-01-15 23:40:21 +08:00
type PromptInputMessage,
} from "@/components/ai-elements/prompt-input";
2026-01-22 13:43:45 +08:00
import {
DropdownMenuGroup,
DropdownMenuLabel,
} from "@/components/ui/dropdown-menu";
2026-01-20 14:06:47 +08:00
import { useI18n } from "@/core/i18n/hooks";
2026-01-19 18:54:04 +08:00
import { useModels } from "@/core/models/hooks";
2026-01-16 09:37:04 +08:00
import type { AgentThreadContext } from "@/core/threads";
2026-01-15 23:40:21 +08:00
import { cn } from "@/lib/utils";
2026-01-16 09:37:04 +08:00
import {
ModelSelector,
ModelSelectorContent,
ModelSelectorInput,
ModelSelectorItem,
ModelSelectorList,
ModelSelectorName,
ModelSelectorTrigger,
} from "../ai-elements/model-selector";
2026-01-29 16:17:41 +08:00
import { Tooltip } from "./tooltip";
2026-01-15 23:40:21 +08:00
export function InputBox({
className,
2026-01-24 18:01:27 +08:00
disabled,
2026-01-15 23:40:21 +08:00
autoFocus,
status = "ready",
2026-01-16 09:37:04 +08:00
context,
2026-01-22 13:43:45 +08:00
extraHeader,
2026-01-22 14:28:10 +08:00
isNewThread,
2026-01-31 22:31:25 +08:00
initialValue,
2026-01-16 09:37:04 +08:00
onContextChange,
2026-01-15 23:40:21 +08:00
onSubmit,
onStop,
...props
}: Omit<ComponentProps<typeof PromptInput>, "onSubmit"> & {
assistantId?: string | null;
status?: ChatStatus;
2026-01-24 18:01:27 +08:00
disabled?: boolean;
context: Omit<
AgentThreadContext,
"thread_id" | "is_plan_mode" | "thinking_enabled"
> & {
mode: "flash" | "thinking" | "pro" | undefined;
};
2026-01-22 00:26:11 +08:00
extraHeader?: React.ReactNode;
2026-01-22 14:28:10 +08:00
isNewThread?: boolean;
2026-01-31 22:31:25 +08:00
initialValue?: string;
onContextChange?: (
context: Omit<
AgentThreadContext,
"thread_id" | "is_plan_mode" | "thinking_enabled"
> & {
mode: "flash" | "thinking" | "pro" | undefined;
},
) => void;
2026-01-15 23:40:21 +08:00
onSubmit?: (message: PromptInputMessage) => void;
onStop?: () => void;
}) {
2026-01-20 14:06:47 +08:00
const { t } = useI18n();
2026-01-16 09:37:04 +08:00
const [modelDialogOpen, setModelDialogOpen] = useState(false);
2026-01-19 18:54:04 +08:00
const { models } = useModels();
const selectedModel = useMemo(() => {
if (!context.model_name && models.length > 0) {
const model = models[0]!;
setTimeout(() => {
onContextChange?.({
...context,
model_name: model.name,
mode: model.supports_thinking ? "pro" : "flash",
});
}, 0);
return model;
}
return models.find((m) => m.name === context.model_name);
}, [context, models, onContextChange]);
2026-01-22 13:43:45 +08:00
const supportThinking = useMemo(
() => selectedModel?.supports_thinking ?? false,
[selectedModel],
);
2026-01-16 09:37:04 +08:00
const handleModelSelect = useCallback(
2026-01-16 14:03:34 +08:00
(model_name: string) => {
2026-01-16 09:37:04 +08:00
onContextChange?.({
...context,
2026-01-16 14:03:34 +08:00
model_name,
2026-01-16 09:37:04 +08:00
});
setModelDialogOpen(false);
},
[onContextChange, context],
2026-01-16 09:37:04 +08:00
);
2026-01-22 13:43:45 +08:00
const handleModeSelect = useCallback(
(mode: "flash" | "thinking" | "pro") => {
onContextChange?.({
...context,
mode,
});
2026-01-22 13:43:45 +08:00
},
[onContextChange, context],
);
2026-01-15 23:40:21 +08:00
const handleSubmit = useCallback(
async (message: PromptInputMessage) => {
if (status === "streaming") {
onStop?.();
return;
}
if (!message.text) {
return;
}
onSubmit?.(message);
},
[onSubmit, onStop, status],
);
return (
<PromptInput
className={cn(
2026-01-19 18:54:04 +08:00
"bg-background/85 rounded-2xl backdrop-blur-sm transition-all duration-300 ease-out *:data-[slot='input-group']:rounded-2xl",
2026-01-15 23:40:21 +08:00
className,
)}
2026-01-24 18:01:27 +08:00
disabled={disabled}
2026-01-15 23:40:21 +08:00
globalDrop
multiple
onSubmit={handleSubmit}
{...props}
>
2026-01-22 13:43:45 +08:00
{extraHeader && (
<div className="absolute top-0 right-0 left-0 z-10">
<div className="absolute right-0 bottom-0 left-0 flex items-center justify-center">
{extraHeader}
</div>
</div>
)}
<PromptInputAttachments>
{(attachment) => <PromptInputAttachment data={attachment} />}
</PromptInputAttachments>
2026-01-22 09:41:01 +08:00
<PromptInputBody className="absolute top-0 right-0 left-0 z-3">
2026-01-15 23:40:21 +08:00
<PromptInputTextarea
className={cn("size-full")}
2026-01-24 18:01:27 +08:00
disabled={disabled}
2026-01-20 14:06:47 +08:00
placeholder={t.inputBox.placeholder}
2026-01-15 23:40:21 +08:00
autoFocus={autoFocus}
2026-01-31 22:31:25 +08:00
defaultValue={initialValue}
2026-01-15 23:40:21 +08:00
/>
</PromptInputBody>
<PromptInputFooter className="flex">
2026-01-22 13:43:45 +08:00
<PromptInputTools>
2026-01-29 16:17:41 +08:00
<AddAttachmentsButton className="px-2!" />
2026-01-22 13:43:45 +08:00
<PromptInputActionMenu>
2026-01-29 16:17:41 +08:00
<PromptInputActionMenuTrigger className="px-2!">
<div>
{context.mode === "flash" && <ZapIcon className="size-3" />}
{context.mode === "thinking" && (
<LightbulbIcon className="size-3" />
)}
{context.mode === "pro" && (
<GraduationCapIcon className="size-3" />
)}
</div>
<div className="text-xs font-normal">
{(context.mode === "flash" && t.inputBox.flashMode) ||
(context.mode === "thinking" && t.inputBox.reasoningMode) ||
(context.mode === "pro" && t.inputBox.proMode)}
</div>
</PromptInputActionMenuTrigger>
2026-01-22 13:43:45 +08:00
<PromptInputActionMenuContent className="w-80">
<DropdownMenuGroup>
<DropdownMenuLabel className="text-muted-foreground text-xs">
{t.inputBox.mode}
</DropdownMenuLabel>
<PromptInputActionMenu>
<PromptInputActionMenuItem
2026-01-21 10:31:54 +08:00
className={cn(
context.mode === "flash"
2026-01-22 13:43:45 +08:00
? "text-accent-foreground"
: "text-muted-foreground/65",
2026-01-21 10:31:54 +08:00
)}
2026-01-22 13:43:45 +08:00
onSelect={() => handleModeSelect("flash")}
2026-01-21 10:31:54 +08:00
>
2026-01-22 13:43:45 +08:00
<div className="flex flex-col gap-2">
<div className="flex items-center gap-1 font-bold">
<ZapIcon
className={cn(
"mr-2 size-4",
context.mode === "flash" &&
"text-accent-foreground",
2026-01-22 13:43:45 +08:00
)}
/>
{t.inputBox.flashMode}
</div>
<div className="pl-7 text-xs">
{t.inputBox.flashModeDescription}
</div>
</div>
{context.mode === "flash" ? (
2026-01-22 13:43:45 +08:00
<CheckIcon className="ml-auto size-4" />
) : (
<div className="ml-auto size-4" />
)}
</PromptInputActionMenuItem>
{supportThinking && (
<PromptInputActionMenuItem
className={cn(
context.mode === "thinking"
2026-01-22 13:43:45 +08:00
? "text-accent-foreground"
: "text-muted-foreground/65",
)}
onSelect={() => handleModeSelect("thinking")}
>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-1 font-bold">
<LightbulbIcon
className={cn(
"mr-2 size-4",
context.mode === "thinking" &&
"text-accent-foreground",
2026-01-22 13:43:45 +08:00
)}
/>
{t.inputBox.reasoningMode}
</div>
<div className="pl-7 text-xs">
{t.inputBox.reasoningModeDescription}
</div>
</div>
{context.mode === "thinking" ? (
2026-01-22 13:43:45 +08:00
<CheckIcon className="ml-auto size-4" />
) : (
<div className="ml-auto size-4" />
)}
</PromptInputActionMenuItem>
2026-01-22 00:26:11 +08:00
)}
2026-01-22 13:43:45 +08:00
<PromptInputActionMenuItem
2026-01-22 00:26:11 +08:00
className={cn(
context.mode === "pro"
2026-01-22 13:43:45 +08:00
? "text-accent-foreground"
: "text-muted-foreground/65",
2026-01-22 00:26:11 +08:00
)}
2026-01-22 13:43:45 +08:00
onSelect={() => handleModeSelect("pro")}
2026-01-22 00:26:11 +08:00
>
2026-01-22 13:43:45 +08:00
<div className="flex flex-col gap-2">
<div className="flex items-center gap-1 font-bold">
<GraduationCapIcon
className={cn(
"mr-2 size-4",
context.mode === "pro" && "text-accent-foreground",
2026-01-22 13:43:45 +08:00
)}
/>
{t.inputBox.proMode}
</div>
<div className="pl-7 text-xs">
{t.inputBox.proModeDescription}
</div>
</div>
{context.mode === "pro" ? (
2026-01-22 13:43:45 +08:00
<CheckIcon className="ml-auto size-4" />
) : (
<div className="ml-auto size-4" />
)}
</PromptInputActionMenuItem>
</PromptInputActionMenu>
</DropdownMenuGroup>
</PromptInputActionMenuContent>
</PromptInputActionMenu>
</PromptInputTools>
<PromptInputTools>
2026-01-16 09:37:04 +08:00
<ModelSelector
open={modelDialogOpen}
onOpenChange={setModelDialogOpen}
>
<ModelSelectorTrigger asChild>
<PromptInputButton>
2026-01-18 09:55:17 +08:00
<ModelSelectorName className="text-xs font-normal">
2026-01-19 18:54:04 +08:00
{selectedModel?.display_name}
2026-01-16 09:37:04 +08:00
</ModelSelectorName>
</PromptInputButton>
</ModelSelectorTrigger>
<ModelSelectorContent>
2026-01-20 14:06:47 +08:00
<ModelSelectorInput placeholder={t.inputBox.searchModels} />
2026-01-16 09:37:04 +08:00
<ModelSelectorList>
2026-01-19 18:54:04 +08:00
{models.map((m) => (
2026-01-16 09:37:04 +08:00
<ModelSelectorItem
key={m.name}
value={m.name}
onSelect={() => handleModelSelect(m.name)}
>
2026-01-19 18:54:04 +08:00
<ModelSelectorName>{m.display_name}</ModelSelectorName>
2026-01-16 14:03:34 +08:00
{m.name === context.model_name ? (
2026-01-16 09:37:04 +08:00
<CheckIcon className="ml-auto size-4" />
) : (
<div className="ml-auto size-4" />
)}
</ModelSelectorItem>
))}
</ModelSelectorList>
</ModelSelectorContent>
</ModelSelector>
2026-01-15 23:40:21 +08:00
<PromptInputSubmit
className="rounded-full"
2026-01-24 18:01:27 +08:00
disabled={disabled}
2026-01-15 23:40:21 +08:00
variant="outline"
status={status}
/>
2026-01-22 13:43:45 +08:00
</PromptInputTools>
2026-01-15 23:40:21 +08:00
</PromptInputFooter>
2026-01-22 14:28:10 +08:00
{!isNewThread && (
<div className="bg-background absolute right-0 -bottom-[17px] left-0 z-0 h-4"></div>
)}
2026-01-15 23:40:21 +08:00
</PromptInput>
);
}
2026-01-29 16:17:41 +08:00
function AddAttachmentsButton({ className }: { className?: string }) {
const { t } = useI18n();
const attachments = usePromptInputAttachments();
return (
<Tooltip content={t.inputBox.addAttachments}>
<PromptInputButton
className={cn("px-2!", className)}
onClick={() => attachments.openFileDialog()}
>
<PaperclipIcon className="size-3" />
</PromptInputButton>
</Tooltip>
);
}