feat(webui): add assistant reply fork-from-here
This commit is contained in:
committed by
Xubin Ren
parent
4a58b83acc
commit
03bca4c0a9
@@ -77,6 +77,7 @@ interface ThreadShellProps {
|
||||
onGoHome?: () => void;
|
||||
onNewChat?: () => void;
|
||||
onCreateChat?: (workspaceScope?: WorkspaceScopePayload | null) => Promise<string | null>;
|
||||
onForkChat?: (sourceChatId: string, beforeUserIndex: number) => Promise<string | null>;
|
||||
onTurnEnd?: () => void;
|
||||
theme?: "light" | "dark";
|
||||
onToggleTheme?: () => void;
|
||||
@@ -226,6 +227,7 @@ export function ThreadShell({
|
||||
title,
|
||||
onToggleSidebar,
|
||||
onCreateChat,
|
||||
onForkChat,
|
||||
onTurnEnd,
|
||||
theme = "light",
|
||||
onToggleTheme = () => {},
|
||||
@@ -275,6 +277,8 @@ export function ThreadShell({
|
||||
const [filePreviewPath, setFilePreviewPath] = useState<string | null>(null);
|
||||
const [filePreviewClosing, setFilePreviewClosing] = useState(false);
|
||||
const [filePreviewWidth, setFilePreviewWidth] = useState(FILE_PREVIEW_DEFAULT_WIDTH);
|
||||
const [forkError, setForkError] = useState<string | null>(null);
|
||||
const [forkHydratingChatId, setForkHydratingChatId] = useState<string | null>(null);
|
||||
const shellRef = useRef<HTMLElement | null>(null);
|
||||
const filePreviewWidthRef = useRef(FILE_PREVIEW_DEFAULT_WIDTH);
|
||||
const filePreviewCloseTimerRef = useRef<number | null>(null);
|
||||
@@ -283,6 +287,7 @@ export function ThreadShell({
|
||||
const messageCacheRef = useRef<Map<string, UIMessage[]>>(new Map());
|
||||
/** Last chatId we associated with the in-memory thread (for cache-on-switch). */
|
||||
const prevChatIdForCacheRef = useRef<string | null>(null);
|
||||
const prevChatIdForComposerRef = useRef<string | null>(chatId);
|
||||
/** Skip one message-cache write right after chatId changes (messages may not match yet). */
|
||||
const skipLayoutCacheRef = useRef(false);
|
||||
const appliedHistoryVersionRef = useRef<Map<string, number>>(new Map());
|
||||
@@ -334,6 +339,12 @@ export function ThreadShell({
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (prevChatIdForComposerRef.current === chatId) return;
|
||||
prevChatIdForComposerRef.current = chatId;
|
||||
setForkError(null);
|
||||
}, [chatId]);
|
||||
|
||||
const displayMessages = useMemo(() => projectWebuiThreadMessages(messages), [messages]);
|
||||
|
||||
const showHeroComposer = messages.length === 0 && !loading;
|
||||
@@ -443,6 +454,12 @@ export function ThreadShell({
|
||||
setMessages(projectWebuiThreadMessages(historical));
|
||||
}, [chatId, historical, setMessages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chatId || loading || forkHydratingChatId !== chatId) return;
|
||||
setForkHydratingChatId(null);
|
||||
setScrollToBottomSignal((value) => value + 1);
|
||||
}, [chatId, forkHydratingChatId, loading]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (chatId) {
|
||||
const prev = prevChatIdForCacheRef.current;
|
||||
@@ -521,6 +538,7 @@ export function ThreadShell({
|
||||
|
||||
const handleThreadSend = useCallback(
|
||||
(content: string, images?: SendImage[], options?: SendOptions) => {
|
||||
setForkError(null);
|
||||
setScrollToBottomSignal((value) => value + 1);
|
||||
send(content, images, withWorkspaceScope(options));
|
||||
},
|
||||
@@ -615,6 +633,26 @@ export function ThreadShell({
|
||||
};
|
||||
}, [filePreviewPath]);
|
||||
|
||||
const handleForkFromMessage = useCallback(
|
||||
async (beforeUserIndex: number) => {
|
||||
if (!chatId || !onForkChat) return;
|
||||
setForkError(null);
|
||||
const forkedChatId = await onForkChat(chatId, beforeUserIndex);
|
||||
if (!forkedChatId) {
|
||||
setForkError(t("thread.fork.failed", {
|
||||
defaultValue: "Could not fork this chat. Try again.",
|
||||
}));
|
||||
return;
|
||||
}
|
||||
messageCacheRef.current.delete(forkedChatId);
|
||||
appliedHistoryVersionRef.current.delete(forkedChatId);
|
||||
pendingCanonicalHydrateRef.current.add(forkedChatId);
|
||||
setForkHydratingChatId(forkedChatId);
|
||||
setForkError(null);
|
||||
},
|
||||
[chatId, onForkChat, t],
|
||||
);
|
||||
|
||||
const composer = (
|
||||
<>
|
||||
{streamError ? (
|
||||
@@ -626,7 +664,7 @@ export function ThreadShell({
|
||||
{session ? (
|
||||
<ThreadComposer
|
||||
onSend={handleThreadSend}
|
||||
disabled={!chatId}
|
||||
disabled={!chatId || forkHydratingChatId === chatId}
|
||||
isStreaming={isStreaming}
|
||||
placeholder={
|
||||
showHeroComposer
|
||||
@@ -653,6 +691,7 @@ export function ThreadShell({
|
||||
workspaceError={workspaceError}
|
||||
onWorkspaceScopeChange={onWorkspaceScopeChange}
|
||||
pendingQueueKey={chatId}
|
||||
externalError={forkError}
|
||||
/>
|
||||
) : (
|
||||
<ThreadComposer
|
||||
@@ -736,7 +775,9 @@ export function ThreadShell({
|
||||
showScrollToBottomButton={!!session}
|
||||
cliApps={cliApps}
|
||||
mcpPresets={mcpPresets}
|
||||
allMessages={displayMessages}
|
||||
onOpenFilePreview={historyKey ? handleOpenFilePreview : undefined}
|
||||
onForkFromMessage={onForkChat ? handleForkFromMessage : undefined}
|
||||
/>
|
||||
</div>
|
||||
{filePreviewPath && historyKey ? (
|
||||
|
||||
Reference in New Issue
Block a user