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

193 lines
5.9 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-18 13:07:56 +08:00
import { CheckIcon, LightbulbIcon, LightbulbOffIcon } 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,
PromptInputBody,
PromptInputButton,
2026-01-15 23:40:21 +08:00
PromptInputFooter,
PromptInputSubmit,
PromptInputTextarea,
type PromptInputMessage,
} from "@/components/ai-elements/prompt-input";
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";
import { Tooltip } from "./tooltip";
2026-01-15 23:40:21 +08:00
export function InputBox({
className,
autoFocus,
status = "ready",
2026-01-16 09:37:04 +08:00
context,
onContextChange,
2026-01-15 23:40:21 +08:00
onSubmit,
onStop,
...props
}: Omit<ComponentProps<typeof PromptInput>, "onSubmit"> & {
assistantId?: string | null;
status?: ChatStatus;
2026-01-16 09:55:02 +08:00
context: Omit<AgentThreadContext, "thread_id">;
onContextChange?: (context: Omit<AgentThreadContext, "thread_id">) => 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();
2026-01-16 09:37:04 +08:00
const selectedModel = useMemo(
2026-01-19 18:54:04 +08:00
() => models.find((m) => m.name === context.model_name),
[context.model_name, models],
2026-01-16 09:37:04 +08:00
);
const handleModelSelect = useCallback(
2026-01-16 14:03:34 +08:00
(model_name: string) => {
2026-01-19 18:54:04 +08:00
const supports_thinking = selectedModel?.supports_thinking ?? false;
2026-01-16 09:37:04 +08:00
onContextChange?.({
...context,
2026-01-16 14:03:34 +08:00
model_name,
2026-01-19 18:54:04 +08:00
thinking_enabled: supports_thinking && context.thinking_enabled,
2026-01-16 09:37:04 +08:00
});
setModelDialogOpen(false);
},
2026-01-19 18:54:04 +08:00
[selectedModel?.supports_thinking, onContextChange, context],
2026-01-16 09:37:04 +08:00
);
const handleThinkingToggle = useCallback(() => {
onContextChange?.({
...context,
thinking_enabled: !context.thinking_enabled,
});
}, [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-21 10:31:54 +08:00
"-translate-y-4 overflow-hidden",
2026-01-15 23:40:21 +08:00
className,
)}
globalDrop
multiple
onSubmit={handleSubmit}
{...props}
>
<PromptInputBody>
<PromptInputTextarea
className={cn("size-full")}
2026-01-20 14:06:47 +08:00
placeholder={t.inputBox.placeholder}
2026-01-15 23:40:21 +08:00
autoFocus={autoFocus}
/>
</PromptInputBody>
<PromptInputFooter className="flex">
<div>
2026-01-16 09:37:04 +08:00
<Tooltip
content={
context.thinking_enabled ? (
<div className="tex-sm flex flex-col gap-1">
2026-01-20 14:06:47 +08:00
<div>{t.inputBox.thinkingEnabled}</div>
<div className="opacity-50">
{t.inputBox.clickToDisableThinking}
</div>
2026-01-16 09:37:04 +08:00
</div>
) : (
<div className="tex-sm flex flex-col gap-1">
2026-01-20 14:06:47 +08:00
<div>{t.inputBox.thinkingDisabled}</div>
<div className="opacity-50">
{t.inputBox.clickToEnableThinking}
</div>
2026-01-16 09:37:04 +08:00
</div>
)
}
>
2026-01-19 18:54:04 +08:00
{selectedModel?.supports_thinking && (
<PromptInputButton onClick={handleThinkingToggle}>
2026-01-21 10:31:54 +08:00
<>
{context.thinking_enabled ? (
<LightbulbIcon className="text-primary size-4" />
) : (
<LightbulbOffIcon className="size-4" />
)}
<span
className={cn(
"text-xs font-normal",
context.thinking_enabled
? "text-primary"
: "text-muted-foreground",
)}
>
Thinking
</span>
</>
2026-01-19 18:54:04 +08:00
</PromptInputButton>
)}
</Tooltip>
</div>
2026-01-15 23:40:21 +08:00
<div className="flex items-center gap-2">
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"
variant="outline"
status={status}
/>
</div>
</PromptInputFooter>
</PromptInput>
);
}