"use client"; import type { ChatStatus } from "ai"; import { CheckIcon, GraduationCapIcon, LightbulbIcon, Loader2Icon, 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 { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { DropdownMenuGroup, DropdownMenuLabel, DropdownMenuSeparator, } from "@/components/ui/dropdown-menu"; import { Tag } from "@/components/ui/tag"; import { useI18n } from "@/core/i18n/hooks"; import type { SelectedSkillPayloadItem, } from "@/core/i18n/locales/types"; import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages"; import { useModels } from "@/core/models/hooks"; import type { AgentThreadContext } from "@/core/threads"; import { useIframeSkill } from "@/hooks/use-iframe-skill"; 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 { ModeHoverGuide } from "./mode-hover-guide"; import { Tooltip } from "./tooltip"; export function InputBox({ className, threadId: threadIdFromProps, disabled, autoFocus, status, context, extraHeader, showWelcomeStyle, hasSubmitted, initialValue, onContextChange, onSubmit, onStop, ...props }: Omit, "onSubmit"> & { assistantId?: string | null; threadId: string; status?: ChatStatus; disabled?: boolean; context: Omit< AgentThreadContext, "thread_id" | "is_plan_mode" | "thinking_enabled" | "subagent_enabled" > & { mode: "flash" | "thinking" | "pro" | "ultra" | undefined; }; extraHeader?: React.ReactNode; showWelcomeStyle?: boolean; hasSubmitted?: boolean; initialValue?: string; onContextChange?: ( context: Omit< AgentThreadContext, "thread_id" | "is_plan_mode" | "thinking_enabled" | "subagent_enabled" > & { mode: "flash" | "thinking" | "pro" | "ultra" | undefined; }, ) => void; onSubmit?: (message: PromptInputMessage) => void; onStop?: () => void; }) { const { t } = useI18n(); const searchParams = useSearchParams(); const iframeSkill = useIframeSkill({ threadId: threadIdFromProps }); const isInputDisabled = (disabled ?? false) || iframeSkill.isBootstrapping; const threadId = threadIdFromProps; const { textInput } = usePromptInputController(); const attachments = usePromptInputAttachments(); const promptRootRef = useRef(null); const textareaRef = useRef(null); const containerRef = useRef(null); const [followups, setFollowups] = useState([]); const [followupsHidden, setFollowupsHidden] = useState(false); const [followupsLoading, setFollowupsLoading] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false); const [pendingSuggestion, setPendingSuggestion] = useState( null, ); const [isFocused, setIsFocused] = useState(false); // isNewThread 时禁用收缩,始终保持展开(除非已提交消息) const effectiveIsFocused = ((showWelcomeStyle ?? false) && !hasSubmitted) || isFocused; // 点击外部区域时收起输入框 useEffect(() => { if (!isFocused) return; const handleClickOutside = (event: MouseEvent) => { if ( containerRef.current && !containerRef.current.contains(event.target as Node) ) { setIsFocused(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [isFocused]); const [modelDialogOpen, setModelDialogOpen] = useState(false); 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]); const supportThinking = useMemo( () => selectedModel?.supports_thinking ?? false, [selectedModel], ); const handleModelSelect = useCallback( (model_name: string) => { onContextChange?.({ ...context, model_name, }); setModelDialogOpen(false); }, [onContextChange, context], ); const handleModeSelect = useCallback( (mode: "flash" | "thinking" | "pro" | "ultra") => { onContextChange?.({ ...context, mode, }); }, [onContextChange, context], ); const handleSubmit = useCallback( async (message: PromptInputMessage) => { if (status === "streaming") { onStop?.(); return; } if (!message.text) { return; } setIsFocused(false); if (showWelcomeStyle) { sendToParent({ type: POST_MESSAGE_TYPES.IS_CHATTING, isChatting: true, }); } onSubmit?.(message); }, [showWelcomeStyle, 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) return; textInput?.setInput(pendingSuggestion); setFollowupsHidden(true); setConfirmOpen(false); setTimeout(() => requestFormSubmit(), 0); }, [pendingSuggestion, requestFormSubmit, textInput]); const confirmAppendAndSend = useCallback(() => { if (!pendingSuggestion) return; const current = (textInput?.value ?? "").trim(); textInput?.setInput( current ? `${current}\n${pendingSuggestion}` : pendingSuggestion, ); setFollowupsHidden(true); setConfirmOpen(false); setTimeout(() => requestFormSubmit(), 0); }, [pendingSuggestion, requestFormSubmit, textInput]); useEffect(() => { /* // 暂时禁用 suggestions 接口(/api/threads/{thread_id}/suggestions) if (!threadId || isNewThread || disabled) return; const controller = new AbortController(); setFollowupsHidden(false); setFollowupsLoading(true); setFollowups([]); fetch(`/api/threads/${String(threadId)}/suggestions`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages: [], n: 3 }), signal: controller.signal, }) .then(async (res) => { if (!res.ok) return { suggestions: [] }; return await res.json(); }) .then((data) => { const suggestions = (data.suggestions ?? []) .filter(Boolean) .slice(0, 5); setFollowups(suggestions); }) .catch(() => setFollowups([])) .finally(() => setFollowupsLoading(false)); return () => controller.abort(); */ }, [disabled, showWelcomeStyle, threadId]); return (
{ promptRootRef.current = el; containerRef.current = el; }} className="relative w-full" > {extraHeader && ( 0}> {extraHeader} )} setIsFocused(true)} /> {!effectiveIsFocused && (
{ setIsFocused(true); textareaRef.current?.focus(); }} /> )} {/* TODO: Add more connectors here */} {/* 参考 kexue 版本隐藏运行模式切换按钮 */} {/* {selectedModel?.display_name} {models.map((m) => ( handleModelSelect(m.name)} > {m.display_name} {m.name === context.model_name ? ( ) : (
)} ))} */} {/* 占位符 */}
{showWelcomeStyle && !hasSubmitted && searchParams.get("mode") !== "skill" && ( )} {!disabled && !showWelcomeStyle && !followupsHidden && (followupsLoading || followups.length > 0) && (
{followupsLoading ? (
加载中...
) : ( {followups.map((s) => ( handleFollowupClick(s)} /> ))} )}
)} 提示 请确认要如何处理当前的追加建议内容?
); } // SuggestionList 容器 function SuggestionListContainer({ bootstrapAndLockSkills, isBootstrapping, }: { bootstrapAndLockSkills: (params: { selectedSkills: SelectedSkillPayloadItem[]; title: string; }) => Promise; isBootstrapping: boolean; }) { return (
); } // 快速选择skillbutton function SuggestionList({ bootstrapAndLockSkills, isBootstrapping, }: { bootstrapAndLockSkills: (params: { selectedSkills: SelectedSkillPayloadItem[]; title: string; }) => Promise; isBootstrapping: boolean; }) { const { t } = useI18n(); const { textInput } = usePromptInputController(); const suggestions = t.inputBox.suggestions; const promptSuggestions = suggestions.filter( ( suggestion, ): suggestion is Exclude<(typeof suggestions)[number], { type: "separator" }> => !("type" in suggestion), ); const handleSuggestionClick = useCallback( ( suggestion: { prompt: string; skill_id?: string[]; children?: SelectedSkillPayloadItem[]; suggestion: string; }, ) => { if (isBootstrapping) return; // 优先使用 children 中的 skill(保留每个 skill 自己的 name,用于 tag 展示) const childSkills = (suggestion.children ?? []) .map((item) => ({ id: String(item.id).trim(), name: item.name?.trim() ?? "", })) .filter((item): item is { id: string; name: string } => Boolean(item.id) && Boolean(item.name), ); if (childSkills.length > 0) { void bootstrapAndLockSkills({ selectedSkills: childSkills, title: suggestion.suggestion, }); return; } if (suggestion.skill_id && suggestion.skill_id.length > 0) { void bootstrapAndLockSkills({ selectedSkills: suggestion.skill_id.map((id) => ({ id, name: suggestion.suggestion, })), title: suggestion.suggestion, }); 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); }, [bootstrapAndLockSkills, isBootstrapping, textInput], ); return ( {promptSuggestions.map((suggestion) => ( handleSuggestionClick(suggestion)} /> ))} ); } function AddAttachmentsButton({ className }: { className?: string }) { const { t } = useI18n(); const attachments = usePromptInputAttachments(); return ( attachments.openFileDialog()} > ); } // 启动iframeSkillDialog function IframeSkillDialogButton({ className, selectedSkills, isBootstrapping, openSkillDialog, clearSkill, }: { className?: string; selectedSkills: Array<{ skill_id: string; title: string }>; isBootstrapping: boolean; openSkillDialog: () => void; clearSkill: () => void; }) { const { t } = useI18n(); return (
{isBootstrapping ? ( {t.common.loading} ) : null} {!isBootstrapping && selectedSkills.length > 0 ? (
{selectedSkills.map((skill, index) => ( {skill.title} ))}
) : null}
); } // 附件预览栏 - 在输入框上方显示 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}
); }