Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion src/web/src/components/InstanceView/Composer.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<ComposerHandle>;
instance: InstanceInfo | undefined;
draft: InputDraft;
onDraftChange: (value: InputDraftUpdater) => void;
Expand All @@ -35,6 +47,7 @@ interface ComposerProps {
}

export function Composer({
ref,
instance,
draft,
onDraftChange,
Expand All @@ -51,6 +64,19 @@ export function Composer({
}: ComposerProps) {
const { t } = useTranslation();
const textareaRef = useRef<HTMLTextAreaElement>(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<HTMLInputElement>(null);
const draftRef = useRef(draft);
draftRef.current = draft;
Expand Down
8 changes: 6 additions & 2 deletions src/web/src/components/InstanceView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -134,7 +134,10 @@ export function InstanceView() {
);

// ── 重新编辑 ──
const onReEdit = useReEdit(instanceId, entries, instance?.status);
const composerRef = useRef<ComposerHandle>(null);
const onReEdit = useReEdit(instanceId, entries, instance?.status, () => {
composerRef.current?.focus();
});

// ── 滚动管理 ──
const scrollRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -354,6 +357,7 @@ export function InstanceView() {
>
<div className="pointer-events-auto mx-auto w-full max-w-[920px]">
<Composer
ref={composerRef}
instance={instance}
draft={draft}
onDraftChange={handleDraftChange}
Expand Down
9 changes: 7 additions & 2 deletions src/web/src/components/InstanceView/useReEdit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// 行为:提取 text + images → 检查 draft 是否为空 → 设置 draft 或 toast 提示

import { useCallback, useMemo } from "react";
import { flushSync } from "react-dom";
import type {
InstanceId,
InstanceStatus,
Expand All @@ -24,6 +25,7 @@ export function useReEdit(
instanceId: InstanceId,
entries: SessionEntry[],
instanceStatus: InstanceStatus | undefined,
onComplete?: () => void,
): (() => void) | undefined {
const { t } = useTranslation();
const setDraft = useDrafts((s) => s.setDraft);
Expand Down Expand Up @@ -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;
}
Expand Down