Merge pull request #96 from ollama-webui/dev

feat: improved chat history support (response regeneration history)
This commit is contained in:
Timothy Jaeryang Baek 2023-11-12 13:30:07 -05:00 committed by GitHub
commit 4e608a9989
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 421 additions and 137 deletions

View file

@ -749,7 +749,7 @@
<div class=" space-y-3"> <div class=" space-y-3">
<div> <div>
<div class=" py-1 flex w-full justify-between"> <div class=" py-1 flex w-full justify-between">
<div class=" self-center text-sm font-medium">Speech Auto-Send</div> <div class=" self-center text-sm font-medium">Voice Input Auto-Send</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"

View file

@ -8,7 +8,7 @@ export const API_BASE_URL =
: `http://localhost:11434/api` : `http://localhost:11434/api`
: PUBLIC_API_BASE_URL; : PUBLIC_API_BASE_URL;
export const WEB_UI_VERSION = 'v1.0.0-alpha.2'; export const WEB_UI_VERSION = 'v1.0.0-alpha.3';
// Source: https://kit.svelte.dev/docs/modules#$env-static-public // Source: https://kit.svelte.dev/docs/modules#$env-static-public
// This feature, akin to $env/static/private, exclusively incorporates environment variables // This feature, akin to $env/static/private, exclusively incorporates environment variables

View file

