diff --git a/src/web/src/components/InstanceView/Composer.tsx b/src/web/src/components/InstanceView/Composer.tsx index 22181b9..dede698 100644 --- a/src/web/src/components/InstanceView/Composer.tsx +++ b/src/web/src/components/InstanceView/Composer.tsx @@ -1,6 +1,12 @@ // 消息输入区:textarea + 图片上传/粘贴/预览 + 工具栏 + 状态指示 + 发送/停止 -import { useRef, useLayoutEffect, useCallback, useState } from "react"; +import { + useRef, + useLayoutEffect, + useCallback, + useState, + useImperativeHandle, +} from "react"; import { ArrowUp, Square, Minimize2, ImagePlus, X } from "lucide-react"; import type { InstanceInfo, @@ -18,7 +24,13 @@ import { ThinkingSelector } from "../ui/ThinkingSelector"; import { CompactModal } from "../ui/CompactModal"; import { useTranslation } from "react-i18next"; +/** Composer 暴露给父组件的命令式 API */ +export interface ComposerHandle { + focus(): void; +} + interface ComposerProps { + ref?: React.Ref; instance: InstanceInfo | undefined; draft: InputDraft; onDraftChange: (value: InputDraftUpdater) => void; @@ -35,6 +47,7 @@ interface ComposerProps { } export function Composer({ + ref, instance, draft, onDraftChange, @@ -51,6 +64,19 @@ export function Composer({ }: ComposerProps) { const { t } = useTranslation(); const textareaRef = useRef(null); + + // 暴露 focus 方法给父组件 + useImperativeHandle(ref, () => ({ + focus() { + const el = textareaRef.current; + if (!el) return; + el.focus(); + // 光标移到末尾 + 滚动到底部 + const len = el.value.length; + el.selectionStart = el.selectionEnd = len; + el.scrollTop = el.scrollHeight; + }, + })); const fileInputRef = useRef(null); const draftRef = useRef(draft); draftRef.current = draft; diff --git a/src/web/src/components/InstanceView/index.tsx b/src/web/src/components/InstanceView/index.tsx index c9fc614..50f8ba5 100644 --- a/src/web/src/components/InstanceView/index.tsx +++ b/src/web/src/components/InstanceView/index.tsx @@ -20,7 +20,7 @@ import { MobileNavBar } from "../ui/MobileNavBar"; import { InstanceHeader } from "../ui/InstanceHeader"; import { SessionPopover } from "../ui/SessionPopover"; import { useTranslation } from "react-i18next"; -import { Composer } from "./Composer"; +import { Composer, type ComposerHandle } from "./Composer"; import { useConversation } from "./useConversation"; import { useScrollAnchor } from "./useScrollAnchor"; import { useReEdit } from "./useReEdit"; @@ -134,7 +134,10 @@ export function InstanceView() { ); // ── 重新编辑 ── - const onReEdit = useReEdit(instanceId, entries, instance?.status); + const composerRef = useRef(null); + const onReEdit = useReEdit(instanceId, entries, instance?.status, () => { + composerRef.current?.focus(); + }); // ── 滚动管理 ── const scrollRef = useRef(null); @@ -354,6 +357,7 @@ export function InstanceView() { >
void, ): (() => void) | undefined { const { t } = useTranslation(); const setDraft = useDrafts((s) => s.setDraft); @@ -69,8 +71,11 @@ export function useReEdit( // 从 user message content 提取 text 和 images const newDraft = extractDraftFromContent(userContent); - setDraft(instanceId, newDraft); - }, [canReEdit, userContent, instanceId, setDraft, t]); + flushSync(() => { + setDraft(instanceId, newDraft); + }); + onComplete?.(); + }, [canReEdit, userContent, instanceId, setDraft, t, onComplete]); return canReEdit ? handleReEdit : undefined; }