This commit is contained in:
Timothy Jaeryang Baek committed 2026-07-23 02:55:06 -04:00
1 parent cf887b68ea
commit 73421c5b42
8 files changed
+103 -21

No files matched your search

+41
View File
@@ -920,6 +920,47 @@ export const cloneChatById = async (token: string, id: string, title?: string) =
return res; return res;
}; };
export const forkChatById = async (token: string, id: string, messageId?: string | null) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/fork`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({
message_id: messageId ?? null
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err;
if ('detail' in err) {
error = err.detail;
} else {
error = err;
}
console.error(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const cloneSharedChatById = async (token: string, id: string) => { export const cloneSharedChatById = async (token: string, id: string) => {
let error = null; let error = null;
+7 -9
View File
@@ -70,10 +70,10 @@
import { import {
archiveChatById, archiveChatById,
cloneChatById,
compactChatById, compactChatById,
createNewChat, createNewChat,
deleteChatById, deleteChatById,
forkChatById,
getAllTags, getAllTags,
getChatById, getChatById,
getTagsById, getTagsById,
@@ -1884,6 +1884,9 @@
(chatContent?.history ?? undefined) !== undefined (chatContent?.history ?? undefined) !== undefined
? chatContent.history ? chatContent.history
: convertMessagesToHistory(chatContent.messages); : convertMessagesToHistory(chatContent.messages);
if (chat?.current_message_id && history?.messages?.[chat.current_message_id]) {
history.currentId = chat.current_message_id;
}
// Sanitize history: repair orphaned references and structurally-malformed // Sanitize history: repair orphaned references and structurally-malformed
// nodes from failed regenerations (#24424, #24157, #20474) // nodes from failed regenerations (#24424, #24157, #20474)
@@ -2466,7 +2469,7 @@
document.getElementById('chat-input')?.focus(); document.getElementById('chat-input')?.focus();
}; };
const handleForkChat = async () => { const handleForkChat = async (messageId: string | null = null) => {
if (!$chatId || !history?.currentId) { if (!$chatId || !history?.currentId) {
toast.message($i18n.t('No chat to fork')); toast.message($i18n.t('No chat to fork'));
return; return;
@@ -2489,13 +2492,7 @@
const toastId = toast.loading($i18n.t('Forking chat...')); const toastId = toast.loading($i18n.t('Forking chat...'));
try { try {
const result = await cloneChatById( const result = await forkChatById(localStorage.token, $chatId, messageId ?? history.currentId);
localStorage.token,
$chatId,
$i18n.t('Clone of {{TITLE}}', {
TITLE: $chatTitle || 'Chat'
})
);
if (result?.id) { if (result?.id) {
if (!embedded) { if (!embedded) {
@@ -3725,6 +3722,7 @@
{mergeResponses} {mergeResponses}
{chatActionHandler} {chatActionHandler}
{addMessages} {addMessages}
forkHandler={generating || taskIds?.length ? null : handleForkChat}
topPadding={!embedded} topPadding={!embedded}
bottomPadding={files.length > 0} bottomPadding={files.length > 0}
{onSelect} {onSelect}
@@ -183,10 +183,10 @@
</button> </button>
</Tooltip> </Tooltip>
{:else if item.data.id === 'fork'} {:else if item.data.id === 'fork'}
<Tooltip content="Clone this chat into a new copy." placement="top"> <Tooltip content="Fork the current chat branch into a new chat." placement="top">
<button <button
type="button" type="button"
aria-label="Fork: clone this chat into a new copy." aria-label="Fork: fork the current chat branch into a new chat."
class="slash-command-row flex items-center gap-2 w-full h-6 px-2 rounded-xl text-xs text-left transition-colors duration-75 class="slash-command-row flex items-center gap-2 w-full h-6 px-2 rounded-xl text-xs text-left transition-colors duration-75
{commandIdx === selectedIdx ? 'app-interactive-active' : ''} disabled:opacity-50" {commandIdx === selectedIdx ? 'app-interactive-active' : ''} disabled:opacity-50"
disabled={forkDisabled} disabled={forkDisabled}
@@ -222,7 +222,7 @@
</span> </span>
<span class="flex-1 min-w-0 flex items-baseline gap-1.5 overflow-hidden"> <span class="flex-1 min-w-0 flex items-baseline gap-1.5 overflow-hidden">
<span class="truncate">Fork</span> <span class="truncate">Fork</span>
<span class="app-muted text-[0.625rem] truncate shrink-0">Clone chat</span> <span class="app-muted text-[0.625rem] truncate shrink-0">Current branch</span>
</span> </span>
</button> </button>
</Tooltip> </Tooltip>
+2
View File
@@ -40,6 +40,7 @@
export let showMessage: Function = () => {}; export let showMessage: Function = () => {};
export let submitMessage: Function = () => {}; export let submitMessage: Function = () => {};
export let addMessages: Function = () => {}; export let addMessages: Function = () => {};
export let forkHandler: Function | null = null;
export let readOnly = false; export let readOnly = false;
export let editCodeBlock = true; export let editCodeBlock = true;
@@ -539,6 +540,7 @@
{continueResponse} {continueResponse}
{mergeResponses} {mergeResponses}
{addMessages} {addMessages}
{forkHandler}
{triggerScroll} {triggerScroll}
{readOnly} {readOnly}
{editCodeBlock} {editCodeBlock}
@@ -39,6 +39,7 @@
export let mergeResponses; export let mergeResponses;
export let addMessages; export let addMessages;
export let forkHandler: Function | null = null;
export let triggerScroll; export let triggerScroll;
export let readOnly = false; export let readOnly = false;
export let editCodeBlock = true; export let editCodeBlock = true;
@@ -97,6 +98,7 @@
{continueResponse} {continueResponse}
{regenerateResponse} {regenerateResponse}
{addMessages} {addMessages}
{forkHandler}
{readOnly} {readOnly}
{editCodeBlock} {editCodeBlock}
{topPadding} {topPadding}
@@ -122,6 +124,7 @@
{mergeResponses} {mergeResponses}
{triggerScroll} {triggerScroll}
{addMessages} {addMessages}
{forkHandler}
{readOnly} {readOnly}
{editCodeBlock} {editCodeBlock}
{topPadding} {topPadding}
@@ -45,6 +45,7 @@
export let mergeResponses: Function; export let mergeResponses: Function;
export let addMessages: Function; export let addMessages: Function;
export let forkHandler: Function | null = null;
export let triggerScroll: Function; export let triggerScroll: Function;
@@ -319,6 +320,7 @@
groupedMessageIds[selectedModelIdx].messageIds.length - 1; groupedMessageIds[selectedModelIdx].messageIds.length - 1;
}} }}
{addMessages} {addMessages}
{forkHandler}
{readOnly} {readOnly}
{topPadding} {topPadding}
{onInsertToNote} {onInsertToNote}
@@ -376,6 +378,7 @@
groupedMessageIds[modelIdx].messageIds.length - 1; groupedMessageIds[modelIdx].messageIds.length - 1;
}} }}
{addMessages} {addMessages}
{forkHandler}
{readOnly} {readOnly}
{editCodeBlock} {editCodeBlock}
{topPadding} {topPadding}
@@ -159,6 +159,7 @@
export let submitMessage: Function; export let submitMessage: Function;
export let continueResponse: Function; export let continueResponse: Function;
export let regenerateResponse: Function; export let regenerateResponse: Function;
export let forkHandler: Function | null = null;
export let addMessages: Function; export let addMessages: Function;
@@ -1054,6 +1055,37 @@
</button> </button>
</Tooltip> </Tooltip>
{#if message.done && !readOnly && forkHandler}
<Tooltip content="Fork chat" placement="bottom">
<button
aria-label="Fork chat"
class="{isLastMessage || ($settings?.highContrastMode ?? false)
? 'visible'
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
on:click={() => {
forkHandler?.(message.id);
}}
>
<svg
class="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M4 12H9" />
<path d="M9 12C12.5 12 12.5 7 16 7H20" />
<path d="M17 4L20 7L17 10" />
<path d="M9 12C12.5 12 12.5 17 16 17H20" />
<path d="M17 14L20 17L17 20" />
</svg>
</button>
</Tooltip>
{/if}
{#if onInsertToNote && visibleResponseContent} {#if onInsertToNote && visibleResponseContent}
<Tooltip content={$i18n.t('Insert into note')} placement="bottom"> <Tooltip content={$i18n.t('Insert into note')} placement="bottom">
<button <button
+12 -9
View File
@@ -203,25 +203,28 @@ export const convertMessagesToHistory = (messages) => {
let messageId = null; let messageId = null;
for (const message of messages) { for (const message of messages) {
messageId = uuidv4(); messageId = message?.id ?? uuidv4();
const parentId = message?.parentId ?? parentMessageId;
if (parentMessageId !== null) {
history.messages[parentMessageId].childrenIds = [
...history.messages[parentMessageId].childrenIds,
messageId
];
}
history.messages[messageId] = { history.messages[messageId] = {
...message, ...message,
id: messageId, id: messageId,
parentId: parentMessageId, parentId,
childrenIds: [] childrenIds: []
}; };
parentMessageId = messageId; parentMessageId = messageId;
} }
for (const message of Object.values(history.messages)) {
if (message.parentId && history.messages[message.parentId]) {
history.messages[message.parentId].childrenIds = [
...history.messages[message.parentId].childrenIds,
message.id
];
}
}
history.currentId = messageId; history.currentId = messageId;
return history; return history;
}; };