@ -39,6 +39,22 @@
let title = ''; let title = '';
let prompt = ''; let prompt = '';
let messages = []; let messages = [];
let history = {
messages: {},
currentId: null
};
$: if (history.currentId !== null) {
let _messages = [];
let currentMessage = history.messages[history.currentId];
while (currentMessage !== null) {
_messages.unshift({ ...currentMessage });
currentMessage =
currentMessage.parentId !== null ? history.messages[currentMessage.parentId] : null;
}
messages = _messages;
}
let showSettings = false; let showSettings = false;
let stopResponseFlag = false; let stopResponseFlag = false;
@ -260,8 +276,13 @@
if (init || messages.length > 0) { if (init || messages.length > 0) {
chatId = uuidv4(); chatId = uuidv4();
autoScroll = true; autoScroll = true;
messages = [];
title = ''; title = '';
messages = [];
history = {
messages: {},
currentId: null
};
settings = JSON.parse(localStorage.getItem('settings') ?? JSON.stringify(settings)); settings = JSON.parse(localStorage.getItem('settings') ?? JSON.stringify(settings));
@ -311,20 +332,59 @@
const loadChat = async (id) => { const loadChat = async (id) => {
const chat = await db.get('chats', id); const chat = await db.get('chats', id);
console.log(chat);
if (chatId !== chat.id) { if (chatId !== chat.id) {
if (chat.messages.length > 0) { if ('history' in chat) {
chat.messages.at(-1).done = true; history = chat.history;
} else {
let _history = {
messages: {},
currentId: null
};
let parentMessageId = null;
let messageId = null;
for (const message of chat.messages) {
messageId = uuidv4();
if (parentMessageId !== null) {
_history.messages[parentMessageId].childrenIds = [
..._history.messages[parentMessageId].childrenIds,
messageId
];
} }
messages = chat.messages;
_history.messages[messageId] = {
...message,
id: messageId,
parentId: parentMessageId,
childrenIds: []
};
parentMessageId = messageId;
}
_history.currentId = messageId;
history = _history;
}
console.log(history);
title = chat.title; title = chat.title;
chatId = chat.id; chatId = chat.id;
selectedModel = chat.model ?? selectedModel; selectedModel = chat.model ?? selectedModel;
settings.system = chat.system ?? settings.system; settings.system = chat.system ?? settings.system;
settings.temperature = chat.temperature ?? settings.temperature; settings.temperature = chat.temperature ?? settings.temperature;
autoScroll = true;
await tick(); await tick();
renderLatex();
if (messages.length > 0) {
history.messages[messages.at(-1).id].done = true;
}
renderLatex();
hljs.highlightAll(); hljs.highlightAll();
createCopyCodeBlockButton(); createCopyCodeBlockButton();
} }
@ -368,7 +428,8 @@
options: chat.options, options: chat.options,
title: chat.title, title: chat.title,
timestamp: chat.timestamp, timestamp: chat.timestamp,
messages: chat.messages messages: chat.messages,
history: chat.history
}); });
} }
chats = await db.getAllFromIndex('chats', 'timestamp'); chats = await db.getAllFromIndex('chats', 'timestamp');
@ -386,35 +447,45 @@
showSettings = true; showSettings = true;
}; };
const editMessage = async (messageIdx) => { const editMessageHandler = async (messageId) => {
messages = messages.map((message, idx) => { // let editMessage = history.messages[messageId];
if (messageIdx === idx) { history.messages[messageId].edit = true;
message.edit = true; history.messages[messageId].editedContent = history.messages[messageId].content;
message.editedContent = message.content;
}
return message;
});
}; };
const confirmEditMessage = async (messageIdx) => { const confirmEditMessage = async (messageId) => {
let userPrompt = messages.at(messageIdx).editedContent; history.messages[messageId].edit = false;
messages.splice(messageIdx, messages.length - messageIdx); let userPrompt = history.messages[messageId].editedContent;
messages = messages; let userMessageId = uuidv4();
await submitPrompt(userPrompt); let userMessage = {
id: userMessageId,
parentId: history.messages[messageId].parentId,
childrenIds: [],
role: 'user',
content: userPrompt
}; };
const cancelEditMessage = (messageIdx) => { let messageParentId = history.messages[messageId].parentId;
messages = messages.map((message, idx) => {
if (messageIdx === idx) {
message.edit = undefined;
message.editedContent = undefined;
}
return message;
});
console.log(messages); if (messageParentId !== null) {
history.messages[messageParentId].childrenIds = [
...history.messages[messageParentId].childrenIds,
userMessageId
];
}
history.messages[userMessageId] = userMessage;
history.currentId = userMessageId;
await tick();
await sendPrompt(userPrompt, userMessageId);
};
const cancelEditMessage = (messageId) => {
history.messages[messageId].edit = false;
history.messages[messageId].editedContent = undefined;
}; };
const rateMessage = async (messageIdx, rating) => { const rateMessage = async (messageIdx, rating) => {
@ -434,12 +505,101 @@
temperature: settings.temperature temperature: settings.temperature
}, },
timestamp: Date.now(), timestamp: Date.now(),
messages: messages messages: messages,
history: history
}); });
console.log(messages); console.log(messages);
}; };
const showPreviousMessage = async (message) => {
if (message.parentId !== null) {
let messageId =
history.messages[message.parentId].childrenIds[
Math.max(history.messages[message.parentId].childrenIds.indexOf(message.id) - 1, 0)
];
if (message.id !== messageId) {
let messageChildrenIds = history.messages[messageId].childrenIds;
while (messageChildrenIds.length !== 0) {
messageId = messageChildrenIds.at(-1);
messageChildrenIds = history.messages[messageId].childrenIds;
}
history.currentId = messageId;
}
} else {
let childrenIds = Object.values(history.messages)
.filter((message) => message.parentId === null)
.map((message) => message.id);
let messageId = childrenIds[Math.max(childrenIds.indexOf(message.id) - 1, 0)];
if (message.id !== messageId) {
let messageChildrenIds = history.messages[messageId].childrenIds;
while (messageChildrenIds.length !== 0) {
messageId = messageChildrenIds.at(-1);
messageChildrenIds = history.messages[messageId].childrenIds;
}
history.currentId = messageId;
}
}
await tick();
renderLatex();
hljs.highlightAll();
createCopyCodeBlockButton();
};
const showNextMessage = async (message) => {
if (message.parentId !== null) {
let messageId =
history.messages[message.parentId].childrenIds[
Math.min(
history.messages[message.parentId].childrenIds.indexOf(message.id) + 1,
history.messages[message.parentId].childrenIds.length - 1
)
];
if (message.id !== messageId) {
let messageChildrenIds = history.messages[messageId].childrenIds;
while (messageChildrenIds.length !== 0) {
messageId = messageChildrenIds.at(-1);
messageChildrenIds = history.messages[messageId].childrenIds;
}
history.currentId = messageId;
}
} else {
let childrenIds = Object.values(history.messages)
.filter((message) => message.parentId === null)
.map((message) => message.id);
let messageId =
childrenIds[Math.min(childrenIds.indexOf(message.id) + 1, childrenIds.length - 1)];
if (message.id !== messageId) {
let messageChildrenIds = history.messages[messageId].childrenIds;
while (messageChildrenIds.length !== 0) {
messageId = messageChildrenIds.at(-1);
messageChildrenIds = history.messages[messageId].childrenIds;
}
history.currentId = messageId;
}
}
await tick();
renderLatex();
hljs.highlightAll();
createCopyCodeBlockButton();
};
////////////////////////// //////////////////////////
// Ollama functions // Ollama functions
////////////////////////// //////////////////////////
@ -507,21 +667,46 @@
} }
}; };
const sendPrompt = async (userPrompt) => { const sendPrompt = async (userPrompt, parentId) => {
// await Promise.all(
// selectedModels.map((model) => {
// if (selectedModel.includes('gpt-')) {
// await sendPromptOpenAI(userPrompt, parentId);
// } else {
// await sendPromptOllama(userPrompt, parentId);
// }
// })
// );
if (selectedModel.includes('gpt-')) { if (selectedModel.includes('gpt-')) {
await sendPromptOpenAI(userPrompt); await sendPromptOpenAI(userPrompt, parentId);
} else { } else {
await sendPromptOllama(userPrompt); await sendPromptOllama(userPrompt, parentId);
} }
console.log(history);
}; };
const sendPromptOllama = async (userPrompt) => { const sendPromptOllama = async (userPrompt, parentId) => {
let responseMessageId = uuidv4();
let responseMessage = { let responseMessage = {
parentId: parentId,
id: responseMessageId,
childrenIds: [],
role: 'assistant', role: 'assistant',
content: '' content: ''
}; };
messages = [...messages, responseMessage]; history.messages[responseMessageId] = responseMessage;
history.currentId = responseMessageId;
if (parentId !== null) {
history.messages[parentId].childrenIds = [
...history.messages[parentId].childrenIds,
responseMessageId
];
}
window.scrollTo({ top: document.body.scrollHeight }); window.scrollTo({ top: document.body.scrollHeight });
const res = await fetch(`${API_BASE_URL}/generate`, { const res = await fetch(`${API_BASE_URL}/generate`, {
@ -542,8 +727,9 @@
}, },
format: settings.requestFormat ?? undefined, format: settings.requestFormat ?? undefined,
context: context:
messages.length > 3 && messages.at(-3).context != undefined history.messages[parentId] !== null &&
? messages.at(-3).context history.messages[parentId].parentId in history.messages
? history.messages[history.messages[parentId].parentId]?.context ?? undefined
: undefined : undefined
}) })
}); });
@ -608,7 +794,8 @@
temperature: settings.temperature temperature: settings.temperature
}, },
timestamp: Date.now(), timestamp: Date.now(),
messages: messages messages: messages,
history: history
}); });
} }
@ -623,15 +810,28 @@
} }
}; };
const sendPromptOpenAI = async (userPrompt) => { const sendPromptOpenAI = async (userPrompt, parentId) => {
if (settings.OPENAI_API_KEY) { if (settings.OPENAI_API_KEY) {
if (models) { if (models) {
let responseMessageId = uuidv4();
let responseMessage = { let responseMessage = {
parentId: parentId,
id: responseMessageId,
childrenIds: [],
role: 'assistant', role: 'assistant',
content: '' content: ''
}; };
messages = [...messages, responseMessage]; history.messages[responseMessageId] = responseMessage;
history.currentId = responseMessageId;
if (parentId !== null) {
history.messages[parentId].childrenIds = [
...history.messages[parentId].childrenIds,
responseMessageId
];
}
window.scrollTo({ top: document.body.scrollHeight }); window.scrollTo({ top: document.body.scrollHeight });
const res = await fetch(`https://api.openai.com/v1/chat/completions`, { const res = await fetch(`https://api.openai.com/v1/chat/completions`, {
@ -653,7 +853,7 @@
...messages ...messages
] ]
.filter((message) => message) .filter((message) => message)
.map((message) => ({ ...message, done: undefined })), .map((message) => ({ role: message.role, content: message.content })),
temperature: settings.temperature ?? undefined, temperature: settings.temperature ?? undefined,
top_p: settings.top_p ?? undefined, top_p: settings.top_p ?? undefined,
frequency_penalty: settings.repeat_penalty ?? undefined frequency_penalty: settings.repeat_penalty ?? undefined
@ -715,7 +915,8 @@
temperature: settings.temperature temperature: settings.temperature
}, },
timestamp: Date.now(), timestamp: Date.now(),
messages: messages messages: messages,
history: history
}); });
} }
@ -747,13 +948,22 @@
} else { } else {
document.getElementById('chat-textarea').style.height = ''; document.getElementById('chat-textarea').style.height = '';
messages = [ let userMessageId = uuidv4();
...messages,
{ let userMessage = {
id: userMessageId,
parentId: messages.length !== 0 ? messages.at(-1).id : null,
childrenIds: [],
role: 'user', role: 'user',
content: userPrompt content: userPrompt
};
if (messages.length !== 0) {
history.messages[messages.at(-1).id].childrenIds.push(userMessageId);
} }
];
history.messages[userMessageId] = userMessage;
history.currentId = userMessageId;
prompt = ''; prompt = '';
@ -767,7 +977,8 @@
}, },
title: 'New Chat', title: 'New Chat',
timestamp: Date.now(), timestamp: Date.now(),
messages: messages messages: messages,
history: history
}); });
chats = await db.getAllFromIndex('chats', 'timestamp'); chats = await db.getAllFromIndex('chats', 'timestamp');
} }
@ -776,7 +987,7 @@
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
}, 50); }, 50);
await sendPrompt(userPrompt); await sendPrompt(userPrompt, userMessageId);
chats = await db.getAllFromIndex('chats', 'timestamp'); chats = await db.getAllFromIndex('chats', 'timestamp');
} }
@ -791,7 +1002,7 @@
let userMessage = messages.at(-1); let userMessage = messages.at(-1);
let userPrompt = userMessage.content; let userPrompt = userMessage.content;
await sendPrompt(userPrompt); await sendPrompt(userPrompt, userMessage.id);
chats = await db.getAllFromIndex('chats', 'timestamp'); chats = await db.getAllFromIndex('chats', 'timestamp');
} }
@ -1078,7 +1289,7 @@
<div class=" w-full"> <div class=" w-full">
<textarea <textarea
class=" bg-transparent outline-none w-full resize-none" class=" bg-transparent outline-none w-full resize-none"
bind:value={message.editedContent} bind:value={history.messages[message.id].editedContent}
on:input={(e) => { on:input={(e) => {
e.target.style.height = ''; e.target.style.height = '';
e.target.style.height = `${e.target.scrollHeight}px`; e.target.style.height = `${e.target.scrollHeight}px`;
@ -1093,7 +1304,7 @@
<button <button
class="px-4 py-2.5 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded-lg" class="px-4 py-2.5 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded-lg"
on:click={() => { on:click={() => {
confirmEditMessage(messageIdx); confirmEditMessage(message.id);
}} }}
> >
Save & Submit Save & Submit
@ -1102,7 +1313,7 @@
<button <button
class=" px-4 py-2.5 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 transition outline outline-1 outline-gray-200 dark:outline-gray-600 rounded-lg" class=" px-4 py-2.5 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 transition outline outline-1 outline-gray-200 dark:outline-gray-600 rounded-lg"
on:click={() => { on:click={() => {
cancelEditMessage(messageIdx); cancelEditMessage(message.id);
}} }}
> >
Cancel Cancel
@ -1113,16 +1324,13 @@
<div class="w-full"> <div class="w-full">
{message.content} {message.content}
<!-- <div class=" flex justify-start space-x-1"> <div class=" flex justify-start space-x-1">
{#if message.parentId !== null && message.parentId in history.messages && (history.messages[message.parentId]?.childrenIds.length ?? 0) > 1}
<div class="flex self-center"> <div class="flex self-center">
<button <button
class="self-center" class="self-center"
on:click={() => { on:click={() => {
message.selectedContentIdx = Math.max( showPreviousMessage(message);
0,
message.selectedContentIdx - 1
);
messages = messages;
}} }}
> >
<svg <svg
@ -1140,19 +1348,16 @@
</button> </button>
<div class="text-xs font-bold self-center"> <div class="text-xs font-bold self-center">
{message.selectedContentIdx + 1} / {message.contents.length} {history.messages[message.parentId].childrenIds.indexOf(
message.id
) + 1} / {history.messages[message.parentId].childrenIds
.length}
</div> </div>
<button <button
class="self-center" class="self-center"
on:click={() => { on:click={() => {
message.selectedContentIdx = Math.min( showNextMessage(message);
message.contents.length - 1,
message.selectedContentIdx + 1
);
messages = messages;
console.log(message);
}} }}
> >
<svg <svg
@ -1169,34 +1374,63 @@
</svg> </svg>
</button> </button>
</div> </div>
{:else if message.parentId === null && Object.values(history.messages).filter((message) => message.parentId === null).length > 1}
<div class="flex self-center">
<button <button
class="invisible group-hover:visible p-1 rounded dark:hover:bg-gray-800 transition" class="self-center"
on:click={() => { on:click={() => {
editMessage(messageIdx); showPreviousMessage(message);
}} }}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 20 20"
viewBox="0 0 24 24" fill="currentColor"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4" class="w-4 h-4"
> >
<path <path
stroke-linecap="round" fill-rule="evenodd"
stroke-linejoin="round" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" clip-rule="evenodd"
/> />
</svg> </svg>
</button> </button>
</div> -->
<div class=" flex justify-start space-x-1"> <div class="text-xs font-bold self-center">
{Object.values(history.messages)
.filter((message) => message.parentId === null)
.map((message) => message.id)
.indexOf(message.id) + 1} / {Object.values(
history.messages
).filter((message) => message.parentId === null).length}
</div>
<button
class="self-center"
on:click={() => {
showNextMessage(message);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
{/if}
<button <button
class="invisible group-hover:visible p-1 rounded dark:hover:bg-gray-800 transition" class="invisible group-hover:visible p-1 rounded dark:hover:bg-gray-800 transition"
on:click={() => { on:click={() => {
editMessage(messageIdx); editMessageHandler(message.id);
}} }}
> >
<svg <svg
@ -1223,6 +1457,56 @@
{#if message.done} {#if message.done}
<div class=" flex justify-start space-x-1 -mt-2"> <div class=" flex justify-start space-x-1 -mt-2">
{#if message.parentId !== null && message.parentId in history.messages && (history.messages[message.parentId]?.childrenIds.length ?? 0) > 1}
<div class="flex self-center">
<button
class="self-center"
on:click={() => {
showPreviousMessage(message);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z"
clip-rule="evenodd"
/>
</svg>
</button>
<div class="text-xs font-bold self-center">
{history.messages[message.parentId].childrenIds.indexOf(
message.id
) + 1} / {history.messages[message.parentId].childrenIds
.length}
</div>
<button
class="self-center"
on:click={() => {
showNextMessage(message);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
{/if}
<button <button
class="{messageIdx + 1 === messages.length class="{messageIdx + 1 === messages.length
? 'visible' ? 'visible'