"use client"; import type { ChatStatus } from "ai"; import { CheckIcon, GraduationCapIcon, LightbulbIcon, PaperclipIcon, PlusIcon, SparklesIcon, RocketIcon, XIcon, ZapIcon, } from "lucide-react"; import { useSearchParams } from "next/navigation"; import { useCallback, useEffect, useMemo, useRef, useState, type ComponentProps, } from "react"; import { PromptInput, PromptInputActionMenu, PromptInputActionMenuContent, PromptInputActionMenuItem, PromptInputActionMenuTrigger, PromptInputAttachment, PromptInputAttachments, PromptInputBody, PromptInputButton, PromptInputFooter, PromptInputSubmit, PromptInputTextarea, PromptInputTools, usePromptInputAttachments, usePromptInputController, type PromptInputMessage, } from "@/components/ai-elements/prompt-input"; import { Button } from "@/components/ui/button"; import { ConfettiButton } from "@/components/ui/confetti-button"; import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { DropdownMenuGroup, DropdownMenuLabel, DropdownMenuSeparator, } from "@/components/ui/dropdown-menu"; import { getBackendBaseURL } from "@/core/config"; import { useI18n } from "@/core/i18n/hooks"; import { useModels } from "@/core/models/hooks"; import type { AgentThreadContext } from "@/core/threads"; import { textOfMessage } from "@/core/threads/utils"; import { cn } from "@/lib/utils"; import { ModelSelector, ModelSelectorContent, ModelSelectorInput, ModelSelectorItem, ModelSelectorList, ModelSelectorName, ModelSelectorTrigger, } from "../ai-elements/model-selector"; import { Suggestion, Suggestions } from "../ai-elements/suggestion"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "../ui/dropdown-menu"; import { useThread } from "./messages/context"; import { ModeHoverGuide } from "./mode-hover-guide"; import { Tooltip } from "./tooltip"; import { useIframeSkill } from "@/hooks/use-iframe-skill"; type InputMode = "flash" | "thinking" | "pro" | "ultra"; function getResolvedMode( mode: InputMode | undefined, supportsThinking: boolean, ): InputMode { if (!supportsThinking && mode !== "flash") { return "flash"; } if (mode) { return mode; } return supportsThinking ? "pro" : "flash"; } export function InputBox({ className, disabled, autoFocus, status = "ready", context, extraHeader, isNewThread, threadId, initialValue, onContextChange, onSubmit, onStop, onFocusChange, ...props }: Omit, "onSubmit"> & { assistantId?: string | null; status?: ChatStatus; disabled?: boolean; context: Omit< AgentThreadContext, "thread_id" | "is_plan_mode" | "thinking_enabled" | "subagent_enabled" > & { mode: "flash" | "thinking" | "pro" | "ultra" | undefined; reasoning_effort?: "minimal" | "low" | "medium" | "high"; }; extraHeader?: React.ReactNode; isNewThread?: boolean; threadId: string; initialValue?: string; onContextChange?: ( context: Omit< AgentThreadContext, "thread_id" | "is_plan_mode" | "thinking_enabled" | "subagent_enabled" > & { mode: "flash" | "thinking" | "pro" | "ultra" | undefined; reasoning_effort?: "minimal" | "low" | "medium" | "high"; }, ) => void; onSubmit?: (message: PromptInputMessage) => void; onStop?: () => void; onFocusChange?: (focused: boolean) => void; }) { const { t } = useI18n(); const searchParams = useSearchParams(); const [modelDialogOpen, setModelDialogOpen] = useState(false); const { models } = useModels(); const { thread, isMock } = useThread(); const { textInput } = usePromptInputController(); const iframeSkill = useIframeSkill(); const promptRootRef = useRef(null); const textareaRef = useRef(null); const attachments = usePromptInputAttachments(); const [followups, setFollowups] = useState([]); const [followupsHidden, setFollowupsHidden] = useState(false); const [followupsLoading, setFollowupsLoading] = useState(false); const lastGeneratedForAiIdRef = useRef(null); const wasStreamingRef = useRef(false); const [confirmOpen, setConfirmOpen] = useState(false); const [pendingSuggestion, setPendingSuggestion] = useState( null, ); const [isFocused, setIsFocused] = useState(false); const containerRef = useRef(null); // isNewThread 时禁用收缩,始终保持展开 const effectiveIsFocused = isNewThread || isFocused; useEffect(() => { if (!isFocused) return; const handleClickOutside = (event: MouseEvent) => { if ( containerRef.current && !containerRef.current.contains(event.target as Node) ) { setIsFocused(false); onFocusChange?.(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [isFocused, onFocusChange]); useEffect(() => { if (models.length === 0) { return; } const currentModel = models.find((m) => m.name === context.model_name); const fallbackModel = currentModel ?? models[0]!; const supportsThinking = fallbackModel.supports_thinking ?? false; const nextModelName = fallbackModel.name; const nextMode = getResolvedMode(context.mode, supportsThinking); if (context.model_name === nextModelName && context.mode === nextMode) { return; } onContextChange?.({ ...context, model_name: nextModelName, mode: nextMode, }); }, [context, models, onContextChange]); const selectedModel = useMemo(() => { if (models.length === 0) { return undefined; } return models.find((m) => m.name === context.model_name) ?? models[0]; }, [context.model_name, models]); const supportThinking = useMemo( () => selectedModel?.supports_thinking ?? false, [selectedModel], ); const supportReasoningEffort = useMemo( () => selectedModel?.supports_reasoning_effort ?? false, [selectedModel], ); const handleModelSelect = useCallback( (model_name: string) => { const model = models.find((m) => m.name === model_name); if (!model) { return; } onContextChange?.({ ...context, model_name, mode: getResolvedMode(context.mode, model.supports_thinking ?? false), reasoning_effort: context.reasoning_effort, }); setModelDialogOpen(false); }, [onContextChange, context, models], ); const handleModeSelect = useCallback( (mode: InputMode) => { onContextChange?.({ ...context, mode: getResolvedMode(mode, supportThinking), reasoning_effort: mode === "ultra" ? "high" : mode === "pro" ? "medium" : mode === "thinking" ? "low" : "minimal", }); }, [onContextChange, context, supportThinking], ); const handleReasoningEffortSelect = useCallback( (effort: "minimal" | "low" | "medium" | "high") => { onContextChange?.({ ...context, reasoning_effort: effort, }); }, [onContextChange, context], ); const handleSubmit = useCallback( async (message: PromptInputMessage) => { if (status === "streaming") { onStop?.(); return; } if (!message.text) { return; } setFollowups([]); setFollowupsHidden(false); setFollowupsLoading(false); onSubmit?.(message); }, [onSubmit, onStop, status], ); const requestFormSubmit = useCallback(() => { const form = promptRootRef.current?.querySelector("form"); form?.requestSubmit(); }, []); const handleFollowupClick = useCallback( (suggestion: string) => { if (status === "streaming") { return; } const current = (textInput.value ?? "").trim(); if (current) { setPendingSuggestion(suggestion); setConfirmOpen(true); return; } textInput.setInput(suggestion); setFollowupsHidden(true); setTimeout(() => requestFormSubmit(), 0); }, [requestFormSubmit, status, textInput], ); const confirmReplaceAndSend = useCallback(() => { if (!pendingSuggestion) { setConfirmOpen(false); return; } textInput.setInput(pendingSuggestion); setFollowupsHidden(true); setConfirmOpen(false); setPendingSuggestion(null); setTimeout(() => requestFormSubmit(), 0); }, [pendingSuggestion, requestFormSubmit, textInput]); const confirmAppendAndSend = useCallback(() => { if (!pendingSuggestion) { setConfirmOpen(false); return; } const current = (textInput.value ?? "").trim(); const next = current ? `${current}\n${pendingSuggestion}` : pendingSuggestion; textInput.setInput(next); setFollowupsHidden(true); setConfirmOpen(false); setPendingSuggestion(null); setTimeout(() => requestFormSubmit(), 0); }, [pendingSuggestion, requestFormSubmit, textInput]); useEffect(() => { const streaming = status === "streaming"; const wasStreaming = wasStreamingRef.current; wasStreamingRef.current = streaming; if (!wasStreaming || streaming) { return; } if (disabled || isMock) { return; } const lastAi = [...thread.messages].reverse().find((m) => m.type === "ai"); const lastAiId = lastAi?.id ?? null; if (!lastAiId || lastAiId === lastGeneratedForAiIdRef.current) { return; } lastGeneratedForAiIdRef.current = lastAiId; const recent = thread.messages .filter((m) => m.type === "human" || m.type === "ai") .map((m) => { const role = m.type === "human" ? "user" : "assistant"; const content = textOfMessage(m) ?? ""; return { role, content }; }) .filter((m) => m.content.trim().length > 0) .slice(-6); if (recent.length === 0) { return; } const controller = new AbortController(); setFollowupsHidden(false); setFollowupsLoading(true); setFollowups([]); fetch(`${getBackendBaseURL()}/api/threads/${threadId}/suggestions`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages: recent, n: 3, model_name: context.model_name ?? undefined, }), signal: controller.signal, }) .then(async (res) => { if (!res.ok) { return { suggestions: [] as string[] }; } return (await res.json()) as { suggestions?: string[] }; }) .then((data) => { const suggestions = (data.suggestions ?? []) .map((s) => (typeof s === "string" ? s.trim() : "")) .filter((s) => s.length > 0) .slice(0, 5); setFollowups(suggestions); }) .catch(() => { setFollowups([]); }) .finally(() => { setFollowupsLoading(false); }); return () => controller.abort(); }, [context.model_name, disabled, isMock, status, thread.messages, threadId]); return (
{ promptRootRef.current = el; containerRef.current = el; }} className="relative" > {/* 附件预览区域 - 在输入框上方 */} {extraHeader && ( 0}> {extraHeader} )} {/* 输入框主容器 */} { setIsFocused(true); onFocusChange?.(true); }} /> {!effectiveIsFocused && (
{ setIsFocused(true); textareaRef.current?.focus(); }} /> )} {/* ========== 左侧工具栏 ========== */} {/* 附件上传按钮 */} {/* [已禁用] 模式选择器触发器 (flash/thinking/pro/ultra) */} {/*
{context.mode === "flash" && } {context.mode === "thinking" && ( )} {context.mode === "pro" && ( )} {context.mode === "ultra" && ( )}
{(context.mode === "flash" && t.inputBox.flashMode) || (context.mode === "thinking" && t.inputBox.reasoningMode) || (context.mode === "pro" && t.inputBox.proMode) || (context.mode === "ultra" && t.inputBox.ultraMode)}
*/} {/* Skill 选择按钮 (iframe 与宿主页通信) */} {/* [已禁用] 模式选择下拉菜单内容 */} {/* {t.inputBox.mode} handleModeSelect("flash")} >
{t.inputBox.flashMode}
{t.inputBox.flashModeDescription}
{context.mode === "flash" ? ( ) : (
)} {supportThinking && ( handleModeSelect("thinking")} >
{t.inputBox.reasoningMode}
{t.inputBox.reasoningModeDescription}
{context.mode === "thinking" ? ( ) : (
)} )} handleModeSelect("pro")} >
{t.inputBox.proMode}
{t.inputBox.proModeDescription}
{context.mode === "pro" ? ( ) : (
)} handleModeSelect("ultra")} >
{t.inputBox.ultraMode}
{t.inputBox.ultraModeDescription}
{context.mode === "ultra" ? ( ) : (
)} {/* [已禁用] 推理强度选择器 (minimal/low/medium/high) */} {supportReasoningEffort && context.mode !== "flash" && (
{t.inputBox.reasoningEffort}: {context.reasoning_effort === "minimal" && " " + t.inputBox.reasoningEffortMinimal} {context.reasoning_effort === "low" && " " + t.inputBox.reasoningEffortLow} {context.reasoning_effort === "medium" && " " + t.inputBox.reasoningEffortMedium} {context.reasoning_effort === "high" && " " + t.inputBox.reasoningEffortHigh}
{t.inputBox.reasoningEffort} handleReasoningEffortSelect("minimal")} >
{t.inputBox.reasoningEffortMinimal}
{t.inputBox.reasoningEffortMinimalDescription}
{context.reasoning_effort === "minimal" ? ( ) : (
)} handleReasoningEffortSelect("low")} >
{t.inputBox.reasoningEffortLow}
{t.inputBox.reasoningEffortLowDescription}
{context.reasoning_effort === "low" ? ( ) : (
)} handleReasoningEffortSelect("medium")} >
{t.inputBox.reasoningEffortMedium}
{t.inputBox.reasoningEffortMediumDescription}
{context.reasoning_effort === "medium" || !context.reasoning_effort ? ( ) : (
)} handleReasoningEffortSelect("high")} >
{t.inputBox.reasoningEffortHigh}
{t.inputBox.reasoningEffortHighDescription}
{context.reasoning_effort === "high" ? ( ) : (
)} )} {/* ========== 右侧工具栏 ========== */} {/* [已禁用] 模型选择器 */} {/* {selectedModel?.display_name} {models.map((m) => ( handleModelSelect(m.name)} > {m.display_name} {m.name === context.model_name ? ( ) : (
)} ))} */} {/* 占位符 */}
{/* 移动出来 */} {/* TODO: 神秘空div */} {/* {!isNewThread && (
)} */} {/* 小惊喜等 */} {isNewThread && searchParams.get("mode") !== "skill" && ( )} {!disabled && !isNewThread && !followupsHidden && (followupsLoading || followups.length > 0) && (
{followupsLoading ? (
{t.inputBox.followupLoading}
) : ( {followups.map((s) => ( handleFollowupClick(s)} /> ))} )}
)} {t.inputBox.followupConfirmTitle} {t.inputBox.followupConfirmDescription}
); } // SuggestionList 容器 function SuggestionListContainer({ sendSelectSkill, }: { sendSelectSkill: (skill_id: string) => void; }) { return (
); } // 快速选择skillbutton function SuggestionList({ sendSelectSkill, }: { sendSelectSkill: (skill_id: string) => void; }) { const { t } = useI18n(); const { textInput } = usePromptInputController(); const handleSuggestionClick = useCallback( (suggestion: { prompt: string; skill_id?: string }) => { // 如果有 skill_id,发送给宿主页 if (suggestion.skill_id) { sendSelectSkill(suggestion.skill_id); return; } // 原有逻辑 if (!suggestion.prompt) return; textInput.setInput(suggestion.prompt); setTimeout(() => { const textarea = document.querySelector( "textarea[name='message']", ); if (textarea) { const selStart = suggestion.prompt.indexOf("["); const selEnd = suggestion.prompt.indexOf("]"); if (selStart !== -1 && selEnd !== -1) { textarea.setSelectionRange(selStart, selEnd + 1); textarea.focus(); } } }, 500); }, [textInput, sendSelectSkill], ); return ( {/* handleSuggestionClick(t.inputBox.surpriseMePrompt)} > {t.inputBox.surpriseMe} */} {t.inputBox.suggestions.map((suggestion) => ( handleSuggestionClick(suggestion)} /> ))} {t.inputBox.suggestionsCreate.map((suggestion, index) => "type" in suggestion && suggestion.type === "separator" ? ( ) : ( !("type" in suggestion) && ( handleSuggestionClick(suggestion)} > {suggestion.icon && } {suggestion.suggestion} ) ), )} ); } // 上传附件 function AddAttachmentsButton({ className }: { className?: string }) { const { t } = useI18n(); const attachments = usePromptInputAttachments(); return ( attachments.openFileDialog()} > ); } // 启动iframeSkillDialog function IframeSkillDialogButton({ className, selectedSkill, openSkillDialog, clearSkill, }: { className?: string; selectedSkill: { skill_id: string; title: string } | null; openSkillDialog: () => void; clearSkill: () => void; }) { const { t } = useI18n(); return (
{selectedSkill && ( {selectedSkill.title} )}
); } // 附件预览栏 - 在输入框上方显示 function AttachmentPreviewBar() { const attachments = usePromptInputAttachments(); if (!attachments.files.length) { return null; } return (
{(attachment) => }
); } // ExtraHeader 容器 - 有附件时上浮 function ExtraHeaderContainer({ hasAttachments, children, }: { hasAttachments: boolean; children: React.ReactNode; }) { return (
{children}
); }