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,
Children,
type ClipboardEventHandler,
type ComponentProps,
createContext,
type FormEvent,
type FormEventHandler,
Fragment,
type HTMLAttributes,
type ComponentProps,
createContext,
type FormEvent,
type FormEventHandler,
type HTMLAttributes,
type KeyboardEventHandler,
type PropsWithChildren,
type ReactNode,
@@ -124,9 +123,11 @@ export const usePromptInputController = () => {
return ctx;
};
// Optional variants (do NOT throw). Useful for dual-mode components.
const useOptionalPromptInputController = () =>
useContext(PromptInputController);
// Optional variants (do NOT throw). Useful for dual-mode components and for
// Fast Refresh transitions where a page can briefly retain the previous
// provider context instance.
export const useOptionalPromptInputController = () =>
useContext(PromptInputController);
export const useProviderAttachments = () => {
const ctx = useContext(ProviderAttachmentsContext);
@@ -403,14 +404,14 @@ export function PromptInputAttachments({
return (
<div
className={cn("flex w-full flex-wrap items-center gap-2 p-3", className)}
{...props}
>
{attachments.files.map((file) => (
<Fragment key={file.id}>
<div className="max-w-60">{children(file)}</div>
</Fragment>
))}
</div>
{...props}
>
{attachments.files.map((file) => (
<div key={file.id} className="max-w-60">
{children(file)}
</div>
))}
</div>
);
}
@@ -1,7 +1,7 @@
import { useParams, useSearchParams } from "next/navigation";
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";
/**
@@ -11,7 +11,11 @@ export function useSpecificChatMode() {
const { t } = useI18n();
const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>();
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(() => {
if (threadIdFromPath !== "new" || searchParams.get("mode") !== "skill") {
return undefined;
@@ -19,16 +23,21 @@ export function useSpecificChatMode() {
return t.inputBox.createSkillPrompt;
}, [threadIdFromPath, searchParams, t.inputBox.createSkillPrompt]);
const lastInitialValueRef = useRef<string | undefined>(undefined);
const setInputRef = useRef(promptInputController.textInput.setInput);
setInputRef.current = promptInputController.textInput.setInput;
const setInputRef = useRef<((value: string) => void) | undefined>(
promptInputController?.textInput.setInput,
);
setInputRef.current = promptInputController?.textInput.setInput;
useEffect(() => {
if (
inputInitialValue &&
inputInitialValue !== lastInitialValueRef.current
) {
lastInitialValueRef.current = inputInitialValue;
setTimeout(() => {
setInputRef.current(inputInitialValue);
setTimeout(() => {
const setInput = setInputRef.current;
if (!setInput) return;
setInput(inputInitialValue);
const textarea = document.querySelector("textarea");
if (textarea) {
textarea.focus();