fix(prompt-input): 提供可选上下文入口并避免历史路由切换期异常

This commit is contained in:
董宏凯 committed 2026-09-01 14:51:21 +08:00
1 parent 519e4c0c01
commit 9455160101
2 files changed
+33 -23

No files matched your search

@@ -56,12 +56,11 @@ import {
type ChangeEventHandler, type ChangeEventHandler,
Children, Children,
type ClipboardEventHandler, type ClipboardEventHandler,
type ComponentProps, type ComponentProps,
createContext, createContext,
type FormEvent, type FormEvent,
type FormEventHandler, type FormEventHandler,
Fragment, type HTMLAttributes,
type HTMLAttributes,
type KeyboardEventHandler, type KeyboardEventHandler,
type PropsWithChildren, type PropsWithChildren,
type ReactNode, type ReactNode,
@@ -124,9 +123,11 @@ export const usePromptInputController = () => {
return ctx; return ctx;
}; };
// Optional variants (do NOT throw). Useful for dual-mode components. // Optional variants (do NOT throw). Useful for dual-mode components and for
const useOptionalPromptInputController = () => // Fast Refresh transitions where a page can briefly retain the previous
useContext(PromptInputController); // provider context instance.
export const useOptionalPromptInputController = () =>
useContext(PromptInputController);
export const useProviderAttachments = () => { export const useProviderAttachments = () => {
const ctx = useContext(ProviderAttachmentsContext); const ctx = useContext(ProviderAttachmentsContext);
@@ -403,14 +404,14 @@ export function PromptInputAttachments({
return ( return (
<div <div
className={cn("flex w-full flex-wrap items-center gap-2 p-3", className)} className={cn("flex w-full flex-wrap items-center gap-2 p-3", className)}
{...props} {...props}
> >
{attachments.files.map((file) => ( {attachments.files.map((file) => (
<Fragment key={file.id}> <div key={file.id} className="max-w-60">
<div className="max-w-60">{children(file)}</div> {children(file)}
</Fragment> </div>
))} ))}
</div> </div>
); );
} }
@@ -1,7 +1,7 @@
import { useParams, useSearchParams } from "next/navigation"; import { useParams, useSearchParams } from "next/navigation";
import { useEffect, useMemo, useRef } from "react"; import { useEffect, useMemo, useRef } from "react";
import { usePromptInputController } from "@/components/ai-elements/prompt-input"; import { useOptionalPromptInputController } from "@/components/ai-elements/prompt-input";
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
/** /**
@@ -11,7 +11,11 @@ export function useSpecificChatMode() {
const { t } = useI18n(); const { t } = useI18n();
const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>(); const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const promptInputController = usePromptInputController(); // A regular history item can navigate here while Vite Fast Refresh is
// reconciling the previous page tree. In that short window the page may see
// a stale provider context. This hook only pre-fills the skill-mode draft,
// so it must not make an ordinary chat page fail to render.
const promptInputController = useOptionalPromptInputController();
const inputInitialValue = useMemo(() => { const inputInitialValue = useMemo(() => {
if (threadIdFromPath !== "new" || searchParams.get("mode") !== "skill") { if (threadIdFromPath !== "new" || searchParams.get("mode") !== "skill") {
return undefined; return undefined;
@@ -19,16 +23,21 @@ export function useSpecificChatMode() {
return t.inputBox.createSkillPrompt; return t.inputBox.createSkillPrompt;
}, [threadIdFromPath, searchParams, t.inputBox.createSkillPrompt]); }, [threadIdFromPath, searchParams, t.inputBox.createSkillPrompt]);
const lastInitialValueRef = useRef<string | undefined>(undefined); const lastInitialValueRef = useRef<string | undefined>(undefined);
const setInputRef = useRef(promptInputController.textInput.setInput); const setInputRef = useRef<((value: string) => void) | undefined>(
setInputRef.current = promptInputController.textInput.setInput; promptInputController?.textInput.setInput,
);
setInputRef.current = promptInputController?.textInput.setInput;
useEffect(() => { useEffect(() => {
if ( if (
inputInitialValue && inputInitialValue &&
inputInitialValue !== lastInitialValueRef.current inputInitialValue !== lastInitialValueRef.current
) { ) {
lastInitialValueRef.current = inputInitialValue; lastInitialValueRef.current = inputInitialValue;
setTimeout(() => { setTimeout(() => {
setInputRef.current(inputInitialValue); const setInput = setInputRef.current;
if (!setInput) return;
setInput(inputInitialValue);
const textarea = document.querySelector("textarea"); const textarea = document.querySelector("textarea");
if (textarea) { if (textarea) {
textarea.focus(); textarea.focus();