修复「新对话」流式中无法重置 + 参考文献开关默认关闭并加对勾

- 流式回答过程中点「新对话」:URL 同路径导致 react-router navigate 静默 no-op,
  下游 hook 收不到事件;改为显式 navigate(..., {replace:true, state:{resetAt}})
  保证每次点击刷新 location.key,并让 useThreadChat 监听 location.key 触发重置
- useThreadStream 在 threadId 变化时 stop() 掉上一条 SSE,避免旧流继续派发事件
- 引入 KeyedChatPage 用 location.key 作 ChatRuntime key,真正的导航触发整树 remount,
  解决 useStream 内部 stream.values 残留导致欢迎页底下还能看到旧消息的过渡帧问题
- 「参考文献」开关默认关闭(local.ts),开启态在文字前额外多一个 ✓ 对勾
This commit is contained in:
吉鑫升 committed 2026-05-25 19:47:55 +08:00
1 parent a800baf8ae
commit b7aa8b06ca
6 files changed
+120 -62

No files matched your search

+27 -9
View File
@@ -12,8 +12,8 @@ import { apiFetch } from "../api";
import { getBackendBaseURL } from "../config";
import { useI18n } from "../i18n/hooks";
import type { FileInMessage } from "../messages/utils";
import type { LocalSettings } from "../settings";
import { useLocalSettings } from "../settings";
import type { LocalSettings } from "../settings";
import { useLocalSettings } from "../settings";
import { useUpdateSubtask } from "../tasks/context";
import type { UploadedFileInfo } from "../uploads";
import { promptInputFilePartToFile, uploadFiles } from "../uploads";
@@ -146,8 +146,8 @@ export function useThreadStream({
onFinish,
onToolEnd,
}: ThreadStreamOptions) {
const { t } = useI18n();
const [localSettings] = useLocalSettings();
const { t } = useI18n();
const [localSettings] = useLocalSettings();
// Track the thread ID that is currently streaming to handle thread changes during streaming
const [onStreamThreadId, setOnStreamThreadId] = useState(() => threadId);
// Ref to track current thread ID across async callbacks without causing re-renders,
@@ -309,9 +309,27 @@ export function useThreadStream({
// Track message count before sending so we know when server has responded
const prevMsgCountRef = useRef(thread.messages.length);
// Keep a ref to the latest `thread` so the threadId-change effect can stop
// an in-flight stream without re-running on every render that produces a
// new `thread` identity.
const threadStreamRef = useRef(thread);
threadStreamRef.current = thread;
// Reset thread-local pending UI state when switching between threads so
// optimistic messages and in-flight guards do not leak across chat views.
// Also abort any in-flight SSE from the previous thread: without this,
// clicking "新对话" mid-stream leaves the old run firing onUpdate events
// against the now-detached thread context, which manifests as the page
// appearing unresponsive to the navigation.
useEffect(() => {
const prevThread = threadStreamRef.current;
if (prevThread?.isLoading) {
try {
prevThread.stop();
} catch {
// best-effort; nothing useful to do if the SDK fails to abort
}
}
startedRef.current = false;
sendInFlightRef.current = false;
prevMsgCountRef.current = 0;
@@ -508,9 +526,9 @@ export function useThreadStream({
: context.mode === "thinking"
? "low"
: undefined),
memory_injection_enabled: context.memory_injection_enabled ?? true,
rag_mode_enabled: localSettings.display.ragModeEnabled,
thread_id: threadId,
memory_injection_enabled: context.memory_injection_enabled ?? true,
rag_mode_enabled: localSettings.display.ragModeEnabled,
thread_id: threadId,
},
},
);
@@ -523,8 +541,8 @@ export function useThreadStream({
sendInFlightRef.current = false;
}
},
[thread, _handleOnStart, t.uploads.uploadingFiles, context, localSettings.display.ragModeEnabled, queryClient],
);
[thread, _handleOnStart, t.uploads.uploadingFiles, context, localSettings.display.ragModeEnabled, queryClient],
);
// Merge thread with optimistic messages for display
const mergedThread =