|
@@ -6,17 +6,19 @@
|
|
|
|
|
|
import type { PageData } from './$types';
|
|
import type { PageData } from './$types';
|
|
import { ENDPOINT } from '$lib/contants';
|
|
import { ENDPOINT } from '$lib/contants';
|
|
|
|
+ import { tick } from 'svelte';
|
|
|
|
|
|
export let data: PageData;
|
|
export let data: PageData;
|
|
$: ({ models } = data);
|
|
$: ({ models } = data);
|
|
|
|
+ let textareaElement;
|
|
|
|
|
|
let selectedModel = '';
|
|
let selectedModel = '';
|
|
let prompt = '';
|
|
let prompt = '';
|
|
- let context = '';
|
|
|
|
|
|
+ let messages = [];
|
|
|
|
|
|
- let chatHistory = {};
|
|
|
|
-
|
|
|
|
- let textareaElement = '';
|
|
|
|
|
|
+ //////////////////////////
|
|
|
|
+ // Helper functions
|
|
|
|
+ //////////////////////////
|
|
|
|
|
|
const splitStream = (splitOn) => {
|
|
const splitStream = (splitOn) => {
|
|
let buffer = '';
|
|
let buffer = '';
|
|
@@ -33,37 +35,77 @@
|
|
});
|
|
});
|
|
};
|
|
};
|
|
|
|
|
|
|
|
+ const copyToClipboard = (text) => {
|
|
|
|
+ if (!navigator.clipboard) {
|
|
|
|
+ var textArea = document.createElement('textarea');
|
|
|
|
+ textArea.value = text;
|
|
|
|
+
|
|
|
|
+ // Avoid scrolling to bottom
|
|
|
|
+ textArea.style.top = '0';
|
|
|
|
+ textArea.style.left = '0';
|
|
|
|
+ textArea.style.position = 'fixed';
|
|
|
|
+
|
|
|
|
+ document.body.appendChild(textArea);
|
|
|
|
+ textArea.focus();
|
|
|
|
+ textArea.select();
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ var successful = document.execCommand('copy');
|
|
|
|
+ var msg = successful ? 'successful' : 'unsuccessful';
|
|
|
|
+ console.log('Fallback: Copying text command was ' + msg);
|
|
|
|
+ } catch (err) {
|
|
|
|
+ console.error('Fallback: Oops, unable to copy', err);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ document.body.removeChild(textArea);
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ navigator.clipboard.writeText(text).then(
|
|
|
|
+ function () {
|
|
|
|
+ console.log('Async: Copying to clipboard was successful!');
|
|
|
|
+ toast.success('Copying to clipboard was successful!');
|
|
|
|
+ },
|
|
|
|
+ function (err) {
|
|
|
|
+ console.error('Async: Could not copy text: ', err);
|
|
|
|
+ }
|
|
|
|
+ );
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ //////////////////////////
|
|
|
|
+ // Ollama functions
|
|
|
|
+ //////////////////////////
|
|
|
|
+
|
|
const submitPrompt = async () => {
|
|
const submitPrompt = async () => {
|
|
console.log('submitPrompt');
|
|
console.log('submitPrompt');
|
|
|
|
|
|
if (selectedModel === '') {
|
|
if (selectedModel === '') {
|
|
toast.error('Model not selected');
|
|
toast.error('Model not selected');
|
|
- } else if (
|
|
|
|
- Object.keys(chatHistory).length != 0 &&
|
|
|
|
- chatHistory[Object.keys(chatHistory).length - 1].done != true
|
|
|
|
- ) {
|
|
|
|
|
|
+ } else if (messages.length != 0 && messages.at(-1).done != true) {
|
|
console.log('wait');
|
|
console.log('wait');
|
|
} else {
|
|
} else {
|
|
console.log(prompt);
|
|
console.log(prompt);
|
|
|
|
|
|
let user_prompt = prompt;
|
|
let user_prompt = prompt;
|
|
-
|
|
|
|
- chatHistory[Object.keys(chatHistory).length] = {
|
|
|
|
- role: 'user',
|
|
|
|
- content: user_prompt
|
|
|
|
- };
|
|
|
|
-
|
|
|
|
|
|
+ messages = [
|
|
|
|
+ ...messages,
|
|
|
|
+ {
|
|
|
|
+ role: 'user',
|
|
|
|
+ content: user_prompt
|
|
|
|
+ }
|
|
|
|
+ ];
|
|
prompt = '';
|
|
prompt = '';
|
|
- textareaElement.style.height = '';
|
|
|
|
|
|
|
|
|
|
+ textareaElement.style.height = '';
|
|
setTimeout(() => {
|
|
setTimeout(() => {
|
|
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
|
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
|
}, 50);
|
|
}, 50);
|
|
|
|
|
|
- chatHistory[Object.keys(chatHistory).length] = {
|
|
|
|
|
|
+ let responseMessage = {
|
|
role: 'assistant',
|
|
role: 'assistant',
|
|
content: ''
|
|
content: ''
|
|
};
|
|
};
|
|
|
|
+
|
|
|
|
+ messages = [...messages, responseMessage];
|
|
window.scrollTo({ top: document.body.scrollHeight });
|
|
window.scrollTo({ top: document.body.scrollHeight });
|
|
|
|
|
|
const res = await fetch(`${ENDPOINT}/api/generate`, {
|
|
const res = await fetch(`${ENDPOINT}/api/generate`, {
|
|
@@ -74,7 +116,10 @@
|
|
body: JSON.stringify({
|
|
body: JSON.stringify({
|
|
model: selectedModel,
|
|
model: selectedModel,
|
|
prompt: user_prompt,
|
|
prompt: user_prompt,
|
|
- context: context != '' ? context : undefined
|
|
|
|
|
|
+ context:
|
|
|
|
+ messages.length > 3 && messages.at(-3).context != undefined
|
|
|
|
+ ? messages.at(-3).context
|
|
|
|
+ : undefined
|
|
})
|
|
})
|
|
});
|
|
});
|
|
|
|
|
|
@@ -82,6 +127,7 @@
|
|
.pipeThrough(new TextDecoderStream())
|
|
.pipeThrough(new TextDecoderStream())
|
|
.pipeThrough(splitStream('\n'))
|
|
.pipeThrough(splitStream('\n'))
|
|
.getReader();
|
|
.getReader();
|
|
|
|
+
|
|
while (true) {
|
|
while (true) {
|
|
const { value, done } = await reader.read();
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
if (done) break;
|
|
@@ -93,20 +139,17 @@
|
|
if (line !== '') {
|
|
if (line !== '') {
|
|
console.log(line);
|
|
console.log(line);
|
|
let data = JSON.parse(line);
|
|
let data = JSON.parse(line);
|
|
-
|
|
|
|
if (data.done == false) {
|
|
if (data.done == false) {
|
|
- if (
|
|
|
|
- chatHistory[Object.keys(chatHistory).length - 1].content == '' &&
|
|
|
|
- data.response == '\n'
|
|
|
|
- ) {
|
|
|
|
|
|
+ if (responseMessage.content == '' && data.response == '\n') {
|
|
continue;
|
|
continue;
|
|
} else {
|
|
} else {
|
|
- chatHistory[Object.keys(chatHistory).length - 1].content += data.response;
|
|
|
|
|
|
+ responseMessage.content += data.response;
|
|
|
|
+ messages = messages;
|
|
}
|
|
}
|
|
} else {
|
|
} else {
|
|
- context = data.context;
|
|
|
|
- console.log(context);
|
|
|
|
- chatHistory[Object.keys(chatHistory).length - 1].done = true;
|
|
|
|
|
|
+ responseMessage.done = true;
|
|
|
|
+ responseMessage.context = data.context;
|
|
|
|
+ messages = messages;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@@ -120,40 +163,78 @@
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
- const copyToClipboard = (text) => {
|
|
|
|
- if (!navigator.clipboard) {
|
|
|
|
- var textArea = document.createElement('textarea');
|
|
|
|
- textArea.value = text;
|
|
|
|
|
|
+ const regenerateResponse = async () => {
|
|
|
|
+ console.log('regenerateResponse');
|
|
|
|
|
|
- // Avoid scrolling to bottom
|
|
|
|
- textArea.style.top = '0';
|
|
|
|
- textArea.style.left = '0';
|
|
|
|
- textArea.style.position = 'fixed';
|
|
|
|
|
|
+ if (messages.length != 0 && messages.at(-1).done == true) {
|
|
|
|
+ messages.splice(messages.length - 1, 1);
|
|
|
|
+ messages = messages;
|
|
|
|
|
|
- document.body.appendChild(textArea);
|
|
|
|
- textArea.focus();
|
|
|
|
- textArea.select();
|
|
|
|
|
|
+ let lastUserMessage = messages.at(-1);
|
|
|
|
|
|
- try {
|
|
|
|
- var successful = document.execCommand('copy');
|
|
|
|
- var msg = successful ? 'successful' : 'unsuccessful';
|
|
|
|
- console.log('Fallback: Copying text command was ' + msg);
|
|
|
|
- } catch (err) {
|
|
|
|
- console.error('Fallback: Oops, unable to copy', err);
|
|
|
|
|
|
+ let responseMessage = {
|
|
|
|
+ role: 'assistant',
|
|
|
|
+ content: ''
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ messages = [...messages, responseMessage];
|
|
|
|
+ window.scrollTo({ top: document.body.scrollHeight });
|
|
|
|
+
|
|
|
|
+ const res = await fetch(`${ENDPOINT}/api/generate`, {
|
|
|
|
+ method: 'POST',
|
|
|
|
+ headers: {
|
|
|
|
+ 'Content-Type': 'text/event-stream'
|
|
|
|
+ },
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
+ model: selectedModel,
|
|
|
|
+ prompt: lastUserMessage.content,
|
|
|
|
+ context:
|
|
|
|
+ messages.length > 3 && messages.at(-3).context != undefined
|
|
|
|
+ ? messages.at(-3).context
|
|
|
|
+ : undefined
|
|
|
|
+ })
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ const reader = res.body
|
|
|
|
+ .pipeThrough(new TextDecoderStream())
|
|
|
|
+ .pipeThrough(splitStream('\n'))
|
|
|
|
+ .getReader();
|
|
|
|
+
|
|
|
|
+ while (true) {
|
|
|
|
+ const { value, done } = await reader.read();
|
|
|
|
+ if (done) break;
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ let lines = value.split('\n');
|
|
|
|
+
|
|
|
|
+ for (const line of lines) {
|
|
|
|
+ if (line !== '') {
|
|
|
|
+ console.log(line);
|
|
|
|
+ let data = JSON.parse(line);
|
|
|
|
+ if (data.done == false) {
|
|
|
|
+ if (responseMessage.content == '' && data.response == '\n') {
|
|
|
|
+ continue;
|
|
|
|
+ } else {
|
|
|
|
+ responseMessage.content += data.response;
|
|
|
|
+ messages = messages;
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ responseMessage.done = true;
|
|
|
|
+ responseMessage.context = data.context;
|
|
|
|
+ messages = messages;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ } catch (error) {
|
|
|
|
+ console.log(error);
|
|
|
|
+ }
|
|
|
|
+ window.scrollTo({ top: document.body.scrollHeight });
|
|
}
|
|
}
|
|
|
|
|
|
- document.body.removeChild(textArea);
|
|
|
|
- return;
|
|
|
|
|
|
+ window.scrollTo({ top: document.body.scrollHeight });
|
|
}
|
|
}
|
|
- navigator.clipboard.writeText(text).then(
|
|
|
|
- function () {
|
|
|
|
- console.log('Async: Copying to clipboard was successful!');
|
|
|
|
- toast.success('Copying to clipboard was successful!');
|
|
|
|
- },
|
|
|
|
- function (err) {
|
|
|
|
- console.error('Async: Could not copy text: ', err);
|
|
|
|
- }
|
|
|
|
- );
|
|
|
|
|
|
+
|
|
|
|
+ console.log(messages);
|
|
};
|
|
};
|
|
</script>
|
|
</script>
|
|
|
|
|
|
@@ -171,7 +252,7 @@
|
|
id="models"
|
|
id="models"
|
|
class="outline-none border border-gray-600 bg-gray-700 text-gray-200 text-sm rounded-lg block w-full p-2.5 placeholder-gray-400"
|
|
class="outline-none border border-gray-600 bg-gray-700 text-gray-200 text-sm rounded-lg block w-full p-2.5 placeholder-gray-400"
|
|
bind:value={selectedModel}
|
|
bind:value={selectedModel}
|
|
- disabled={Object.keys(chatHistory).length != 0}
|
|
|
|
|
|
+ disabled={messages.length != 0}
|
|
>
|
|
>
|
|
<option value="" selected>Select a model</option>
|
|
<option value="" selected>Select a model</option>
|
|
|
|
|
|
@@ -183,8 +264,8 @@
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
- <div class=" h-full mb-32 w-full flex flex-col">
|
|
|
|
- {#if Object.keys(chatHistory).length == 0}
|
|
|
|
|
|
+ <div class=" h-full mb-44 w-full flex flex-col">
|
|
|
|
+ {#if messages.length == 0}
|
|
<div class="m-auto text-center max-w-md">
|
|
<div class="m-auto text-center max-w-md">
|
|
<div class="flex justify-center mt-8">
|
|
<div class="flex justify-center mt-8">
|
|
<img src="/ollama.png" class="w-16 invert-[80%]" />
|
|
<img src="/ollama.png" class="w-16 invert-[80%]" />
|
|
@@ -198,18 +279,18 @@
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
{:else}
|
|
- {#each Object.keys(chatHistory) as messageIdx}
|
|
|
|
- <div class=" w-full {chatHistory[messageIdx].role == 'user' ? '' : ' bg-gray-700'}">
|
|
|
|
|
|
+ {#each messages as message, messageIdx}
|
|
|
|
+ <div class=" w-full {message.role == 'user' ? '' : ' bg-gray-700'}">
|
|
<div class="flex justify-between p-5 py-10 max-w-3xl mx-auto rounded-lg">
|
|
<div class="flex justify-between p-5 py-10 max-w-3xl mx-auto rounded-lg">
|
|
<div class="space-x-7 flex w-full">
|
|
<div class="space-x-7 flex w-full">
|
|
<div class="">
|
|
<div class="">
|
|
<img
|
|
<img
|
|
- src="/{chatHistory[messageIdx].role == 'user' ? 'user' : 'favicon'}.png"
|
|
|
|
|
|
+ src="/{message.role == 'user' ? 'user' : 'favicon'}.png"
|
|
class=" max-w-[32px] object-cover rounded"
|
|
class=" max-w-[32px] object-cover rounded"
|
|
/>
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
- {#if chatHistory[messageIdx].role != 'user' && chatHistory[messageIdx].content == ''}
|
|
|
|
|
|
+ {#if message.role != 'user' && message.content == ''}
|
|
<div class="w-full pr-28">
|
|
<div class="w-full pr-28">
|
|
<div class="animate-pulse flex w-full">
|
|
<div class="animate-pulse flex w-full">
|
|
<div class="space-y-2 w-full">
|
|
<div class="space-y-2 w-full">
|
|
@@ -231,18 +312,18 @@
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
{:else}
|
|
<div class="whitespace-pre-line">
|
|
<div class="whitespace-pre-line">
|
|
- {@html marked.parse(chatHistory[messageIdx].content)}
|
|
|
|
|
|
+ {@html marked.parse(message.content)}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
<!-- {} -->
|
|
<!-- {} -->
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div>
|
|
<div>
|
|
- {#if chatHistory[messageIdx].role != 'user' && chatHistory[messageIdx].done}
|
|
|
|
|
|
+ {#if message.role != 'user' && message.done}
|
|
<button
|
|
<button
|
|
class="p-1 rounded hover:bg-gray-700 transition"
|
|
class="p-1 rounded hover:bg-gray-700 transition"
|
|
on:click={() => {
|
|
on:click={() => {
|
|
- copyToClipboard(chatHistory[messageIdx].content);
|
|
|
|
|
|
+ copyToClipboard(message.content);
|
|
}}
|
|
}}
|
|
>
|
|
>
|
|
<svg
|
|
<svg
|
|
@@ -274,6 +355,30 @@
|
|
|
|
|
|
<div class=" bg-gradient-to-t from-gray-900 pt-5">
|
|
<div class=" bg-gradient-to-t from-gray-900 pt-5">
|
|
<div class="max-w-3xl p-2.5 -mb-0.5 mx-auto inset-x-0">
|
|
<div class="max-w-3xl p-2.5 -mb-0.5 mx-auto inset-x-0">
|
|
|
|
+ {#if messages.length != 0 && messages.at(-1).role == 'assistant' && messages.at(-1).done == true}
|
|
|
|
+ <div class=" flex justify-end mb-2.5">
|
|
|
|
+ <button
|
|
|
|
+ class=" flex px-4 py-2.5 bg-gray-800 hover:bg-gray-700 outline outline-1 outline-gray-600 rounded"
|
|
|
|
+ on:click={regenerateResponse}
|
|
|
|
+ >
|
|
|
|
+ <div class=" self-center mr-1">
|
|
|
|
+ <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="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
|
|
|
|
+ clip-rule="evenodd"
|
|
|
|
+ />
|
|
|
|
+ </svg>
|
|
|
|
+ </div>
|
|
|
|
+ <div class=" self-center text-sm">Regenerate</div>
|
|
|
|
+ </button>
|
|
|
|
+ </div>
|
|
|
|
+ {/if}
|
|
<form class=" flex shadow-sm relative w-full" on:submit|preventDefault={submitPrompt}>
|
|
<form class=" flex shadow-sm relative w-full" on:submit|preventDefault={submitPrompt}>
|
|
<textarea
|
|
<textarea
|
|
class="rounded-xl bg-gray-700 outline-none w-full py-3 px-5 pr-12 resize-none"
|
|
class="rounded-xl bg-gray-700 outline-none w-full py-3 px-5 pr-12 resize-none"
|
|
@@ -296,7 +401,7 @@
|
|
/>
|
|
/>
|
|
<div class=" absolute right-0 bottom-0">
|
|
<div class=" absolute right-0 bottom-0">
|
|
<div class="pr-3 pb-2">
|
|
<div class="pr-3 pb-2">
|
|
- {#if Object.keys(chatHistory).length == 0 || chatHistory[Object.keys(chatHistory).length - 1].done == true}
|
|
|
|
|
|
+ {#if messages.length == 0 || messages.at(-1).done == true}
|
|
<button
|
|
<button
|
|
class="{prompt !== ''
|
|
class="{prompt !== ''
|
|
? 'bg-emerald-600 text-gray-100 hover:bg-emerald-700 '
|
|
? 'bg-emerald-600 text-gray-100 hover:bg-emerald-700 '
|