"use client"; import type { ChatStatus } from "ai"; import { CheckIcon, GraduationCapIcon, LightbulbIcon, PaperclipIcon, PlusIcon, SparklesIcon, RocketIcon, XIcon, ZapIcon, } from "lucide-react"; import { useParams, 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 { Badge } from "@/components/ui/badge"; 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 { useI18n } from "@/core/i18n/hooks"; import { useModels } from "@/core/models/hooks"; import type { AgentThreadContext } from "@/core/threads"; 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"; const POST_MESSAGE_TYPES = { SELECT_SKILL: "selectSkill", OPEN_SKILL_DIALOG: "openSkillDialog", } as const; const RECEIVE_MESSAGE_TYPES = { SELECTED_SKILL: "selectedSkill", } as const; type IframeSelectedSkillMessage = { type: typeof RECEIVE_MESSAGE_TYPES.SELECTED_SKILL; id: string | number; title: string; }; type IframeSkillData = { skill_id: string; title: string; }; function sendIframeMessageToParent(message: unknown): void { if (window.parent !== window) { window.parent.postMessage(message, "*"); } } function useEmbeddedIframeSkill() { const searchParams = useSearchParams(); const skillIdFromQuery = searchParams.get("skill_id"); const titleFromQuery = searchParams.get("title"); const [selectedSkill, setSelectedSkill] = useState( null, ); useEffect(() => { if (skillIdFromQuery && titleFromQuery) { setSelectedSkill({ skill_id: skillIdFromQuery, title: titleFromQuery }); } }, [skillIdFromQuery, titleFromQuery]); useEffect(() => { const handleMessage = (event: MessageEvent) => { if (event.data?.type === RECEIVE_MESSAGE_TYPES.SELECTED_SKILL) { const { id, title } = event.data as IframeSelectedSkillMessage; setSelectedSkill({ skill_id: String(id), title }); } }; window.addEventListener("message", handleMessage); return () => window.removeEventListener("message", handleMessage); }, []); const sendSelectSkill = useCallback((skill_id: string) => { sendIframeMessageToParent({ type: POST_MESSAGE_TYPES.SELECT_SKILL, skill_id }); }, []); const openSkillDialog = useCallback(() => { sendIframeMessageToParent({ type: POST_MESSAGE_TYPES.OPEN_SKILL_DIALOG, openSkillDialog: true, }); }, []); const clearSkill = useCallback(() => { setSelectedSkill(null); sendIframeMessageToParent({ type: POST_MESSAGE_TYPES.SELECT_SKILL, skill_id: "0", }); }, []); return { selectedSkill, sendSelectSkill, openSkillDialog, clearSkill }; } export function InputBox({ className, disabled, autoFocus, status = "ready", context, extraHeader, isNewThread, hasSubmitted, threadId: threadIdProp, initialValue, onContextChange, onSubmit, onStop, ...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; }; extraHeader?: React.ReactNode; isNewThread?: boolean; hasSubmitted?: boolean; threadId?: string; 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 = useEmbeddedIframeSkill(); const params = useParams(); const threadId = threadIdProp ?? params?.thread_id; 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 = ((isNewThread ?? 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); 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) 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(() => { 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, isNewThread, 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 ? ( ) : (
)} ))} */} {/* 占位符 */}
{isNewThread && !hasSubmitted && searchParams.get("mode") !== "skill" && ( )} {!disabled && !isNewThread && !followupsHidden && (followupsLoading || followups.length > 0) && (
{followupsLoading ? (
加载中...
) : ( {followups.map((s) => ( handleFollowupClick(s)} /> ))} )}
)} 提示 请确认要如何处理当前的追加建议内容?
); } // 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 ( {t.inputBox.suggestions.map((suggestion) => ( handleSuggestionClick(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}
); }