fix(web): 移除 ChatContainer 的 key remount,修复新建话题列表刷新延迟
话题切换时 key={selectedTopic} 导致整个 ChatContainer 子树卸载重建,
触发 ExpertSelector/ModelSelector 重新挂载并发起 3 次冗余 HTTP 请求
(getSelectedExpert/listModelOptions/模型选择刷新),引入 200-500ms 延迟。
改用 topicId prop 透传话题 ID:
- App.tsx: 移除 key,viewKey 纳入 selectedTopic 保持各话题独立滚动位置
- ChatContainer: 新增 topicId prop 透传给 MessageInput
- MessageInput: 监听 topicId 变化清空草稿,替代原 key remount 重置机制
This commit is contained in:
parent
3910bc324b
commit
a9ce308c05
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1728,7 +1728,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "picobot"
|
name = "picobot"
|
||||||
version = "0.3.3"
|
version = "0.3.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## [0.3.5] - 2026-08-12
|
## [0.3.5] - 2026-08-12
|
||||||
|
|
||||||
较 [0.3.4] 的 4 个 commit 迭代,聚焦 **可观测性**、**并发性能** 与 **外部集成健壮性** 三大方向。
|
较 [0.3.4] 的 5 个 commit 迭代,聚焦 **可观测性**、**并发性能** 与 **外部集成健壮性** 三大方向。
|
||||||
|
|
||||||
### 新增功能
|
### 新增功能
|
||||||
|
|
||||||
@ -36,6 +36,13 @@
|
|||||||
- 清空输入框时回退到默认值 300 而非 0(0 表示不超时,与用户预期不符)。
|
- 清空输入框时回退到默认值 300 而非 0(0 表示不超时,与用户预期不符)。
|
||||||
- 新增两个 tokio 测试覆盖有超时和无超时(error pass-through)路径。
|
- 新增两个 tokio 测试覆盖有超时和无超时(error pass-through)路径。
|
||||||
|
|
||||||
|
#### 移除 ChatContainer key remount 修复新建话题列表刷新延迟
|
||||||
|
- 话题切换时 `key={selectedTopic}` 导致整个 `ChatContainer` 子树卸载重建,触发 `ExpertSelector` / `ModelSelector` 重新挂载并发起 3 次冗余 HTTP 请求(`getSelectedExpert` / `listModelOptions` / 模型选择刷新),引入 200-500ms 延迟。
|
||||||
|
- 改用 `topicId` prop 透传话题 ID:
|
||||||
|
- `App.tsx`:移除 key,`viewKey` 纳入 `selectedTopic` 保持各话题独立滚动位置。
|
||||||
|
- `ChatContainer`:新增 `topicId` prop 透传给 `MessageInput`。
|
||||||
|
- `MessageInput`:监听 `topicId` 变化清空草稿,替代原 key remount 重置机制。
|
||||||
|
|
||||||
### 测试
|
### 测试
|
||||||
- MCP 超时路径新增 2 个 tokio 单元测试。
|
- MCP 超时路径新增 2 个 tokio 单元测试。
|
||||||
|
|
||||||
|
|||||||
@ -611,8 +611,8 @@ function App() {
|
|||||||
const viewKey = useMemo(() => {
|
const viewKey = useMemo(() => {
|
||||||
if (schedulerView) return `scheduler:${schedulerView.jobId}`;
|
if (schedulerView) return `scheduler:${schedulerView.jobId}`;
|
||||||
if (subAgentView) return `subagent:${subAgentView.taskId}`;
|
if (subAgentView) return `subagent:${subAgentView.taskId}`;
|
||||||
return 'main';
|
return `main:${selectedTopic ?? ''}`;
|
||||||
}, [schedulerView, subAgentView]);
|
}, [schedulerView, subAgentView, selectedTopic]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen flex-col bg-[var(--bg-primary)] text-[var(--text-primary)] overflow-hidden">
|
<div className="flex h-screen flex-col bg-[var(--bg-primary)] text-[var(--text-primary)] overflow-hidden">
|
||||||
@ -906,9 +906,9 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
<div className="flex-1 min-h-0">
|
<div className="flex-1 min-h-0">
|
||||||
<ChatContainer
|
<ChatContainer
|
||||||
key={selectedTopic ?? 'no-topic'}
|
|
||||||
messages={chatMessages}
|
messages={chatMessages}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
topicId={selectedTopic}
|
||||||
isReadOnly={subAgentView || schedulerView ? true : isReadOnly}
|
isReadOnly={subAgentView || schedulerView ? true : isReadOnly}
|
||||||
channelName={
|
channelName={
|
||||||
schedulerView
|
schedulerView
|
||||||
|
|||||||
@ -24,6 +24,8 @@ interface ChatContainerProps {
|
|||||||
onOpenSettings?: () => void;
|
onOpenSettings?: () => void;
|
||||||
/** 设置弹窗关闭信号(每次关闭递增,用于触发 ExpertSelector 刷新) */
|
/** 设置弹窗关闭信号(每次关闭递增,用于触发 ExpertSelector 刷新) */
|
||||||
settingsClosedTick?: number;
|
settingsClosedTick?: number;
|
||||||
|
/** 当前话题 ID,用于切换话题时清空输入框草稿 */
|
||||||
|
topicId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatContainer({
|
export function ChatContainer({
|
||||||
@ -40,6 +42,7 @@ export function ChatContainer({
|
|||||||
sessionId,
|
sessionId,
|
||||||
onOpenSettings,
|
onOpenSettings,
|
||||||
settingsClosedTick,
|
settingsClosedTick,
|
||||||
|
topicId,
|
||||||
}: ChatContainerProps) {
|
}: ChatContainerProps) {
|
||||||
const [selectedExpert, setSelectedExpert] = useState<{
|
const [selectedExpert, setSelectedExpert] = useState<{
|
||||||
name: string;
|
name: string;
|
||||||
@ -74,6 +77,7 @@ export function ChatContainer({
|
|||||||
isReadOnly={isReadOnly}
|
isReadOnly={isReadOnly}
|
||||||
channelName={channelName}
|
channelName={channelName}
|
||||||
selectedExpert={selectedExpert}
|
selectedExpert={selectedExpert}
|
||||||
|
topicId={topicId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -25,6 +25,8 @@ interface MessageInputProps {
|
|||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
channelName?: string;
|
channelName?: string;
|
||||||
selectedExpert?: { name: string; description: string } | null;
|
selectedExpert?: { name: string; description: string } | null;
|
||||||
|
/** 当前话题 ID,切换话题时自动清空草稿 */
|
||||||
|
topicId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileAttachment {
|
interface FileAttachment {
|
||||||
@ -51,6 +53,7 @@ export function MessageInput({
|
|||||||
isReadOnly = false,
|
isReadOnly = false,
|
||||||
channelName,
|
channelName,
|
||||||
selectedExpert,
|
selectedExpert,
|
||||||
|
topicId,
|
||||||
}: MessageInputProps) {
|
}: MessageInputProps) {
|
||||||
const effectivePlaceholder =
|
const effectivePlaceholder =
|
||||||
placeholder ??
|
placeholder ??
|
||||||
@ -62,6 +65,16 @@ export function MessageInput({
|
|||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const wasLoadingRef = useRef(false);
|
const wasLoadingRef = useRef(false);
|
||||||
|
const prevTopicIdRef = useRef<string | null | undefined>(topicId);
|
||||||
|
|
||||||
|
// 切换话题时清空草稿(替代原来通过 key remount 的重置机制)
|
||||||
|
useEffect(() => {
|
||||||
|
if (prevTopicIdRef.current !== topicId) {
|
||||||
|
prevTopicIdRef.current = topicId;
|
||||||
|
setContent('');
|
||||||
|
setAttachments([]);
|
||||||
|
}
|
||||||
|
}, [topicId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const textarea = textareaRef.current;
|
const textarea = textareaRef.current;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user